AppsMenu.kt 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. package eu.mjdev.desktop.components.appsmenu
  2. import androidx.compose.animation.EnterTransition
  3. import androidx.compose.animation.ExitTransition
  4. import androidx.compose.desktop.ui.tooling.preview.Preview
  5. import androidx.compose.foundation.background
  6. import androidx.compose.foundation.border
  7. import androidx.compose.foundation.layout.*
  8. import androidx.compose.foundation.lazy.LazyListState
  9. import androidx.compose.foundation.shape.RoundedCornerShape
  10. import androidx.compose.material.Divider
  11. import androidx.compose.runtime.Composable
  12. import androidx.compose.runtime.MutableState
  13. import androidx.compose.runtime.getValue
  14. import androidx.compose.runtime.setValue
  15. import androidx.compose.ui.Alignment
  16. import androidx.compose.ui.Modifier
  17. import androidx.compose.ui.draw.clip
  18. import androidx.compose.ui.graphics.Brush
  19. import androidx.compose.ui.graphics.RectangleShape
  20. import androidx.compose.ui.layout.onPlaced
  21. import androidx.compose.ui.unit.dp
  22. import androidx.compose.ui.window.WindowPosition
  23. import eu.mjdev.desktop.components.blur.BlurPanel
  24. import eu.mjdev.desktop.components.user.UserAvatar
  25. import eu.mjdev.desktop.data.App
  26. import eu.mjdev.desktop.data.Category
  27. import eu.mjdev.desktop.extensions.ColorUtils.alpha
  28. import eu.mjdev.desktop.extensions.ColorUtils.darker
  29. import eu.mjdev.desktop.extensions.Compose.clear
  30. import eu.mjdev.desktop.extensions.Compose.launchedEffect
  31. import eu.mjdev.desktop.extensions.Compose.onMouseEnter
  32. import eu.mjdev.desktop.extensions.Compose.plus
  33. import eu.mjdev.desktop.extensions.Compose.rememberCalculated
  34. import eu.mjdev.desktop.extensions.Compose.rememberComputed
  35. import eu.mjdev.desktop.extensions.Compose.rememberState
  36. import eu.mjdev.desktop.extensions.Compose.removeLast
  37. import eu.mjdev.desktop.extensions.Custom.flowBlock
  38. import eu.mjdev.desktop.extensions.Custom.trimIsNotEmpty
  39. import eu.mjdev.desktop.extensions.Modifier.dropShadow
  40. import eu.mjdev.desktop.helpers.animation.Animations.AppsMenuEnterAnimation
  41. import eu.mjdev.desktop.helpers.animation.Animations.AppsMenuExitAnimation
  42. import eu.mjdev.desktop.helpers.compose.rememberForeverLazyListState
  43. import eu.mjdev.desktop.helpers.internal.KeyEventHandler.Companion.globalKeyEventHandler
  44. import eu.mjdev.desktop.helpers.shape.BarShape
  45. import eu.mjdev.desktop.provider.DesktopScope
  46. import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
  47. import eu.mjdev.desktop.windows.ChromeWindow
  48. import eu.mjdev.desktop.windows.ChromeWindowState
  49. import eu.mjdev.desktop.windows.ChromeWindowState.Companion.rememberChromeWindowState
  50. @Suppress("FunctionName")
  51. @Composable
  52. fun AppsMenu(
  53. panelState: ChromeWindowState = rememberChromeWindowState(),
  54. menuState: ChromeWindowState = rememberChromeWindowState(),
  55. enterAnimation: EnterTransition = AppsMenuEnterAnimation,
  56. exitAnimation: ExitTransition = AppsMenuExitAnimation,
  57. searchTextState: MutableState<String> = rememberState(""),
  58. listState: LazyListState = rememberForeverLazyListState("AppsMenu"),
  59. onFocusChange: ChromeWindowState.(Boolean) -> Unit = {},
  60. onAppClick: DesktopScope.(App) -> Unit = { app ->
  61. app.start()
  62. searchTextState.clear()
  63. menuState.hide()
  64. },
  65. onAppContextMenuClick: DesktopScope.(App) -> Unit = {},
  66. onCategoryContextMenuClick: DesktopScope.(Category) -> Unit = {},
  67. onUserAvatarClick: () -> Unit = {}
  68. ) = withDesktopScope {
  69. var category by rememberState("")
  70. val appCategories by flowBlock(emptyList()) { appCategories }
  71. val items by rememberComputed(searchTextState.value, category) {
  72. when {
  73. // todo fuzzy sort
  74. searchTextState.trimIsNotEmpty() -> allApps.filter { app ->
  75. app.desktopData.contains(searchTextState.value, ignoreCase = true)
  76. }
  77. category.isNotEmpty() -> allApps.filter { app ->
  78. app.categories.contains(category)
  79. }.sortedBy { app ->
  80. app.name
  81. }
  82. else -> appCategories
  83. }
  84. }
  85. val position by rememberCalculated {
  86. WindowPosition.Absolute(
  87. panelState.bounds.x,
  88. containerSize.height - (panelState.bounds.height + appMenuMinHeight)
  89. )
  90. }
  91. val windowState: ChromeWindowState = rememberChromeWindowState(position = position)
  92. globalKeyEventHandler(
  93. isEnabled = { menuState.isVisible && menuState.enabled }
  94. ) {
  95. onEscape {
  96. menuState.hide()
  97. true
  98. }
  99. onBack {
  100. searchTextState.clear()
  101. true
  102. }
  103. onBackSpace {
  104. searchTextState.removeLast()
  105. true
  106. }
  107. onDelete {
  108. searchTextState.clear()
  109. true
  110. }
  111. onChar { char ->
  112. searchTextState + char
  113. true
  114. }
  115. }
  116. ChromeWindow(
  117. visible = menuState.isVisible,
  118. enterAnimation = enterAnimation,
  119. exitAnimation = exitAnimation,
  120. windowState = windowState,
  121. onFocusChange = onFocusChange
  122. ) {
  123. Box(
  124. modifier = Modifier
  125. .width(appMenuMinWidth)
  126. .heightIn(
  127. min = appMenuMinHeight,
  128. max = appMenuMinHeight
  129. )
  130. .dropShadow(
  131. RoundedCornerShape(24.dp),
  132. borderColor.alpha(0.5f),
  133. 0.dp,
  134. 0.dp,
  135. 4.dp,
  136. 0.dp
  137. )
  138. .onPlaced(menuState::onPlaced)
  139. .onMouseEnter {
  140. windowState.requestFocus()
  141. }
  142. ) {
  143. BlurPanel(
  144. modifier = Modifier.fillMaxSize()
  145. .clip(RoundedCornerShape(24.dp))
  146. )
  147. Box(
  148. modifier = Modifier
  149. .fillMaxSize()
  150. .padding(menuPadding)
  151. .background(backgroundColor.copy(alpha = 0.2f), RoundedCornerShape(24.dp))
  152. .border(2.dp, borderColor, RoundedCornerShape(24.dp))
  153. .clip(RoundedCornerShape(24.dp))
  154. ) {
  155. Column(
  156. modifier = Modifier.fillMaxSize()
  157. ) {
  158. Box(
  159. modifier = Modifier.clip(RectangleShape),
  160. contentAlignment = Alignment.BottomStart
  161. ) {
  162. val shape = BarShape(
  163. offset = 52f,
  164. circleRadius = 24.dp,
  165. cornerRadius = 4.dp,
  166. circleGap = 8.dp,
  167. )
  168. val brush = Brush.horizontalGradient(
  169. listOf(
  170. backgroundColor.darker(0.1f),
  171. backgroundColor.darker(0.1f),
  172. backgroundColor.darker(0.1f),
  173. backgroundColor.alpha(0.9f),
  174. backgroundColor.alpha(0.7f),
  175. backgroundColor.alpha(0.3f),
  176. ),
  177. // startX = 0f,
  178. // endX = 0f
  179. )
  180. Box(
  181. modifier = Modifier.fillMaxWidth()
  182. .height(64.dp)
  183. .background(
  184. brush = brush,
  185. shape = shape
  186. )
  187. )
  188. UserAvatar(
  189. titleVerticalAlignment = Alignment.Bottom,
  190. iconPadding = PaddingValues(bottom = 16.dp),
  191. titlePadding = PaddingValues(top = 24.dp, start = 20.dp),
  192. avatarSize = 64.dp,
  193. onUserAvatarClick = onUserAvatarClick
  194. )
  195. }
  196. Divider(
  197. modifier = Modifier
  198. .fillMaxWidth()
  199. .height(2.dp)
  200. .padding(
  201. start = 2.dp,
  202. end = 2.dp
  203. ),
  204. color = borderColor,
  205. thickness = 2.dp
  206. )
  207. AppsList(
  208. modifier = Modifier
  209. .padding(bottom = 62.dp)
  210. .fillMaxSize()
  211. .padding(24.dp),
  212. category = category,
  213. listState = listState,
  214. onCategoryClick = { c -> category = c.name },
  215. onAppClick = onAppClick,
  216. onAppContextMenuClick = onAppContextMenuClick,
  217. onCategoryContextMenuClick = onCategoryContextMenuClick,
  218. items = items
  219. )
  220. }
  221. Column(
  222. modifier = Modifier
  223. .fillMaxWidth()
  224. .wrapContentHeight()
  225. .align(Alignment.BottomStart),
  226. ) {
  227. Divider(
  228. modifier = Modifier
  229. .fillMaxWidth()
  230. .height(2.dp)
  231. .padding(
  232. start = 2.dp,
  233. end = 2.dp
  234. ),
  235. color = borderColor,
  236. thickness = 2.dp
  237. )
  238. AppsBottomBar(
  239. modifier = Modifier
  240. .fillMaxWidth()
  241. .wrapContentHeight()
  242. .background(backgroundColor),
  243. backButtonVisible = items.firstOrNull() is App,
  244. searchTextState = searchTextState,
  245. onActionClick = {
  246. menuState.hide()
  247. },
  248. onBackClick = {
  249. category = ""
  250. },
  251. onContextMenuClick = {
  252. // todo : context menu
  253. }
  254. )
  255. }
  256. }
  257. }
  258. launchedEffect(menuState.isVisible) {
  259. if (!menuState.isVisible) {
  260. category = ""
  261. }
  262. }
  263. }
  264. }
  265. @Preview
  266. @Composable
  267. fun AppsMenuPreview() = AppsMenu()