AppsMenu.kt 11 KB

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