Kaynağa Gözat

new custom components

new custom components

new custom components
Milan Jurkulak 2 yıl önce
ebeveyn
işleme
085b258f9d
32 değiştirilmiş dosya ile 1148 ekleme ve 182 silme
  1. 0 5
      build.gradle.kts
  2. 1 1
      src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsMenu.kt
  3. 5 2
      src/main/kotlin/eu/mjdev/desktop/components/controlcenter/pages/MainSettingsPage.kt
  4. 31 0
      src/main/kotlin/eu/mjdev/desktop/components/custom/BrushedLayout.kt
  5. 22 37
      src/main/kotlin/eu/mjdev/desktop/components/custom/MeasureUnconstrainedView.kt
  6. 64 0
      src/main/kotlin/eu/mjdev/desktop/components/custom/NeonCircle.kt
  7. 40 0
      src/main/kotlin/eu/mjdev/desktop/components/custom/VerticalScrollableBox.kt
  8. 5 3
      src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/DesktopPanel.kt
  9. 1 12
      src/main/kotlin/eu/mjdev/desktop/components/file/FolderIcon.kt
  10. 102 103
      src/main/kotlin/eu/mjdev/desktop/components/file/FolderView.kt
  11. 3 3
      src/main/kotlin/eu/mjdev/desktop/components/greeter/Greeter.kt
  12. 60 0
      src/main/kotlin/eu/mjdev/desktop/components/grid/GridPlaced.kt
  13. 16 0
      src/main/kotlin/eu/mjdev/desktop/components/grid/base/CellPlacedInfo.kt
  14. 51 0
      src/main/kotlin/eu/mjdev/desktop/components/grid/base/GridPlacedCellSize.kt
  15. 96 0
      src/main/kotlin/eu/mjdev/desktop/components/grid/base/GridPlacedCells.kt
  16. 22 0
      src/main/kotlin/eu/mjdev/desktop/components/grid/base/GridPlacedContent.kt
  17. 15 0
      src/main/kotlin/eu/mjdev/desktop/components/grid/base/GridPlacedItemScope.kt
  18. 11 0
      src/main/kotlin/eu/mjdev/desktop/components/grid/base/GridPlacedItemScopeImpl.kt
  19. 46 0
      src/main/kotlin/eu/mjdev/desktop/components/grid/base/GridPlacedPlacementPolicy.kt
  20. 28 0
      src/main/kotlin/eu/mjdev/desktop/components/grid/base/GridPlacedScope.kt
  21. 204 0
      src/main/kotlin/eu/mjdev/desktop/components/grid/base/GridPlacedScopeImpl.kt
  22. 4 0
      src/main/kotlin/eu/mjdev/desktop/components/grid/base/GridPlacedScopeMarker.kt
  23. 51 0
      src/main/kotlin/eu/mjdev/desktop/components/grid/base/GridPlacedSpanAnchor.kt
  24. 101 0
      src/main/kotlin/eu/mjdev/desktop/components/grid/base/Measure.kt
  25. 16 0
      src/main/kotlin/eu/mjdev/desktop/components/grid/base/TotalSize.kt
  26. 57 0
      src/main/kotlin/eu/mjdev/desktop/components/guide/GuideView.kt
  27. 51 0
      src/main/kotlin/eu/mjdev/desktop/components/progress/DashedProgressIndicator.kt
  28. 17 3
      src/main/kotlin/eu/mjdev/desktop/components/user/UserAvatar.kt
  29. 8 8
      src/main/kotlin/eu/mjdev/desktop/data/DesktopFile.kt
  30. 12 0
      src/main/kotlin/eu/mjdev/desktop/extensions/Compose.kt
  31. 5 5
      src/main/kotlin/eu/mjdev/desktop/helpers/desktopparser/DesktopFileParser.kt
  32. 3 0
      src/main/kotlin/eu/mjdev/desktop/provider/DesktopProvider.kt

+ 0 - 5
build.gradle.kts

