Explorar o código

window improvements

milan jurkulák %!s(int64=2) %!d(string=hai) anos
pai
achega
83d523cb7c

+ 13 - 0
build.gradle.kts

@@ -24,6 +24,8 @@ allprojects {
         mavenCentral()
     }
     dependencies {
+//        implementation(libs.ktor.java)
+        runtimeOnly(libs.kotlinx.coroutines.swing)
         // reflection
         implementation(kotlin("reflect"))
         // compose desktop
@@ -42,6 +44,16 @@ allprojects {
         implementation(libs.ini4j)
         // http client
         implementation(libs.okhttp.client)
+        // ktor wasm
+        implementation(libs.ktor.client.core)
+        // coil
+        implementation(libs.coil.compose.core)
+        implementation(libs.coil.compose)
+        implementation(libs.coil.mp)
+//        implementation(libs.coil.gif)
+        implementation(libs.coil.network.ktor)
+        // glide
+//        implementation(libs.glide)
         // palette
         implementation(libs.material.kolor)
         // json
@@ -55,6 +67,7 @@ allprojects {
         // implementation(libs.google.generativeai)
         // no log
         implementation("org.slf4j:slf4j-nop:2.0.7")
+        // fuzzy search
         implementation("me.xdrop:fuzzywuzzy:1.4.0")
 
 //        implementation("com.github.SmartToolFactory:Compose-Extended-Colors:1.0.0-alpha06")

+ 11 - 0
gradle/libs.versions.toml

@@ -15,6 +15,9 @@ kotlin = "1.9.22"
 compose = "1.7.0-alpha03"
 okhttp3 = "5.0.0-alpha.12"
 materialKolor = "1.7.0"
+coil3 = "3.0.0-alpha06"
+ktor = "3.0.0-wasm2"
+kotlinxCoroutinesSwing = "1.8.0"
 #generativeai = "0.5.0-1.0.0-wasm"
 #compose-webview = "1.9.20"
 #decompose = "3.0.0-alpha07"
@@ -25,6 +28,12 @@ gradle-api = { module = "com.android.tools.build:gradle-api", version.ref = "gra
 #gradle-kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
 #dbus-java-core = { module = "com.github.hypfvieh:dbus-java-core", version = "5.0.0" }
 #dbus-java-transport-native-unixsocket = { module = "com.github.hypfvieh:dbus-java-transport-native-unixsocket", version = "5.0.0" }
+ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
+coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil3" }
+coil-compose-core = { module = "io.coil-kt.coil3:coil-compose-core", version.ref = "coil3" }
+coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor", version.ref = "coil3" }
+coil-mp = { module = "io.coil-kt.coil3:coil", version.ref = "coil3" }
+#coil-gif = { module = "io.coil-kt.coil3:coil-gif", version = "2.2.7" }
 mozilla-rhino = { module = "org.mozilla:rhino", version = "1.7.13" }
 okhttp-client = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp3" }
 material-kolor = { module = "com.materialkolor:material-kolor", version.ref = "materialKolor" }
@@ -34,7 +43,9 @@ font-icons-awesome = { module = "br.com.devsrsouza.compose.icons:font-awesome-de
 #decompose = { module = "com.arkivanov.decompose:decompose", version.ref = "decompose" }
 #decompose-extensions-compose = { module = "com.arkivanov.decompose:extensions-compose", version.ref = "decompose" }
 ini4j = { module = "org.ini4j:ini4j", version = "0.5.4" }
+kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinxCoroutinesSwing" }
 #google-generativeai = { module = "dev.shreyaspatil.generativeai:generativeai-google-jvm", version.ref = "generativeai" }
+glide = { module = "com.github.bumptech.glide:glide", version = "4.16.0" }
 
 #// open ai
 #//        implementation("com.aallam.openai:openai-client:3.8.2")

+ 1 - 2
gradle/wrapper/gradle-wrapper.properties

@@ -1,6 +1,5 @@
-#Thu Sep 12 09:54:13 CEST 2024
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists

+ 6 - 9
src/main/kotlin/eu/mjdev/desktop/Main.kt

@@ -3,10 +3,9 @@ package eu.mjdev.desktop
 import androidx.compose.material.MaterialTheme
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.rememberCoroutineScope
-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.sliding.VisibilityState.Companion.rememberVisibilityState
 import eu.mjdev.desktop.provider.DesktopProvider
@@ -17,8 +16,8 @@ fun main() = application(
     exitProcessOnExit = true
 ) {
     val scope = rememberCoroutineScope()
-    val controlCenterState = rememberVisibilityState(enabled = false, startState = true)
-    val panelState = rememberVisibilityState(enabled = false, startState = true)
+    val controlCenterState = rememberVisibilityState()
+    val panelState = rememberVisibilityState()
     val menuState = rememberVisibilityState()
 
     MaterialTheme {
@@ -31,7 +30,6 @@ fun main() = application(
                 menuState = menuState
             ) {
                 DesktopPanel(
-                    modifier = Modifier.align(Alignment.BottomCenter),
                     panelState = panelState,
                     onMenuIconClicked = {
                         menuState.toggle()
@@ -41,10 +39,9 @@ fun main() = application(
                     menuState = menuState,
                     panelState = panelState
                 )
-//                ControlCenter(
-//                    modifier = Modifier.align(Alignment.TopEnd),
-//                    controlCenterState = controlCenterState
-//                )
+                ControlCenter(
+                    controlCenterState = controlCenterState
+                )
             }
         }
     }

+ 80 - 87
src/main/kotlin/eu/mjdev/desktop/components/controlcenter/ControlCenter.kt

@@ -3,17 +3,14 @@ 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.Horizontal
 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.rememberCoroutineScope
+import androidx.compose.runtime.*
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -23,20 +20,18 @@ 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.sliding.SlidingMenu
 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.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
 fun ControlCenter(
-    modifier: Modifier = Modifier,
     api: DesktopProvider = LocalDesktop.current,
     backgroundColor: Color = api.currentUser.theme.backgroundColor,
     backgroundAlpha: Float = api.currentUser.theme.controlCenterBackgroundAlpha,
@@ -47,94 +42,99 @@ fun ControlCenter(
     controlCenterIconColor: Color = api.currentUser.theme.controlCenterIconColor,
     controlCenterIconSize: DpSize = api.currentUser.theme.controlCenterIconSize,
     pages: List<ControlCenterPage> = api.controlCenterPages,
-    pagerState: PagerState = rememberPagerState(pageCount = { pages.size }, initialPage = 0),
+    pagerState: MutableState<Int> = remember { mutableStateOf(0) },
     controlCenterState: VisibilityState = rememberVisibilityState(),
     scope: CoroutineScope = rememberCoroutineScope(),
-    windowState: ChromeWindowState = rememberChromeWindowState(
-        position = WindowPosition.Aligned(Alignment.TopEnd),
-        size = DpSize(api.containerSize.width, Dp.Unspecified)
-    ),
     enterAnimation: EnterTransition = fadeIn() + slideInHorizontally(initialOffsetX = { it }),
     exitAnimation: ExitTransition = slideOutHorizontally(targetOffsetX = { it }) + fadeOut(),
 ) = ChromeWindow(
-    visible = controlCenterState.isVisible,
-    windowState = windowState,
+    visible = true,
     enterAnimation = enterAnimation,
     exitAnimation = exitAnimation,
-    size = if (controlCenterState.isVisible) DpSize(controlCenterExpandedWidth, api.containerSize.height)
-    else DpSize(controlCenterDividerWidth, api.containerSize.height)
+    position = WindowPosition.Aligned(Alignment.TopEnd),
+    size = if (controlCenterState.isVisible) {
+        DpSize(controlCenterExpandedWidth, api.containerSize.height)
+    } else {
+        DpSize(controlCenterDividerWidth, api.containerSize.height)
+    }
 ) {
-//    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(),
-        ) {
+    SlidingMenu(
+        modifier = Modifier.fillMaxHeight(),
+        orientation = Horizontal,
+        state = controlCenterState,
+    ) { isVisible ->
+        if (!isVisible) {
             Divider(
                 modifier = Modifier.fillMaxHeight().width(controlCenterDividerWidth),
-                color = controlCenterDividerColor,
+                color = Color.Transparent,
                 thickness = controlCenterDividerWidth
             )
-            Box(
-                contentAlignment = Alignment.TopEnd
+        } else {
+            LeftShadow(
+                modifier = Modifier.fillMaxHeight().wrapContentSize(),
+                color = shadowColor,
+                contentBackgroundColor = backgroundColor.copy(alpha = backgroundAlpha)
             ) {
-                LazyColumn(
-                    modifier = Modifier
-                        .fillMaxHeight()
-                        .background(backgroundColor)
-                        .padding(8.dp),
-                    verticalArrangement = Arrangement.Top,
-                    horizontalAlignment = Alignment.Start
+                Row(
+                    modifier = Modifier.fillMaxHeight().wrapContentSize(),
                 ) {
-                    itemsIndexed(pages) { idx, page ->
-                        val isSelected = (pagerState.currentPage == idx)
-                        Icon(
+                    Divider(
+                        modifier = Modifier.fillMaxHeight().width(controlCenterDividerWidth),
+                        color = controlCenterDividerColor,
+                        thickness = controlCenterDividerWidth
+                    )
+                    Box(
+                        contentAlignment = Alignment.TopEnd
+                    ) {
+                        LazyColumn(
                             modifier = Modifier
-                                .padding(vertical = 8.dp)
-                                .size(controlCenterIconSize)
-                                .background(
-                                    color = if (isSelected) Color.White else Color.Transparent,
-                                    shape = RoundedCornerShape(8.dp)
+                                .fillMaxHeight()
+                                .background(backgroundColor)
+                                .padding(8.dp),
+                            verticalArrangement = Arrangement.Top,
+                            horizontalAlignment = Alignment.Start
+                        ) {
+                            itemsIndexed(pages) { idx, page ->
+                                val isSelected = (pagerState.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 {
+                                            scope.launch {
+                                                pagerState.value = idx
+                                            }
+                                        },
+                                    imageVector = page.icon,
+                                    contentDescription = "",
+                                    tint = if (isSelected) backgroundColor else controlCenterIconColor
                                 )
-                                .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 {
-                        HorizontalPager(
-                            modifier = Modifier.width(controlCenterExpandedWidth),
-                            state = pagerState
-                        ) { page ->
-                            pages[page].content(backgroundColor)
+                            }
+                        }
+                        RightShadow(
+                            modifier = Modifier.padding(
+                                end = controlCenterIconSize.width + 18.dp
+                            ).fillMaxHeight().wrapContentSize(),
+                            color = Color.Red,
+                            alpha = 0.3f,
+                            contentBackgroundColor = Color.Transparent
+                        ) {
+                            Row {
+                                Box(
+                                    modifier = Modifier.width(controlCenterExpandedWidth),
+                                ) {
+                                    pages[pagerState.value].content(backgroundColor)
+                                }
+                                Divider(
+                                    modifier = Modifier.fillMaxHeight().width(controlCenterDividerWidth),
+                                    color = controlCenterDividerColor,
+                                    thickness = controlCenterDividerWidth
+                                )
+                            }
                         }
                         Divider(
                             modifier = Modifier.fillMaxHeight().width(controlCenterDividerWidth),
@@ -143,14 +143,7 @@ fun ControlCenter(
                         )
                     }
                 }
-                Divider(
-                    modifier = Modifier.fillMaxHeight().width(controlCenterDividerWidth),
-                    color = controlCenterDividerColor,
-                    thickness = controlCenterDividerWidth
-                )
             }
         }
     }
 }
-//    }
-//}

+ 116 - 110
src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/DesktopPanel.kt

@@ -1,9 +1,11 @@
 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.Vertical
 import androidx.compose.foundation.layout.*
 import androidx.compose.foundation.lazy.LazyRow
 import androidx.compose.foundation.lazy.items
@@ -14,153 +16,157 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.DpOffset
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.WindowPosition
 import eu.mjdev.desktop.components.shadow.TopShadow
+import eu.mjdev.desktop.components.sliding.SlidingMenu
 import eu.mjdev.desktop.components.sliding.VisibilityState
 import eu.mjdev.desktop.components.sliding.VisibilityState.Companion.rememberVisibilityState
+import eu.mjdev.desktop.extensions.Compose.height
 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.ChromeWindow
 
 @OptIn(ExperimentalFoundationApi::class)
 @Suppress("FunctionName")
 @Composable
 fun DesktopPanel(
-    modifier: Modifier = Modifier,
     api: DesktopProvider = LocalDesktop.current,
     backgroundColor: Color = api.currentUser.theme.backgroundColor,
     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,
     showMenuIcon: Boolean = true,
     panelState: VisibilityState = rememberVisibilityState(),
-//    enterAnimation: EnterTransition = fadeIn() + slideInVertically(initialOffsetY = { it }),
-//    exitAnimation: ExitTransition = slideOutVertically(targetOffsetY = { it }) + fadeOut(),
+    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 },
-//    windowState: ChromeWindowState = rememberChromeWindowState(
-//        position = WindowPosition.Aligned(Alignment.BottomCenter),
-//        size = DpSize(api.containerSize.width, Dp.Unspecified)
-//    ),
+    panelHeight: (visible: Boolean) -> Dp = { visible ->
+        if (visible) iconSize.height + iconOuterPadding.height else dividerWidth
+    },
     onMenuIconClicked: () -> Unit = {},
-) = Box(
-    modifier = modifier
+) = ChromeWindow(
+    visible = true,
+    enterAnimation = enterAnimation,
+    exitAnimation = exitAnimation,
+    position = WindowPosition.Aligned(Alignment.BottomCenter),
+    size = DpSize(
+        api.containerSize.width,
+        panelHeight(panelState.isVisible)
+    )
 ) {
-//    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()
-            },
-            color = shadowColor,
-            contentBackgroundColor = backgroundColor.copy(alpha = panelAlpha)
-        ) {
-            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
+    SlidingMenu(
+        modifier = Modifier.fillMaxWidth(),
+        orientation = Vertical,
+        state = panelState,
+    ) { isVisible ->
+        if (!isVisible) {
+            Divider(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .height(dividerWidth),
+                color = Color.Transparent,
+                thickness = dividerWidth
+            )
+        } else {
+            TooltipArea(
+                modifier = Modifier.fillMaxWidth().wrapContentHeight(),
+                tooltip = {
+                    // todo more types
+                    when (tooltipData.value) {
+                        is App -> DesktopPanelAppTooltip(tooltipData.value as App)
+                        else -> {}
+                    }
+                },
+                tooltipPlacement = TooltipPlacement.CursorPoint(
+                    alignment = Alignment.TopEnd,
+                    offset = DpOffset(32.dp, 32.dp)
+                )
             ) {
-                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()
+                TopShadow(
+                    modifier = Modifier.fillMaxWidth().wrapContentHeight().onPlaced {
+                        panelState.bounds = it.toDpBounds()
+                    },
+                    color = shadowColor,
+                    contentBackgroundColor = backgroundColor.copy(alpha = panelAlpha)
+                ) {
+                    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.4f),
+                                    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))
+                                )
                             }
-                        )
-                        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
+                        items(favoriteApps.value) { app ->
+                            DesktopPanelIcon(
+                                app = app,
+                                iconColor = iconColor,
+                                iconBackgroundColor = iconBackgroundColor,
+                                iconBackgroundHover = Color.White.copy(alpha = 0.4f),
+                                iconSize = iconSize,
+                                iconPadding = iconPadding,
+                                iconOuterPadding = iconOuterPadding,
+                                onToolTip = onToolTip,
+                                onClick = {
+                                    app.start()
+                                }
+                            )
+                        }
+                        item {
+                            DesktopPanelText(
+                                text = api.appsProvider.currentLocale.country,
+                                backgroundHover = Color.White.copy(alpha = 0.4f),
+                                onToolTip = onToolTip,
+                                onClick = {
+                                    // todo
+                                }
+                            )
                         }
-                    )
+                    }
                 }
             }
         }
     }
 }
-//        }
-//    }
-//}

+ 14 - 40
src/main/kotlin/eu/mjdev/desktop/components/image/ImageAny.kt

@@ -1,27 +1,21 @@
 package eu.mjdev.desktop.components.image
 
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.material.CircularProgressIndicator
-import androidx.compose.runtime.*
+import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.ColorFilter
 import androidx.compose.ui.graphics.DefaultAlpha
 import androidx.compose.ui.graphics.FilterQuality
-import androidx.compose.ui.graphics.ImageBitmap
 import androidx.compose.ui.graphics.drawscope.DrawScope.Companion.DefaultFilterQuality
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.layout.ContentScale
-import eu.mjdev.desktop.extensions.Compose.launchedEffect
-import eu.mjdev.desktop.extensions.Image.loadPicture
+import coil3.compose.AsyncImage
 import androidx.compose.foundation.Image as ComposeImage
 
 @Suppress("FunctionName")
 @Composable
 fun ImageAny(
     src: Any?,
-    showLoading: Boolean = false,
     contentDescription: String? = "",
     modifier: Modifier = Modifier,
     alignment: Alignment = Alignment.Center,
@@ -29,20 +23,10 @@ fun ImageAny(
     alpha: Float = DefaultAlpha,
     colorFilter: ColorFilter? = null,
     filterQuality: FilterQuality = DefaultFilterQuality,
+    onLoading: () -> Unit = {},
     onFail: (error: Throwable) -> Unit = {}
 ) {
-    var isLoading by remember { mutableStateOf(false) }
-    var error by remember { mutableStateOf<Throwable?>(null) }
-    var imageBitmap by remember { mutableStateOf(if (src is ImageBitmap) src else null) }
     when {
-        (isLoading && showLoading) -> Column(
-            modifier = modifier,
-            horizontalAlignment = Alignment.CenterHorizontally,
-            verticalArrangement = Arrangement.Center
-        ) {
-            CircularProgressIndicator()
-        }
-
         src is ImageVector -> ComposeImage(
             modifier = modifier,
             imageVector = src,
@@ -53,30 +37,20 @@ fun ImageAny(
             contentScale = contentScale
         )
 
-        imageBitmap != null -> ComposeImage(
+        else -> AsyncImage(
             modifier = modifier,
-            bitmap = imageBitmap!!,
-            alignment = alignment,
-            alpha = alpha,
+            model = src,
             contentDescription = contentDescription,
-            colorFilter = colorFilter,
+            contentScale = contentScale,
             filterQuality = filterQuality,
-            contentScale = contentScale
+            onError = {
+                onFail(it.result.throwable)
+            },
+            onLoading = {
+                onLoading()
+            },
         )
     }
-    launchedEffect(src) {
-        isLoading = true
-        loadPicture(src)
-            .onSuccess {
-                imageBitmap = it
-                isLoading = false
-            }
-            .onFailure { e ->
-                error = e
-                isLoading = false
-            }
-    }
-    launchedEffect(error) {
-        if (error != null) onFail(error!!)
-    }
 }
+
+

+ 27 - 0
src/main/kotlin/eu/mjdev/desktop/extensions/Compose.kt

@@ -9,6 +9,11 @@ import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.MutableState
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.*
+import coil3.ImageLoader
+import coil3.PlatformContext
+import coil3.compose.LocalPlatformContext
+import coil3.memory.MemoryCache
+import coil3.request.CachePolicy
 import kotlinx.coroutines.CoroutineScope
 
 object Compose {
@@ -85,4 +90,26 @@ object Compose {
         disabledBackgroundColor = Color.Transparent
     )
 
+    @Composable
+    fun imageLoaderContext() = LocalPlatformContext.current
+
+    @Composable
+    fun imageLoaderMemoryCache() = MemoryCache.Builder()
+        .maxSizePercent(imageLoaderContext(), 0.3)
+        .strongReferencesEnabled(true)
+        .build()
+
+    @Composable
+    fun AsyncImageLoader(
+        imageLoaderContext: PlatformContext = imageLoaderContext(),
+        imageLoaderMemoryCache: MemoryCache = imageLoaderMemoryCache()
+    ) = ImageLoader.Builder(imageLoaderContext)
+        .memoryCachePolicy(CachePolicy.ENABLED)
+        .components {
+//            add(SvgDecoder.Factory())
+//            add(GifDecoder.Factory())
+        }
+        .memoryCache { imageLoaderMemoryCache }
+        .build()
+
 }

+ 0 - 53
src/main/kotlin/eu/mjdev/desktop/helpers/WindowFocusState.kt

@@ -1,53 +0,0 @@
-package eu.mjdev.desktop.helpers
-
-import androidx.compose.runtime.*
-import androidx.compose.ui.awt.ComposeWindow
-import androidx.compose.ui.window.FrameWindowScope
-import java.awt.event.WindowEvent
-import java.awt.event.WindowFocusListener
-
-@Suppress("MemberVisibilityCanBePrivate")
-class WindowFocusState(
-    private val window: ComposeWindow? = null,
-    private val onFocusChange: (focused: Boolean) -> Unit = {}
-) : WindowFocusListener {
-    private val focusState: MutableState<Boolean> = mutableStateOf(false)
-
-    val isFocused: Boolean
-        get() = focusState.value
-
-    override fun windowGainedFocus(we: WindowEvent?) {
-        focusState.value = true
-        onFocusChange(isFocused)
-    }
-
-    override fun windowLostFocus(we: WindowEvent?) {
-        focusState.value = false
-        onFocusChange(isFocused)
-    }
-
-    fun init() {
-        window?.addWindowFocusListener(this)
-    }
-
-    fun destroy() {
-        window?.removeWindowFocusListener(this)
-    }
-
-    companion object {
-        @Composable
-        fun FrameWindowScope.windowFocusHandler(
-            onFocusChange: (focused: Boolean) -> Unit = {}
-        ) {
-            val state = remember(window) {
-                WindowFocusState(window, onFocusChange)
-            }
-            DisposableEffect(Unit) {
-                state.init()
-                onDispose {
-                    state.destroy()
-                }
-            }
-        }
-    }
-}

+ 3 - 1
src/main/kotlin/eu/mjdev/desktop/provider/DesktopProvider.kt

@@ -21,7 +21,7 @@ import javax.script.ScriptEngineManager
 
 @Suppress("unused", "MemberVisibilityCanBePrivate", "PrivatePropertyName", "SameParameterValue")
 class DesktopProvider(
-    private val scope: CoroutineScope? = null,
+    private val scope: CoroutineScope? = null
 ) {
     private val __currentUser: User by lazy { User.load() }
     private val _currentUser: MutableState<User> = mutableStateOf(__currentUser)
@@ -37,6 +37,7 @@ class DesktopProvider(
         }
     }
 
+
 //    val adbHandler = adbDevicesHandler(
 //        coroutineScope = scope
 //    ) { device ->
@@ -130,6 +131,7 @@ class DesktopProvider(
             DisplaySettingsPage(),
             SoundSettingsPage()
         )
+
         val LocalDesktop = compositionLocalOf {
             DesktopProvider()
         }

+ 9 - 2
src/main/kotlin/eu/mjdev/desktop/provider/data/Theme.kt

@@ -35,8 +35,11 @@ class Theme(
 ) {
 
     val backgroundColorState = mutableStateOf(backgroundColor)
+    val backgroundRotationDelayState = mutableStateOf(backgroundRotationDelay)
+
     val panelLocationState = mutableStateOf(panelLocation)
     val panelHideDelayState = mutableStateOf(panelHideDelay)
+
     val controlCenterLocationState = mutableStateOf(controlCenterLocation)
     val controlPanelHideDelayState = mutableStateOf(controlPanelHideDelay)
     var controlCenterExpandedWidthState = mutableStateOf(controlCenterExpandedWidth)
@@ -44,9 +47,8 @@ class Theme(
     val controlCenterDividerWidthState = mutableStateOf(controlCenterDividerWidth)
     val controlCenterIconColorState = mutableStateOf(controlCenterIconColor)
     val controlCenterIconSizeState = mutableStateOf(controlCenterIconSize)
+
     val iconSetState = mutableStateOf(iconSet)
-    val backgroundRotationDelayState = mutableStateOf(backgroundRotationDelay)
-    val controlCenterBackgroundAlphaState = mutableStateOf(controlCenterBackgroundAlpha)
 
     val appMenuMinWidthState = mutableStateOf(appMenuMinWidth)
     val appMenuMinHeightState = mutableStateOf(appMenuMinHeight)
@@ -57,8 +59,11 @@ class Theme(
         set(value) {
             backgroundColorState.value = value
         }
+
     val panelLocation get() = panelLocationState.value
     val panelHideDelay get() = panelHideDelayState.value
+
+    val controlCenterBackgroundAlphaState = mutableStateOf(controlCenterBackgroundAlpha)
     val controlCenterLocation get() = controlCenterLocationState.value
     val controlPanelHideDelay get() = controlPanelHideDelayState.value
     var controlCenterExpandedWidth
@@ -71,7 +76,9 @@ class Theme(
     val controlCenterIconColor get() = controlCenterIconColorState.value
     val controlCenterIconSize get() = controlCenterIconSizeState.value
     val controlCenterBackgroundAlpha get() = controlCenterBackgroundAlphaState.value
+
     val iconSet get() = iconSetState.value
+
     val backgroundRotationDelay get() = backgroundRotationDelayState.value
 
     val appMenuMinWidth get() = appMenuMinWidthState.value

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

@@ -104,7 +104,9 @@ class AppsProvider(
     val allApps
         get() = allAppsDesktopFiles.map { file -> App(desktopFile = file, file = file.file) }
     val backgrounds
-        get() = runCatching { backgroundFilesDir?.listFiles()?.toList() }.getOrNull() ?: emptyList<File>()
+        get() = runCatching {
+            backgroundFilesDir?.listFiles()?.toList()?.sortedBy { it.name }
+        }.getOrNull() ?: emptyList<File>()
     val appCategories
         get() = mutableListOf<String>().apply {
             allApps.forEach { app ->
@@ -133,7 +135,8 @@ class AppsProvider(
                 "favorite-apps"
             ).execute()?.toList<String>()?.map { deskFileName ->
                 allAppsDesktopFiles.filter { deskFile ->
-                    deskFile.file.name.contentEquals(deskFileName) == true
+                    @Suppress("PlatformExtensionReceiverOfInline")
+                    deskFile.file.name.contentEquals(deskFileName)
                 }.map { deskFile ->
                     App(desktopFile = deskFile, file = deskFile.file)
                 }.firstOrNull()

+ 95 - 81
src/main/kotlin/eu/mjdev/desktop/windows/ChromeWindow.kt

@@ -1,16 +1,12 @@
 package eu.mjdev.desktop.windows
 
 import androidx.compose.animation.*
-import androidx.compose.animation.core.MutableTransitionState
-import androidx.compose.runtime.*
+import androidx.compose.runtime.Composable
 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
@@ -19,26 +15,111 @@ import eu.mjdev.desktop.windows.ChromeWindowState.Companion.updateWindowState
 
 @Composable
 fun ChromeWindow(
-    position: WindowPosition = WindowPosition.Aligned(Alignment.Center),
     size: DpSize = DpSize(Dp.Unspecified, Dp.Unspecified),
+    position: WindowPosition = WindowPosition.Aligned(Alignment.Center),
+    windowState: ChromeWindowState = rememberChromeWindowState(position = position, size = size),
+    visible: Boolean = true,
+    transparent: Boolean = true,
+    resizable: Boolean = false,
+    enabled: Boolean = true,
+    focusable: Boolean = true,
+    alwaysOnTop: Boolean = true,
+    enterAnimation: EnterTransition = fadeIn() + slideInVertically(initialOffsetY = { it }),
+    exitAnimation: ExitTransition = slideOutVertically(targetOffsetY = { it }) + fadeOut(),
+    onCloseRequest: () -> Unit = {},
+    onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
+    onKeyEvent: (KeyEvent) -> Boolean = { false },
+    onFocusChange: (Boolean) -> Unit = {},
+    onOpened: () -> Unit = {},
+    onClosed: () -> Unit = {},
+    content: @Composable () -> Unit = {}
+) = ChromeWindow(
+    windowState,
+    visible,
+    transparent,
+    resizable,
+    enabled,
+    focusable,
+    alwaysOnTop,
+    enterAnimation,
+    exitAnimation,
+    onCloseRequest,
+    onPreviewKeyEvent,
+    onKeyEvent,
+    onFocusChange,
+    onOpened,
+    onClosed,
+    content
+)
+
+@Composable
+fun ChromeWindow(
+    position: WindowPosition = WindowPosition.Aligned(Alignment.Center),
+    windowState: ChromeWindowState = rememberChromeWindowState(position = position),
     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 = {},
+    onFocusChange: (Boolean) -> Unit = {},
+    onOpened: () -> Unit = {},
+    onClosed: () -> Unit = {},
+    content: @Composable () -> Unit = {}
+) = ChromeWindow(
+    windowState,
+    visible,
+    transparent,
+    resizable,
+    enabled,
+    focusable,
+    alwaysOnTop,
+    enterAnimation,
+    exitAnimation,
+    onCloseRequest,
+    onPreviewKeyEvent,
+    onKeyEvent,
+    onFocusChange,
+    onOpened,
+    onClosed,
+    content
+)
+
+@Composable
+fun ChromeWindow(
+    windowState: ChromeWindowState = rememberChromeWindowState(),
+    visible: Boolean = true,
+    transparent: Boolean = true,
+    resizable: Boolean = false,
+    enabled: Boolean = true,
+    focusable: Boolean = true,
+    alwaysOnTop: Boolean = true,
+    enterAnimation: EnterTransition = fadeIn() + slideInVertically(initialOffsetY = { it }),
+    exitAnimation: ExitTransition = slideOutVertically(targetOffsetY = { it }) + fadeOut(),
+    onCloseRequest: () -> Unit = {},
+    onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
+    onKeyEvent: (KeyEvent) -> Boolean = { false },
+    onFocusChange: (Boolean) -> Unit = {},
+    onOpened: () -> Unit = {},
+    onClosed: () -> Unit = {},
     content: @Composable () -> Unit = {}
 ) {
     val animState = rememberAnimState(visible)
     val wnState = rememberWnState()
+    windowState.onOpened.add({
+        windowState.updatePositionAndSize()
+        if (visible) {
+            animState.value.targetState = true
+        }
+        onOpened()
+    })
+    windowState.onClosed.add(onClosed)
+    windowState.onFocusChange.add(onFocusChange)
     WindowEx(
         onCloseRequest,
         windowState,
@@ -53,13 +134,9 @@ fun ChromeWindow(
         alwaysOnTop,
         onPreviewKeyEvent,
         onKeyEvent,
-        {
-            if (visible) {
-                animState.value.targetState = true
-            }
-            onUpdate()
-        },
-        {
+        stateHelper = windowState.stateHelper,
+        focusHelper = windowState.focusHelper,
+        content = {
             AnimatedVisibility(
                 animState.value,
                 enter = enterAnimation,
@@ -69,69 +146,6 @@ fun ChromeWindow(
             }
         }
     )
-    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
-            }
-        }
-
-    }
-
+    updateAnimState(visible, animState, wnState, windowState)
+    updateWindowState(visible, animState, wnState, windowState)
 }

+ 129 - 0
src/main/kotlin/eu/mjdev/desktop/windows/ChromeWindowState.kt

@@ -0,0 +1,129 @@
+package eu.mjdev.desktop.windows
+
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+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
+
+class ChromeWindowState(
+    position: WindowPosition = WindowPosition.Aligned(Alignment.Center),
+    size: DpSize = DpSize(Dp.Unspecified, Dp.Unspecified),
+    placement: WindowPlacement = WindowPlacement.Floating,
+    closeAction: WindowCloseAction,
+) : WindowState {
+
+    var onFocusChange: MutableList<(focus: Boolean) -> Unit> = mutableStateListOf()
+    var onOpened: MutableList<() -> Unit> = mutableStateListOf()
+    var onClosed: MutableList<() -> Unit> = mutableStateListOf()
+
+    val focusHelper: WindowFocusHelper = WindowFocusHelper({ _, focus ->
+        this@ChromeWindowState.onFocusChange.forEach {
+            it.invoke(focus)
+        }
+    })
+
+    val stateHelper: WindowStateHelper = WindowStateHelper({
+        setSize(size)
+        this@ChromeWindowState.onOpened.forEach {
+            it.invoke()
+        }
+    }, {
+        this@ChromeWindowState.onClosed.forEach {
+            it.invoke()
+        }
+    })
+
+    @Suppress("CanBePrimaryConstructorProperty")
+    val closeAction: WindowCloseAction = closeAction
+
+    override var position: WindowPosition = position
+        set(value) {
+            field = value
+            stateHelper.setPosition(position)
+        }
+
+    override var size: DpSize = size
+        set(value) {
+            field = value
+            stateHelper.setSize(value)
+        }
+
+    @Suppress("CanBePrimaryConstructorProperty")
+    override var placement: WindowPlacement = placement
+    override var isMinimized: Boolean = false
+
+    fun updatePositionAndSize() {
+        stateHelper.setSize(size)
+        stateHelper.setPosition(position)
+    }
+
+    companion object {
+
+        @Composable
+        fun rememberChromeWindowState(
+            position: WindowPosition = WindowPosition.Aligned(Alignment.Center),
+            size: DpSize = DpSize(Dp.Unspecified, Dp.Unspecified),
+            placement: WindowPlacement = WindowPlacement.Floating,
+            closeAction: WindowCloseAction = WindowCloseAction.CLOSE,
+        ) = remember(position, size, placement, closeAction) {
+            ChromeWindowState(position, size, placement, closeAction)
+        }
+
+        @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>,
+            state: ChromeWindowState
+        ) = launchedEffect(animState.value.currentState) { currentState ->
+            if (animState.value.targetState == currentState && !visible) {
+                if (state.closeAction == WindowCloseAction.CLOSE) {
+                    wnState.value = false
+                } else {
+                    state.updatePositionAndSize()
+                }
+            }
+        }
+
+        @Suppress("UNUSED_PARAMETER")
+        @Composable
+        fun updateAnimState(
+            visible: Boolean,
+            animState: State<MutableTransitionState<Boolean>>,
+            wnState: MutableState<Boolean>,
+            state: ChromeWindowState
+        ) = launchedEffect(visible) {
+            if (visible) {
+                wnState.value = true
+            } else {
+                animState.value.targetState = false
+            }
+        }
+
+    }
+
+    @Suppress("unused")
+    enum class WindowCloseAction {
+        CLOSE,
+        MOVE
+    }
+
+}

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

@@ -1,31 +0,0 @@
-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++
-        }
-    }
-}

+ 12 - 3
src/main/kotlin/eu/mjdev/desktop/windows/FullScreenWindow.kt

@@ -1,10 +1,13 @@
 package eu.mjdev.desktop.windows
 
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.input.key.KeyEvent
 import androidx.compose.ui.window.WindowPlacement
+import androidx.compose.ui.window.WindowPosition
 import eu.mjdev.desktop.provider.DesktopProvider
 import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
+import eu.mjdev.desktop.windows.ChromeWindowState.Companion.rememberChromeWindowState
 
 @Composable
 fun FullScreenWindow(
@@ -12,17 +15,23 @@ fun FullScreenWindow(
     onCloseRequest: () -> Unit = {},
     onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
     onKeyEvent: (KeyEvent) -> Boolean = { false },
+    windowState: ChromeWindowState = rememberChromeWindowState(
+        placement = WindowPlacement.Fullscreen,
+        size = api.containerSize,
+        position = WindowPosition.Aligned(Alignment.Center)
+    ),
     content: @Composable () -> Unit = {}
 ) = ChromeWindow(
-    size = api.containerSize,
+    windowState = windowState,
     resizable = false,
     transparent = true,
     visible = true,
-    focusable = true,
+    focusable = false,
     alwaysOnTop = false,
     onPreviewKeyEvent = onPreviewKeyEvent,
     onKeyEvent = onKeyEvent,
     onCloseRequest = onCloseRequest,
-    placement = WindowPlacement.Fullscreen,
+    onFocusChange = {
+    },
     content = content
 )

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

@@ -1,45 +0,0 @@
-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
-        )
-
-    }
-}

+ 28 - 274
src/main/kotlin/eu/mjdev/desktop/windows/WindowEx.kt

@@ -1,28 +1,12 @@
 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
+import androidx.compose.ui.window.FrameWindowScope
+import androidx.compose.ui.window.Window
+import androidx.compose.ui.window.WindowState
+import androidx.compose.ui.window.rememberWindowState
 
 @Composable
 fun WindowEx(
@@ -31,266 +15,36 @@ fun WindowEx(
     visible: Boolean = true,
     title: String = "Untitled",
     icon: Painter? = null,
-    undecorated: Boolean = false,
-    transparent: Boolean = false,
+    undecorated: Boolean = true,
+    transparent: Boolean = true,
     resizable: Boolean = true,
     enabled: Boolean = true,
     focusable: Boolean = true,
     alwaysOnTop: Boolean = false,
     onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
     onKeyEvent: (KeyEvent) -> Boolean = { false },
-    onUpdate: () -> Unit = {},
+    focusHelper: WindowFocusHelper,
+    stateHelper: WindowStateHelper,
     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
+) = Window(
+    onCloseRequest = onCloseRequest,
+    state = state,
+    visible = visible,
+    title = title,
+    icon = icon,
+    undecorated = undecorated,
+    transparent = transparent,
+    resizable = resizable,
+    enabled = enabled,
+    focusable = focusable,
+    alwaysOnTop = alwaysOnTop,
+    onPreviewKeyEvent = onPreviewKeyEvent,
+    onKeyEvent = onKeyEvent,
+    content = {
+        content()
+        with(window) {
+            stateHelper.register(this)
+            focusHelper.register(this)
         }
     }
-
-    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)
-    }
-}
-
-
-
+)

+ 40 - 0
src/main/kotlin/eu/mjdev/desktop/windows/WindowFocusHelper.kt

@@ -0,0 +1,40 @@
+package eu.mjdev.desktop.windows
+
+import androidx.compose.ui.awt.ComposeWindow
+import java.awt.event.WindowEvent
+import java.awt.event.WindowEvent.WINDOW_CLOSED
+import java.awt.event.WindowFocusListener
+import java.awt.event.WindowStateListener
+
+class WindowFocusHelper(
+    val onFocusChange: WindowFocusHelper.(window: ComposeWindow?, focus: Boolean) -> Unit
+) : WindowFocusListener, WindowStateListener {
+
+    var window: ComposeWindow? = null
+
+    fun register(window: ComposeWindow) {
+        this.window = window
+        window.addWindowFocusListener(this)
+        window.addWindowStateListener(this)
+    }
+
+    fun unregister(window: ComposeWindow) {
+        window.removeWindowFocusListener(this)
+        window.removeWindowStateListener(this)
+    }
+
+    override fun windowGainedFocus(e: WindowEvent?) {
+        onFocusChange(window, true)
+    }
+
+    override fun windowLostFocus(e: WindowEvent?) {
+        onFocusChange(window, false)
+    }
+
+    override fun windowStateChanged(e: WindowEvent?) {
+        when (e?.newState) {
+            null -> Unit
+            WINDOW_CLOSED -> window?.let { unregister(it) }
+        }
+    }
+}

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

@@ -1,62 +0,0 @@
-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)
-}

+ 83 - 0
src/main/kotlin/eu/mjdev/desktop/windows/WindowStateHelper.kt

@@ -0,0 +1,83 @@
+package eu.mjdev.desktop.windows
+
+import androidx.compose.ui.awt.ComposeWindow
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.WindowPosition
+import java.awt.event.WindowEvent
+import java.awt.event.WindowEvent.WINDOW_CLOSED
+import java.awt.event.WindowEvent.WINDOW_OPENED
+import java.awt.event.WindowStateListener
+
+class WindowStateHelper(
+    val onOpened: WindowStateHelper.(window: ComposeWindow?) -> Unit = {},
+    val onClosed: WindowStateHelper.(window: ComposeWindow?) -> Unit = {},
+) : WindowStateListener {
+
+    val x
+        get() = (window?.x ?: 0).dp
+
+    val y
+        get() = (window?.y ?: 0).dp
+
+    val width
+        get() = (window?.width ?: 0).dp
+
+    val height
+        get() = (window?.height ?: 0).dp
+
+    var window: ComposeWindow? = null
+
+    fun register(window: ComposeWindow) {
+        this.window = window
+        window.addWindowStateListener(this)
+        onOpened(window)
+    }
+
+    fun unregister(window: ComposeWindow) {
+        window.removeWindowStateListener(this)
+    }
+
+    fun setBounds(
+        x: Dp = this.x,
+        y: Dp = this.y,
+        width: Dp = this.width,
+        height: Dp = this.height
+    ) {
+        window?.setBounds(
+            x.value.toInt(),
+            y.value.toInt(),
+            width.value.toInt(),
+            height.value.toInt()
+        )
+    }
+
+    fun setOffset(offset: DpOffset) = setBounds(x = offset.x, y = offset.y)
+
+    fun setSize(size: DpSize) = setBounds(width = size.width, height = size.height)
+
+    override fun windowStateChanged(e: WindowEvent?) {
+        when (e?.newState) {
+            null -> Unit
+            WINDOW_OPENED -> {
+                onOpened(window)
+            }
+
+            WINDOW_CLOSED -> {
+                onClosed(window)
+                window?.let { unregister(it) }
+            }
+        }
+    }
+
+    fun setPosition(position: WindowPosition) {
+        when (position) {
+            is WindowPosition.Absolute -> setOffset(position.let { DpOffset(it.x, it.y) })
+            else -> {
+                // todo
+            }
+        }
+    }
+}