Bläddra i källkod

apps menu #1 chrome window

milan jurkulák 2 år sedan
förälder
incheckning
4d48ed5cb7
30 ändrade filer med 1052 tillägg och 705 borttagningar
  1. 28 33
      src/main/kotlin/eu/mjdev/desktop/Main.kt
  2. 25 6
      src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsBottomBar.kt
  3. 39 54
      src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsMenu.kt
  4. 103 117
      src/main/kotlin/eu/mjdev/desktop/components/controlcenter/ControlCenter.kt
  5. 1 2
      src/main/kotlin/eu/mjdev/desktop/components/controlcenter/pages/BluetoothSettingsPage.kt
  6. 1 2
      src/main/kotlin/eu/mjdev/desktop/components/controlcenter/pages/DisplaySettingsPage.kt
  7. 1 2
      src/main/kotlin/eu/mjdev/desktop/components/controlcenter/pages/SoundSettingsPage.kt
  8. 1 2
      src/main/kotlin/eu/mjdev/desktop/components/controlcenter/pages/WifiSettingsPage.kt
  9. 26 0
      src/main/kotlin/eu/mjdev/desktop/components/custom/MeasurableComponent.kt
  10. 102 126
      src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/DesktopPanel.kt
  11. 2 1
      src/main/kotlin/eu/mjdev/desktop/components/icon/ShapedIcon.kt
  12. 0 57
      src/main/kotlin/eu/mjdev/desktop/components/slidemenu/VisibilityState.kt
  13. 4 8
      src/main/kotlin/eu/mjdev/desktop/components/sliding/SlidingMenu.kt
  14. 71 0
      src/main/kotlin/eu/mjdev/desktop/components/sliding/VisibilityState.kt
  15. 7 8
      src/main/kotlin/eu/mjdev/desktop/extensions/Compose.kt
  16. 21 0
      src/main/kotlin/eu/mjdev/desktop/helpers/DpBounds.kt
  17. 13 20
      src/main/kotlin/eu/mjdev/desktop/helpers/Palette.kt
  18. 0 2
      src/main/kotlin/eu/mjdev/desktop/provider/DesktopProvider.kt
  19. 17 1
      src/main/kotlin/eu/mjdev/desktop/provider/data/Theme.kt
  20. 2 2
      src/main/kotlin/eu/mjdev/desktop/provider/providers/AppsProvider.kt
  21. 137 0
      src/main/kotlin/eu/mjdev/desktop/windows/ChromeWindow.kt
  22. 31 0
      src/main/kotlin/eu/mjdev/desktop/windows/ComponentUpdater.kt
  23. 9 23
      src/main/kotlin/eu/mjdev/desktop/windows/FullScreenWindow.kt
  24. 45 0
      src/main/kotlin/eu/mjdev/desktop/windows/ListenerOnWindowRef.kt
  25. 8 4
      src/main/kotlin/eu/mjdev/desktop/windows/MainWindow.kt
  26. 0 173
      src/main/kotlin/eu/mjdev/desktop/windows/TopWindow.kt
  27. 0 20
      src/main/kotlin/eu/mjdev/desktop/windows/TranslucentPane.kt
  28. 0 42
      src/main/kotlin/eu/mjdev/desktop/windows/TranslucentWindow.kt
  29. 296 0
      src/main/kotlin/eu/mjdev/desktop/windows/WindowEx.kt
  30. 62 0
      src/main/kotlin/eu/mjdev/desktop/windows/WindowLocationTracker.kt

+ 28 - 33
src/main/kotlin/eu/mjdev/desktop/Main.kt

@@ -3,12 +3,12 @@ package eu.mjdev.desktop
 import androidx.compose.material.MaterialTheme
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.unit.dp
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.window.application
 import eu.mjdev.desktop.components.appsmenu.AppsMenu
-import eu.mjdev.desktop.components.controlcenter.ControlCenter
 import eu.mjdev.desktop.components.desktoppanel.DesktopPanel
-import eu.mjdev.desktop.components.slidemenu.VisibilityState.Companion.rememberVisibilityState
+import eu.mjdev.desktop.components.sliding.VisibilityState.Companion.rememberVisibilityState
 import eu.mjdev.desktop.provider.DesktopProvider
 import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
 import eu.mjdev.desktop.windows.MainWindow
