Modifier.kt 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. package org.mjdev.desktop.extensions
  2. import androidx.compose.animation.core.animateFloatAsState
  3. import androidx.compose.foundation.border
  4. import androidx.compose.foundation.interaction.InteractionSource
  5. import androidx.compose.foundation.interaction.collectIsPressedAsState
  6. import androidx.compose.foundation.layout.size
  7. import androidx.compose.foundation.shape.CircleShape
  8. import androidx.compose.foundation.shape.RoundedCornerShape
  9. import androidx.compose.runtime.Composable
  10. import androidx.compose.runtime.Stable
  11. import androidx.compose.runtime.getValue
  12. import androidx.compose.runtime.rememberUpdatedState
  13. import androidx.compose.ui.ExperimentalComposeUiApi
  14. import androidx.compose.ui.Modifier
  15. import androidx.compose.ui.composed
  16. import androidx.compose.ui.draw.clip
  17. import androidx.compose.ui.draw.drawWithCache
  18. import androidx.compose.ui.geometry.CornerRadius
  19. import androidx.compose.ui.graphics.Color
  20. import androidx.compose.ui.graphics.PathEffect
  21. import androidx.compose.ui.graphics.RectangleShape
  22. import androidx.compose.ui.graphics.drawscope.Stroke
  23. import androidx.compose.ui.graphics.graphicsLayer
  24. import androidx.compose.ui.input.key.Key
  25. import androidx.compose.ui.input.key.KeyEventType
  26. import androidx.compose.ui.input.key.key
  27. import androidx.compose.ui.input.key.onKeyEvent
  28. import androidx.compose.ui.input.key.type
  29. import androidx.compose.ui.input.pointer.AwaitPointerEventScope
  30. import androidx.compose.ui.input.pointer.PointerEvent
  31. import androidx.compose.ui.input.pointer.PointerEventPass
  32. import androidx.compose.ui.input.pointer.PointerEventType
  33. import androidx.compose.ui.input.pointer.isPrimaryPressed
  34. import androidx.compose.ui.input.pointer.isSecondaryPressed
  35. import androidx.compose.ui.input.pointer.pointerInput
  36. import androidx.compose.ui.platform.LocalDensity
  37. import androidx.compose.ui.unit.Dp
  38. import androidx.compose.ui.unit.dp
  39. @Suppress("unused")
  40. @OptIn(ExperimentalComposeUiApi::class)
  41. object Modifier {
  42. @Composable
  43. fun Modifier.conditional(
  44. condition: Boolean,
  45. onFalse: (Modifier.() -> Modifier)? = null,
  46. onTrue: Modifier.() -> Modifier,
  47. ): Modifier = if (condition) {
  48. then(onTrue(Modifier))
  49. } else if (onFalse != null) {
  50. then(onFalse(Modifier))
  51. } else {
  52. this
  53. }
  54. fun Modifier.onKey(
  55. keyCode: Key,
  56. action: KeyEventType = KeyEventType.KeyDown,
  57. block: () -> Unit
  58. ): Modifier = this then onKeyEvent { ev ->
  59. if (ev.type == action) {
  60. if (ev.key == keyCode) {
  61. block()
  62. true
  63. } else false
  64. } else false
  65. }
  66. @Composable
  67. fun Modifier.scaleOnPress(
  68. interactionSource: InteractionSource,
  69. pressScale: Float = 0.95f,
  70. leaveScale: Float = 1f
  71. ) = composed {
  72. val isPressed by interactionSource.collectIsPressedAsState()
  73. val scale by animateFloatAsState(if (isPressed) pressScale else leaveScale)
  74. this.graphicsLayer {
  75. scaleX = scale
  76. scaleY = scale
  77. }
  78. }
  79. @Composable
  80. fun Modifier.size(width: Int, height: Int) = size(width.dp, height.dp)
  81. @Composable
  82. fun Modifier.size(value: Int) = size(value.dp, value.dp)
  83. @Composable
  84. fun Modifier.dashedBorder(
  85. strokeWidth: Dp,
  86. color: Color,
  87. cornerRadiusDp: Dp
  88. ) = composed(
  89. factory = {
  90. val density = LocalDensity.current
  91. val strokeWidthPx = density.run { strokeWidth.toPx() }
  92. val cornerRadiusPx = density.run { cornerRadiusDp.toPx() }
  93. then(
  94. Modifier.drawWithCache {
  95. onDrawBehind {
  96. val stroke = Stroke(
  97. width = strokeWidthPx,
  98. pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
  99. )
  100. drawRoundRect(
  101. color = color,
  102. style = stroke,
  103. cornerRadius = CornerRadius(cornerRadiusPx)
  104. )
  105. }
  106. }
  107. )
  108. }
  109. )
  110. @Composable
  111. fun Modifier.onPointerEvent(
  112. eventType: PointerEventType,
  113. pass: PointerEventPass = PointerEventPass.Main,
  114. onEvent: AwaitPointerEventScope.(event: PointerEvent) -> Unit
  115. ): Modifier = composed {
  116. val currentEventType by rememberUpdatedState(eventType)
  117. val currentOnEvent by rememberUpdatedState(onEvent)
  118. pointerInput(pass) {
  119. awaitPointerEventScope {
  120. while (true) {
  121. val event = awaitPointerEvent(pass)
  122. if (event.type == currentEventType) {
  123. currentOnEvent(event)
  124. }
  125. }
  126. }
  127. }
  128. }
  129. @Composable
  130. fun Modifier.onMouseEnter(
  131. pass: PointerEventPass = PointerEventPass.Main,
  132. onEvent: AwaitPointerEventScope.(event: PointerEvent) -> Unit
  133. ): Modifier = onPointerEvent(
  134. eventType = PointerEventType.Enter,
  135. pass = pass,
  136. onEvent = onEvent
  137. )
  138. @Composable
  139. fun Modifier.onMouseLeave(
  140. pass: PointerEventPass = PointerEventPass.Main,
  141. onEvent: AwaitPointerEventScope.(event: PointerEvent) -> Unit
  142. ): Modifier = onPointerEvent(
  143. eventType = PointerEventType.Exit,
  144. pass = pass,
  145. onEvent = onEvent
  146. )
  147. @Composable
  148. fun Modifier.onMousePress(
  149. pass: PointerEventPass = PointerEventPass.Main,
  150. onEvent: AwaitPointerEventScope.(event: PointerEvent) -> Unit
  151. ): Modifier = onPointerEvent(
  152. eventType = PointerEventType.Release,
  153. pass = pass,
  154. onEvent = onEvent
  155. )
  156. @Composable
  157. fun Modifier.onMouseLongPress(
  158. pass: PointerEventPass = PointerEventPass.Main,
  159. onEvent: AwaitPointerEventScope.(event: PointerEvent) -> Unit
  160. ): Modifier {
  161. // return onPointerEvent(
  162. // eventType = PointerEventType.Press,
  163. // pass = pass,
  164. // onEvent = onEvent
  165. // )
  166. return this
  167. }
  168. val AwaitPointerEventScope.isPrimary
  169. get() = !isSecondary
  170. val AwaitPointerEventScope.isSecondary
  171. get() = currentEvent.buttons.isSecondaryPressed
  172. fun AwaitPointerEventScope.onLeftClick(
  173. block: AwaitPointerEventScope.() -> Unit
  174. ) {
  175. if (isPrimary) block()
  176. }
  177. fun AwaitPointerEventScope.onRightClick(
  178. block: AwaitPointerEventScope.() -> Unit
  179. ) {
  180. if (isSecondary) block()
  181. }
  182. // val PointerEvent.isConsumed
  183. // get() = changes.firstOrNull()?.isConsumed ?: false
  184. // val PointerEvent.isNotConsumed
  185. // get() = isConsumed.not()
  186. // val PointerEvent.isRelease
  187. // get() = type == PointerEventType.Release
  188. // val PointerEvent.isPress
  189. // get() = type == PointerEventType.Press
  190. // val PointerEvent.isPressed
  191. // get() = changes.firstOrNull()?.pressed ?: false
  192. // @OptIn(ExperimentalComposeUiApi::class)
  193. // val PointerEvent.isLeftButton
  194. // get() = buttons.isPrimaryPressed
  195. // val PointerEvent.isRightButton
  196. // get() = buttons.isSecondaryPressed
  197. // val PointerEvent.isNotPressed
  198. // get() = isPressed.not()
  199. // fun Modifier.onLeftClick(
  200. // onClick: () -> Unit
  201. // ): Modifier = pointerInput(Unit) {
  202. // awaitPointerEventScope {
  203. // while (true) {
  204. // val event = awaitPointerEvent(PointerEventPass.Main)
  205. // if (event.isPressed && event.isNotConsumed && event.isLeftButton) {
  206. // onClick()
  207. // }
  208. // }
  209. // }
  210. // }
  211. // fun Modifier.onRightClick(
  212. // onClick: () -> Unit
  213. // ): Modifier = pointerInput(Unit) {
  214. // awaitPointerEventScope {
  215. // while (true) {
  216. // val event = awaitPointerEvent(PointerEventPass.Main)
  217. // if (event.isPressed && event.isNotConsumed && event.isRightButton) {
  218. // onClick()
  219. // }
  220. // }
  221. // }
  222. // }
  223. @Stable
  224. @Composable
  225. fun Modifier.clipCircle() = clip(CircleShape)
  226. @Stable
  227. @Composable
  228. fun Modifier.clipRect() = clip(RectangleShape)
  229. @Stable
  230. @Composable
  231. fun Modifier.clipRoundRect(round: Dp) = clip(RoundedCornerShape(round))
  232. @Stable
  233. @Composable
  234. fun Modifier.circleBorder(
  235. width: Dp = 2.dp,
  236. color: Color = Color.White
  237. ) = clipCircle().border(width, color, CircleShape)
  238. }