@@ -117,11 +117,6 @@ allprojects {
 //        implementation("org.freedesktop:wayland-client:1.5.1")
         // gettext
         implementation("org.gnu.gettext:libintl:0.18.3")
-        // constraint
-//        implementation("tech.annexflow.compose:constraintlayout-compose-multiplatform:0.4.0")
-//        implementation("tech.annexflow.compose:constraintlayout-compose-multiplatform:0.5.0-alpha03")
-//        implementation("tech.annexflow.compose:constraintlayout-compose-multiplatform:0.5.0-alpha03-shaded-core")
-//        implementation("tech.annexflow.compose:constraintlayout-compose-multiplatform:0.5.0-alpha03-shaded")
         // paths
 //        implementation("me.sujanpoudel.multiplatform.utils:multiplatform-paths:0.2.2")
         // files

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

@@ -16,9 +16,9 @@ import androidx.compose.ui.draw.clip
 import androidx.compose.ui.layout.onPlaced
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.window.WindowPosition
-import eu.mjdev.desktop.components.custom.UserAvatar
 import eu.mjdev.desktop.components.sliding.base.VisibilityState
 import eu.mjdev.desktop.components.sliding.base.VisibilityState.Companion.rememberVisibilityState
+import eu.mjdev.desktop.components.user.UserAvatar
 import eu.mjdev.desktop.data.App
 import eu.mjdev.desktop.data.Category
 import eu.mjdev.desktop.extensions.ColorUtils.alpha

+ 5 - 2
src/main/kotlin/eu/mjdev/desktop/components/controlcenter/pages/MainSettingsPage.kt

@@ -10,10 +10,11 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
 import eu.mjdev.desktop.components.controlcenter.base.ControlCenterPage
 import eu.mjdev.desktop.components.custom.DateTime
 import eu.mjdev.desktop.components.custom.PowerBlock
-import eu.mjdev.desktop.components.custom.UserAvatar
+import eu.mjdev.desktop.components.user.UserAvatar
 import eu.mjdev.desktop.extensions.ColorUtils.alpha
 import eu.mjdev.desktop.extensions.Modifier.rectShadow
 import eu.mjdev.desktop.helpers.compose.Orientation
@@ -35,8 +36,10 @@ fun MainSettingsPage() = ControlCenterPage(
         ) {
             UserAvatar(
                 modifier = Modifier.background(backgroundColor),
-                avatarSize = 128.dp,
+                avatarSize = 96.dp,
                 orientation = Orientation.Vertical,
+                titleTextSize = 20.sp,
+                detailTextSize = 16.sp,
                 onUserAvatarClick = {
                     // todo
                 }

+ 31 - 0
src/main/kotlin/eu/mjdev/desktop/components/custom/BrushedLayout.kt

@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.custom
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+
+@Composable
+fun BrushedLayout(
+    modifier: Modifier = Modifier,
+    alpha: Float = 1f,
+    colorStart: Color = Color.Black.copy(alpha = if (alpha.isNaN()) 1f else alpha),
+    colorEnd: Color = Color.Transparent,
+) {
+    val brush = Brush.radialGradient(listOf(colorStart, colorEnd))
+    Canvas(
+        modifier = modifier,
+        onDraw = {
+            drawRect(brush)
+        }
+    )
+}

+ 22 - 37
src/main/kotlin/eu/mjdev/desktop/components/custom/MeasureUnconstrainedView.kt

@@ -1,40 +1,25 @@
 package eu.mjdev.desktop.components.custom
 
-//import androidx.compose.material.Text
-//import androidx.compose.runtime.Composable
-//import androidx.compose.ui.layout.SubcomposeLayout
-//import androidx.compose.ui.unit.Constraints
-//import androidx.compose.ui.unit.Dp
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.layout.SubcomposeLayout
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
 
-//@Suppress("FunctionName", "unused")
-//@Composable
-//fun MeasureUnconstrainedView(
-//    viewToMeasure: @Composable () -> Unit,
-//    content: @Composable (measuredWidth: Dp, measuredHeight: Dp) -> Unit,
-//) {
-//    SubcomposeLayout { constraints ->
-//        val measured = subcompose("viewToMeasure", viewToMeasure)[0]
-//            .measure(Constraints())
-//        val measuredWidth = measured.width.toDp()
-//        val measuredHeight = measured.height.toDp()
-//        val contentPlaceable = subcompose("content") {
-//            content(measuredWidth, measuredHeight)
-//        }[0].measure(constraints)
-//        layout(contentPlaceable.width, contentPlaceable.height) {
-//            contentPlaceable.place(0, 0)
-//        }
-//    }
-//}
-
-//@Suppress("FunctionName", "unused")
-//@Composable
-//fun TestMeasurable() {
-//    MeasureUnconstrainedView(
-//        viewToMeasure = {
-//            Text("your sample text")
-//        }
-//    ) { mw, mh ->
-//        println("size: $mw,$mh")
-//        // measure view by another view
-//    }
-//}
+@Composable
+fun MeasureUnconstrainedView(
+    viewToMeasure: @Composable () -> Unit,
+    content: @Composable (measuredWidth: Dp, measuredHeight: Dp) -> Unit,
+) {
+    SubcomposeLayout { constraints ->
+        val measured = subcompose("viewToMeasure", viewToMeasure)[0]
+            .measure(Constraints())
+        val measuredWidth = measured.width.toDp()
+        val measuredHeight = measured.height.toDp()
+        val contentPlaceable = subcompose("content") {
+            content(measuredWidth, measuredHeight)
+        }[0].measure(constraints)
+        layout(contentPlaceable.width, contentPlaceable.height) {
+            contentPlaceable.place(0, 0)
+        }
+    }
+}

+ 64 - 0
src/main/kotlin/eu/mjdev/desktop/components/custom/NeonCircle.kt

@@ -0,0 +1,64 @@
+package eu.mjdev.desktop.components.custom
+
+//import androidx.compose.runtime.Composable
+//import androidx.compose.ui.Modifier
+//import androidx.compose.ui.graphics.Color
+//import androidx.compose.ui.unit.Dp
+//import androidx.compose.ui.unit.dp
+
+//@Composable
+//fun NeonCircle(
+//    modifier: Modifier = Modifier,
+//    colorCircle: Color = Color.White,
+//    colorStroke: Color = Color.Red,
+//    padding: Dp = 20.dp,
+//    strokeWidth: Dp = 30.dp,
+//    circleWidth: Dp = 3.dp
+//) {
+//    val paint = remember {
+//        Paint().apply {
+//            style = PaintingStyle.Stroke
+//            this.strokeWidth = strokeWidth.value
+//        }
+//    }
+//    val frameworkPaint = remember {
+//        paint.asFrameworkPaint().apply {
+//            setColor(colorStroke.copy(alpha = 0f).toArgb())
+//        }
+//    }
+//    BoxWithConstraints(
+//        modifier = modifier.fillMaxSize(),
+//        contentAlignment = Alignment.Center
+//    ) {
+//        val baseSize = min(maxWidth, maxHeight)
+//        val size = min(maxWidth, maxHeight) - (circleWidth + padding)
+//        val pad = padding / 2
+//        Canvas(
+//            modifier = Modifier.size(baseSize, baseSize)
+//        ) {
+//            drawIntoCanvas { canvas ->
+//                frameworkPaint.setShadowLayer(
+//                    circleWidth.toPx(),
+//                    pad.toPx(),
+//                    pad.toPx(),
+//                    colorStroke.copy(alpha = .5f).toArgb()
+//                )
+//                canvas.drawOval(
+//                    left = 0f,
+//                    top = 0f,
+//                    right = size.toPx(),
+//                    bottom = size.toPx(),
+//                    paint = paint
+//                )
+//                drawOval(
+//                    colorCircle,
+//                    topLeft = Offset(pad.toPx(), pad.toPx()),
+//                    size = Size(size.toPx(), size.toPx()),
+//                    alpha = 1f,
+//                    colorFilter = null,
+//                    style = Stroke(width = circleWidth.toPx())
+//                )
+//            }
+//        }
+//    }
+//}

+ 40 - 0
src/main/kotlin/eu/mjdev/desktop/components/custom/VerticalScrollableBox.kt

@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.custom
+
+import androidx.compose.foundation.gestures.detectVerticalDragGestures
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.pointerInput
+import eu.mjdev.desktop.extensions.Compose.isLandscapeMode
+
+@Composable
+fun VerticalScrollableBox(
+    modifier: Modifier = Modifier,
+    contentAlignment: Alignment = Alignment.TopStart,
+    propagateMinConstraints: Boolean = isLandscapeMode(),
+    state: LazyListState? = rememberLazyListState(),
+    content: @Composable BoxScope.() -> Unit = {}
+) = Box(
+    modifier = modifier
+        .pointerInput(Unit) {
+            detectVerticalDragGestures { _, dragAmount ->
+                state?.dispatchRawDelta(-dragAmount)
+            }
+        },
+    contentAlignment = contentAlignment,
+    propagateMinConstraints = propagateMinConstraints,
+) {
+    content.invoke(this)
+}

+ 5 - 3
src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/DesktopPanel.kt

@@ -123,14 +123,16 @@ fun DesktopPanel(
             )
             if (isVisible) {
                 TooltipArea(
-                    modifier = Modifier.fillMaxWidth().wrapContentHeight(), tooltip = {
+                    modifier = Modifier.fillMaxWidth()
+                        .wrapContentHeight(),
+                    tooltip = {
                         Tooltip(
                             tooltipState = tooltipState,
                         )
                     },
                     tooltipPlacement = TooltipPlacement.CursorPoint(
-                        alignment = Alignment.TopStart,
-                        offset = DpOffset(32.dp, 32.dp)
+                        alignment = Alignment.TopEnd,
+                        offset = DpOffset(16.dp, 64.dp)
                     )
                 ) {
                     Column {

+ 1 - 12
src/main/kotlin/eu/mjdev/desktop/components/file/FolderIcon.kt

@@ -27,28 +27,17 @@ fun FolderIcon(
     dragEnabled: Boolean = true,
     iconSize: DpSize = DpSize(128.dp, 128.dp),
     modifier: Modifier = Modifier,
-    zIndex: () -> Int = { 0 },
     onDragStart: () -> Unit = {},
     onDragEnd: () -> Unit = {},
     onContextMenuClick: () -> Unit = {},
     onClick: () -> Unit = {},
 ) = withDesktopScope {
-    val iconName = remember(path) {
-        path.let {
-            it.extension.ifEmpty {
-                // todo
-//                if (it.list() != null)
-                "folder"
-//                else ""
-            }
-        }
-    }
+    val iconName = remember(path) { path.extension.ifEmpty { "folder" } } // todo mime type
     val iconId: Int = remember(path) { iconSet.iconForName(iconName) ?: "?".toInt() }
     val computedSize = remember(path) { iconSize + 2.dp }
     DraggableView(
         modifier = modifier,
         dragEnabled = dragEnabled,
-        zIndex = zIndex().toFloat(),
         onClick = onClick,
         onContextMenuClick = onContextMenuClick,
         onDragStart = onDragStart,

+ 102 - 103
src/main/kotlin/eu/mjdev/desktop/components/file/FolderView.kt

@@ -1,22 +1,30 @@
 package eu.mjdev.desktop.components.file
 
 import androidx.compose.desktop.ui.tooling.preview.Preview
-import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.horizontalScroll
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.runtime.*
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
+import eu.mjdev.desktop.components.custom.MeasureUnconstrainedView
+import eu.mjdev.desktop.components.grid.GridPlaced
+import eu.mjdev.desktop.components.grid.base.GridPlacedCells
+import eu.mjdev.desktop.components.grid.base.GridPlacedPlacementPolicy
+import eu.mjdev.desktop.components.guide.GuideLines
+import eu.mjdev.desktop.components.sliding.base.VisibilityState
+import eu.mjdev.desktop.components.sliding.base.VisibilityState.Companion.rememberVisibilityState
 import eu.mjdev.desktop.data.DesktopFolderItem
-import eu.mjdev.desktop.extensions.Modifier.conditional
-import eu.mjdev.desktop.provider.DesktopProvider
-import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
+import eu.mjdev.desktop.extensions.ColorUtils.alpha
 import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
 import java.io.File
+import kotlin.math.max
 
 // ScreencastHelper
 // DefaultShellFolder
@@ -28,8 +36,8 @@ import java.io.File
 // XWrapperBase
 // ShellFolderManager
 // X11GraphicsEnvironment
-@Suppress("FunctionName", "SimplifyBooleanWithConstants")
-@OptIn(ExperimentalLayoutApi::class)
+
+@Suppress("FunctionName")
 @Composable
 fun FolderView(
     modifier: Modifier = Modifier,
@@ -37,109 +45,100 @@ fun FolderView(
     directoryFirst: Boolean = true,
     showHomeFolder: Boolean = false,
     iconSpacing: Dp = 2.dp,
+    iconSize: DpSize = DpSize(128.dp, 128.dp),
     path: File? = File("/"),
-    api: DesktopProvider = LocalDesktop.current,
-    zIndexState: MutableState<Int> = remember { mutableStateOf(0) },
     orientation: Orientation = Orientation.Vertical,
-    scrollState: ScrollState = rememberScrollState(),
-//    scrollbarsState: ScrollAreaState = rememberScrollAreaState(scrollState)
+    guideVisibleState: VisibilityState = rememberVisibilityState(false),
 ) = withDesktopScope {
-    val files by remember(path) {
-        derivedStateOf {
-            path?.list()
-                ?.toList<String>()
-                ?.sorted()
-                ?.filter { s ->
-                    (showHidden == true) || (!s.startsWith("."))
-                }?.map { filePath ->
-                    File(filePath)
-                }?.sortedByDescending {
-                    (directoryFirst == false) || (it.extension.isEmpty()) // todo isDirectory
-                }?.map { path ->
-                    DesktopFolderItem(path)
-                }?.toMutableList()
-                ?.apply {
-                    if (showHomeFolder) add(
-                        DesktopFolderItem(
-                            path = api.homeDir,
-                            customName = "Home",
-                            priority = 1
-                        )
-                    )
-                }?.sortedByDescending {
-                    it.priority
-                } ?: emptyList()
-        }
-    }
-    val content: @Composable () -> Unit = {
-        files.forEach { file ->
+    val files by rememberFiles(path, api.homeDir, showHidden, directoryFirst, showHomeFolder)
+    MeasureUnconstrainedView(
+        viewToMeasure = {
             FolderIcon(
                 modifier = Modifier.padding(iconSpacing),
-                path = file.path,
-                customName = file.customName,
-                zIndex = { zIndexState.value },
-                onDragStart = {
-                    zIndexState.value += 1
+                iconSize = iconSize,
+            )
+        }
+    ) { mw, mh ->
+        val rows = max(1, ((api.containerSize.height / mh).toInt()))
+        val columns = max(1, (files.size.div(rows)))
+        BoxWithConstraints(
+            modifier = modifier,
+            contentAlignment = Alignment.TopStart
+        ) {
+            GridPlaced(
+                cells = GridPlacedCells(
+                    rowCount = rows,
+                    columnCount = columns
+                ),
+                placementPolicy = GridPlacedPlacementPolicy(
+                    mainAxis = GridPlacedPlacementPolicy.MainAxis.VERTICAL,
+                    horizontalDirection = GridPlacedPlacementPolicy.HorizontalDirection.START_END,
+                    verticalDirection = GridPlacedPlacementPolicy.VerticalDirection.TOP_BOTTOM
+                ),
+            ) {
+                files.forEach { file ->
+                    item {
+                        FolderIcon(
+                            modifier = Modifier.padding(iconSpacing),
+                            path = file.path,
+                            customName = file.customName,
+                            iconSize = iconSize,
+                            onDragStart = {
+                                guideVisibleState.show()
+                            },
+                            onDragEnd = {
+                                guideVisibleState.hide()
+                            }
+                        )
+                    }
                 }
+            }
+            GuideLines(
+                modifier = modifier,
+                rows = (constraints.maxHeight.dp / mh).toInt(),
+                columns = (constraints.maxWidth.dp / mw).toInt(),
+                color = iconsTintColor.alpha(0.5f),
+                lineSize = 1.dp,
+                visible = guideVisibleState.isVisible
             )
         }
     }
-    Box(
-        modifier = modifier.conditional(
-            condition = orientation == Orientation.Horizontal,
-            onTrue = {
-                verticalScroll(
-                    state = scrollState,
-                    enabled = scrollState.canScrollForward
-                )
-            },
-            onFalse = {
-                horizontalScroll(
-                    state = scrollState,
-                    enabled = scrollState.canScrollForward
+}
+
+@Suppress("SimplifyBooleanWithConstants")
+@Composable
+private fun rememberFiles(
+    path: File? = null,
+    homeDir: File? = null,
+    showHidden: Boolean = true,
+    directoryFirst: Boolean = true,
+    showHomeFolder: Boolean = true
+) = remember(path, homeDir, showHidden, directoryFirst, showHomeFolder) {
+    derivedStateOf {
+        path?.listFiles()
+            ?.sorted()
+            ?.filter { f ->
+                showHidden == true || (!f.name.startsWith("."))
+            }
+            ?.sortedByDescending {
+                directoryFirst == false || it.extension.isEmpty() // todo isDirectory
+            }
+            ?.map { path ->
+                DesktopFolderItem(path)
+            }
+            ?.toMutableList()
+            ?.apply {
+                if (showHomeFolder) add(
+                    DesktopFolderItem(
+                        path = homeDir ?: File("/"),
+                        customName = "Home",
+                        priority = 1
+                    )
                 )
             }
-        )
-    ) {
-//    ScrollArea(
-//        state = scrollbarsState
-//    ) {
-        if (orientation == Orientation.Horizontal) {
-            FlowRow(
-                modifier = Modifier.fillMaxSize(),
-                overflow = FlowRowOverflow.Visible,
-                horizontalArrangement = Arrangement.Start,
-                verticalArrangement = Arrangement.Top,
-                content = { content() }
-            )
-//            HorizontalScrollbar(
-//                modifier = Modifier.align(Alignment.TopEnd)
-//                    .fillMaxWidth()
-//                    .height(4.dp)
-//            ) {
-//                Thumb(
-//                    modifier = Modifier.background(Color.SuperDarkGray)
-//                )
-//            }
-        } else {
-            FlowColumn(
-                modifier = Modifier.fillMaxSize(),
-                overflow = FlowColumnOverflow.Visible,
-                horizontalArrangement = Arrangement.Start,
-                verticalArrangement = Arrangement.Top,
-                content = { content() }
-            )
-//            VerticalScrollbar(
-//                modifier = Modifier.align(Alignment.TopEnd)
-//                    .fillMaxHeight()
-//                    .width(4.dp)
-//            ) {
-//                Thumb(
-//                    modifier = Modifier.background(Color.SuperDarkGray)
-//                )
-//            }
-//        }
-        }
+            ?.sortedByDescending {
+                it.priority
+            } ?: emptyList()
     }
 }
 

+ 3 - 3
src/main/kotlin/eu/mjdev/desktop/components/greeter/Greeter.kt

@@ -14,14 +14,14 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.window.WindowPosition
-import eu.mjdev.desktop.components.custom.UserAvatar
 import eu.mjdev.desktop.components.input.PasswordTextView
+import eu.mjdev.desktop.components.user.UserAvatar
 import eu.mjdev.desktop.data.User
-import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
-import eu.mjdev.desktop.windows.ChromeWindow
 import eu.mjdev.desktop.extensions.ColorUtils.alpha
 import eu.mjdev.desktop.extensions.Compose.rememberState
 import eu.mjdev.desktop.helpers.compose.Orientation
+import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
+import eu.mjdev.desktop.windows.ChromeWindow
 
 // todo : all users
 @Suppress("FunctionName", "unused")

+ 60 - 0
src/main/kotlin/eu/mjdev/desktop/components/grid/GridPlaced.kt

@@ -0,0 +1,60 @@
+package eu.mjdev.desktop.components.grid
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import eu.mjdev.desktop.components.grid.base.*
+import eu.mjdev.desktop.components.grid.base.Measure.calculateCellPlaces
+import eu.mjdev.desktop.components.grid.base.Measure.measure
+import eu.mjdev.desktop.components.sliding.base.VisibilityState
+import eu.mjdev.desktop.components.sliding.base.VisibilityState.Companion.rememberVisibilityState
+import kotlin.math.min
+
+@Composable
+fun GridPlaced(
+    cells: GridPlacedCells,
+    modifier: Modifier = Modifier,
+    placementPolicy: GridPlacedPlacementPolicy = GridPlacedPlacementPolicy.DEFAULT,
+    guideVisible: Boolean = true,
+    guideVisibleState: VisibilityState = rememberVisibilityState(guideVisible),
+    guideLineColor: Color = Color.Black,
+    guideLineSize: Dp = 1.dp,
+    content: GridPlacedScope.() -> Unit
+) {
+    val scopeContent: GridPlacedScopeImpl = GridPlacedScopeImpl(cells, placementPolicy).apply(content)
+    val displayContent: List<GridPlacedContent> = scopeContent.data.toList()
+    Layout(
+        modifier = modifier,
+        content = {
+            displayContent.forEach {
+                it.item(GridPlacedItemScopeImpl)
+            }
+        }
+    ) { measurables, constraints ->
+        check(constraints.maxWidth != Constraints.Infinity) { "Infinity width not allowed in GridPad" }
+        check(constraints.maxHeight != Constraints.Infinity) { "Infinity height not allowed in GridPad" }
+        val cellPlaces = calculateCellPlaces(cells, width = constraints.maxWidth, height = constraints.maxHeight)
+        val placeables = measurables.measure(displayContent, cellPlaces, constraints)
+        val layoutWidth = if (cells.columnsTotalSize.weight == 0f) {
+            min(constraints.maxWidth, cells.columnsTotalSize.fixed.roundToPx())
+        } else {
+            constraints.maxWidth
+        }
+        val layoutHeight = if (cells.rowsTotalSize.weight == 0f) {
+            min(constraints.maxHeight, cells.rowsTotalSize.fixed.roundToPx())
+        } else {
+            constraints.maxHeight
+        }
+        layout(layoutWidth, layoutHeight) {
+            placeables.forEachIndexed { index, placeable ->
+                val contentMetaInfo = displayContent[index]
+                val cellPlaceInfo = cellPlaces[contentMetaInfo.top][contentMetaInfo.left]
+                placeable.placeRelative(x = cellPlaceInfo.x, y = cellPlaceInfo.y)
+            }
+        }
+    }
+}

+ 16 - 0
src/main/kotlin/eu/mjdev/desktop/components/grid/base/CellPlacedInfo.kt

@@ -0,0 +1,16 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.grid.base
+
+data class CellPlacedInfo(
+    val x: Int,
+    val y: Int,
+    val width: Int,
+    val height: Int
+)

+ 51 - 0
src/main/kotlin/eu/mjdev/desktop/components/grid/base/GridPlacedCellSize.kt

@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.grid.base
+
+import androidx.compose.ui.unit.Dp
+
+@Suppress("unused")
+sealed class GridPlacedCellSize {
+    data class Fixed(val size: Dp) : GridPlacedCellSize() {
+        init {
+            check(size.value > 0) { "size have to be > 0" }
+        }
+    }
+
+    data class Weight(val size: Float = 1f) : GridPlacedCellSize() {
+        init {
+            check(size > 0) { "size have to be > 0" }
+        }
+    }
+
+    companion object {
+        private fun <T> mutableListOfElement(size: Int, fillElement: T): MutableList<T> {
+            return (0 until size).map { fillElement }.toMutableList()
+        }
+
+        fun fixed(count: Int, size: Dp): MutableList<GridPlacedCellSize> {
+            return mutableListOfElement(count, Fixed(size = size))
+        }
+
+        fun fixed(sizes: Array<Dp>): MutableList<GridPlacedCellSize> {
+            return sizes.map { Fixed(size = it) }.toMutableList()
+        }
+
+        fun weight(
+            count: Int,
+            size: Float = 1f
+        ): MutableList<GridPlacedCellSize> {
+            return mutableListOfElement(count, Weight(size = size))
+        }
+
+        fun weight(vararg sizes: Float): MutableList<GridPlacedCellSize> {
+            return sizes.map { Weight(size = it) }.toMutableList()
+        }
+    }
+}

+ 96 - 0
src/main/kotlin/eu/mjdev/desktop/components/grid/base/GridPlacedCells.kt

@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.grid.base
+
+import eu.mjdev.desktop.components.grid.base.GridPlacedSpanAnchor.Companion.bottomBound
+import eu.mjdev.desktop.components.grid.base.GridPlacedSpanAnchor.Companion.leftBound
+import eu.mjdev.desktop.components.grid.base.GridPlacedSpanAnchor.Companion.rightBound
+import eu.mjdev.desktop.components.grid.base.GridPlacedSpanAnchor.Companion.topBound
+import eu.mjdev.desktop.components.grid.base.Measure.calculateTotalSize
+
+@Suppress("unused")
+data class GridPlacedCells(
+    val rowSizes: List<GridPlacedCellSize>,
+    val columnSizes: List<GridPlacedCellSize>
+) {
+    constructor(
+        rowSizes: Iterable<GridPlacedCellSize>, columnSizes: Iterable<GridPlacedCellSize>
+    ) : this(rowSizes = rowSizes.toList(), columnSizes = columnSizes.toList())
+
+    constructor(rowCount: Int, columnCount: Int) : this(
+        rowSizes = GridPlacedCellSize.weight(rowCount),
+        columnSizes = GridPlacedCellSize.weight(columnCount)
+    )
+
+    val rowCount: Int = rowSizes.size
+    val columnCount: Int = columnSizes.size
+
+    val rowsTotalSize: TotalSize = rowSizes.calculateTotalSize()
+    val columnsTotalSize: TotalSize = columnSizes.calculateTotalSize()
+
+    class Builder(rowCount: Int, columnCount: Int) {
+        private val rowSizes: MutableList<GridPlacedCellSize> = GridPlacedCellSize.weight(rowCount)
+        private val columnSizes: MutableList<GridPlacedCellSize> = GridPlacedCellSize.weight(columnCount)
+
+        fun rowSize(index: Int, size: GridPlacedCellSize): Builder = apply {
+            rowSizes[index] = size
+        }
+
+        fun rowsSize(size: GridPlacedCellSize): Builder = apply {
+            rowSizes.fill(size)
+        }
+
+        fun columnSize(index: Int, size: GridPlacedCellSize): Builder = apply {
+            columnSizes[index] = size
+        }
+
+        fun columnsSize(size: GridPlacedCellSize): Builder = apply {
+            columnSizes.fill(size)
+        }
+
+        fun build(): GridPlacedCells =
+            GridPlacedCells(rowSizes = rowSizes, columnSizes = columnSizes)
+    }
+
+    companion object {
+        fun GridPlacedCells.firstRow(placementPolicy: GridPlacedPlacementPolicy): Int {
+            return when (placementPolicy.verticalDirection) {
+                GridPlacedPlacementPolicy.VerticalDirection.TOP_BOTTOM -> 0
+                GridPlacedPlacementPolicy.VerticalDirection.BOTTOM_TOP -> rowCount - 1
+            }
+        }
+
+        fun GridPlacedCells.firstColumn(placementPolicy: GridPlacedPlacementPolicy): Int {
+            return when (placementPolicy.horizontalDirection) {
+                GridPlacedPlacementPolicy.HorizontalDirection.START_END -> 0
+                GridPlacedPlacementPolicy.HorizontalDirection.END_START -> columnCount - 1
+            }
+        }
+
+        fun GridPlacedCells.isRowOutsideOfGrid(
+            row: Int,
+            rowSpan: Int,
+            anchor: GridPlacedSpanAnchor
+        ): Boolean {
+            val top = anchor.topBound(row, rowSpan)
+            val bottom = anchor.bottomBound(row, rowSpan)
+            return top < 0 || bottom >= rowCount
+        }
+
+        fun GridPlacedCells.isColumnOutsideOfGrid(
+            column: Int,
+            columnSpan: Int,
+            anchor: GridPlacedSpanAnchor
+        ): Boolean {
+            val left = anchor.leftBound(column, columnSpan)
+            val right = anchor.rightBound(column, columnSpan)
+            return left < 0 || right >= columnCount
+        }
+    }
+}

+ 22 - 0
src/main/kotlin/eu/mjdev/desktop/components/grid/base/GridPlacedContent.kt

@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.grid.base
+
+import androidx.compose.runtime.Composable
+
+class GridPlacedContent(
+    val left: Int,
+    val top: Int,
+    val right: Int,
+    val bottom: Int,
+    val item: @Composable GridPlacedItemScope.() -> Unit
+) {
+    val rowSpan: Int = bottom - top + 1
+    val columnSpan: Int = right - left + 1
+}

+ 15 - 0
src/main/kotlin/eu/mjdev/desktop/components/grid/base/GridPlacedItemScope.kt

@@ -0,0 +1,15 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.grid.base
+
+import androidx.compose.runtime.Stable
+
+@Stable
+@GridPlacedScopeMarker
+interface GridPlacedItemScope

+ 11 - 0
src/main/kotlin/eu/mjdev/desktop/components/grid/base/GridPlacedItemScopeImpl.kt

@@ -0,0 +1,11 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.grid.base
+
+object GridPlacedItemScopeImpl : GridPlacedItemScope

+ 46 - 0
src/main/kotlin/eu/mjdev/desktop/components/grid/base/GridPlacedPlacementPolicy.kt

@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.grid.base
+
+data class GridPlacedPlacementPolicy(
+    val mainAxis: MainAxis = MainAxis.HORIZONTAL,
+    val horizontalDirection: HorizontalDirection = HorizontalDirection.START_END,
+    val verticalDirection: VerticalDirection = VerticalDirection.TOP_BOTTOM
+) {
+    internal val anchor: GridPlacedSpanAnchor = run {
+        val horizontalDirection = when (this.horizontalDirection) {
+            HorizontalDirection.START_END -> GridPlacedSpanAnchor.Horizontal.START
+            HorizontalDirection.END_START -> GridPlacedSpanAnchor.Horizontal.END
+        }
+        val verticalDirection = when (this.verticalDirection) {
+            VerticalDirection.TOP_BOTTOM -> GridPlacedSpanAnchor.Vertical.TOP
+            VerticalDirection.BOTTOM_TOP -> GridPlacedSpanAnchor.Vertical.BOTTOM
+        }
+        GridPlacedSpanAnchor(horizontalDirection, verticalDirection)
+    }
+
+    enum class MainAxis {
+        HORIZONTAL,
+        VERTICAL
+    }
+
+    enum class HorizontalDirection {
+        START_END,
+        END_START
+    }
+
+    enum class VerticalDirection {
+        TOP_BOTTOM,
+        BOTTOM_TOP
+    }
+
+    companion object {
+        val DEFAULT: GridPlacedPlacementPolicy = GridPlacedPlacementPolicy()
+    }
+}

+ 28 - 0
src/main/kotlin/eu/mjdev/desktop/components/grid/base/GridPlacedScope.kt

@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.grid.base
+
+import androidx.compose.runtime.Composable
+
+@GridPlacedScopeMarker
+sealed interface GridPlacedScope {
+    fun item(
+        row: Int,
+        column: Int,
+        rowSpan: Int = 1,
+        columnSpan: Int = 1,
+        itemContent: @Composable GridPlacedItemScope.() -> Unit
+    )
+
+    fun item(
+        rowSpan: Int = 1,
+        columnSpan: Int = 1,
+        itemContent: @Composable GridPlacedItemScope.() -> Unit
+    )
+}

+ 204 - 0
src/main/kotlin/eu/mjdev/desktop/components/grid/base/GridPlacedScopeImpl.kt

@@ -0,0 +1,204 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.grid.base
+
+import androidx.compose.runtime.Composable
+import eu.mjdev.desktop.components.grid.base.GridPlacedCells.Companion.firstColumn
+import eu.mjdev.desktop.components.grid.base.GridPlacedCells.Companion.firstRow
+import eu.mjdev.desktop.components.grid.base.GridPlacedCells.Companion.isColumnOutsideOfGrid
+import eu.mjdev.desktop.components.grid.base.GridPlacedCells.Companion.isRowOutsideOfGrid
+import eu.mjdev.desktop.components.grid.base.GridPlacedSpanAnchor.Companion.bottomBound
+import eu.mjdev.desktop.components.grid.base.GridPlacedSpanAnchor.Companion.leftBound
+import eu.mjdev.desktop.components.grid.base.GridPlacedSpanAnchor.Companion.rightBound
+import eu.mjdev.desktop.components.grid.base.GridPlacedSpanAnchor.Companion.topBound
+
+class GridPlacedScopeImpl(
+    private val cells: GridPlacedCells,
+    private val placementPolicy: GridPlacedPlacementPolicy
+) : GridPlacedScope {
+    val data: MutableList<GridPlacedContent> = mutableListOf()
+
+    override fun item(
+        row: Int,
+        column: Int,
+        rowSpan: Int,
+        columnSpan: Int,
+        itemContent: @Composable GridPlacedItemScope.() -> Unit
+    ) {
+        checkSpan(rowSpan = rowSpan, columnSpan = columnSpan)
+        placeExplicitly(
+            row = row,
+            column = column,
+            rowSpan = rowSpan,
+            columnSpan = columnSpan,
+            itemContent = itemContent
+        )
+    }
+
+    override fun item(
+        rowSpan: Int,
+        columnSpan: Int,
+        itemContent: @Composable GridPlacedItemScope.() -> Unit
+    ) {
+        checkSpan(rowSpan = rowSpan, columnSpan = columnSpan)
+        placeImplicitly(
+            rowSpan = rowSpan,
+            columnSpan = columnSpan,
+            itemContent = itemContent
+        )
+    }
+
+    private fun checkSpan(rowSpan: Int, columnSpan: Int) {
+        check(rowSpan > 0) { "`rowSpan` must be > 0" }
+        check(columnSpan > 0) { "`columnSpan` must be > 0" }
+    }
+
+    private fun placeImplicitly(
+        rowSpan: Int,
+        columnSpan: Int,
+        itemContent: @Composable GridPlacedItemScope.() -> Unit
+    ) {
+        val anchor = placementPolicy.anchor
+        val lastItem = data.lastOrNull()
+        var row: Int
+        var column: Int
+        when (placementPolicy.mainAxis) {
+            GridPlacedPlacementPolicy.MainAxis.HORIZONTAL -> {
+                row = findCurrentRow(cells, placementPolicy, lastItem)
+                column = findNextColumn(cells, placementPolicy, lastItem)
+                if (cells.isColumnOutsideOfGrid(column, columnSpan, anchor)) {
+                    column = cells.firstColumn(placementPolicy)
+                    row = findNextRow(cells, placementPolicy, lastItem)
+                }
+            }
+
+            GridPlacedPlacementPolicy.MainAxis.VERTICAL -> {
+                column = findCurrentColumn(cells, placementPolicy, lastItem)
+                row = findNextRow(cells, placementPolicy, lastItem)
+                if (cells.isRowOutsideOfGrid(row, rowSpan, anchor)) {
+                    row = cells.firstRow(placementPolicy)
+                    column = findNextColumn(cells, placementPolicy, lastItem)
+                }
+            }
+        }
+        placeExplicitly(
+            row = row,
+            column = column,
+            rowSpan = rowSpan,
+            columnSpan = columnSpan,
+            itemContent = itemContent
+        )
+    }
+
+    private fun placeExplicitly(
+        row: Int,
+        column: Int,
+        rowSpan: Int,
+        columnSpan: Int,
+        itemContent: @Composable GridPlacedItemScope.() -> Unit
+    ) {
+        val anchor = placementPolicy.anchor
+        val rowOutside = cells.isRowOutsideOfGrid(row, rowSpan, anchor)
+        val columnOutside = cells.isColumnOutsideOfGrid(column, columnSpan, anchor)
+        if (rowOutside || columnOutside) {
+            onSkipped(row, column, rowSpan, columnSpan)
+        } else {
+            onPlaced(
+                left = anchor.leftBound(column, columnSpan),
+                top = anchor.topBound(row, rowSpan),
+                right = anchor.rightBound(column, columnSpan),
+                bottom = anchor.bottomBound(row, rowSpan),
+                itemContent = itemContent
+            )
+        }
+    }
+
+    private fun onSkipped(row: Int?, column: Int?, rowSpan: Int, columnSpan: Int) {
+        println(
+            """
+                Skipped position: [${row}x$column], span size: [${rowSpan}x$columnSpan]
+                Grid size: [${cells.rowCount}x${cells.columnCount}]
+            """.trimIndent()
+        )
+    }
+
+    private fun onPlaced(
+        left: Int,
+        top: Int,
+        right: Int,
+        bottom: Int,
+        itemContent: @Composable GridPlacedItemScope.() -> Unit
+    ) {
+        data.add(
+            GridPlacedContent(
+                top = top,
+                left = left,
+                right = right,
+                bottom = bottom,
+                item = { itemContent() }
+            )
+        )
+    }
+
+    private fun findCurrentRow(
+        cells: GridPlacedCells,
+        placementPolicy: GridPlacedPlacementPolicy,
+        lastItem: GridPlacedContent?
+    ): Int {
+        return if (lastItem != null) {
+            when (placementPolicy.verticalDirection) {
+                GridPlacedPlacementPolicy.VerticalDirection.TOP_BOTTOM -> lastItem.top
+                GridPlacedPlacementPolicy.VerticalDirection.BOTTOM_TOP -> lastItem.bottom
+            }
+        } else {
+            cells.firstRow(placementPolicy)
+        }
+    }
+
+    private fun findNextRow(
+        cells: GridPlacedCells,
+        placementPolicy: GridPlacedPlacementPolicy,
+        lastItem: GridPlacedContent?
+    ): Int {
+        val lastRow = findCurrentRow(cells, placementPolicy, lastItem)
+        val lastRowSpan = lastItem?.rowSpan ?: 0
+        return when (placementPolicy.verticalDirection) {
+            GridPlacedPlacementPolicy.VerticalDirection.TOP_BOTTOM -> lastRow + lastRowSpan
+            GridPlacedPlacementPolicy.VerticalDirection.BOTTOM_TOP -> lastRow - lastRowSpan
+        }
+    }
+
+    private fun findCurrentColumn(
+        cells: GridPlacedCells,
+        placementPolicy: GridPlacedPlacementPolicy,
+        lastItem: GridPlacedContent?
+    ): Int {
+        return if (lastItem != null) {
+            when (placementPolicy.horizontalDirection) {
+                GridPlacedPlacementPolicy.HorizontalDirection.START_END -> lastItem.left
+                GridPlacedPlacementPolicy.HorizontalDirection.END_START -> lastItem.right
+            }
+        } else {
+            cells.firstColumn(placementPolicy)
+        }
+    }
+
+    private fun findNextColumn(
+        cells: GridPlacedCells,
+        placementPolicy: GridPlacedPlacementPolicy,
+        lastItem: GridPlacedContent?
+    ): Int {
+        val lastColumn = findCurrentColumn(cells, placementPolicy, lastItem)
+        val lastColumnSpan = lastItem?.columnSpan ?: 0
+        return when (placementPolicy.horizontalDirection) {
+            GridPlacedPlacementPolicy.HorizontalDirection.START_END -> lastColumn + lastColumnSpan
+            GridPlacedPlacementPolicy.HorizontalDirection.END_START -> lastColumn - lastColumnSpan
+        }
+    }
+}

+ 4 - 0
src/main/kotlin/eu/mjdev/desktop/components/grid/base/GridPlacedScopeMarker.kt

@@ -0,0 +1,4 @@
+package eu.mjdev.desktop.components.grid.base
+
+@DslMarker
+annotation class GridPlacedScopeMarker

+ 51 - 0
src/main/kotlin/eu/mjdev/desktop/components/grid/base/GridPlacedSpanAnchor.kt

@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.grid.base
+
+data class GridPlacedSpanAnchor(val horizontal: Horizontal, val vertical: Vertical) {
+    enum class Horizontal {
+        START,
+        END
+    }
+
+    enum class Vertical {
+        TOP,
+        BOTTOM
+    }
+
+    companion object {
+        fun GridPlacedSpanAnchor.leftBound(column: Int, span: Int): Int {
+            return when (horizontal) {
+                Horizontal.START -> column
+                Horizontal.END -> column - span + 1
+            }
+        }
+
+        fun GridPlacedSpanAnchor.rightBound(column: Int, span: Int): Int {
+            return when (this.horizontal) {
+                Horizontal.START -> column + span - 1
+                Horizontal.END -> column
+            }
+        }
+
+        fun GridPlacedSpanAnchor.topBound(row: Int, span: Int): Int {
+            return when (this.vertical) {
+                Vertical.TOP -> row
+                Vertical.BOTTOM -> row - span + 1
+            }
+        }
+
+        fun GridPlacedSpanAnchor.bottomBound(row: Int, span: Int): Int {
+            return when (this.vertical) {
+                Vertical.TOP -> row + span - 1
+                Vertical.BOTTOM -> row
+            }
+        }
+    }
+}

+ 101 - 0
src/main/kotlin/eu/mjdev/desktop/components/grid/base/Measure.kt

@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.grid.base
+
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.dp
+import kotlin.math.min
+import kotlin.math.roundToInt
+
+object Measure {
+    fun List<Measurable>.measure(
+        content: List<GridPlacedContent>,
+        cellPlaces: Array<Array<CellPlacedInfo>>,
+        constraints: Constraints
+    ): List<Placeable> = mapIndexed { index, measurable ->
+        val contentMetaInfo = content[index]
+        val maxWidth = (contentMetaInfo.left..contentMetaInfo.right).sumOf { column ->
+            cellPlaces[contentMetaInfo.top][column].width
+        }
+        val maxHeight = (contentMetaInfo.top..contentMetaInfo.bottom).sumOf { row ->
+            cellPlaces[row][contentMetaInfo.left].height
+        }
+        measurable.measure(
+            constraints.copy(
+                minWidth = min(constraints.minWidth, maxWidth),
+                maxWidth = maxWidth,
+                minHeight = min(constraints.minHeight, maxHeight),
+                maxHeight = maxHeight
+            )
+        )
+    }
+
+    fun MeasureScope.calculateCellPlaces(
+        cells: GridPlacedCells, width: Int, height: Int
+    ): Array<Array<CellPlacedInfo>> {
+        val columnWidths = calculateSizesForDimension(width, cells.columnSizes, cells.columnsTotalSize)
+        val columnHeights = calculateSizesForDimension(height, cells.rowSizes, cells.rowsTotalSize)
+        var y = 0f
+        val cellPlaces = columnHeights.map { columnHeight ->
+            var x = 0f
+            val cellY = y
+            y += columnHeight
+            columnWidths.map { columnWidth ->
+                val cellX = x
+                x += columnWidth
+                CellPlacedInfo(
+                    x = cellX.roundToInt(),
+                    y = cellY.roundToInt(),
+                    width = columnWidth,
+                    height = columnHeight
+                )
+            }
+        }
+        return cellPlaces.map { it.toTypedArray() }.toTypedArray()
+    }
+
+    private fun MeasureScope.calculateSizesForDimension(
+        availableSize: Int, cellSizes: List<GridPlacedCellSize>, totalSize: TotalSize
+    ): List<Int> {
+        val availableWeight = availableSize - totalSize.fixed.toPx()
+        var reminder = 0f
+        return cellSizes.map { cellSize ->
+            when (cellSize) {
+                is GridPlacedCellSize.Fixed -> {
+                    val floatSize = cellSize.size.toPx() + reminder
+                    val size = floatSize.roundToInt()
+                    reminder = floatSize - size
+                    size
+                }
+
+                is GridPlacedCellSize.Weight -> {
+                    val floatSize = availableWeight * cellSize.size / totalSize.weight + reminder
+                    val size = floatSize.roundToInt()
+                    reminder = floatSize - size
+                    size
+                }
+            }
+        }.toMutableList()
+    }
+
+    fun Iterable<GridPlacedCellSize>.calculateTotalSize(): TotalSize {
+        var totalWeightSize = 0f
+        var totalFixedSize = 0f.dp
+        forEach {
+            when (it) {
+                is GridPlacedCellSize.Weight -> totalWeightSize += it.size
+                is GridPlacedCellSize.Fixed -> totalFixedSize += it.size
+            }
+        }
+        return TotalSize(weight = totalWeightSize, fixed = totalFixedSize)
+    }
+}

+ 16 - 0
src/main/kotlin/eu/mjdev/desktop/components/grid/base/TotalSize.kt

@@ -0,0 +1,16 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.grid.base
+
+import androidx.compose.ui.unit.Dp
+
+data class TotalSize(
+    val weight: Float,
+    val fixed: Dp
+)

+ 57 - 0
src/main/kotlin/eu/mjdev/desktop/components/guide/GuideView.kt

@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.guide
+
+import androidx.compose.desktop.ui.tooling.preview.Preview
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun GuideLines(
+    modifier: Modifier = Modifier,
+    rows: Int = 10,
+    columns: Int = 10,
+    color: Color = Color.Black,
+    lineSize: Dp = 1.dp,
+    visible: Boolean = true,
+) = Canvas(
+    modifier = modifier
+) {
+    (0..columns).forEach { i ->
+        val x = i * size.width / columns
+        drawLine(
+            start = Offset(x, 0f),
+            end = Offset(x, size.height),
+            color = if (visible) color else Color.Transparent,
+            strokeWidth = lineSize.toPx(),
+            cap = StrokeCap.Round
+        )
+    }
+    (0..rows).forEach { i ->
+        val y = i * size.height / rows
+        drawLine(
+            start = Offset(0f, y),
+            end = Offset(size.width, y),
+            color = if (visible) color else Color.Transparent,
+            strokeWidth = lineSize.toPx(),
+            cap = StrokeCap.Round
+        )
+    }
+}
+
+@Preview
+@Composable
+fun GuideLinesPreview() = GuideLines(modifier = Modifier.fillMaxSize())

+ 51 - 0
src/main/kotlin/eu/mjdev/desktop/components/progress/DashedProgressIndicator.kt

@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.progress
+
+import androidx.compose.desktop.ui.tooling.preview.Preview
+import androidx.compose.foundation.Canvas
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun DashedProgressIndicator(
+    modifier: Modifier = Modifier,
+    progress: Int = 3,
+    totalNumberOfBars: Int = 10,
+    dashColor: Color = Color.LightGray,
+    strokeWidth: Float = 13f
+) = Canvas(
+    modifier = modifier
+) {
+    val barArea = size.width / totalNumberOfBars
+    val barLength = barArea - 10.dp.toPx()
+    var nextBarStartPosition = 0F
+    for (i in 0..totalNumberOfBars) {
+        val barStartPosition = nextBarStartPosition + 5.dp.toPx()
+        val barEndPosition = barStartPosition + barLength
+        val start = Offset(x = barStartPosition, y = size.height / 2)
+        val end = Offset(x = barEndPosition, y = size.height / 2)
+        drawLine(
+            cap = StrokeCap.Round,
+            color = if (i < progress) dashColor else dashColor.copy(alpha = .5F),
+            start = start,
+            end = end,
+            strokeWidth = strokeWidth
+        )
+        nextBarStartPosition = barEndPosition + 5.dp.toPx()
+    }
+}
+
+@Preview
+@Composable
+fun DashedProgressIndicatorPreview() = DashedProgressIndicator()

+ 17 - 3
src/main/kotlin/eu/mjdev/desktop/components/custom/UserAvatar.kt → src/main/kotlin/eu/mjdev/desktop/components/user/UserAvatar.kt

@@ -1,4 +1,12 @@
-package eu.mjdev.desktop.components.custom
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package eu.mjdev.desktop.components.user
 
 import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.foundation.background
@@ -12,7 +20,9 @@ import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.TextUnit
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
 import eu.mjdev.desktop.components.appbar.AppBar
 import eu.mjdev.desktop.components.button.TransparentButton
 import eu.mjdev.desktop.components.image.ImageAny
@@ -29,6 +39,8 @@ fun UserAvatar(
     modifier: Modifier = Modifier,
     avatarSize: Dp = 64.dp,
     orientation: Orientation = Orientation.Vertical,
+    titleTextSize: TextUnit = 16.sp,
+    detailTextSize: TextUnit = 14.sp,
     onUserAvatarClick: () -> Unit = {},
     actions: @Composable RowScope.() -> Unit = {},
 ) = withDesktopScope {
@@ -65,13 +77,15 @@ fun UserAvatar(
                         text = api.currentUser.userName,
                         color = textColor,
                         textAlign = TextAlign.Start,
-                        fontWeight = FontWeight.Bold
+                        fontWeight = FontWeight.Bold,
+                        fontSize = titleTextSize
                     )
                     TextAny(
                         text = api.machineName,
                         color = textColor,
                         textAlign = TextAlign.Start,
-                        fontWeight = FontWeight.Medium
+                        fontWeight = FontWeight.Medium,
+                        fontSize = detailTextSize
                     )
                 }
             },

+ 8 - 8
src/main/kotlin/eu/mjdev/desktop/data/DesktopFile.kt

@@ -4,7 +4,7 @@ import eu.mjdev.desktop.extensions.Custom.ParsedBoolean
 import eu.mjdev.desktop.extensions.Custom.ParsedList
 import eu.mjdev.desktop.extensions.Custom.ParsedString
 import eu.mjdev.desktop.extensions.Custom.text
-import eu.mjdev.desktop.helpers.system.LinuxIni
+import eu.mjdev.desktop.helpers.desktopparser.DesktopFileParser
 import org.ini4j.Ini
 import org.ini4j.Profile.Section
 import java.io.File
@@ -15,11 +15,11 @@ class DesktopFile(
     val correctDir: File = File("/tmp/.corrected-desktop-files/"),
     val correctedFile: File = File(correctDir, file.name),
     var fileData: String = (if (correctedFile.exists()) correctedFile else file).text,
-    private var content: LinuxIni = (runCatching {
-        LinuxIni(fileData)
+    private var content: DesktopFileParser = (runCatching {
+        DesktopFileParser(fileData)
     }.onFailure { e ->
-       e.printStackTrace()
-    }.getOrNull() ?: LinuxIni())
+        e.printStackTrace()
+    }.getOrNull() ?: DesktopFileParser())
 ) {
     var fileName: String = if (correctedFile.exists()) correctedFile.name else file.name
     val absolutePath: String = if (correctedFile.exists()) correctedFile.absolutePath else file.absolutePath
@@ -34,7 +34,7 @@ class DesktopFile(
             it.value.toString()
         }
 
-    val desktopSection : DesktopSectionScope?
+    val desktopSection: DesktopSectionScope?
         get() = content[DesktopEntryType.DesktopEntry]?.let { DesktopSectionScope(it) }
 
     val themeSection
@@ -57,7 +57,7 @@ class DesktopFile(
                     writeTo(f)
                     file = f
                     fileData = file.text
-                    content = LinuxIni(fileData)
+                    content = DesktopFileParser(fileData)
                 }
             }.onFailure { e ->
                 e.printStackTrace()
@@ -302,7 +302,7 @@ class DesktopFile(
 
         operator fun Ini.get(
             type: DesktopEntryType
-        ) : Section? = get(type.text)
+        ): Section? = get(type.text)
 
         fun desktopFile(
             file: File,

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

@@ -39,6 +39,8 @@ import coil3.memory.MemoryCache
 import coil3.request.CachePolicy
 import coil3.svg.SvgDecoder
 import eu.mjdev.desktop.helpers.compose.FocusHelper
+import eu.mjdev.desktop.provider.DesktopProvider
+import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
@@ -433,6 +435,16 @@ object Compose {
         content = content
     )
 
+    @Composable
+    fun isLandscapeMode(
+        api: DesktopProvider = LocalDesktop.current
+    ) = api.isLandscapeMode
+
+    @Composable
+    fun rememberDpSize(
+        initial: DpSize = DpSize.Zero
+    ) = remember { mutableStateOf(initial) }
+
 //    Toolkit.getDefaultToolkit().addAWTEventListener({ event ->
 //        }, AWTEvent.MOUSE_EVENT_MASK or AWTEvent.FOCUS_EVENT_MASK
 //    )

+ 5 - 5
src/main/kotlin/eu/mjdev/desktop/helpers/system/LinuxIni.kt → src/main/kotlin/eu/mjdev/desktop/helpers/desktopparser/DesktopFileParser.kt

@@ -8,7 +8,7 @@
 
 @file:Suppress("DEPRECATION")
 
-package eu.mjdev.desktop.helpers.system
+package eu.mjdev.desktop.helpers.desktopparser
 
 import org.ini4j.Config
 import org.ini4j.Wini
@@ -16,7 +16,7 @@ import java.io.InputStream
 import java.io.StringBufferInputStream
 import java.nio.charset.Charset
 
-class LinuxIni(
+class DesktopFileParser(
     stream: InputStream,
     charset: Charset = Charsets.UTF_8
 ) : Wini(stream) {
@@ -40,8 +40,8 @@ class LinuxIni(
     ) {
         try {
             super.load(input)
-        } catch (e:Exception) {
-            throw(Exception("Error in file: file:///${file.absolutePath}", e))
+        } catch (e: Exception) {
+//            throw(Exception("Error in file: file:///${file.absolutePath}", e))
         }
     }
-}
+}

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

@@ -98,6 +98,9 @@ class DesktopProvider(
             toolkit.screenSize.let { screen -> DpSize(screen.width.dp, screen.height.dp) }
         }.getOrNull() ?: DpSize.Zero
 
+    val isLandscapeMode: Boolean
+        get() = containerSize.let { it.width > it.height }
+
     val isAlwaysOnTopSupported
         get() = runCatching {
             toolkit.isAlwaysOnTopSupported