@@ -17,40 +17,35 @@ fun main() = application(
     exitProcessOnExit = true
 ) {
     val scope = rememberCoroutineScope()
-    CompositionLocalProvider(
-        LocalDesktop provides DesktopProvider(scope)
-    ) {
-        MaterialTheme {
-//            val api: DesktopProvider = LocalDesktop.current
-            val controlCenterState = rememberVisibilityState()
-            val panelState = rememberVisibilityState()
-            val menuState = rememberVisibilityState()
-            // todo : test
-//            api.askGemini("How are you today?").let {
-//                log { "Gemini answer: $it" }
-//            }
-            // todo : test
+    val controlCenterState = rememberVisibilityState(enabled = false, startState = true)
+    val panelState = rememberVisibilityState(enabled = false, startState = true)
+    val menuState = rememberVisibilityState()
+
+    MaterialTheme {
+        CompositionLocalProvider(
+            LocalDesktop provides DesktopProvider(scope)
+        ) {
             MainWindow(
                 panelState = panelState,
                 controlCenterState = controlCenterState,
                 menuState = menuState
-            )
-            DesktopPanel(
-                panelState = panelState,
-                menuState = menuState,
-                onMenuIconClicked = {
-                    menuState.toggle()
-                }
-            )
-            AppsMenu(
-                // todo
-                bottomY = 64.dp,
-                menuState = menuState,
-                panelState = panelState
-            )
-            ControlCenter(
-                controlCenterState = controlCenterState
-            )
+            ) {
+                DesktopPanel(
+                    modifier = Modifier.align(Alignment.BottomCenter),
+                    panelState = panelState,
+                    onMenuIconClicked = {
+                        menuState.toggle()
+                    }
+                )
+                AppsMenu(
+                    menuState = menuState,
+                    panelState = panelState
+                )
+//                ControlCenter(
+//                    modifier = Modifier.align(Alignment.TopEnd),
+//                    controlCenterState = controlCenterState
+//                )
+            }
         }
     }
 }

+ 25 - 6
src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsBottomBar.kt

@@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
 import androidx.compose.material.icons.automirrored.filled.Logout
 import androidx.compose.material.icons.filled.PowerOff
 import androidx.compose.material.icons.filled.RestartAlt
@@ -17,34 +18,52 @@ import kotlin.system.exitProcess
 
 @Composable
 fun AppsBottomBar(
-    modifier: Modifier = Modifier
+    modifier: Modifier = Modifier,
+    iconBackgroundColor: Color = Color.White,
+    iconColor: Color = Color.Black,
+    backButtonVisible: Boolean = false,
+    onBackClick: () -> Unit
 ) = Box(
     modifier = modifier.padding(8.dp)
 ) {
     Row(
         modifier = Modifier.align(Alignment.BottomStart)
     ) {
-
+        if (backButtonVisible) {
+            ShapedIcon(
+                imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+                iconColor = iconColor,
+                iconBackgroundColor = iconBackgroundColor,
+                onClick = onBackClick
+            )
+        }
+    }
+    Row(
+        modifier = Modifier.align(Alignment.BottomCenter)
+    ) {
+        // todo : center content if needed
     }
-
     Row(
         modifier = Modifier.align(Alignment.BottomEnd)
     ) {
         ShapedIcon(
             imageVector = Icons.Filled.RestartAlt,
-            iconBackgroundColor = Color.White.copy(alpha = 0.7f),
+            iconColor = iconColor,
+            iconBackgroundColor = iconBackgroundColor,
         ) {
             exitProcess(0)
         }
         ShapedIcon(
             imageVector = Icons.AutoMirrored.Filled.Logout,
-            iconBackgroundColor = Color.White.copy(alpha = 0.7f),
+            iconColor = iconColor,
+            iconBackgroundColor = iconBackgroundColor,
         ) {
             exitProcess(0)
         }
         ShapedIcon(
             imageVector = Icons.Filled.PowerOff,
-            iconBackgroundColor = Color.White.copy(alpha = 0.7f),
+            iconColor = iconColor,
+            iconBackgroundColor = iconBackgroundColor,
         ) {
             exitProcess(0)
         }

+ 39 - 54
src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsMenu.kt

@@ -16,82 +16,66 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onPlaced
 import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.WindowPosition
 import eu.mjdev.desktop.components.custom.UserAvatar
-import eu.mjdev.desktop.components.slidemenu.VisibilityState
-import eu.mjdev.desktop.components.slidemenu.VisibilityState.Companion.rememberVisibilityState
+import eu.mjdev.desktop.components.sliding.VisibilityState
+import eu.mjdev.desktop.components.sliding.VisibilityState.Companion.rememberVisibilityState
 import eu.mjdev.desktop.extensions.Compose.launchedEffect
 import eu.mjdev.desktop.provider.DesktopProvider
 import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
 import eu.mjdev.desktop.provider.data.App
 import eu.mjdev.desktop.provider.data.Category
-import eu.mjdev.desktop.windows.TopWindow
-import eu.mjdev.desktop.windows.TopWindowState
-import eu.mjdev.desktop.windows.TopWindowState.Companion.rememberTopWindowState
-import eu.mjdev.desktop.windows.WindowBounds
+import eu.mjdev.desktop.windows.ChromeWindow
 
 @Composable
 fun AppsMenu(
-    modifier: Modifier = Modifier,
     api: DesktopProvider = LocalDesktop.current,
-    appMenuMinWidth: Dp = 480.dp, // todo
-    appMenuMinHeight: Dp = 640.dp, // todo
-    bottomY: Dp = 64.dp, // todo panel height
-    backgroundColor: Color = api.currentUser.theme.backgroundColor,
-    menuPadding: PaddingValues = PaddingValues(2.dp), // todo
-    menuState: VisibilityState = rememberVisibilityState(),
     panelState: VisibilityState = rememberVisibilityState(),
-    items: MutableState<List<Any>> = remember { mutableStateOf(api.appsProvider.appCategories) },
-    enter: EnterTransition = fadeIn() + slideInVertically(initialOffsetY = { it }),
-    exit: ExitTransition = slideOutVertically(targetOffsetY = { it }) + fadeOut(),
-    appMenuMaxHeight: Dp = api.containerSize.height - bottomY,
-    windowState: TopWindowState = rememberTopWindowState(
-        position = api.currentUser.theme.panelLocation.alignment,
-        size = DpSize(appMenuMinWidth, appMenuMinHeight),
-        exit = exit,
-        enter = enter,
-        visible = menuState.isVisible,
-        computeBounds = { isVisible ->
-            val containerHeight = api.containerSize.height
-            WindowBounds(
-                0.dp,
-                if (isVisible) (containerHeight - appMenuMinHeight) else containerHeight,
-                appMenuMinWidth,
-                if (isVisible) appMenuMinHeight else 0.dp
-            )
-        }
+    menuState: VisibilityState = rememberVisibilityState(),
+    appMenuMinWidth: Dp = api.currentUser.theme.appMenuMinWidth,
+    appMenuMinHeight: Dp = api.currentUser.theme.appMenuMinHeight,
+    appMenuBackgroundColor: Color = api.currentUser.theme.backgroundColor,
+    menuPadding: PaddingValues = PaddingValues(api.currentUser.theme.appMenuOuterPadding),// todo
+    enterAnimation: EnterTransition = fadeIn() + slideInVertically(initialOffsetY = { it }),
+    exitAnimation: ExitTransition = slideOutVertically(targetOffsetY = { it }) + fadeOut(),
+    items: MutableState<List<Any>> = remember(api.appsProvider) { mutableStateOf(api.appsProvider.appCategories) },
+) = ChromeWindow(
+    visible = menuState.isVisible,
+    enterAnimation = enterAnimation,
+    exitAnimation = exitAnimation,
+    position = WindowPosition.Absolute(
+        panelState.bounds.x,
+        api.containerSize.height - (panelState.bounds.height + appMenuMinHeight)
     )
-) = TopWindow(
-    windowState = windowState,
-    onFocusChange = { hasFocus ->
-        if (!hasFocus) menuState.hide()
-    }
 ) {
     Box(
-        modifier = modifier
-            .padding(bottom = bottomY)
+        modifier = Modifier
             .width(appMenuMinWidth)
             .heightIn(
                 min = appMenuMinHeight,
-                max = appMenuMaxHeight
+                max = appMenuMinHeight
             )
+            .onPlaced {
+                // menuState.bounds = it.toDPBounds()
+            }
     ) {
         Box(
             modifier = Modifier
                 .fillMaxSize()
                 .padding(menuPadding)
-                .background(backgroundColor.copy(alpha = 0.7f), RoundedCornerShape(24.dp))
+                .background(appMenuBackgroundColor.copy(alpha = 0.7f), RoundedCornerShape(24.dp))
                 .clip(RoundedCornerShape(24.dp))
-                .border(2.dp, backgroundColor, RoundedCornerShape(24.dp))
+                .border(2.dp, appMenuBackgroundColor, RoundedCornerShape(24.dp))
         ) {
             Column(
                 modifier = Modifier.fillMaxSize()
             ) {
                 UserAvatar(
                     avatarSize = 64.dp,
-                    backgroundColor = backgroundColor,
+                    backgroundColor = appMenuBackgroundColor,
                     orientation = Orientation.Horizontal
                 )
                 LazyColumn(
@@ -105,7 +89,7 @@ fun AppsMenu(
                             items(items.value) { item ->
                                 AppsMenuCategory(
                                     category = item as Category,
-                                    backgroundColor = backgroundColor,
+                                    backgroundColor = appMenuBackgroundColor,
                                     iconTint = Color.White
                                 ) { category ->
                                     items.value = api.appsProvider.categoriesAndApps[category.name]
@@ -118,7 +102,7 @@ fun AppsMenu(
                             items(items.value) { item ->
                                 AppsMenuApp(
                                     app = item as App,
-                                    backgroundColor = backgroundColor,
+                                    backgroundColor = appMenuBackgroundColor,
                                     iconTint = Color.White
                                 ) { app ->
                                     app?.start()
@@ -134,16 +118,17 @@ fun AppsMenu(
                     .fillMaxWidth()
                     .wrapContentHeight()
                     .align(Alignment.BottomStart)
-                    .background(backgroundColor)
+                    .background(appMenuBackgroundColor),
+                backButtonVisible = items.value.first() is App,// todo
+                onBackClick = {
+                    items.value = api.appsProvider.appCategories
+                }
             )
         }
-        launchedEffect(panelState.isVisible) { isVisible ->
-            if (!isVisible) menuState.isVisible = false
-        }
-        launchedEffect(menuState.isVisible) { isVisible ->
-            if (!isVisible) {
-                items.value = api.appsProvider.appCategories
-            }
+    }
+    launchedEffect(menuState.isVisible) {
+        if (!menuState.isVisible) {
+            items.value = api.appsProvider.appCategories
         }
     }
 }

+ 103 - 117
src/main/kotlin/eu/mjdev/desktop/components/controlcenter/ControlCenter.kt

@@ -3,35 +3,35 @@ package eu.mjdev.desktop.components.controlcenter
 import androidx.compose.animation.*
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
-import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.layout.*
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.foundation.pager.rememberPagerState
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.Divider
 import androidx.compose.material.Icon
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.WindowPosition
 import eu.mjdev.desktop.components.shadow.LeftShadow
 import eu.mjdev.desktop.components.shadow.RightShadow
-import eu.mjdev.desktop.components.slidemenu.SlidingMenu
-import eu.mjdev.desktop.components.slidemenu.VisibilityState
-import eu.mjdev.desktop.components.slidemenu.VisibilityState.Companion.rememberVisibilityState
-import eu.mjdev.desktop.extensions.Compose.launchedEffect
+import eu.mjdev.desktop.components.sliding.VisibilityState
+import eu.mjdev.desktop.components.sliding.VisibilityState.Companion.rememberVisibilityState
 import eu.mjdev.desktop.provider.DesktopProvider
 import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
-import eu.mjdev.desktop.windows.TopWindow
-import eu.mjdev.desktop.windows.TopWindowState
-import eu.mjdev.desktop.windows.TopWindowState.Companion.rememberTopWindowState
-import eu.mjdev.desktop.windows.WindowBounds
+import eu.mjdev.desktop.windows.ChromeWindow
+import eu.mjdev.desktop.windows.ChromeWindowState
+import eu.mjdev.desktop.windows.ChromeWindowState.Companion.rememberChromeWindowState
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
 
 @Suppress("FunctionName")
 @Composable
@@ -46,125 +46,111 @@ fun ControlCenter(
     controlCenterDividerWidth: Dp = api.currentUser.theme.controlCenterDividerWidth,
     controlCenterIconColor: Color = api.currentUser.theme.controlCenterIconColor,
     controlCenterIconSize: DpSize = api.currentUser.theme.controlCenterIconSize,
-    currentPage: MutableState<Int> = remember { mutableStateOf(0) },
-    pages: List<ControlCenterPage> = remember(api.controlCenterPages) { api.controlCenterPages },
+    pages: List<ControlCenterPage> = api.controlCenterPages,
+    pagerState: PagerState = rememberPagerState(pageCount = { pages.size }, initialPage = 0),
     controlCenterState: VisibilityState = rememberVisibilityState(),
-    enter: EnterTransition = fadeIn() + slideInHorizontally(initialOffsetX = { it }),
-    exit: ExitTransition = slideOutHorizontally(targetOffsetX = { it }) + fadeOut(),
-    windowState: TopWindowState = rememberTopWindowState(
-        position = api.currentUser.theme.controlCenterLocation.alignment,
-        size = api.containerSize.copy(width = controlCenterDividerWidth),
-        enter = enter,
-        exit = exit,
-        minSize = DpSize(controlCenterDividerWidth, api.containerSize.height),
-        computeBounds = { isVisible ->
-            val containerHeight = api.containerSize.height
-            val containerWidth = api.containerSize.width
-            when {
-                isVisible -> WindowBounds(
-                    containerWidth - controlCenterExpandedWidth,
-                    0.dp,
-                    controlCenterExpandedWidth,
-                    containerHeight
-                )
-
-                else -> WindowBounds(
-                    containerWidth - controlCenterDividerWidth,
-                    0.dp,
-                    controlCenterDividerWidth,
-                    containerHeight
-                )
-            }
-        }
+    scope: CoroutineScope = rememberCoroutineScope(),
+    windowState: ChromeWindowState = rememberChromeWindowState(
+        position = WindowPosition.Aligned(Alignment.TopEnd),
+        size = DpSize(api.containerSize.width, Dp.Unspecified)
     ),
-    onVisibilityChange: (visible: Boolean) -> Unit = {},
-) = TopWindow(
+    enterAnimation: EnterTransition = fadeIn() + slideInHorizontally(initialOffsetX = { it }),
+    exitAnimation: ExitTransition = slideOutHorizontally(targetOffsetX = { it }) + fadeOut(),
+) = ChromeWindow(
+    visible = controlCenterState.isVisible,
     windowState = windowState,
-    onFocusChange = { hasFocus ->
-        if (!hasFocus) controlCenterState.hide()
-    }
+    enterAnimation = enterAnimation,
+    exitAnimation = exitAnimation,
+    size = if (controlCenterState.isVisible) DpSize(controlCenterExpandedWidth, api.containerSize.height)
+    else DpSize(controlCenterDividerWidth, api.containerSize.height)
 ) {
-    SlidingMenu(
-        modifier = modifier,
-        orientation = Orientation.Horizontal,
-        state = controlCenterState,
-        onVisibilityChange = onVisibilityChange
-    ) { isVisible ->
-        if (!isVisible) {
+//    SlidingMenu(
+//        modifier = modifier,
+//        orientation = Orientation.Horizontal,
+//        state = controlCenterState,
+//    ) { isVisible ->
+//        if (!isVisible) {
+//            Divider(
+//                modifier = Modifier.fillMaxHeight().width(controlCenterDividerWidth),
+//                color = Color.Transparent,
+//                thickness = controlCenterDividerWidth
+//            )
+//        } else {
+    LeftShadow(
+        modifier = Modifier.fillMaxHeight().wrapContentSize(),
+        color = shadowColor,
+        contentBackgroundColor = backgroundColor.copy(alpha = backgroundAlpha)
+    ) {
+        Row(
+            modifier = Modifier.fillMaxHeight().wrapContentSize(),
+        ) {
             Divider(
                 modifier = Modifier.fillMaxHeight().width(controlCenterDividerWidth),
-                color = Color.Transparent,
+                color = controlCenterDividerColor,
                 thickness = controlCenterDividerWidth
             )
-        }
-        LeftShadow(
-            modifier = Modifier.fillMaxHeight().wrapContentSize(),
-            color = shadowColor,
-            contentBackgroundColor = backgroundColor.copy(alpha = backgroundAlpha)
-        ) {
-            Row(
-                modifier = Modifier.fillMaxHeight().wrapContentSize(),
+            Box(
+                contentAlignment = Alignment.TopEnd
             ) {
-                Divider(
-                    modifier = Modifier.fillMaxHeight().width(controlCenterDividerWidth),
-                    color = controlCenterDividerColor,
-                    thickness = controlCenterDividerWidth
-                )
-                Box(
-                    contentAlignment = Alignment.TopEnd
+                LazyColumn(
+                    modifier = Modifier
+                        .fillMaxHeight()
+                        .background(backgroundColor)
+                        .padding(8.dp),
+                    verticalArrangement = Arrangement.Top,
+                    horizontalAlignment = Alignment.Start
                 ) {
-                    LazyColumn(
-                        modifier = Modifier
-                            .fillMaxHeight()
-                            .background(backgroundColor)
-                            .padding(8.dp),
-                        verticalArrangement = Arrangement.Top,
-                        horizontalAlignment = Alignment.Start
-                    ) {
-                        itemsIndexed(pages) { idx, page ->
-                            val isSelected = (currentPage.value == idx)
-                            Icon(
-                                modifier = Modifier
-                                    .padding(vertical = 8.dp)
-                                    .size(controlCenterIconSize)
-                                    .background(
-                                        color = if (isSelected) Color.White else Color.Transparent,
-                                        shape = RoundedCornerShape(8.dp)
-                                    )
-                                    .clickable {
-                                        currentPage.value = idx
-                                    },
-                                imageVector = page.icon,
-                                contentDescription = "",
-                                tint = if (isSelected) backgroundColor else controlCenterIconColor
-                            )
-                        }
+                    itemsIndexed(pages) { idx, page ->
+                        val isSelected = (pagerState.currentPage == idx)
+                        Icon(
+                            modifier = Modifier
+                                .padding(vertical = 8.dp)
+                                .size(controlCenterIconSize)
+                                .background(
+                                    color = if (isSelected) Color.White else Color.Transparent,
+                                    shape = RoundedCornerShape(8.dp)
+                                )
+                                .clickable {
+                                    scope.launch {
+                                        pagerState.scrollToPage(idx)
+                                    }
+                                },
+                            imageVector = page.icon,
+                            contentDescription = "",
+                            tint = if (isSelected) backgroundColor else controlCenterIconColor
+                        )
                     }
-                    RightShadow(
-                        modifier = Modifier.padding(
-                            end = controlCenterIconSize.width + 8.dp
-                        ).fillMaxHeight().wrapContentSize(),
-                        color = Color.Black,
-                        alpha = 0.3f,
-                        contentBackgroundColor = Color.Transparent
-                    ) {
-                        Row {
-                            pages[currentPage.value].content(backgroundColor)
-                            Divider(
-                                modifier = Modifier.fillMaxHeight().width(controlCenterDividerWidth),
-                                color = controlCenterDividerColor,
-                                thickness = controlCenterDividerWidth
-                            )
+                }
+                RightShadow(
+                    modifier = Modifier.padding(
+                        end = controlCenterIconSize.width + 8.dp
+                    ).fillMaxHeight().wrapContentSize(),
+                    color = Color.Black,
+                    alpha = 0.3f,
+                    contentBackgroundColor = Color.Transparent
+                ) {
+                    Row {
+                        HorizontalPager(
+                            modifier = Modifier.width(controlCenterExpandedWidth),
+                            state = pagerState
+                        ) { page ->
+                            pages[page].content(backgroundColor)
                         }
+                        Divider(
+                            modifier = Modifier.fillMaxHeight().width(controlCenterDividerWidth),
+                            color = controlCenterDividerColor,
+                            thickness = controlCenterDividerWidth
+                        )
                     }
-                    Divider(
-                        modifier = Modifier.fillMaxHeight().width(controlCenterDividerWidth),
-                        color = controlCenterDividerColor,
-                        thickness = controlCenterDividerWidth
-                    )
                 }
+                Divider(
+                    modifier = Modifier.fillMaxHeight().width(controlCenterDividerWidth),
+                    color = controlCenterDividerColor,
+                    thickness = controlCenterDividerWidth
+                )
             }
         }
     }
-    launchedEffect(controlCenterState.isVisible) { isVisible -> windowState.isVisible = isVisible }
-}
+}
+//    }
+//}

+ 1 - 2
src/main/kotlin/eu/mjdev/desktop/components/controlcenter/pages/BluetoothSettingsPage.kt

@@ -1,6 +1,5 @@
 package eu.mjdev.desktop.components.controlcenter.pages
 
-import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.material.icons.Icons
@@ -14,6 +13,6 @@ fun BluetoothSettingsPage() = ControlCenterPage(
     name = "Bluetooth"
 ) { backgroundColor ->
     Box(
-        modifier = Modifier.fillMaxSize().background(backgroundColor)
+        modifier = Modifier.fillMaxSize()
     )
 }

+ 1 - 2
src/main/kotlin/eu/mjdev/desktop/components/controlcenter/pages/DisplaySettingsPage.kt

@@ -1,6 +1,5 @@
 package eu.mjdev.desktop.components.controlcenter.pages
 
-import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.material.icons.Icons
@@ -14,6 +13,6 @@ fun DisplaySettingsPage() = ControlCenterPage(
     name = "Display"
 ) { backgroundColor ->
     Box(
-        modifier = Modifier.fillMaxSize().background(backgroundColor)
+        modifier = Modifier.fillMaxSize()
     )
 }

+ 1 - 2
src/main/kotlin/eu/mjdev/desktop/components/controlcenter/pages/SoundSettingsPage.kt

@@ -1,6 +1,5 @@
 package eu.mjdev.desktop.components.controlcenter.pages
 
-import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.material.icons.Icons
@@ -14,6 +13,6 @@ fun SoundSettingsPage() = ControlCenterPage(
     name = "Sound"
 ) { backgroundColor ->
     Box(
-        modifier = Modifier.fillMaxSize().background(backgroundColor)
+        modifier = Modifier.fillMaxSize()
     )
 }

+ 1 - 2
src/main/kotlin/eu/mjdev/desktop/components/controlcenter/pages/WifiSettingsPage.kt

@@ -1,6 +1,5 @@
 package eu.mjdev.desktop.components.controlcenter.pages
 
-import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.material.icons.Icons
@@ -14,6 +13,6 @@ fun WifiSettingsPage() = ControlCenterPage(
     name = "Wifi"
 ) { backgroundColor ->
     Box(
-        modifier = Modifier.fillMaxSize().background(backgroundColor)
+        modifier = Modifier.fillMaxSize()
     )
 }

+ 26 - 0
src/main/kotlin/eu/mjdev/desktop/components/custom/MeasurableComponent.kt

@@ -0,0 +1,26 @@
+package eu.mjdev.desktop.components.custom
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.layout.SubcomposeLayout
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun MeasurableComponent(
+    key: Any,
+    onMeasure: (size: DpSize) -> Unit = {},
+    content: @Composable () -> Unit = {},
+) = SubcomposeLayout(
+) { constraints ->
+    subcompose(
+        key,
+        content
+    ).map { measurable ->
+        measurable.measure(constraints.copy(minWidth = 0, minHeight = 0))
+    }.first().let { placeable ->
+        onMeasure(DpSize(placeable.width.dp, placeable.height.dp))
+        layout(placeable.width, placeable.height) {
+            placeable.placeRelative(0, 0)
+        }
+    }
+}

+ 102 - 126
src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/DesktopPanel.kt

@@ -1,11 +1,9 @@
 package eu.mjdev.desktop.components.desktoppanel
 
-import androidx.compose.animation.*
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.TooltipArea
 import androidx.compose.foundation.TooltipPlacement
 import androidx.compose.foundation.background
-import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.layout.*
 import androidx.compose.foundation.lazy.LazyRow
 import androidx.compose.foundation.lazy.items
@@ -15,22 +13,17 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.layout.onPlaced
 import androidx.compose.ui.unit.DpOffset
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
 import eu.mjdev.desktop.components.shadow.TopShadow
-import eu.mjdev.desktop.components.slidemenu.SlidingMenu
-import eu.mjdev.desktop.components.slidemenu.VisibilityState
-import eu.mjdev.desktop.components.slidemenu.VisibilityState.Companion.rememberVisibilityState
-import eu.mjdev.desktop.extensions.Compose.height
+import eu.mjdev.desktop.components.sliding.VisibilityState
+import eu.mjdev.desktop.components.sliding.VisibilityState.Companion.rememberVisibilityState
+import eu.mjdev.desktop.helpers.DpBounds.Companion.toDpBounds
 import eu.mjdev.desktop.provider.DesktopProvider
 import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
 import eu.mjdev.desktop.provider.data.App
-import eu.mjdev.desktop.windows.TopWindow
-import eu.mjdev.desktop.windows.TopWindowState
-import eu.mjdev.desktop.windows.TopWindowState.Companion.rememberTopWindowState
-import eu.mjdev.desktop.windows.WindowBounds
 
 @OptIn(ExperimentalFoundationApi::class)
 @Suppress("FunctionName")
@@ -42,118 +35,83 @@ fun DesktopPanel(
     shadowColor: Color = backgroundColor,
     iconColor: Color = Color.Black,
     iconBackgroundColor: Color = Color.White,
-    dividerWidth: Dp = 4.dp,
+//    dividerWidth: Dp = 4.dp,
     iconSize: DpSize = DpSize(56.dp, 56.dp),
     iconPadding: PaddingValues = PaddingValues(4.dp),
     iconOuterPadding: PaddingValues = PaddingValues(2.dp),
     panelAlpha: Float = 0.7f,
-    panelHeight: Dp = iconSize.height + iconPadding.height + iconOuterPadding.height,
     showMenuIcon: Boolean = true,
     panelState: VisibilityState = rememberVisibilityState(),
-    enter: EnterTransition = fadeIn() + slideInVertically(initialOffsetY = { it }),
-    exit: ExitTransition = slideOutVertically(targetOffsetY = { it }) + fadeOut(),
-    menuState: VisibilityState = rememberVisibilityState(),
-    windowState: TopWindowState = rememberTopWindowState(
-        position = api.currentUser.theme.panelLocation.alignment,
-        size = api.containerSize.copy(height = panelHeight),
-        exit = exit,
-        enter = enter,
-        computeBounds = { isVisible ->
-            val containerHeight = api.containerSize.height
-            val containerWidth = api.containerSize.width
-            WindowBounds(
-                0.dp,
-                if (isVisible) (containerHeight - panelHeight) else (containerHeight - dividerWidth),
-                containerWidth,
-                if (isVisible) panelHeight else dividerWidth
-            )
-        }
-    ),
+//    enterAnimation: EnterTransition = fadeIn() + slideInVertically(initialOffsetY = { it }),
+//    exitAnimation: ExitTransition = slideOutVertically(targetOffsetY = { it }) + fadeOut(),
     favoriteApps: State<List<App>> = api.appsProvider.favoriteApps.collectAsState(emptyList()),
     tooltipData: MutableState<Any?> = mutableStateOf(null),
     onToolTip: (item: Any?) -> Unit = { item -> tooltipData.value = item },
-    onVisibilityChange: (visible: Boolean) -> Unit = {},
-    onMenuIconClicked: () -> Unit,
-) = TopWindow(
-    windowState = windowState,
-    onFocusChange = { hasFocus ->
-        if (!hasFocus && !menuState.isVisible) panelState.hide()
-    }
+//    windowState: ChromeWindowState = rememberChromeWindowState(
+//        position = WindowPosition.Aligned(Alignment.BottomCenter),
+//        size = DpSize(api.containerSize.width, Dp.Unspecified)
+//    ),
+    onMenuIconClicked: () -> Unit = {},
+) = Box(
+    modifier = modifier
 ) {
-    SlidingMenu(
-        modifier = modifier,
-        orientation = Orientation.Vertical,
-        state = panelState,
-        onVisibilityChange = onVisibilityChange
-    ) { isVisible ->
-        if (!isVisible) {
-            Divider(
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .height(dividerWidth),
-                color = Color.Transparent,
-                thickness = dividerWidth
-            )
-        }
-        TooltipArea(
-            tooltip = {
-                // todo more types
-                when (tooltipData.value) {
-                    is App -> DesktopPanelAppTooltip(tooltipData.value as App)
-                    else -> {}
-                }
+//    ChromeWindow(
+//    windowState = windowState,
+//    enterAnimation = enterAnimation,
+//    exitAnimation = exitAnimation
+//) {
+//    SlidingMenu(
+//        modifier = modifier,
+//        orientation = Orientation.Vertical,
+//        state = panelState,
+//    ) { isVisible ->
+//        if (!isVisible) {
+//            Divider(
+//                modifier = Modifier
+//                    .fillMaxWidth()
+//                    .height(dividerWidth),
+//                color = Color.Red,
+//                thickness = dividerWidth
+//            )
+//        } else {
+    TooltipArea(
+        modifier = Modifier.fillMaxWidth().wrapContentHeight().padding(top = 32.dp),
+        tooltip = {
+            // todo more types
+            when (tooltipData.value) {
+                is App -> DesktopPanelAppTooltip(tooltipData.value as App)
+                else -> {}
+            }
+        },
+        tooltipPlacement = TooltipPlacement.CursorPoint(
+            alignment = Alignment.TopEnd,
+            offset = DpOffset(16.dp, 32.dp)
+        )
+    ) {
+        TopShadow(
+            modifier = Modifier.fillMaxWidth().wrapContentHeight().onPlaced {
+                panelState.bounds = it.toDpBounds()
             },
-            modifier = Modifier.fillMaxSize(),
-            tooltipPlacement = TooltipPlacement.CursorPoint(
-                alignment = Alignment.TopEnd,
-                offset = DpOffset(16.dp, 32.dp)
-            )
+            color = shadowColor,
+            contentBackgroundColor = backgroundColor.copy(alpha = panelAlpha)
         ) {
-            TopShadow(
-                modifier = Modifier.fillMaxWidth(),
-                color = shadowColor,
-                contentBackgroundColor = backgroundColor.copy(alpha = panelAlpha)
-            ) {
-                LazyRow(
-                    modifier = Modifier.fillMaxSize().background(
-                        brush = Brush.verticalGradient(
-                            colors = listOf(
-                                backgroundColor.copy(alpha = 0.3f),
-                                backgroundColor.copy(alpha = 0.7f),
-                                backgroundColor.copy(alpha = 0.3f),
-                            )
+            LazyRow(
+                modifier = Modifier.fillMaxWidth().wrapContentHeight().background(
+                    brush = Brush.verticalGradient(
+                        colors = listOf(
+                            backgroundColor.copy(alpha = 0.3f),
+                            backgroundColor.copy(alpha = 0.7f),
+                            backgroundColor.copy(alpha = 0.3f),
                         )
-                    ),
-                    horizontalArrangement = Arrangement.Start,
-                    verticalAlignment = Alignment.CenterVertically
-                ) {
-                    if (showMenuIcon) {
-                        item {
-                            DesktopPanelIcon(
-                                icon = "menu",
-                                iconColor = iconColor,
-                                iconBackgroundColor = iconBackgroundColor,
-                                iconBackgroundHover = Color.White.copy(alpha = 0.3f),
-                                iconSize = iconSize,
-                                iconPadding = iconPadding,
-                                iconOuterPadding = iconOuterPadding,
-                                onToolTip = onToolTip,
-                                onClick = {
-                                    onMenuIconClicked()
-                                }
-                            )
-                            Divider(
-                                modifier = Modifier
-                                    .padding(2.dp)
-                                    .height(iconSize.height - 8.dp)
-                                    .width(2.dp)
-                                    .background(Color.White.copy(alpha = 0.4f))
-                            )
-                        }
-                    }
-                    items(favoriteApps.value) { app ->
+                    )
+                ),
+                horizontalArrangement = Arrangement.Start,
+                verticalAlignment = Alignment.CenterVertically
+            ) {
+                if (showMenuIcon) {
+                    item {
                         DesktopPanelIcon(
-                            app = app,
+                            icon = "menu",
                             iconColor = iconColor,
                             iconBackgroundColor = iconBackgroundColor,
                             iconBackgroundHover = Color.White.copy(alpha = 0.3f),
@@ -162,29 +120,47 @@ fun DesktopPanel(
                             iconOuterPadding = iconOuterPadding,
                             onToolTip = onToolTip,
                             onClick = {
-                                app.start()
+                                onMenuIconClicked()
                             }
                         )
-                    }
-                    item {
-                        DesktopPanelText(
-                            text = api.appsProvider.currentLocale.country,
-                            backgroundHover = Color.White.copy(alpha = 0.3f),
-                            onToolTip = onToolTip,
-                            onClick = {
-                                // todo
-                            }
+                        Divider(
+                            modifier = Modifier
+                                .padding(2.dp)
+                                .height(iconSize.height - 8.dp)
+                                .width(2.dp)
+                                .background(Color.White.copy(alpha = 0.4f))
                         )
                     }
                 }
+                items(favoriteApps.value) { app ->
+                    DesktopPanelIcon(
+                        app = app,
+                        iconColor = iconColor,
+                        iconBackgroundColor = iconBackgroundColor,
+                        iconBackgroundHover = Color.White.copy(alpha = 0.3f),
+                        iconSize = iconSize,
+                        iconPadding = iconPadding,
+                        iconOuterPadding = iconOuterPadding,
+                        onToolTip = onToolTip,
+                        onClick = {
+                            app.start()
+                        }
+                    )
+                }
+                item {
+                    DesktopPanelText(
+                        text = api.appsProvider.currentLocale.country,
+                        backgroundHover = Color.White.copy(alpha = 0.3f),
+                        onToolTip = onToolTip,
+                        onClick = {
+                            // todo
+                        }
+                    )
+                }
             }
         }
     }
-    LaunchedEffect(
-        panelState.isVisible,
-        api.currentUser.theme.panelHideDelay
-    ) {
-        panelState.enabled = api.currentUser.theme.panelHideDelay > 0
-        windowState.isVisible = api.currentUser.theme.panelHideDelay < 1 || panelState.isVisible
-    }
 }
+//        }
+//    }
+//}

+ 2 - 1
src/main/kotlin/eu/mjdev/desktop/components/icon/ShapedIcon.kt

@@ -21,6 +21,7 @@ import eu.mjdev.desktop.extensions.Compose.size
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
 fun ShapedIcon(
+    modifier: Modifier = Modifier,
     imageVector: ImageVector = Icons.Filled.Apps,
     iconBackgroundColor: Color = Color.White,
     iconColor: Color = Color.Black,
@@ -32,7 +33,7 @@ fun ShapedIcon(
     outerPadding: PaddingValues = PaddingValues(2.dp),
     onClick: () -> Unit = {}
 ) = Box(
-    modifier = Modifier
+    modifier = modifier
         .padding(outerPadding)
         .size(iconSize + innerPadding.size + outerPadding.size)
         .background(iconBackgroundColor, iconShape)

+ 0 - 57
src/main/kotlin/eu/mjdev/desktop/components/slidemenu/VisibilityState.kt

@@ -1,57 +0,0 @@
-package eu.mjdev.desktop.components.slidemenu
-
-import androidx.compose.runtime.*
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-
-@Suppress("CanBeParameter", "unused")
-class VisibilityState(
-    private val scope: CoroutineScope,
-    private val startState: Boolean = false,
-    var enabled: Boolean = true
-) {
-    var isVisible: Boolean
-        get() = visible.value
-        set(value) {
-            if (enabled) {
-                visible.value = value
-            } else {
-                println("Visible state disabled, can not set value.")
-            }
-        }
-
-    private val visible: MutableState<Boolean> = mutableStateOf(startState)
-
-    fun show() {
-        if (!isVisible) {
-            if (enabled) scope.launch {
-                visible.value = true
-            }
-        }
-    }
-
-    fun hide() {
-        if (isVisible) {
-            if (enabled) scope.launch {
-                visible.value = false
-            }
-        }
-    }
-
-    fun toggle() {
-        visible.value = !visible.value
-    }
-
-    companion object {
-        @Composable
-        fun rememberVisibilityState(
-            startState: Boolean = false,
-            enabled: Boolean = true
-        ): VisibilityState {
-            val scope = rememberCoroutineScope()
-            return remember(startState, enabled) {
-                VisibilityState(scope, startState, enabled)
-            }
-        }
-    }
-}

+ 4 - 8
src/main/kotlin/eu/mjdev/desktop/components/slidemenu/SlidingMenu.kt → src/main/kotlin/eu/mjdev/desktop/components/sliding/SlidingMenu.kt

@@ -1,10 +1,11 @@
-package eu.mjdev.desktop.components.slidemenu
+package eu.mjdev.desktop.components.sliding
 
 import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.Orientation.Horizontal
 import androidx.compose.foundation.gestures.Orientation.Vertical
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.runtime.Composable
@@ -12,8 +13,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.pointer.PointerEventType
 import androidx.compose.ui.input.pointer.onPointerEvent
-import eu.mjdev.desktop.components.slidemenu.VisibilityState.Companion.rememberVisibilityState
-import eu.mjdev.desktop.extensions.Compose.launchedEffect
+import eu.mjdev.desktop.components.sliding.VisibilityState.Companion.rememberVisibilityState
 
 @OptIn(ExperimentalComposeUiApi::class)
 @Suppress("FunctionName")
@@ -29,8 +29,7 @@ fun SlidingMenu(
     onPointerLeave: () -> Unit = {
 //        state.hide()
     },
-    onVisibilityChange: (visible: Boolean) -> Unit = {},
-    content: @Composable (isVisible: Boolean) -> Unit
+    content: @Composable BoxScope.(isVisible: Boolean) -> Unit
 ) {
     when (orientation) {
         Horizontal -> Box(
@@ -51,7 +50,4 @@ fun SlidingMenu(
             content(state.isVisible)
         }
     }
-    launchedEffect(state.isVisible) {
-        onVisibilityChange(state.isVisible)
-    }
 }

+ 71 - 0
src/main/kotlin/eu/mjdev/desktop/components/sliding/VisibilityState.kt

@@ -0,0 +1,71 @@
+package eu.mjdev.desktop.components.sliding
+
+import androidx.compose.runtime.*
+import androidx.compose.ui.unit.DpSize
+import eu.mjdev.desktop.helpers.DpBounds
+import eu.mjdev.desktop.provider.DesktopProvider
+import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+@Suppress("CanBeParameter", "unused")
+class VisibilityState(
+    private val scope: CoroutineScope,
+    private val startState: Boolean = false,
+    var enabled: Boolean = true,
+    val containerSize: DpSize = DpSize.Zero
+) {
+    val visibleState: MutableState<Boolean> = mutableStateOf(startState)
+    val boundsState: MutableState<DpBounds> = mutableStateOf(DpBounds.Zero)
+
+    var bounds: DpBounds
+        get() = boundsState.value
+        set(value) {
+            boundsState.value = value
+            println("Bounds : $value")
+        }
+
+    var isVisible: Boolean
+        get() = visibleState.value
+        set(value) {
+            if (enabled) {
+                visibleState.value = value
+            } else {
+                println("Visible state disabled, can not set value.")
+            }
+        }
+
+    fun show() {
+        if (!isVisible) {
+            if (enabled) scope.launch {
+                visibleState.value = true
+            }
+        }
+    }
+
+    fun hide() {
+        if (isVisible) {
+            if (enabled) scope.launch {
+                visibleState.value = false
+            }
+        }
+    }
+
+    fun toggle() {
+        visibleState.value = !visibleState.value
+    }
+
+    companion object {
+        @Composable
+        fun rememberVisibilityState(
+            startState: Boolean = false,
+            enabled: Boolean = true,
+            api: DesktopProvider = LocalDesktop.current,
+        ): VisibilityState {
+            val scope = rememberCoroutineScope()
+            return remember(startState, enabled, api.containerSize) {
+                VisibilityState(scope, startState, enabled, api.containerSize)
+            }
+        }
+    }
+}

+ 7 - 8
src/main/kotlin/eu/mjdev/desktop/extensions/Compose.kt

@@ -48,6 +48,13 @@ object Compose {
         block: suspend CoroutineScope.(key: T) -> Unit
     ) = LaunchedEffect(key) { block(key) }
 
+    @Composable
+    fun <T, E> launchedEffect(
+        key1: T,
+        key2: E,
+        block: suspend CoroutineScope.(key1: T, key2: E) -> Unit
+    ) = LaunchedEffect(key1, key2) { block(key1, key2) }
+
     @Composable
     fun <T> textFrom(text: T?): String = when (text) {
         null -> ""
@@ -78,12 +85,4 @@ object Compose {
         disabledBackgroundColor = Color.Transparent
     )
 
-    fun java.awt.Window.setWindowBounds(x: Dp, y: Dp, w: Dp, h: Dp) {
-        setBounds(x.value.toInt(), y.value.toInt(), w.value.toInt(), h.value.toInt())
-    }
-
-    // todo
-//    fun java.awt.Window.resizeTo(w: Dp, h: Dp) {
-//    }
-
 }

+ 21 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/DpBounds.kt

@@ -0,0 +1,21 @@
+package eu.mjdev.desktop.helpers
+
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.boundsInParent
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+data class DpBounds(
+    val x: Dp,
+    val y: Dp,
+    val width: Dp,
+    val height: Dp
+) {
+    companion object {
+        val Zero = DpBounds(0.dp, 0.dp, 0.dp, 0.dp)
+
+        fun LayoutCoordinates.toDpBounds(): DpBounds = boundsInParent().let {
+            DpBounds(it.left.dp, it.top.dp, it.width.dp, it.height.dp)
+        }
+    }
+}

+ 13 - 20
src/main/kotlin/eu/mjdev/desktop/helpers/Palette.kt

@@ -2,42 +2,35 @@ package eu.mjdev.desktop.helpers
 
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.graphics.Color
-import com.materialkolor.ktx.themeColors
+import com.materialkolor.ktx.themeColor
 import eu.mjdev.desktop.extensions.Compose.SuperDarkGray
 import eu.mjdev.desktop.extensions.Image.loadPicture
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 
 @Suppress("unused")
 class Palette(
+    private val scope: CoroutineScope,
     private val baseColor: Color = Color.Transparent
 ) {
-    private var colorsFromImage: List<Color> = listOf(baseColor)
+    var backgroundColor: Color = Color.SuperDarkGray
 
-    val backgroundColor: Color
-        get() = runCatching {
-            colorsFromImage.first()
-        }.getOrDefault(baseColor)
-
-    fun update(src: Any?) {
-        CoroutineScope(Dispatchers.IO).launch {
-            loadPicture(src)
-                .getOrNull()
-                ?.themeColors(fallback = baseColor)?.also { colors ->
-                    colorsFromImage = colors.sortedBy {
-                        it.value
-                    }
-                }
-        }
+    fun update(src: Any?) = scope.launch {
+        loadPicture(src)
+            .getOrNull()
+            ?.themeColor(fallback = baseColor)?.also { color ->
+                backgroundColor = color
+            }
     }
 
     companion object {
         @Composable
         fun rememberPalette(
-            backgroundColor: Color = Color.SuperDarkGray
-        ) = remember(backgroundColor) { Palette(backgroundColor) }
+            backgroundColor: Color = Color.SuperDarkGray,
+            scope: CoroutineScope = rememberCoroutineScope()
+        ) = remember(scope, backgroundColor) { Palette(scope, backgroundColor) }
     }
 }
 

+ 0 - 2
src/main/kotlin/eu/mjdev/desktop/provider/DesktopProvider.kt

@@ -1,7 +1,5 @@
 package eu.mjdev.desktop.provider
 
-//import dev.shreyaspatil.ai.client.generativeai.GenerativeModel
-//import dev.shreyaspatil.ai.client.generativeai.type.content
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.compositionLocalOf
 import androidx.compose.runtime.mutableStateOf

+ 17 - 1
src/main/kotlin/eu/mjdev/desktop/provider/data/Theme.kt

@@ -28,6 +28,10 @@ class Theme(
     controlCenterBackgroundAlpha: Float,
 
     iconSet: MaterialIconFont,
+
+    appMenuMinWidth: Dp,
+    appMenuMinHeight: Dp,
+    appMenuOuterPadding: Dp,
 ) {
 
     val backgroundColorState = mutableStateOf(backgroundColor)
@@ -44,6 +48,10 @@ class Theme(
     val backgroundRotationDelayState = mutableStateOf(backgroundRotationDelay)
     val controlCenterBackgroundAlphaState = mutableStateOf(controlCenterBackgroundAlpha)
 
+    val appMenuMinWidthState = mutableStateOf(appMenuMinWidth)
+    val appMenuMinHeightState = mutableStateOf(appMenuMinHeight)
+    val appMenuOuterPaddingState = mutableStateOf(appMenuOuterPadding)
+
     var backgroundColor
         get() = backgroundColorState.value
         set(value) {
@@ -66,6 +74,10 @@ class Theme(
     val iconSet get() = iconSetState.value
     val backgroundRotationDelay get() = backgroundRotationDelayState.value
 
+    val appMenuMinWidth get() = appMenuMinWidthState.value
+    val appMenuMinHeight get() = appMenuMinHeightState.value
+    val appMenuOuterPadding get() = appMenuOuterPaddingState.value
+
     companion object {
         val Default = Theme(
             iconSet = MaterialSymbolsSharp,
@@ -84,7 +96,11 @@ class Theme(
             controlCenterIconSize = DpSize(32.dp, 32.dp),
             controlCenterBackgroundAlpha = 0.8f,
 
-            backgroundRotationDelay = 60000
+            backgroundRotationDelay = 60000,
+
+            appMenuMinWidth = 480.dp,
+            appMenuMinHeight = 640.dp,
+            appMenuOuterPadding = 2.dp
         )
     }
 }

+ 2 - 2
src/main/kotlin/eu/mjdev/desktop/provider/providers/AppsProvider.kt

@@ -92,10 +92,10 @@ class AppsProvider(
         }.getOrNull() ?: emptyList()
 
     private val allAppsDesktopFiles
-        get() = allAppsDesktopFilesLocal +
+        get() = (allAppsDesktopFilesLocal +
                 allAppsDesktopFilesShared +
                 allAppsDesktopFilesFlatPack +
-                allAppsDesktopFilesSnap
+                allAppsDesktopFilesSnap).distinctBy { it.file.name }
 
     val currentLocale: Locale
         get() = userDirsLocale ?: Locale.ENGLISH

+ 137 - 0
src/main/kotlin/eu/mjdev/desktop/windows/ChromeWindow.kt

@@ -0,0 +1,137 @@
+package eu.mjdev.desktop.windows
+
+import androidx.compose.animation.*
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.window.WindowPlacement
+import androidx.compose.ui.window.WindowPosition
+import androidx.compose.ui.window.WindowState
+import eu.mjdev.desktop.extensions.Compose.launchedEffect
+import eu.mjdev.desktop.windows.ChromeWindowState.Companion.rememberAnimState
+import eu.mjdev.desktop.windows.ChromeWindowState.Companion.rememberChromeWindowState
+import eu.mjdev.desktop.windows.ChromeWindowState.Companion.rememberWnState
+import eu.mjdev.desktop.windows.ChromeWindowState.Companion.updateAnimState
+import eu.mjdev.desktop.windows.ChromeWindowState.Companion.updateWindowState
+
+@Composable
+fun ChromeWindow(
+    position: WindowPosition = WindowPosition.Aligned(Alignment.Center),
+    size: DpSize = DpSize(Dp.Unspecified, Dp.Unspecified),
+    visible: Boolean = true,
+    transparent: Boolean = true,
+    resizable: Boolean = false,
+    enabled: Boolean = true,
+    focusable: Boolean = true,
+    alwaysOnTop: Boolean = true,
+    placement: WindowPlacement = WindowPlacement.Floating,
+    windowState: ChromeWindowState = rememberChromeWindowState(position, size, placement),
+    enterAnimation: EnterTransition = fadeIn() + slideInVertically(initialOffsetY = { it }),
+    exitAnimation: ExitTransition = slideOutVertically(targetOffsetY = { it }) + fadeOut(),
+    onCloseRequest: () -> Unit = {},
+    onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
+    onKeyEvent: (KeyEvent) -> Boolean = { false },
+    onUpdate: () -> Unit = {},
+    content: @Composable () -> Unit = {}
+) {
+    val animState = rememberAnimState(visible)
+    val wnState = rememberWnState()
+    WindowEx(
+        onCloseRequest,
+        windowState,
+        wnState.value,
+        "",
+        null,
+        true,
+        transparent,
+        resizable,
+        enabled,
+        focusable,
+        alwaysOnTop,
+        onPreviewKeyEvent,
+        onKeyEvent,
+        {
+            if (visible) {
+                animState.value.targetState = true
+            }
+            onUpdate()
+        },
+        {
+            AnimatedVisibility(
+                animState.value,
+                enter = enterAnimation,
+                exit = exitAnimation
+            ) {
+                content()
+            }
+        }
+    )
+    updateAnimState(visible, animState, wnState)
+    updateWindowState(visible, animState, wnState)
+}
+
+class ChromeWindowState(
+    position: WindowPosition = WindowPosition.Aligned(Alignment.Center),
+    size: DpSize = DpSize(Dp.Unspecified, Dp.Unspecified),
+    placement: WindowPlacement = WindowPlacement.Floating,
+) : WindowState {
+
+    override var position: WindowPosition = position
+    override var size: DpSize = size
+    override var placement: WindowPlacement = placement
+    override var isMinimized: Boolean = false
+
+    companion object {
+
+        @Composable
+        fun rememberChromeWindowState(
+            position: WindowPosition = WindowPosition.Aligned(Alignment.Center),
+            size: DpSize = DpSize(Dp.Unspecified, Dp.Unspecified),
+            placement: WindowPlacement = WindowPlacement.Floating,
+        ) = remember(position, size, placement) {
+            ChromeWindowState(position, size, placement)
+        }
+
+        @Composable
+        fun rememberAnimState(visible: Boolean) = remember(visible) {
+            derivedStateOf {
+                when (visible) {
+                    true -> MutableTransitionState(false)
+                    false -> MutableTransitionState(true)
+                }
+            }
+        }
+
+        @Composable
+        fun rememberWnState() = remember { mutableStateOf(false) }
+
+        @Composable
+        fun updateWindowState(
+            visible: Boolean,
+            animState: State<MutableTransitionState<Boolean>>,
+            wnState: MutableState<Boolean>
+        ) = launchedEffect(animState.value.currentState) { currentState ->
+            if (animState.value.targetState == currentState && !visible) {
+                wnState.value = false
+            }
+        }
+
+        @Composable
+        fun updateAnimState(
+            visible: Boolean,
+            animState: State<MutableTransitionState<Boolean>>,
+            wnState: MutableState<Boolean>
+        ) = launchedEffect(visible) {
+            if (visible) {
+                wnState.value = true
+            } else {
+                animState.value.targetState = false
+            }
+        }
+
+    }
+
+}

+ 31 - 0
src/main/kotlin/eu/mjdev/desktop/windows/ComponentUpdater.kt

@@ -0,0 +1,31 @@
+package eu.mjdev.desktop.windows
+
+class ComponentUpdater {
+    private var updatedValues = mutableListOf<Any?>()
+
+    fun update(body: UpdateScope.() -> Unit) {
+        UpdateScope().body()
+    }
+
+    inner class UpdateScope {
+        private var index = 0
+
+        /**
+         * Compare [value] with the old one and if it is changed - store a new value and call
+         * [update]
+         */
+        fun <T : Any?> set(value: T, update: (T) -> Unit) {
+            if (index < updatedValues.size) {
+                if (updatedValues[index] != value) {
+                    update(value)
+                    updatedValues[index] = value
+                }
+            } else {
+                check(index == updatedValues.size)
+                update(value)
+                updatedValues.add(value)
+            }
+            index++
+        }
+    }
+}

+ 9 - 23
src/main/kotlin/eu/mjdev/desktop/windows/FullScreenWindow.kt

@@ -1,42 +1,28 @@
 package eu.mjdev.desktop.windows
 
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.graphics.painter.Painter
 import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.window.FrameWindowScope
-import androidx.compose.ui.window.Window
 import androidx.compose.ui.window.WindowPlacement
-import androidx.compose.ui.window.rememberWindowState
 import eu.mjdev.desktop.provider.DesktopProvider
 import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
 
 @Composable
 fun FullScreenWindow(
-    visible: Boolean = true,
-    title: String = "",
-    icon: Painter? = null,
-    focusable: Boolean = true,
-    alwaysOnTop: Boolean = false,
-    onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
-    onKeyEvent: (KeyEvent) -> Boolean = { false },
     api: DesktopProvider = LocalDesktop.current,
     onCloseRequest: () -> Unit = {},
-    content: @Composable FrameWindowScope.() -> Unit
-) = Window(
-    state = rememberWindowState(
-        placement = WindowPlacement.Floating,
-        size = api.containerSize
-    ),
+    onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
+    onKeyEvent: (KeyEvent) -> Boolean = { false },
+    content: @Composable () -> Unit = {}
+) = ChromeWindow(
+    size = api.containerSize,
     resizable = false,
-    undecorated = true,
     transparent = true,
-    focusable = focusable,
-    alwaysOnTop = alwaysOnTop,
-    visible = visible,
-    title = title,
-    icon = icon,
+    visible = true,
+    focusable = true,
+    alwaysOnTop = false,
     onPreviewKeyEvent = onPreviewKeyEvent,
     onKeyEvent = onKeyEvent,
     onCloseRequest = onCloseRequest,
+    placement = WindowPlacement.Fullscreen,
     content = content
 )

+ 45 - 0
src/main/kotlin/eu/mjdev/desktop/windows/ListenerOnWindowRef.kt

@@ -0,0 +1,45 @@
+package eu.mjdev.desktop.windows
+
+import java.awt.Component
+import java.awt.Window
+import java.awt.event.ComponentListener
+import java.awt.event.WindowListener
+import java.awt.event.WindowStateListener
+
+class ListenerOnWindowRef<T>(
+    private val register: Window.(T) -> Unit,
+    private val unregister: Window.(T) -> Unit
+) {
+    private var value: T? = null
+
+    fun registerWithAndSet(window: Window, listener: T) {
+        window.register(listener)
+        value = listener
+    }
+
+    fun unregisterFromAndClear(window: Window) {
+        value?.let {
+            window.unregister(it)
+            value = null
+        }
+    }
+
+    companion object {
+
+        fun windowStateListenerRef() = ListenerOnWindowRef<WindowStateListener>(
+            register = java.awt.Window::addWindowStateListener,
+            unregister = java.awt.Window::removeWindowStateListener
+        )
+
+        fun windowListenerRef() = ListenerOnWindowRef<WindowListener>(
+            register = java.awt.Window::addWindowListener,
+            unregister = java.awt.Window::removeWindowListener
+        )
+
+        fun componentListenerRef() = ListenerOnWindowRef<ComponentListener>(
+            register = Component::addComponentListener,
+            unregister = Component::removeComponentListener
+        )
+
+    }
+}

+ 8 - 4
src/main/kotlin/eu/mjdev/desktop/windows/MainWindow.kt

@@ -4,6 +4,7 @@ import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.mouseClickable
 import androidx.compose.runtime.Composable
@@ -14,7 +15,7 @@ import eu.mjdev.desktop.components.background.BackgroundImage
 import eu.mjdev.desktop.components.menu.ContextMenu
 import eu.mjdev.desktop.components.menu.ContextMenuState
 import eu.mjdev.desktop.components.menu.ContextMenuState.Companion.rememberContextMenuState
-import eu.mjdev.desktop.components.slidemenu.VisibilityState
+import eu.mjdev.desktop.components.sliding.VisibilityState
 import eu.mjdev.desktop.helpers.Palette
 import eu.mjdev.desktop.helpers.Palette.Companion.rememberPalette
 import eu.mjdev.desktop.provider.DesktopProvider
@@ -32,7 +33,7 @@ fun MainWindow(
     palette: Palette = rememberPalette(api.currentUser.theme.backgroundColor),
     onDesktopMenuShow: () -> Unit = {},
     contextMenuState: ContextMenuState = rememberContextMenuState("item1", "item2"),
-    content: @Composable () -> Unit = {}
+    content: @Composable BoxScope.() -> Unit = {}
 ) = FullScreenWindow {
     Box(
         modifier = Modifier
@@ -55,8 +56,11 @@ fun MainWindow(
             backgrounds = api.appsProvider.backgrounds + api.currentUser.config.desktopBackgroundUrls,
             switchDelay = api.currentUser.theme.backgroundRotationDelay,
             onChange = { src ->
-                palette.update(src)
-                api.currentUser.theme.backgroundColor = palette.backgroundColor
+                palette.apply {
+                    update(src)
+                }.also { p ->
+                    api.currentUser.theme.backgroundColor = p.backgroundColor
+                }
             }
         )
 //        WidgetsPanel(

+ 0 - 173
src/main/kotlin/eu/mjdev/desktop/windows/TopWindow.kt

@@ -1,173 +0,0 @@
-package eu.mjdev.desktop.windows
-
-import androidx.compose.animation.*
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.graphics.painter.Painter
-import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.DpSize
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.*
-import eu.mjdev.desktop.extensions.Compose.setWindowBounds
-import eu.mjdev.desktop.helpers.WindowFocusState.Companion.windowFocusHandler
-import eu.mjdev.desktop.provider.DesktopProvider
-import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
-import java.awt.Window
-
-@Composable
-fun TopWindow(
-    windowState: TopWindowState,
-    resizable: Boolean = false,
-    title: String = "",
-    icon: Painter? = null,
-    focusable: Boolean = true,
-    onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
-    onKeyEvent: (KeyEvent) -> Boolean = { false },
-    onCloseRequest: () -> Unit = {},
-    onFocusChange: (hasFocus: Boolean) -> Unit = {},
-    content: @Composable FrameWindowScope.() -> Unit
-) = Window(
-    state = windowState,
-    resizable = resizable,
-    undecorated = true,
-    onCloseRequest = onCloseRequest,
-    visible = true,
-    transparent = true,
-    title = title,
-    icon = icon,
-    focusable = focusable,
-    alwaysOnTop = true,
-    onPreviewKeyEvent = onPreviewKeyEvent,
-    onKeyEvent = onKeyEvent,
-    content = {
-        windowFocusHandler { hasFocus -> onFocusChange(hasFocus) }
-        windowState.window = window
-        AnimatedVisibility(
-            visible = windowState.isVisible,
-            enter = windowState.enter,
-            exit = windowState.exit,
-        ) {
-            content()
-        }
-    }
-)
-
-data class WindowBounds(
-    val x: Dp = 0.dp,
-    val y: Dp = 0.dp,
-    val width: Dp = 0.dp,
-    val height: Dp = 0.dp
-)
-
-@Suppress("MemberVisibilityCanBePrivate", "unused")
-class TopWindowState(
-    val containerSize: DpSize,
-    override var placement: WindowPlacement,
-    override var position: WindowPosition,
-    override var size: DpSize = containerSize,
-    override var isMinimized: Boolean = false,
-    val minSize: DpSize = DpSize(0.dp, 0.dp),
-    val visible: Boolean = true,
-    val enter: EnterTransition = fadeIn() + expandIn(),
-    val exit: ExitTransition = shrinkOut() + fadeOut(),
-    val computeBounds: (visible: Boolean) -> WindowBounds = { isVisible ->
-        when {
-            isVisible -> WindowBounds(0.dp, 0.dp, size.width, size.height)
-            else -> WindowBounds(0.dp, 0.dp, minSize.width, minSize.height)
-        }
-    },
-
-    ) : WindowState {
-    var isVisible: Boolean = !isMinimized && visible
-        set(value) {
-            field = value
-            if (window != null) {
-                computeBounds(value).let { bounds ->
-                    window?.setWindowBounds(bounds.x, bounds.y, bounds.width, bounds.height)
-                }
-            }
-        }
-
-    var window: Window? = null
-        set(value) {
-            field = value
-            if (window != null) {
-                computeBounds(isVisible).let { bounds ->
-                    value?.setWindowBounds(bounds.x, bounds.y, bounds.width, bounds.height)
-                }
-            }
-        }
-
-    fun hide() {
-        isVisible = false
-    }
-
-    fun show() {
-        isVisible = true
-    }
-
-    companion object {
-        @Composable
-        fun rememberTopWindowState(
-            size: DpSize = DpSize(640.dp, 480.dp),
-            minSize: DpSize = DpSize(0.dp, 0.dp),
-            position: WindowPosition,
-            visible: Boolean = true,
-            placement: WindowPlacement = WindowPlacement.Floating,
-            api: DesktopProvider = LocalDesktop.current,
-            containerSize: DpSize = api.containerSize,
-            enter: EnterTransition = fadeIn() + expandIn(),
-            exit: ExitTransition = shrinkOut() + fadeOut(),
-            computeBounds: (visible: Boolean) -> WindowBounds = { isVisible ->
-                when {
-                    isVisible -> WindowBounds(0.dp, 0.dp, size.width, size.height)
-                    else -> WindowBounds(0.dp, 0.dp, 0.dp, 0.dp)
-                }
-            }
-        ) = remember(visible) {
-            TopWindowState(
-                containerSize = containerSize,
-                placement = placement,
-                position = position,
-                size = size,
-                minSize = minSize,
-                computeBounds = computeBounds,
-                enter = enter,
-                exit = exit,
-                visible = visible
-            )
-        }
-
-        @Composable
-        fun rememberTopWindowState(
-            size: DpSize = DpSize(640.dp, 480.dp),
-            minSize: DpSize = DpSize(0.dp, 0.dp),
-            position: Alignment = Alignment.BottomCenter,
-            visible: Boolean = true,
-            placement: WindowPlacement = WindowPlacement.Floating,
-            api: DesktopProvider = LocalDesktop.current,
-            containerSize: DpSize = api.containerSize,
-            enter: EnterTransition = fadeIn() + expandIn(),
-            exit: ExitTransition = shrinkOut() + fadeOut(),
-            computeBounds: (visible: Boolean) -> WindowBounds = { isVisible ->
-                when {
-                    isVisible -> WindowBounds(0.dp, 0.dp, size.width, size.height)
-                    else -> WindowBounds(0.dp, 0.dp, 0.dp, 0.dp)
-                }
-            }
-        ) = rememberTopWindowState(
-            size = size,
-            minSize = minSize,
-            position = WindowPosition.Aligned(position),
-            placement = placement,
-            api = api,
-            containerSize = containerSize,
-            computeBounds = computeBounds,
-            enter = enter,
-            exit = exit,
-            visible = visible
-        )
-    }
-}

+ 0 - 20
src/main/kotlin/eu/mjdev/desktop/windows/TranslucentPane.kt

@@ -1,20 +0,0 @@
-package eu.mjdev.desktop.windows
-
-import java.awt.AlphaComposite
-import java.awt.Graphics
-import java.awt.Graphics2D
-import javax.swing.JPanel
-
-class TranslucentPane : JPanel() {
-    init {
-        isOpaque = false
-    }
-
-    override fun paintComponent(g: Graphics) {
-        super.paintComponent(g)
-        val g2d = g.create() as Graphics2D
-        g2d.composite = AlphaComposite.SrcOver.derive(0.85f)
-        g2d.color = background
-        g2d.fillRect(0, 0, width, height)
-    }
-}

+ 0 - 42
src/main/kotlin/eu/mjdev/desktop/windows/TranslucentWindow.kt

@@ -1,42 +0,0 @@
-package eu.mjdev.desktop.windows
-
-import java.awt.Color
-import java.awt.EventQueue
-import java.awt.event.MouseAdapter
-import java.awt.event.MouseEvent
-import java.io.IOException
-import javax.imageio.ImageIO
-import javax.swing.*
-
-class TranslucentWindow {
-    init {
-        EventQueue.invokeLater(object : Runnable {
-            override fun run() {
-                try {
-                    try {
-                        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
-                    } catch (ex: Exception) {
-                        ex.printStackTrace()
-                    }
-                    val frame = JWindow()
-                    frame.isAlwaysOnTop = true
-                    frame.addMouseListener(object : MouseAdapter() {
-                        override fun mouseClicked(e: MouseEvent) {
-                            if (e.clickCount == 2) {
-                                SwingUtilities.getWindowAncestor(e.component).dispose()
-                            }
-                        }
-                    })
-                    frame.background = Color(0, 0, 0, 0)
-                    frame.contentPane = TranslucentPane()
-                    frame.add(JLabel(ImageIcon(ImageIO.read(javaClass.getResource("/Puppy.png")))))
-                    frame.pack()
-                    frame.setLocationRelativeTo(null)
-                    frame.isVisible = true
-                } catch (ex: IOException) {
-                    ex.printStackTrace()
-                }
-            }
-        })
-    }
-}

+ 296 - 0
src/main/kotlin/eu/mjdev/desktop/windows/WindowEx.kt

@@ -0,0 +1,296 @@
+package eu.mjdev.desktop.windows
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.awt.ComposeWindow
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.graphics.toAwtImage
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.unit.*
+import androidx.compose.ui.window.*
+import eu.mjdev.desktop.windows.ListenerOnWindowRef.Companion.componentListenerRef
+import eu.mjdev.desktop.windows.ListenerOnWindowRef.Companion.windowListenerRef
+import eu.mjdev.desktop.windows.ListenerOnWindowRef.Companion.windowStateListenerRef
+import java.awt.*
+import java.awt.event.ComponentAdapter
+import java.awt.event.ComponentEvent
+import java.awt.event.WindowAdapter
+import java.awt.event.WindowEvent
+import java.util.*
+import javax.swing.JFrame
+import kotlin.math.roundToInt
+
+@Composable
+fun WindowEx(
+    onCloseRequest: () -> Unit,
+    state: WindowState = rememberWindowState(),
+    visible: Boolean = true,
+    title: String = "Untitled",
+    icon: Painter? = null,
+    undecorated: Boolean = false,
+    transparent: Boolean = false,
+    resizable: Boolean = true,
+    enabled: Boolean = true,
+    focusable: Boolean = true,
+    alwaysOnTop: Boolean = false,
+    onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
+    onKeyEvent: (KeyEvent) -> Boolean = { false },
+    onUpdate: () -> Unit = {},
+    content: @Composable FrameWindowScope.() -> Unit = {}
+) {
+    val currentState by rememberUpdatedState(state)
+    val currentTitle by rememberUpdatedState(title)
+    val currentIcon by rememberUpdatedState(icon)
+    val currentUndecorated by rememberUpdatedState(undecorated)
+    val currentTransparent by rememberUpdatedState(transparent)
+    val currentResizable by rememberUpdatedState(resizable)
+    val currentEnabled by rememberUpdatedState(enabled)
+    val currentFocusable by rememberUpdatedState(focusable)
+    val currentAlwaysOnTop by rememberUpdatedState(alwaysOnTop)
+    val currentOnCloseRequest by rememberUpdatedState(onCloseRequest)
+
+    val updater = remember(::ComponentUpdater)
+
+    val appliedState = remember {
+        object {
+            var size: DpSize? = null
+            var position: WindowPosition? = null
+            var placement: WindowPlacement? = null
+            var isMinimized: Boolean? = null
+        }
+    }
+
+    val listeners = remember {
+        object {
+            var windowListenerRef = windowListenerRef()
+            var windowStateListenerRef = windowStateListenerRef()
+            var componentListenerRef = componentListenerRef()
+
+            fun removeFromAndClear(window: ComposeWindow) {
+                windowListenerRef.unregisterFromAndClear(window)
+                windowStateListenerRef.unregisterFromAndClear(window)
+                componentListenerRef.unregisterFromAndClear(window)
+            }
+        }
+    }
+
+    Window(
+        visible = visible,
+        onPreviewKeyEvent = onPreviewKeyEvent,
+        onKeyEvent = onKeyEvent,
+        create = {
+            val graphicsConfiguration = WindowLocationTracker.lastActiveGraphicsConfiguration
+            ComposeWindow(graphicsConfiguration = graphicsConfiguration).apply {
+                defaultCloseOperation = JFrame.DO_NOTHING_ON_CLOSE
+                listeners.windowListenerRef.registerWithAndSet(
+                    this,
+                    object : WindowAdapter() {
+                        override fun windowClosing(e: WindowEvent) {
+                            currentOnCloseRequest()
+                        }
+                    }
+                )
+                listeners.windowStateListenerRef.registerWithAndSet(this) {
+                    currentState.placement = placement
+                    currentState.isMinimized = isMinimized
+                    appliedState.placement = currentState.placement
+                    appliedState.isMinimized = currentState.isMinimized
+                }
+                listeners.componentListenerRef.registerWithAndSet(
+                    this,
+                    object : ComponentAdapter() {
+                        override fun componentResized(e: ComponentEvent) {
+                            currentState.placement = placement
+                            currentState.size = DpSize(width.dp, height.dp)
+                            appliedState.placement = currentState.placement
+                            appliedState.size = currentState.size
+                        }
+
+                        override fun componentMoved(e: ComponentEvent) {
+                            currentState.position = WindowPosition(x.dp, y.dp)
+                            appliedState.position = currentState.position
+                        }
+                    }
+                )
+                WindowLocationTracker.onWindowCreated(this)
+            }
+        },
+        dispose = {
+            WindowLocationTracker.onWindowDisposed(it)
+            listeners.removeFromAndClear(it)
+            it.dispose()
+        },
+        update = { window ->
+            updater.update {
+                set(currentTitle, window::setTitle)
+                set(currentIcon, window::setIcon)
+                set(currentUndecorated, window::setUndecoratedSafely)
+                set(currentTransparent, window::isTransparent::set)
+                set(currentResizable, window::setResizable)
+                set(currentEnabled, window::setEnabled)
+                set(currentFocusable, window::setFocusableWindowState)
+                set(currentAlwaysOnTop, window::setAlwaysOnTop)
+            }
+            if (state.size != appliedState.size) {
+                window.setSizeSafely(state.size, state.placement)
+                appliedState.size = state.size
+            }
+            if (state.position != appliedState.position) {
+                window.setPositionSafely(
+                    state.position,
+                    state.placement,
+                    platformDefaultPosition = { WindowLocationTracker.getCascadeLocationFor(window) }
+                )
+                appliedState.position = state.position
+            }
+            if (state.placement != appliedState.placement) {
+                window.placement = state.placement
+                appliedState.placement = state.placement
+            }
+            if (state.isMinimized != appliedState.isMinimized) {
+                window.isMinimized = state.isMinimized
+                appliedState.isMinimized = state.isMinimized
+            }
+            onUpdate()
+        },
+        content = content
+    )
+}
+
+fun Window.setPositionSafely(
+    position: WindowPosition,
+    placement: WindowPlacement,
+    platformDefaultPosition: () -> Point = { WindowLocationTracker.getCascadeLocationFor(this) }
+) {
+    if (!isVisible || (placement == WindowPlacement.Floating)) {
+        setPositionImpl(position, platformDefaultPosition)
+    }
+}
+
+fun Window.align(alignment: Alignment) {
+    val screenInsets = Toolkit.getDefaultToolkit().getScreenInsets(graphicsConfiguration)
+    val screenBounds = graphicsConfiguration.bounds
+    val size = IntSize(size.width, size.height)
+    val screenSize = IntSize(
+        screenBounds.width - screenInsets.left - screenInsets.right,
+        screenBounds.height - screenInsets.top - screenInsets.bottom
+    )
+    val location = alignment.align(size, screenSize, LayoutDirection.Ltr)
+
+    setLocation(
+        screenBounds.x + screenInsets.left + location.x,
+        screenBounds.y + screenInsets.top + location.y
+    )
+}
+
+fun Window.setPositionImpl(
+    position: WindowPosition,
+    platformDefaultPosition: () -> Point
+) = when (position) {
+    WindowPosition.PlatformDefault -> location = platformDefaultPosition()
+    is WindowPosition.Aligned -> align(position.alignment)
+    is WindowPosition.Absolute -> setLocation(
+        position.x.value.roundToInt(),
+        position.y.value.roundToInt()
+    )
+}
+
+fun Frame.setUndecoratedSafely(value: Boolean) {
+    if (this.isUndecorated != value) {
+        this.isUndecorated = value
+    }
+}
+
+val ComponentOrientation.layoutDirection: LayoutDirection
+    get() = when {
+        isLeftToRight -> LayoutDirection.Ltr
+        isHorizontal -> LayoutDirection.Rtl
+        else -> LayoutDirection.Ltr
+    }
+
+val Locale.layoutDirection: LayoutDirection
+    get() = ComponentOrientation.getOrientation(this).layoutDirection
+
+fun layoutDirectionFor(component: Component): LayoutDirection {
+    val orientation = component.componentOrientation
+    return if (orientation != ComponentOrientation.UNKNOWN) {
+        orientation.layoutDirection
+    } else {
+        return component.locale.layoutDirection
+    }
+}
+
+fun Window.setIcon(
+    painter: Painter?,
+    width: Float = 192f,
+    height: Float = 192f
+) {
+    setIconImage(
+        painter?.toAwtImage(
+            graphicsConfiguration.let {
+                Density(
+                    it.defaultTransform.scaleX.toFloat(),
+                    fontScale = 1f
+                )
+            },
+            layoutDirectionFor(this), Size(width, height)
+        )
+    )
+}
+
+fun Window.setSizeImpl(size: DpSize) {
+    val availableSize by lazy {
+        val screenBounds = graphicsConfiguration.bounds
+        val screenInsets = Toolkit.getDefaultToolkit().getScreenInsets(graphicsConfiguration)
+
+        IntSize(
+            width = screenBounds.width - screenInsets.left - screenInsets.right,
+            height = screenBounds.height - screenInsets.top - screenInsets.bottom
+        )
+    }
+    val isWidthSpecified = size.isSpecified && size.width.isSpecified
+    val isHeightSpecified = size.isSpecified && size.height.isSpecified
+    val width = if (isWidthSpecified) {
+        size.width.value.roundToInt().coerceAtLeast(0)
+    } else {
+        availableSize.width
+    }
+    val height = if (isHeightSpecified) {
+        size.height.value.roundToInt().coerceAtLeast(0)
+    } else {
+        availableSize.height
+    }
+    var computedPreferredSize: Dimension? = null
+    if (!isWidthSpecified || !isHeightSpecified) {
+        preferredSize = Dimension(width, height)
+        pack()  // Makes it displayable
+
+        // We set preferred size to null, and then call getPreferredSize, which will compute the
+        // actual preferred size determined by the content (see the description of setPreferredSize)
+        preferredSize = null
+        computedPreferredSize = preferredSize
+    }
+    if (!isDisplayable) {
+        // Pack to allow drawing the first frame
+        preferredSize = Dimension(width, height)
+        pack()
+    }
+    setSize(
+        if (isWidthSpecified) width else computedPreferredSize!!.width,
+        if (isHeightSpecified) height else computedPreferredSize!!.height,
+    )
+    revalidate()  // Calls doLayout on the ComposeLayer, causing it to update its size
+}
+
+fun Window.setSizeSafely(size: DpSize, placement: WindowPlacement) {
+    if (!isVisible || (placement == WindowPlacement.Floating)) {
+        setSizeImpl(size)
+    }
+}
+
+
+

+ 62 - 0
src/main/kotlin/eu/mjdev/desktop/windows/WindowLocationTracker.kt

@@ -0,0 +1,62 @@
+package eu.mjdev.desktop.windows
+
+import java.awt.*
+import java.awt.event.WindowEvent
+import java.awt.event.WindowFocusListener
+
+object WindowLocationTracker {
+    private val cascadeOffset = Point(48, 48)
+    private var lastFocusedWindows = mutableSetOf<Window>()
+
+    private val focusListener = object : WindowFocusListener {
+        override fun windowGainedFocus(e: WindowEvent) {
+            with(lastFocusedWindows) {
+                remove(e.window)
+                add(e.window)
+            }
+        }
+
+        override fun windowLostFocus(e: WindowEvent) = Unit
+    }
+
+    val lastActiveGraphicsConfiguration: GraphicsConfiguration?
+        get() = lastFocusedWindows.lastOrNull()?.graphicsConfiguration
+
+    fun onWindowCreated(window: Window) {
+        window.addWindowFocusListener(focusListener)
+    }
+
+    fun onWindowDisposed(window: Window) {
+        window.removeWindowFocusListener(focusListener)
+        lastFocusedWindows.remove(window)
+    }
+
+    fun getCascadeLocationFor(window: Window): Point {
+        val lastWindow = lastFocusedWindows.lastOrNull()
+        val graphicsConfiguration = lastWindow?.graphicsConfiguration
+            ?: GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice?.defaultConfiguration
+        return if (graphicsConfiguration != null) {
+            val screenBounds = graphicsConfiguration.bounds
+            val screenInsets = Toolkit.getDefaultToolkit().getScreenInsets(graphicsConfiguration)
+            val screenLeftTop = screenBounds.leftTop + Point(screenInsets.left, screenInsets.top)
+            val screenRightBottom = screenBounds.rightBottom - Point(screenInsets.right, screenInsets.bottom)
+            val lastLocation = lastWindow?.location ?: screenLeftTop
+            var location = lastLocation + cascadeOffset
+            val rightBottom = location + window.size.rightBottom
+            if (rightBottom.x > screenRightBottom.x || rightBottom.y > screenRightBottom.y) {
+                location = screenLeftTop + cascadeOffset
+            }
+            location
+        } else {
+            cascadeOffset
+        }
+    }
+
+    val Dimension.rightBottom get() = Point(width, height)
+
+    val Rectangle.leftTop get() = Point(x, y)
+    val Rectangle.rightBottom get() = Point(x + width, y + height)
+
+    operator fun Point.plus(other: Point) = Point(x + other.x, y + other.y)
+    operator fun Point.minus(other: Point) = Point(x - other.x, y - other.y)
+}