AppsMenu.kt 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  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.gestures.Orientation
  8. import androidx.compose.foundation.layout.*
  9. import androidx.compose.foundation.lazy.LazyColumn
  10. import androidx.compose.foundation.lazy.items
  11. import androidx.compose.foundation.shape.RoundedCornerShape
  12. import androidx.compose.material.Divider
  13. import androidx.compose.runtime.*
  14. import androidx.compose.ui.Alignment
  15. import androidx.compose.ui.Modifier
  16. import androidx.compose.ui.draw.clip
  17. import androidx.compose.ui.layout.onPlaced
  18. import androidx.compose.ui.unit.dp
  19. import androidx.compose.ui.window.WindowPosition
  20. import eu.mjdev.desktop.components.custom.UserAvatar
  21. import eu.mjdev.desktop.components.sliding.base.VisibilityState
  22. import eu.mjdev.desktop.components.sliding.base.VisibilityState.Companion.rememberVisibilityState
  23. import eu.mjdev.desktop.data.App
  24. import eu.mjdev.desktop.data.Category
  25. import eu.mjdev.desktop.extensions.ColorUtils.alpha
  26. import eu.mjdev.desktop.extensions.Compose.clear
  27. import eu.mjdev.desktop.extensions.Compose.launchedEffect
  28. import eu.mjdev.desktop.extensions.Compose.onMouseEnter
  29. import eu.mjdev.desktop.extensions.Compose.plus
  30. import eu.mjdev.desktop.extensions.Compose.rememberCalculated
  31. import eu.mjdev.desktop.extensions.Compose.rememberComputed
  32. import eu.mjdev.desktop.extensions.Compose.rememberState
  33. import eu.mjdev.desktop.extensions.Compose.removeLast
  34. import eu.mjdev.desktop.extensions.Modifier.dropShadow
  35. import eu.mjdev.desktop.helpers.animation.Animations.AppsMenuEnterAnimation
  36. import eu.mjdev.desktop.helpers.animation.Animations.AppsMenuExitAnimation
  37. import eu.mjdev.desktop.helpers.internal.KeyEventHandler.Companion.globalKeyEventHandler
  38. import eu.mjdev.desktop.provider.DesktopScope
  39. import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
  40. import eu.mjdev.desktop.windows.ChromeWindow
  41. import eu.mjdev.desktop.windows.ChromeWindowState
  42. import eu.mjdev.desktop.windows.ChromeWindowState.Companion.rememberChromeWindowState
  43. @Suppress("FunctionName")
  44. @Composable
  45. fun AppsMenu(
  46. panelState: VisibilityState = rememberVisibilityState(),
  47. menuState: VisibilityState = rememberVisibilityState(),
  48. enterAnimation: EnterTransition = AppsMenuEnterAnimation,
  49. exitAnimation: ExitTransition = AppsMenuExitAnimation,
  50. searchTextState: MutableState<String> = rememberState(""),
  51. onFocusChange: ChromeWindowState.(Boolean) -> Unit = {},
  52. onAppClick: DesktopScope.(App) -> Unit = { app ->
  53. startApp(app)
  54. menuState.hide()
  55. },
  56. onAppContextMenuClick: DesktopScope.(App) -> Unit = {},
  57. onCategoryContextMenuClick: (Category) -> Unit = {},
  58. onUserAvatarClick: () -> Unit = {}
  59. ) = withDesktopScope {
  60. var category by rememberState("")
  61. val items by rememberComputed(searchTextState.value, category) {
  62. when {
  63. searchTextState.value.isNotEmpty() -> appsProvider.allApps
  64. .filter { app ->
  65. app.desktopFile?.fileData?.contains(searchTextState.value) == true
  66. }
  67. category.isNotEmpty() -> appCategoriesAndApps[category]?.sortedBy { app ->
  68. app.name
  69. } ?: emptyList<App>()
  70. else -> appCategories
  71. }
  72. }
  73. val onCategoryClick: (Category) -> Unit = remember {
  74. { c ->
  75. category = c.name
  76. }
  77. }
  78. val position by rememberCalculated {
  79. WindowPosition.Absolute(
  80. panelState.bounds.x,
  81. containerSize.height - (panelState.bounds.height + appMenuMinHeight)
  82. )
  83. }
  84. val windowState: ChromeWindowState = rememberChromeWindowState(position = position)
  85. globalKeyEventHandler(menuState.isVisible && menuState.enabled) {
  86. onEscape {
  87. menuState.hide()
  88. true
  89. }
  90. onBack {
  91. searchTextState.clear()
  92. true
  93. }
  94. onBackSpace {
  95. searchTextState.removeLast()
  96. true
  97. }
  98. onDelete {
  99. searchTextState.clear()
  100. true
  101. }
  102. onChar { char ->
  103. searchTextState + char
  104. true
  105. }
  106. }
  107. ChromeWindow(
  108. visible = menuState.isVisible,
  109. enterAnimation = enterAnimation,
  110. exitAnimation = exitAnimation,
  111. windowState = windowState,
  112. onFocusChange = onFocusChange
  113. ) {
  114. Box(
  115. modifier = Modifier
  116. .width(appMenuMinWidth)
  117. .heightIn(
  118. min = appMenuMinHeight,
  119. max = appMenuMinHeight
  120. )
  121. .dropShadow(
  122. RoundedCornerShape(24.dp),
  123. borderColor.alpha(0.5f),
  124. 0.dp,
  125. 0.dp,
  126. 4.dp,
  127. 0.dp
  128. )
  129. .onPlaced(menuState::onPlaced)
  130. .onMouseEnter {
  131. windowState.requestFocus()
  132. }
  133. ) {
  134. Box(
  135. modifier = Modifier
  136. .fillMaxSize()
  137. .padding(menuPadding)
  138. .background(backgroundColor.copy(alpha = 0.7f), RoundedCornerShape(24.dp))
  139. .border(2.dp, borderColor, RoundedCornerShape(24.dp))
  140. .clip(RoundedCornerShape(24.dp))
  141. ) {
  142. Column(
  143. modifier = Modifier.fillMaxSize()
  144. ) {
  145. UserAvatar(
  146. modifier = Modifier.background(backgroundColor),
  147. avatarSize = 64.dp,
  148. orientation = Orientation.Horizontal,
  149. onUserAvatarClick = onUserAvatarClick
  150. )
  151. Divider(
  152. modifier = Modifier
  153. .fillMaxWidth()
  154. .height(2.dp)
  155. .padding(
  156. start = 2.dp,
  157. end = 2.dp
  158. ),
  159. color = borderColor,
  160. thickness = 2.dp
  161. )
  162. LazyColumn(
  163. modifier = Modifier
  164. .padding(bottom = 62.dp)
  165. .fillMaxSize()
  166. .padding(24.dp)
  167. ) {
  168. when (items.firstOrNull()) {
  169. is Category -> {
  170. items(items) { item ->
  171. AppsMenuCategory(
  172. category = item as Category,
  173. onClick = { onCategoryClick(item) },
  174. onContextMenuClick = { onCategoryContextMenuClick(item) }
  175. )
  176. }
  177. }
  178. is App -> {
  179. items(items) { item ->
  180. AppsMenuApp(
  181. app = item as App,
  182. backgroundColor = backgroundColor,
  183. iconTint = textColor,
  184. textColor = textColor,
  185. onClick = {
  186. onAppClick(item)
  187. },
  188. onContextMenuClick = {
  189. onAppContextMenuClick(item)
  190. }
  191. )
  192. }
  193. }
  194. }
  195. }
  196. }
  197. Column(
  198. modifier = Modifier
  199. .fillMaxWidth()
  200. .wrapContentHeight()
  201. .align(Alignment.BottomStart),
  202. ) {
  203. Divider(
  204. modifier = Modifier
  205. .fillMaxWidth()
  206. .height(2.dp)
  207. .padding(
  208. start = 2.dp,
  209. end = 2.dp
  210. ),
  211. color = borderColor,
  212. thickness = 2.dp
  213. )
  214. AppsBottomBar(
  215. modifier = Modifier
  216. .fillMaxWidth()
  217. .wrapContentHeight()
  218. .background(backgroundColor),
  219. backButtonVisible = items.firstOrNull() is App,
  220. searchTextState = searchTextState,
  221. onHideMenu = {
  222. menuState.hide()
  223. },
  224. onBackClick = {
  225. category = ""
  226. },
  227. onContextMenuClick = {
  228. // todo : context menu
  229. }
  230. )
  231. }
  232. }
  233. }
  234. launchedEffect(menuState.isVisible) {
  235. if (!menuState.isVisible) {
  236. category = ""
  237. }
  238. }
  239. }
  240. }
  241. @Preview
  242. @Composable
  243. fun AppsMenuPreview() = AppsMenu()