From 86a763f89ac0f9b0451846d9d76887d097112c6a Mon Sep 17 00:00:00 2001 From: sanlorng Date: Thu, 30 May 2024 00:41:53 +0800 Subject: [PATCH 001/247] [fluent] Add `FluentTheme.shapes`. --- .../kotlin/com/konyaco/fluent/FluentTheme.kt | 12 ++++++++-- .../kotlin/com/konyaco/fluent/Shape.kt | 24 +++++++++++++++++++ .../com/konyaco/fluent/background/Layer.kt | 5 ++-- .../com/konyaco/fluent/component/Button.kt | 8 +++---- .../com/konyaco/fluent/component/CheckBox.kt | 3 +-- .../konyaco/fluent/component/ColorPicker.kt | 3 +-- .../com/konyaco/fluent/component/Dialog.kt | 5 ++-- .../com/konyaco/fluent/component/Dropdown.kt | 5 ++-- .../com/konyaco/fluent/component/Flyout.kt | 7 +++--- .../com/konyaco/fluent/component/ListItem.kt | 3 +-- .../konyaco/fluent/component/MenuFlyout.kt | 6 ++--- .../com/konyaco/fluent/component/SideNav.kt | 3 +-- .../com/konyaco/fluent/component/TextField.kt | 5 ++-- .../kotlin/com/konyaco/fluent/surface/Card.kt | 5 ++-- .../fluent/gallery/screen/HomeScreen.kt | 5 ++-- .../gallery/screen/design/TypographyScreen.kt | 3 +-- .../gallery/screen/settings/SettingsScreen.kt | 5 ++-- 17 files changed, 62 insertions(+), 45 deletions(-) create mode 100644 fluent/src/commonMain/kotlin/com/konyaco/fluent/Shape.kt diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/FluentTheme.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/FluentTheme.kt index 93045cbc..d9e5f4b5 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/FluentTheme.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/FluentTheme.kt @@ -24,6 +24,7 @@ import com.konyaco.fluent.component.ProvideFontIcon fun FluentTheme( colors: Colors = FluentTheme.colors, typography: Typography = FluentTheme.typography, + shapes: Shapes = FluentTheme.shapes, useAcrylicPopup: Boolean = LocalAcrylicPopupEnabled.current, compactMode: Boolean = true, content: @Composable () -> Unit @@ -40,7 +41,8 @@ fun FluentTheme( colors.fillAccent.selectedTextBackground.copy(0.4f) ), LocalContentDialog provides contentDialogHostState, - LocalCompactMode provides compactMode + LocalCompactMode provides compactMode, + LocalShapes provides shapes ) { ContentDialogHost(contentDialogHostState) Box(modifier = Modifier.behindAcrylic()) { @@ -86,7 +88,7 @@ fun FluentTheme( typography: Typography = FluentTheme.typography, content: @Composable () -> Unit ) { - FluentTheme(colors, typography, useAcrylicPopup = false, compactMode = true, content) + FluentTheme(colors, typography, LocalShapes.current, useAcrylicPopup = false, compactMode = true, content) } @Composable @@ -102,10 +104,16 @@ object FluentTheme { @Composable @ReadOnlyComposable get() = LocalColors.current + val typography: Typography @Composable @ReadOnlyComposable get() = LocalTypography.current + + val shapes: Shapes + @Composable + @ReadOnlyComposable + get() = LocalShapes.current } internal val LocalColors = staticCompositionLocalOf { lightColors() } diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/Shape.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/Shape.kt new file mode 100644 index 00000000..2e4823a8 --- /dev/null +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/Shape.kt @@ -0,0 +1,24 @@ +package com.konyaco.fluent + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.dp + +/** https://learn.microsoft.com/en-us/windows/apps/design/signature-experiences/geometry */ +@Immutable +class Shapes( + val overlay: Shape, + val control: Shape, + val intersectionEdge: Shape, +) + +internal val LocalShapes = staticCompositionLocalOf { + Shapes( + overlay = RoundedCornerShape(8.dp), + control = RoundedCornerShape(4.dp), + intersectionEdge = RectangleShape + ) +} diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Layer.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Layer.kt index 860d5962..a66928c2 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Layer.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Layer.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CornerBasedShape import androidx.compose.foundation.shape.CutCornerShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable @@ -56,7 +55,7 @@ enum class BackgroundSizing { @Composable fun Layer( modifier: Modifier = Modifier, - shape: Shape = RoundedCornerShape(size = 4.dp), + shape: Shape = FluentTheme.shapes.control, color: Color = FluentTheme.colors.background.layer.default, contentColor: Color = FluentTheme.colors.text.text.primary, border: BorderStroke? = BorderStroke(1.dp, FluentTheme.colors.stroke.card.default), @@ -83,7 +82,7 @@ fun Layer( @Composable fun Layer( modifier: Modifier = Modifier, - shape: Shape = RoundedCornerShape(size = 4.dp), + shape: Shape = FluentTheme.shapes.control, color: Color = FluentTheme.colors.background.layer.default, contentColor: Color = FluentTheme.colors.text.text.primary, border: BorderStroke? = BorderStroke(1.dp, FluentTheme.colors.stroke.card.default), diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Button.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Button.kt index b670eea7..35aa6ebc 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Button.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Button.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.selection.selectable -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable @@ -322,7 +321,7 @@ fun SplitButton( val borderBrush = currentColor.borderBrush val endContentOffset = remember { mutableStateOf(0f) } Layer( - modifier = modifier.border(BorderStroke(buttonBorderStrokeWidth, currentColor.borderBrush), buttonShape) + modifier = modifier.border(BorderStroke(buttonBorderStrokeWidth, currentColor.borderBrush), FluentTheme.shapes.control) .drawWithCache { /* draw split broder */ val path = Path() @@ -335,7 +334,7 @@ fun SplitButton( drawPath(path, borderBrush, style = Stroke(strokeWidth)) } }, - shape = buttonShape, + shape = FluentTheme.shapes.control, color = Color.Transparent, contentColor = currentColor.contentColor, /* workaround for outside border padding */ @@ -433,7 +432,7 @@ private fun Button( content: @Composable (RowScope.() -> Unit) ) { ButtonLayer( - shape = buttonShape, + shape = FluentTheme.shapes.control, displayBorder = true, buttonColors = buttonColors, interaction = interaction, @@ -632,5 +631,4 @@ private fun AnimatedDropDownIcon(interaction: MutableInteractionSource) { } private val buttonMinHeight = 32.dp -private val buttonShape = RoundedCornerShape(size = 4.dp) private val buttonBorderStrokeWidth = 1.dp \ No newline at end of file diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/CheckBox.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/CheckBox.kt index b57a5f4d..5ec4fef1 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/CheckBox.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/CheckBox.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable @@ -69,7 +68,7 @@ fun CheckBox( ) Layer( modifier = Modifier.size(20.dp), - shape = RoundedCornerShape(4.dp), + shape = FluentTheme.shapes.control, color = fillColor, contentColor = color.contentColor, border = BorderStroke(1.dp, color.borderColor), diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/ColorPicker.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/ColorPicker.kt index c9d39f37..4dfbaf9e 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/ColorPicker.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/ColorPicker.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -104,7 +103,7 @@ fun ColorPicker( .wrapContentWidth(Alignment.End) .width(44.dp) .height(256.dp) - .alphaBackground(RoundedCornerShape(4.dp), alphaEnabled), + .alphaBackground(FluentTheme.shapes.control, alphaEnabled), backgroundSizing = BackgroundSizing.OuterBorderEdge, color = color ) {} diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dialog.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dialog.kt index 416fd461..01817f0e 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dialog.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dialog.kt @@ -20,7 +20,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -103,10 +102,10 @@ fun FluentDialog( enter = fadeIn(tween) + scaleIn(tween, initialScale = 1.05f), exit = fadeOut(tween) + scaleOut(tween, targetScale = 1.05f) ) { - Mica(Modifier.wrapContentSize().clip(RoundedCornerShape(8.dp))) { + Mica(Modifier.wrapContentSize().clip(FluentTheme.shapes.overlay)) { Layer( Modifier.wrapContentSize().widthIn(size.min, size.max), - shape = RoundedCornerShape(size = 8.dp), + shape = FluentTheme.shapes.overlay, border = BorderStroke(1.dp, FluentTheme.colors.stroke.surface.default), backgroundSizing = BackgroundSizing.InnerBorderEdge, color = FluentTheme.colors.background.solid.base, diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dropdown.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dropdown.kt index 9014fe52..2d370005 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dropdown.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dropdown.kt @@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState @@ -126,10 +125,10 @@ internal fun DropdownMenuContent( ), // TODO: If popup direction is upward, the expanding animation should be bottom-to-top. exit = fadeOut(tween(FluentDuration.ShortDuration, easing = FluentEasing.FastDismissEasing)) ) { - Mica(Modifier.shadow(8.dp, RoundedCornerShape(8.dp)).clip(RoundedCornerShape(8.dp))) { + Mica(Modifier.shadow(8.dp, FluentTheme.shapes.overlay).clip(FluentTheme.shapes.overlay)) { // TODO: Dropdown should use Acrylic material. Layer( - shape = RoundedCornerShape(8.dp), + shape = FluentTheme.shapes.overlay, border = BorderStroke(1.dp, FluentTheme.colors.stroke.surface.flyout), backgroundSizing = BackgroundSizing.InnerBorderEdge ) { diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Flyout.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Flyout.kt index 8fb7ef3f..104bd20f 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Flyout.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Flyout.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue @@ -106,7 +105,7 @@ fun Flyout( modifier: Modifier = Modifier, placement: FlyoutPlacement = FlyoutPlacement.Auto, adaptivePlacement: Boolean = false, - shape: Shape = RoundedCornerShape(8.dp), + shape: Shape = FluentTheme.shapes.overlay, onKeyEvent: ((keyEvent: KeyEvent) -> Boolean)? = null, onPreviewKeyEvent: ((keyEvent: KeyEvent) -> Boolean)? = null, content: @Composable () -> Unit @@ -132,7 +131,7 @@ internal fun BasicFlyout( onDismissRequest: () -> Unit, modifier: Modifier = Modifier, enterPlacementAnimation: (placement: FlyoutPlacement) -> EnterTransition = ::defaultFlyoutEnterPlacementAnimation, - shape: Shape = RoundedCornerShape(8.dp), + shape: Shape = FluentTheme.shapes.overlay, contentPadding: PaddingValues = PaddingValues(12.dp), positionProvider: FlyoutPositionProvider = rememberFlyoutPositionProvider(), onKeyEvent: ((keyEvent: KeyEvent) -> Boolean)? = null, @@ -184,7 +183,7 @@ internal fun FlyoutContent( modifier: Modifier = Modifier, placement: FlyoutPlacement = FlyoutPlacement.Auto, enterPlacementAnimation: (placement: FlyoutPlacement) -> EnterTransition = ::defaultFlyoutEnterPlacementAnimation, - shape: Shape = RoundedCornerShape(8.dp), + shape: Shape = FluentTheme.shapes.overlay, contentPadding: PaddingValues = PaddingValues(12.dp), content: @Composable () -> Unit ) { diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/ListItem.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/ListItem.kt index 3ab032d6..043a8235 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/ListItem.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/ListItem.kt @@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable @@ -128,7 +127,7 @@ fun ListItem( .defaultMinSize(minWidth = 108.dp, if (LocalCompactMode.current) ListItemCompactHeight else ListItemHeight) .padding(horizontal = 5.dp, vertical = 2.dp) .fillMaxWidth(), - shape = RoundedCornerShape(size = 4.dp), + shape = FluentTheme.shapes.control, color = fillColor, contentColor = contentColor, border = BorderStroke(1.dp, color.borderBrush), diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/MenuFlyout.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/MenuFlyout.kt index f56fc872..c19a58b7 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/MenuFlyout.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/MenuFlyout.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable @@ -32,6 +31,7 @@ import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.benasher44.uuid.uuid4 +import com.konyaco.fluent.FluentTheme import com.konyaco.fluent.animation.FluentDuration import com.konyaco.fluent.animation.FluentEasing import com.konyaco.fluent.scheme.VisualStateScheme @@ -78,7 +78,7 @@ fun MenuFlyout( modifier: Modifier = Modifier, placement: FlyoutPlacement = FlyoutPlacement.Auto, adaptivePlacement: Boolean = false, - shape: Shape = RoundedCornerShape(8.dp), + shape: Shape = FluentTheme.shapes.overlay, onKeyEvent: ((keyEvent: KeyEvent) -> Boolean)? = null, onPreviewKeyEvent: ((keyEvent: KeyEvent) -> Boolean)? = null, content: @Composable MenuFlyoutScope.() -> Unit @@ -103,7 +103,7 @@ internal fun MenuFlyout( visible: Boolean, onDismissRequest: () -> Unit, modifier: Modifier = Modifier, - shape: Shape = RoundedCornerShape(8.dp), + shape: Shape = FluentTheme.shapes.overlay, positionProvider: FlyoutPositionProvider = rememberFlyoutPositionProvider(), enterPlacementAnimation: (FlyoutPlacement) -> EnterTransition = ::defaultFlyoutEnterPlacementAnimation, onKeyEvent: ((keyEvent: KeyEvent) -> Boolean)? = null, diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/SideNav.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/SideNav.kt index db1ee3fe..2d49f93d 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/SideNav.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/SideNav.kt @@ -32,7 +32,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -222,7 +221,7 @@ fun SideNavItem( val navigationLevelPadding = 28.dp * LocalNavigationLevel.current Layer( modifier = Modifier.fillMaxWidth().height(36.dp), - shape = RoundedCornerShape(size = 4.dp), + shape = FluentTheme.shapes.control, color = animateColorAsState( color.fillColor, tween(FluentDuration.QuickDuration, easing = FluentEasing.FastInvokeEasing) ).value, diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TextField.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TextField.kt index 2ddc1ff1..b48e0ed5 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TextField.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TextField.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -68,7 +67,7 @@ fun TextField( } BasicTextField( modifier = modifier.defaultMinSize(64.dp, 32.dp) - .clip(RoundedCornerShape(4.dp)), + .clip(FluentTheme.shapes.control), value = value, onValueChange = onValueChange, textStyle = LocalTextStyle.current.copy(color = color.contentColor), @@ -145,7 +144,7 @@ object TextFieldDefaults { ) { Layer( modifier = modifier.hoverable(interactionSource), - shape = RoundedCornerShape(4.dp), + shape = FluentTheme.shapes.control, color = color.fillColor, border = BorderStroke(1.dp, color.borderBrush), backgroundSizing = BackgroundSizing.OuterBorderEdge diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/surface/Card.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/surface/Card.kt index 91810c2d..31ce5c12 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/surface/Card.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/surface/Card.kt @@ -5,7 +5,6 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue @@ -28,7 +27,7 @@ import com.konyaco.fluent.scheme.collectVisualState @Composable fun Card( modifier: Modifier, - shape: Shape = RoundedCornerShape(size = 8.dp), + shape: Shape = FluentTheme.shapes.overlay, content: @Composable () -> Unit ) { Layer( @@ -43,7 +42,7 @@ fun Card( fun Card( onClick: () -> Unit, modifier: Modifier = Modifier, - shape: Shape = RoundedCornerShape(size = 4.dp), + shape: Shape = FluentTheme.shapes.control, disabled: Boolean = false, cardColors: VisualStateScheme = CardDefaults.cardColors(), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/HomeScreen.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/HomeScreen.kt index c5b83286..e0fd9d90 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/HomeScreen.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/HomeScreen.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -78,8 +77,8 @@ fun HomeScreen() { modifier = Modifier .fillMaxWidth() .height(256.dp) - .border(1.dp, FluentTheme.colors.stroke.card.default, shape = RoundedCornerShape(4.dp)) - .clip(RoundedCornerShape(4.dp)) + .border(1.dp, FluentTheme.colors.stroke.card.default, shape = FluentTheme.shapes.control) + .clip(FluentTheme.shapes.control) .background(gradient) ) { Image( diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/design/TypographyScreen.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/design/TypographyScreen.kt index 2fa2501f..56e31c30 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/design/TypographyScreen.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/design/TypographyScreen.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.ui.Alignment @@ -148,7 +147,7 @@ private fun ItemRow( if (index.mod(2) == 1) { Modifier.background( FluentTheme.colors.background.card.default, - shape = RoundedCornerShape(4.dp) + shape = FluentTheme.shapes.control ) } else { Modifier diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/settings/SettingsScreen.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/settings/SettingsScreen.kt index 907b84b9..977201a4 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/settings/SettingsScreen.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/settings/SettingsScreen.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CutCornerShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -299,7 +298,7 @@ private fun Content() { } Layer( - shape = RoundedCornerShape(4.dp), + shape = FluentTheme.shapes.control, color = FluentTheme.colors.fillAccent.default, border = BorderStroke(1.dp, FluentTheme.colors.stroke.control.default), content = { @@ -308,7 +307,7 @@ private fun Content() { backgroundSizing = BackgroundSizing.InnerBorderEdge ) Layer( - shape = RoundedCornerShape(4.dp), + shape = FluentTheme.shapes.control, color = FluentTheme.colors.fillAccent.default, border = BorderStroke(1.dp, FluentTheme.colors.stroke.control.default), content = { From 316963bddc5568822118645f95b7aed4fa98e05f Mon Sep 17 00:00:00 2001 From: sanlorng Date: Fri, 31 May 2024 16:17:19 +0800 Subject: [PATCH 002/247] [fluent] Add `Geometry`. --- .../kotlin/com/konyaco/fluent/FluentTheme.kt | 15 +- .../kotlin/com/konyaco/fluent/Geometry.kt | 68 ++++++ .../kotlin/com/konyaco/fluent/Shape.kt | 24 -- .../com/konyaco/fluent/component/ComboBox.kt | 3 +- .../kotlin/com/konyaco/fluent/gallery/App.kt | 8 +- .../gallery/component/GallerySection.kt | 11 +- .../fluent/gallery/component/ItemRow.kt | 87 ++++++++ .../gallery/screen/design/GeometryScreen.kt | 208 ++++++++++++++++++ .../gallery/screen/design/TypographyScreen.kt | 101 ++------- 9 files changed, 404 insertions(+), 121 deletions(-) create mode 100644 fluent/src/commonMain/kotlin/com/konyaco/fluent/Geometry.kt delete mode 100644 fluent/src/commonMain/kotlin/com/konyaco/fluent/Shape.kt create mode 100644 gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/ItemRow.kt create mode 100644 gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/design/GeometryScreen.kt diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/FluentTheme.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/FluentTheme.kt index d9e5f4b5..ddf093c3 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/FluentTheme.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/FluentTheme.kt @@ -24,7 +24,7 @@ import com.konyaco.fluent.component.ProvideFontIcon fun FluentTheme( colors: Colors = FluentTheme.colors, typography: Typography = FluentTheme.typography, - shapes: Shapes = FluentTheme.shapes, + cornerRadius: CornerRadius = FluentTheme.cornerRadius, useAcrylicPopup: Boolean = LocalAcrylicPopupEnabled.current, compactMode: Boolean = true, content: @Composable () -> Unit @@ -42,7 +42,8 @@ fun FluentTheme( ), LocalContentDialog provides contentDialogHostState, LocalCompactMode provides compactMode, - LocalShapes provides shapes + LocalCornerRadius provides cornerRadius, + LocalShapes provides cornerRadius.toShapes() ) { ContentDialogHost(contentDialogHostState) Box(modifier = Modifier.behindAcrylic()) { @@ -62,6 +63,7 @@ fun FluentTheme( fun FluentThemeConfiguration( colors: Colors = FluentTheme.colors, typography: Typography = FluentTheme.typography, + cornerRadius: CornerRadius = FluentTheme.cornerRadius, useAcrylicPopup: Boolean = LocalAcrylicPopupEnabled.current, compactMode: Boolean = LocalCompactMode.current, contentDialogHostState: ContentDialogHostState = LocalContentDialog.current, @@ -77,6 +79,8 @@ fun FluentThemeConfiguration( ), LocalCompactMode provides compactMode, LocalContentDialog provides contentDialogHostState, + LocalCornerRadius provides cornerRadius, + LocalShapes provides cornerRadius.toShapes(), content = content ) } @@ -88,7 +92,7 @@ fun FluentTheme( typography: Typography = FluentTheme.typography, content: @Composable () -> Unit ) { - FluentTheme(colors, typography, LocalShapes.current, useAcrylicPopup = false, compactMode = true, content) + FluentTheme(colors, typography, LocalCornerRadius.current, useAcrylicPopup = false, compactMode = true, content) } @Composable @@ -114,6 +118,11 @@ object FluentTheme { @Composable @ReadOnlyComposable get() = LocalShapes.current + + val cornerRadius: CornerRadius + @Composable + @ReadOnlyComposable + get() = LocalCornerRadius.current } internal val LocalColors = staticCompositionLocalOf { lightColors() } diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/Geometry.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/Geometry.kt new file mode 100644 index 00000000..f761afd3 --- /dev/null +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/Geometry.kt @@ -0,0 +1,68 @@ +package com.konyaco.fluent + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** https://learn.microsoft.com/en-us/windows/apps/design/signature-experiences/geometry */ + +/** align Shape and CornerRadius style properties */ +interface Geometry { + val overlay: Type + val control: Type + val intersectionEdge: Type +} + +@Immutable +class Shapes( + override val overlay: Shape, + override val control: Shape, + override val intersectionEdge: Shape, +): Geometry + +internal val LocalShapes = staticCompositionLocalOf { + Shapes( + overlay = createShape(overlayCornerRadius), + control = createShape(controlCornerRadius), + intersectionEdge = createShape(intersectionEdgeCornerRadius) + ) +} + +internal fun createShape(cornerRadius: Dp): Shape { + return if (cornerRadius == 0.dp) { + RectangleShape + } else { + RoundedCornerShape(cornerRadius) + } +} + +fun CornerRadius.toShapes(): Shapes { + return Shapes( + overlay = createShape(overlay), + control = createShape(control), + intersectionEdge = createShape(intersectionEdge) + ) +} + +@Immutable +class CornerRadius( + override val overlay: Dp, + override val control: Dp, + override val intersectionEdge: Dp +): Geometry + +internal val LocalCornerRadius = staticCompositionLocalOf { + CornerRadius( + overlay = overlayCornerRadius, + control = controlCornerRadius, + intersectionEdge = intersectionEdgeCornerRadius + ) +} + +private val overlayCornerRadius = 8.dp +private val controlCornerRadius = 4.dp +private val intersectionEdgeCornerRadius = 0.dp \ No newline at end of file diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/Shape.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/Shape.kt deleted file mode 100644 index 2e4823a8..00000000 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/Shape.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.konyaco.fluent - -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.unit.dp - -/** https://learn.microsoft.com/en-us/windows/apps/design/signature-experiences/geometry */ -@Immutable -class Shapes( - val overlay: Shape, - val control: Shape, - val intersectionEdge: Shape, -) - -internal val LocalShapes = staticCompositionLocalOf { - Shapes( - overlay = RoundedCornerShape(8.dp), - control = RoundedCornerShape(4.dp), - intersectionEdge = RectangleShape - ) -} diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/ComboBox.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/ComboBox.kt index 00277353..1ca20c0c 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/ComboBox.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/ComboBox.kt @@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -180,7 +179,7 @@ fun ComboBoxItem( Layer( modifier = Modifier.fillMaxWidth().height(36.dp) .clickable(interactionSource = interactionSource, indication = null, onClick = onClick), - shape = RoundedCornerShape(size = 4.dp), + shape = FluentTheme.shapes.control, color = animateColorAsState( color.fillColor, tween(FluentDuration.QuickDuration, easing = FluentEasing.FastInvokeEasing) diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/App.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/App.kt index 52b0e863..41e15f81 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/App.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/App.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp +import com.konyaco.fluent.FluentTheme import com.konyaco.fluent.animation.FluentDuration import com.konyaco.fluent.animation.FluentEasing import com.konyaco.fluent.component.Icon @@ -85,12 +86,7 @@ fun App() { Card( modifier = Modifier.fillMaxHeight().weight(1f), - shape = RoundedCornerShape( - topStart = 8.dp, - topEnd = 0.dp, - bottomStart = 0.dp, - bottomEnd = 0.dp - ) + shape = RoundedCornerShape(topStart = FluentTheme.cornerRadius.overlay) ) { AnimatedContent(selectedItemWithContent, Modifier.fillMaxSize(), transitionSpec = { (fadeIn( diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/GallerySection.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/GallerySection.kt index dff09b33..ab2b7842 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/GallerySection.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/GallerySection.kt @@ -69,7 +69,8 @@ fun GallerySection( Layer( modifier = Modifier.fillMaxWidth().wrapContentHeight(), shape = RoundedCornerShape( - topStart = 8.dp, topEnd = 8.dp + topStart = FluentTheme.cornerRadius.overlay, + topEnd = FluentTheme.cornerRadius.overlay ), color = FluentTheme.colors.background.solid.base, backgroundSizing = BackgroundSizing.OuterBorderEdge @@ -128,8 +129,8 @@ fun GallerySection( sourceCodeExpanded = !sourceCodeExpanded }), shape = RoundedCornerShape( - bottomEnd = if (sourceCodeExpanded) 0.dp else 8.dp, - bottomStart = if (sourceCodeExpanded) 0.dp else 8.dp + bottomEnd = if (sourceCodeExpanded) 0.dp else FluentTheme.cornerRadius.overlay, + bottomStart = if (sourceCodeExpanded) 0.dp else FluentTheme.cornerRadius.overlay ) ) { Row(Modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically) { @@ -163,8 +164,8 @@ fun GallerySection( Card( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape( - bottomEnd = 8.dp, - bottomStart = 8.dp + bottomEnd = FluentTheme.cornerRadius.overlay, + bottomStart = FluentTheme.cornerRadius.overlay ) ) { Column(Modifier.padding(16.dp, 12.dp)) { diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/ItemRow.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/ItemRow.kt new file mode 100644 index 00000000..a99748e3 --- /dev/null +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/ItemRow.kt @@ -0,0 +1,87 @@ +package com.konyaco.fluent.gallery.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +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.dp +import com.konyaco.fluent.FluentTheme +import com.konyaco.fluent.component.Text + +@Composable +internal fun ItemRow( + index: Int, + text: @Composable () -> Unit, + secondary: @Composable () -> Unit, + third: @Composable () -> Unit, + fourth: @Composable () -> Unit, + modifier: Modifier = Modifier, + textWidth: Dp = 272.dp, + secondaryWidth: Dp = 136.dp, + thirdWidth: Dp = 112.dp +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.fillMaxWidth().heightIn(68.dp).then( + if (index.mod(2) == 1) { + Modifier.background( + FluentTheme.colors.background.card.default, + shape = FluentTheme.shapes.control + ) + } else { + Modifier + } + ) + ) { + Box(modifier = Modifier.width(textWidth).padding(horizontal = 16.dp, vertical = 16.dp)) { + text() + } + Box(modifier = Modifier.width(secondaryWidth).padding(horizontal = 16.dp, vertical = 16.dp)) { + secondary() + } + Box(modifier = Modifier.width(thirdWidth).padding(horizontal = 16.dp, vertical = 16.dp)) { + third() + } + fourth() + } +} + +@Composable +internal fun HeaderItemRow( + text: String, + secondary: String, + third: String, + fourth: String, + modifier: Modifier = Modifier, + textWidth: Dp = 272.dp, + secondaryWidth: Dp = 136.dp, + thirdWidth: Dp = 112.dp +) { + val headerStyle = FluentTheme.typography.caption.copy(color = FluentTheme.colors.text.text.secondary) + ItemRow( + text = { + Text(text, style = headerStyle) + }, + secondary = { + Text(secondary, style = headerStyle) + }, + third = { + Text(third, style = headerStyle) + }, + fourth = { + Text(fourth, style = headerStyle) + }, + index = 0, + modifier = modifier, + textWidth = textWidth, + secondaryWidth = secondaryWidth, + thirdWidth = thirdWidth + ) +} diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/design/GeometryScreen.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/design/GeometryScreen.kt new file mode 100644 index 00000000..efb216e5 --- /dev/null +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/design/GeometryScreen.kt @@ -0,0 +1,208 @@ +package com.konyaco.fluent.gallery.screen.design + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.konyaco.fluent.FluentTheme +import com.konyaco.fluent.LocalContentColor +import com.konyaco.fluent.component.Text +import com.konyaco.fluent.gallery.annotation.Component +import com.konyaco.fluent.gallery.annotation.Sample +import com.konyaco.fluent.gallery.component.ComponentPagePath +import com.konyaco.fluent.gallery.component.CopyButton +import com.konyaco.fluent.gallery.component.GalleryPage +import com.konyaco.fluent.gallery.component.HeaderItemRow +import com.konyaco.fluent.gallery.component.ItemRow +import com.konyaco.fluent.source.generated.FluentSourceFile + +@Component(index = 1, icon = "Shapes") +@Composable +fun GeometryScreen() { + GalleryPage( + title = "Geometry", + description = "Geometry describes the shape, size and position of Ul elements on screen. " + + "These fundamental design elements help experiences feel coherent across the entire design system. " + + "Fluent Design System uses three levels of rounding depending on" + + "component is being rounded and how that component is aranged relative to neighboring elements.", + componentPath = FluentSourceFile.Geometry, + galleryPath = ComponentPagePath.GeometryScreen + ) { + + Section( + title = "Corner Radius", + sourceCode = sourceCodeOfCornerRadiusSample, + content = { CornerRadiusItems() } + ) + + Section( + title = "Shapes", + sourceCode = sourceCodeOfShapesSample, + content = { ShapesItems() } + ) + } +} + +@Sample +@Composable +private fun CornerRadiusSample() { + Column { + Box(shape = RoundedCornerShape(topStart = FluentTheme.cornerRadius.overlay)) + Box(shape = RoundedCornerShape(topStart = FluentTheme.cornerRadius.control)) + Box(shape = RoundedCornerShape(topStart = FluentTheme.cornerRadius.intersectionEdge)) + } +} + +@Sample +@Composable +private fun ShapesSample() { + Column { + Box(shape = FluentTheme.shapes.overlay) + Box(shape = FluentTheme.shapes.control) + Box(shape = FluentTheme.shapes.intersectionEdge) + } +} + +@Composable +private fun Box(shape: Shape) { + Box( + modifier = Modifier.size(20.dp).background( + color = FluentTheme.colors.fillAccent.default, + shape = shape + ) + ) +} + +@Composable +private fun CornerRadiusItems() { + Column { + HeaderItemRow( + text = "Corner Radius", + secondary = "Usage", + third = "Style", + fourth = "", + textWidth = textWidth, + secondaryWidth = secondaryWidth, + thirdWidth = thirdWidth + ) + CornerRadiusList().forEachIndexed { index, (property, value) -> + ItemRow( + text = { + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + Box(shape = RoundedCornerShape(topStart = value)) + Text("${value.value.toInt()}dp") + } + }, + secondary = { UsageText(value) }, + third = { + val content = "FluentTheme.cornerRadius.$property" + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = content, + modifier = Modifier.weight(1f), + style = FluentTheme.typography.caption.copy(LocalContentColor.current) + ) + CopyButton(content) + } + }, + fourth = {}, + index = index + 1, + textWidth = textWidth, + secondaryWidth = secondaryWidth, + thirdWidth = thirdWidth + ) + } + } +} + +@Composable +fun ShapesItems() { + Column { + HeaderItemRow( + text = "Shape", + secondary = "Usage", + third = "Style", + fourth = "", + textWidth = textWidth, + secondaryWidth = secondaryWidth, + thirdWidth = thirdWidth + ) + ShapesList().forEachIndexed { index, (radius, property, value) -> + ItemRow( + text = { + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + Box(shape = value) + Text("${radius.value.toInt()}dp") + } + }, + secondary = { UsageText(radius) }, + third = { + val content = "FluentTheme.shapes.$property" + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = content, + modifier = Modifier.weight(1f), + style = FluentTheme.typography.caption.copy(LocalContentColor.current) + ) + CopyButton(content) + } + }, + fourth = {}, + index = index + 1, + textWidth = textWidth, + secondaryWidth = secondaryWidth, + thirdWidth = thirdWidth + ) + } + } +} + +@Stable +@Composable +private fun CornerRadiusList(): List> { + val geometry = FluentTheme.cornerRadius + return listOf( + Pair( geometry::overlay.name, geometry.overlay), + Pair(geometry::control.name, geometry.control), + Pair(geometry::intersectionEdge.name, geometry.intersectionEdge) + ) +} + +@Stable +@Composable +private fun ShapesList(): List> { + val shapes = FluentTheme.shapes + val geometry = FluentTheme.cornerRadius + return listOf( + Triple(geometry.overlay, shapes::overlay.name, shapes.overlay), + Triple(geometry.control, shapes::control.name, shapes.control), + Triple(geometry.intersectionEdge, shapes::intersectionEdge.name, shapes.intersectionEdge) + ) +} + +@Composable +private fun UsageText(cornerRadius: Dp) { + Text( + text = when (cornerRadius) { + FluentTheme.cornerRadius.overlay -> "Top-level containers such as app windows, flyouts, cards and dialogs." + FluentTheme.cornerRadius.control -> "In-page elements such as controls and list backplates." + FluentTheme.cornerRadius.intersectionEdge -> "Straight edges that intersect with other straight edges." + else -> "" + }, + style = FluentTheme.typography.caption.copy(LocalContentColor.current) + ) +} + +private val textWidth = 120.dp +private val secondaryWidth = 400.dp +private val thirdWidth = 320.dp \ No newline at end of file diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/design/TypographyScreen.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/design/TypographyScreen.kt index 56e31c30..48ee14d8 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/design/TypographyScreen.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/design/TypographyScreen.kt @@ -2,13 +2,8 @@ package com.konyaco.fluent.gallery.screen.design -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable @@ -31,8 +26,11 @@ import com.konyaco.fluent.gallery.annotation.Sample import com.konyaco.fluent.gallery.component.ComponentPagePath import com.konyaco.fluent.gallery.component.CopyButton import com.konyaco.fluent.gallery.component.GalleryPage +import com.konyaco.fluent.gallery.component.HeaderItemRow +import com.konyaco.fluent.gallery.component.ItemRow import com.konyaco.fluent.source.generated.FluentSourceFile +@OptIn(ExperimentalTextApi::class) @Component(index = 1, icon = "TextFont") @Composable fun TypographyScreen() { @@ -87,8 +85,13 @@ private fun BasicTypographySample() { @Composable private fun TypographySample() { Column { - HeaderItemRow() - typographyList().forEachIndexed { index, (name, style) -> + HeaderItemRow( + text = "Example", + secondary = "Variable Font", + third = "Size/Line height", + fourth = "Style" + ) + typographyList().forEachIndexed { index, (name, property, style) -> ItemRow( text = { Text(text = name, style = style) }, secondary = { @@ -108,16 +111,7 @@ private fun TypographySample() { ) }, fourth = { - val content = when (style) { - FluentTheme.typography.caption -> "FluentTheme.typography.caption" - FluentTheme.typography.body -> "FluentTheme.typography.body" - FluentTheme.typography.bodyStrong -> "FluentTheme.typography.bodyStrong" - FluentTheme.typography.subtitle -> "FluentTheme.typography.subtitle" - FluentTheme.typography.title -> "FluentTheme.typography.title" - FluentTheme.typography.titleLarge -> "FluentTheme.typography.titleLarge" - FluentTheme.typography.display -> "FluentTheme.typography.display" - else -> "" - } + val content = "FluentTheme.typography.$property" Row(verticalAlignment = Alignment.CenterVertically) { Text( text = content, @@ -132,72 +126,17 @@ private fun TypographySample() { } } -@Composable -private fun ItemRow( - text: @Composable () -> Unit, - secondary: @Composable () -> Unit, - third: @Composable () -> Unit, - fourth: @Composable () -> Unit, - index: Int, - modifier: Modifier = Modifier -) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = modifier.fillMaxWidth().heightIn(68.dp).then( - if (index.mod(2) == 1) { - Modifier.background( - FluentTheme.colors.background.card.default, - shape = FluentTheme.shapes.control - ) - } else { - Modifier - } - ) - ) { - Box(modifier = Modifier.width(272.dp).padding(horizontal = 16.dp, vertical = 16.dp)) { - text() - } - Box(modifier = Modifier.width(136.dp)) { - secondary() - } - Box(modifier = Modifier.width(112.dp)) { - third() - } - fourth() - } -} - -@Composable -private fun HeaderItemRow() { - val headerStyle = - FluentTheme.typography.caption.copy(color = FluentTheme.colors.text.text.secondary) - ItemRow( - text = { - Text("Example", style = headerStyle) - }, - secondary = { - Text("Variable Font", style = headerStyle) - }, - third = { - Text("Size/Line height", style = headerStyle) - }, - fourth = { - Text("Style", style = headerStyle) - }, - index = 0 - ) -} - @Stable @Composable -private fun typographyList(): List> { +private fun typographyList(): List> { + val typography = FluentTheme.typography return listOf( - "Caption" to FluentTheme.typography.caption, - "Body" to FluentTheme.typography.body, - "Body Strong" to FluentTheme.typography.bodyStrong, - "Subtitle" to FluentTheme.typography.subtitle, - "Title" to FluentTheme.typography.title, - "Title Large" to FluentTheme.typography.titleLarge, - "Display" to FluentTheme.typography.display + Triple("Caption", typography::caption.name, typography.caption), + Triple("Body", typography::body.name, typography.body), + Triple("Body Strong", typography::bodyStrong.name, typography.bodyStrong), + Triple("Subtitle", typography::subtitle.name, typography.subtitle), + Triple("Title", typography::title.name, typography.title), + Triple("Title Large", typography::titleLarge.name, typography.titleLarge), + Triple("Display", typography::display.name, typography.display) ) } \ No newline at end of file From 1782fe97fe0fcb36bf0f8d1b993bbe0840cb8221 Mon Sep 17 00:00:00 2001 From: sanlorng Date: Fri, 31 May 2024 19:34:22 +0800 Subject: [PATCH 003/247] [gallery] support navigator back history. --- .../konyaco/fluent/gallery/MainActivity.kt | 8 +++- .../kotlin/com/konyaco/fluent/gallery/App.kt | 39 +++++++++++------- .../gallery/component/ComponentNavigator.kt | 41 ++++++++++++++++++- 3 files changed, 71 insertions(+), 17 deletions(-) diff --git a/gallery/src/androidMain/kotlin/com/konyaco/fluent/gallery/MainActivity.kt b/gallery/src/androidMain/kotlin/com/konyaco/fluent/gallery/MainActivity.kt index 24d0a973..fa56237f 100644 --- a/gallery/src/androidMain/kotlin/com/konyaco/fluent/gallery/MainActivity.kt +++ b/gallery/src/androidMain/kotlin/com/konyaco/fluent/gallery/MainActivity.kt @@ -2,14 +2,20 @@ package com.konyaco.fluent.gallery import android.os.Bundle import androidx.activity.ComponentActivity +import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent +import com.konyaco.fluent.gallery.component.rememberComponentNavigator class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { + val componentNavigator = rememberComponentNavigator() + BackHandler(componentNavigator.latestBackEntry != null) { + componentNavigator.navigateUp() + } GalleryTheme { - App() + App(componentNavigator) } } } diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/App.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/App.kt index 52b0e863..905a1b3b 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/App.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/App.kt @@ -6,6 +6,7 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize @@ -18,9 +19,11 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp +import com.konyaco.fluent.FluentTheme import com.konyaco.fluent.animation.FluentDuration import com.konyaco.fluent.animation.FluentEasing import com.konyaco.fluent.component.Icon @@ -32,32 +35,31 @@ import com.konyaco.fluent.component.TextField import com.konyaco.fluent.gallery.component.ComponentItem import com.konyaco.fluent.gallery.component.ComponentNavigator import com.konyaco.fluent.gallery.component.components +import com.konyaco.fluent.gallery.component.rememberComponentNavigator import com.konyaco.fluent.gallery.screen.settings.SettingsScreen import com.konyaco.fluent.icons.Icons import com.konyaco.fluent.icons.regular.Settings import com.konyaco.fluent.surface.Card @Composable -fun App() { +fun App( + navigator: ComponentNavigator = rememberComponentNavigator(components.first()) +) { Row(Modifier.fillMaxSize()) { var expanded by remember { mutableStateOf(true) } - val (selectedItem, setSelectedItem) = remember { - mutableStateOf(components.first()) - } var selectedItemWithContent by remember { - mutableStateOf(selectedItem) + mutableStateOf(navigator.latestBackEntry) } - LaunchedEffect(selectedItem) { - if (selectedItem.content != null) { - selectedItemWithContent = selectedItem + LaunchedEffect(navigator.latestBackEntry) { + val latestBackEntry = navigator.latestBackEntry + if (selectedItemWithContent == latestBackEntry) return@LaunchedEffect + if (latestBackEntry == null || latestBackEntry.content != null) { + selectedItemWithContent = latestBackEntry } } var textFieldValue by remember { mutableStateOf(TextFieldValue()) } - val navigator = remember(setSelectedItem) { - ComponentNavigator(setSelectedItem) - } SideNav( modifier = Modifier.fillMaxHeight(), expanded = expanded, @@ -72,11 +74,11 @@ fun App() { ) }, footer = { - NavigationItem(selectedItem, setSelectedItem, settingItem) + NavigationItem(navigator.latestBackEntry, navigator::navigate, settingItem) } ) { components.forEach { navItem -> - NavigationItem(selectedItem, setSelectedItem, navItem) + NavigationItem(navigator.latestBackEntry, navigator::navigate, navItem) if (navItem.name == "All samples") { NavigationItemSeparator(modifier = Modifier.padding(vertical = 2.dp)) } @@ -113,7 +115,13 @@ fun App() { ) ) }) { - it.content?.invoke(it, navigator) + if (it != null) { + it.content?.invoke(it, navigator) + } else { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text("No content selected", style = FluentTheme.typography.bodyStrong) + } + } } } } @@ -121,7 +129,7 @@ fun App() { @Composable private fun NavigationItem( - selectedItem: ComponentItem, + selectedItem: ComponentItem?, onSelectedItemChanged: (ComponentItem) -> Unit, navItem: ComponentItem ) { @@ -129,6 +137,7 @@ private fun NavigationItem( mutableStateOf(false) } LaunchedEffect(selectedItem) { + if (selectedItem == null) return@LaunchedEffect if (navItem != selectedItem) { val navItemAsGroup = "${navItem.group}/${navItem.name}/" if ((selectedItem.group + "/").startsWith(navItemAsGroup)) diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/ComponentNavigator.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/ComponentNavigator.kt index 0f8953c7..63867606 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/ComponentNavigator.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/ComponentNavigator.kt @@ -1,6 +1,45 @@ package com.konyaco.fluent.gallery.component -fun interface ComponentNavigator { +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember + +interface ComponentNavigator { + fun navigate(componentItem: ComponentItem) + fun navigateUp() + + val currentBackstack: List + + val latestBackEntry: ComponentItem? + +} + +@Composable +fun rememberComponentNavigator(startItem: ComponentItem = components.first()): ComponentNavigator { + return remember { ComponentNavigatorImpl(startItem) } +} + +private class ComponentNavigatorImpl(startItem: ComponentItem) : ComponentNavigator { + + private val backstack = mutableStateListOf().apply { add(startItem) } + + override fun navigate(componentItem: ComponentItem) { + backstack.add(componentItem) + } + + override fun navigateUp() { + if (backstack.isNotEmpty()) { + do { + backstack.removeLast() + } while (backstack.lastOrNull().let { it != null && it.content == null }) + } + } + + override val currentBackstack: List + get() = backstack + + override val latestBackEntry: ComponentItem? + get() = backstack.lastOrNull() } \ No newline at end of file From 9d0c45818c7ca5b878965b7a30e3158949a8e4c0 Mon Sep 17 00:00:00 2001 From: sanlorng Date: Thu, 13 Jun 2024 19:26:17 +0800 Subject: [PATCH 004/247] [fluent] **Break Changed** rename `training` to `trailing`. --- .../com/konyaco/fluent/component/ListItem.kt | 22 +++++++++---------- .../konyaco/fluent/component/MenuFlyout.kt | 10 ++++----- .../fluent/component/ContextMenu.desktop.kt | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/ListItem.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/ListItem.kt index 3ab032d6..7881491a 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/ListItem.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/ListItem.kt @@ -60,7 +60,7 @@ fun ListItem( modifier: Modifier = Modifier, selectionType: ListItemSelectionType = ListItemSelectionType.Standard, icon: (@Composable () -> Unit)? = null, - training: (@Composable () -> Unit)? = null, + trailing: (@Composable () -> Unit)? = null, interaction: MutableInteractionSource? = null, enabled: Boolean = true, colors: VisualStateScheme = if (selected) { @@ -86,7 +86,7 @@ fun ListItem( }, text = text, icon = icon, - training = training, + trailing = trailing, interaction = interaction, enabled = enabled, onClick = { onSelectedChanged(!selected) }, @@ -106,7 +106,7 @@ fun ListItem( selectionIcon: (@Composable () -> Unit)? = null, indicator: (@Composable () -> Unit)? = null, icon: (@Composable () -> Unit)? = null, - training: (@Composable () -> Unit)? = null, + trailing: (@Composable () -> Unit)? = null, interaction: MutableInteractionSource? = null, enabled: Boolean = true, colors: VisualStateScheme = ListItemDefaults.defaultListItemColors(), @@ -171,11 +171,11 @@ fun ListItem( text() } CompositionLocalProvider( - LocalContentColor provides color.trainingColor, - LocalContentAlpha provides color.trainingColor.alpha, + LocalContentColor provides color.trailingColor, + LocalContentAlpha provides color.trailingColor.alpha, LocalTextStyle provides FluentTheme.typography.caption.copy(fontWeight = FontWeight.Normal) ) { - training?.invoke() + trailing?.invoke() } } } @@ -288,7 +288,7 @@ object ListItemDefaults { default: ListItemColor = ListItemColor( fillColor = FluentTheme.colors.subtleFill.transparent, contentColor = FluentTheme.colors.text.text.primary, - trainingColor = FluentTheme.colors.text.text.secondary, + trailingColor = FluentTheme.colors.text.text.secondary, borderBrush = SolidColor(Color.Transparent) ), hovered: ListItemColor = default.copy( @@ -301,7 +301,7 @@ object ListItemDefaults { disabled: ListItemColor = default.copy( fillColor = FluentTheme.colors.subtleFill.disabled, contentColor = FluentTheme.colors.text.text.disabled, - trainingColor = FluentTheme.colors.text.text.disabled, + trailingColor = FluentTheme.colors.text.text.disabled, ) ) = ListItemColorScheme( default = default, @@ -316,7 +316,7 @@ object ListItemDefaults { default: ListItemColor = ListItemColor( fillColor = FluentTheme.colors.subtleFill.secondary, contentColor = FluentTheme.colors.text.text.primary, - trainingColor = FluentTheme.colors.text.text.secondary, + trailingColor = FluentTheme.colors.text.text.secondary, borderBrush = SolidColor(Color.Transparent) ), hovered: ListItemColor = default.copy( @@ -327,7 +327,7 @@ object ListItemDefaults { ), disabled: ListItemColor = default.copy( contentColor = FluentTheme.colors.text.text.disabled, - trainingColor = FluentTheme.colors.text.text.disabled, + trailingColor = FluentTheme.colors.text.text.disabled, ) ) = ListItemColorScheme( default = default, @@ -345,7 +345,7 @@ enum class ListItemSelectionType { data class ListItemColor( val fillColor: Color, val contentColor: Color, - val trainingColor: Color, + val trailingColor: Color, val borderBrush: Brush ) diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/MenuFlyout.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/MenuFlyout.kt index f56fc872..fd8d9188 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/MenuFlyout.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/MenuFlyout.kt @@ -142,7 +142,7 @@ fun MenuFlyoutScope.MenuFlyoutItem( text: @Composable () -> Unit, modifier: Modifier = Modifier, icon: (@Composable () -> Unit)? = null, - training: (@Composable () -> Unit)? = null, + trailing: (@Composable () -> Unit)? = null, interaction: MutableInteractionSource? = null, enabled: Boolean = true, selectionType: ListItemSelectionType = ListItemSelectionType.Standard, @@ -161,7 +161,7 @@ fun MenuFlyoutScope.MenuFlyoutItem( icon = icon, text = text, modifier = modifier, - training = training, + trailing = trailing, interaction = interaction, enabled = enabled, colors = colors @@ -174,7 +174,7 @@ fun MenuFlyoutScope.MenuFlyoutItem( text: @Composable () -> Unit, modifier: Modifier = Modifier, icon: (@Composable () -> Unit)? = null, - training: (@Composable () -> Unit)? = null, + trailing: (@Composable () -> Unit)? = null, interaction: MutableInteractionSource? = null, enabled: Boolean = true, colors: VisualStateScheme = ListItemDefaults.defaultListItemColors() @@ -186,7 +186,7 @@ fun MenuFlyoutScope.MenuFlyoutItem( icon = icon, text = text, modifier = modifier, - training = training, + trailing = trailing, interaction = interaction, enabled = enabled, colors = colors @@ -226,7 +226,7 @@ fun MenuFlyoutScope.MenuFlyoutItem( onClick = { isFlyoutVisible = !isFlyoutVisible }, icon = icon, text = text, - training = { ListItemDefaults.CascadingIcon() }, + trailing = { ListItemDefaults.CascadingIcon() }, modifier = modifier, interaction = interactionSource, enabled = enabled, diff --git a/fluent/src/desktopMain/kotlin/com/konyaco/fluent/component/ContextMenu.desktop.kt b/fluent/src/desktopMain/kotlin/com/konyaco/fluent/component/ContextMenu.desktop.kt index 92ccd8dd..b9b4740f 100644 --- a/fluent/src/desktopMain/kotlin/com/konyaco/fluent/component/ContextMenu.desktop.kt +++ b/fluent/src/desktopMain/kotlin/com/konyaco/fluent/component/ContextMenu.desktop.kt @@ -87,7 +87,7 @@ internal object FluentContextMenuRepresentation : ContextMenuRepresentation { } else { null }, - training = { + trailing = { it.keyData?.let { keyData -> val keyString = remember(keyData) { buildString { From ec6b4949c5949d6e60f6fb650182a92637371309 Mon Sep 17 00:00:00 2001 From: sanlorng Date: Fri, 14 Jun 2024 11:54:13 +0800 Subject: [PATCH 005/247] [fluent] Fixed `FontIcon` crashed on Android. --- .../kotlin/com/konyaco/fluent/component/FontIcon.android.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fluent/src/androidMain/kotlin/com/konyaco/fluent/component/FontIcon.android.kt b/fluent/src/androidMain/kotlin/com/konyaco/fluent/component/FontIcon.android.kt index 62808826..de61c70f 100644 --- a/fluent/src/androidMain/kotlin/com/konyaco/fluent/component/FontIcon.android.kt +++ b/fluent/src/androidMain/kotlin/com/konyaco/fluent/component/FontIcon.android.kt @@ -1,8 +1,12 @@ package com.konyaco.fluent.component import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider @Composable internal actual fun ProvideFontIcon(content: @Composable () -> Unit) { - content() + CompositionLocalProvider( + LocalFontIconFontFamily provides null, + content = content + ) } \ No newline at end of file From ccb9a136c13b2ae753c9b021fc0051bf4d170099 Mon Sep 17 00:00:00 2001 From: sanlorng Date: Fri, 14 Jun 2024 18:01:54 +0800 Subject: [PATCH 006/247] [fluent] Update `Acrylic` default style. --- .../fluent/background/Acrylic.android.kt | 7 ++++++ .../com/konyaco/fluent/background/Acrylic.kt | 25 +++++++++++-------- .../fluent/background/Acrylic.desktop.kt | 5 ++++ gradle/libs.versions.toml | 2 +- 4 files changed, 28 insertions(+), 11 deletions(-) create mode 100644 fluent/src/androidMain/kotlin/com/konyaco/fluent/background/Acrylic.android.kt create mode 100644 fluent/src/desktopMain/kotlin/com/konyaco/fluent/background/Acrylic.desktop.kt diff --git a/fluent/src/androidMain/kotlin/com/konyaco/fluent/background/Acrylic.android.kt b/fluent/src/androidMain/kotlin/com/konyaco/fluent/background/Acrylic.android.kt new file mode 100644 index 00000000..72bc37a5 --- /dev/null +++ b/fluent/src/androidMain/kotlin/com/konyaco/fluent/background/Acrylic.android.kt @@ -0,0 +1,7 @@ +package com.konyaco.fluent.background + +import android.os.Build + +internal actual fun supportAcrylic(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S +} \ No newline at end of file diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Acrylic.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Acrylic.kt index c5a2062c..4f3ae814 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Acrylic.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Acrylic.kt @@ -2,6 +2,7 @@ package com.konyaco.fluent.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background import androidx.compose.foundation.layout.BoxScope import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -21,7 +22,7 @@ import dev.chrisbanes.haze.hazeChild @Composable fun AcrylicContainerScope.Acrylic( modifier: Modifier = Modifier, - enabled: () -> Boolean = { true }, + enabled: () -> Boolean = { supportAcrylic() }, tint: Color = AcrylicDefaults.tintColor, shape: Shape = AcrylicDefaults.shape, border: BorderStroke? = null, @@ -30,7 +31,7 @@ fun AcrylicContainerScope.Acrylic( Layer( modifier = modifier.acrylicOverlay(tint = tint, shape = shape, enabled = enabled), shape = shape, - color = if (enabled()) Color.Transparent else FluentTheme.colors.background.layer.default, + color = if (enabled()) Color.Transparent else FluentTheme.colors.background.acrylic.default, border = border, backgroundSizing = BackgroundSizing.InnerBorderEdge ) { @@ -58,8 +59,10 @@ private class AcrylicContainerScopeImpl(boxScope: BoxScope): AcrylicContainerSco } override fun Modifier.acrylicOverlay(tint: Color, shape: Shape, enabled: () -> Boolean): Modifier { - return then(if (enabled()) { - Modifier.hazeChild( + return when { + !supportAcrylic() -> background(tint.copy(1f), shape) + !enabled() -> this + else -> hazeChild( state = hazeState, shape = shape, style = HazeStyle( @@ -68,9 +71,7 @@ private class AcrylicContainerScopeImpl(boxScope: BoxScope): AcrylicContainerSco blurRadius = AcrylicDefaults.blurRadius ) ) - } else { - Modifier - }) + } } } @@ -86,11 +87,15 @@ internal object AcrylicDefaults { const val noise = 0.02f - val blurRadius = 70.dp + val blurRadius = 60.dp val tintColor: Color @Composable - get() = FluentTheme.colors.background.acrylic.default.copy(0.8f) + get() = FluentTheme.colors.background.acrylic.default.copy( + if (FluentTheme.colors.darkMode) 0.96f else 0.85f + ) val shape = RectangleShape -} \ No newline at end of file +} + +internal expect fun supportAcrylic(): Boolean \ No newline at end of file diff --git a/fluent/src/desktopMain/kotlin/com/konyaco/fluent/background/Acrylic.desktop.kt b/fluent/src/desktopMain/kotlin/com/konyaco/fluent/background/Acrylic.desktop.kt new file mode 100644 index 00000000..70c5aed8 --- /dev/null +++ b/fluent/src/desktopMain/kotlin/com/konyaco/fluent/background/Acrylic.desktop.kt @@ -0,0 +1,5 @@ +package com.konyaco.fluent.background + +internal actual fun supportAcrylic(): Boolean { + return true +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ed7a7469..3e268353 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] activityCompose = "1.8.2" -haze = "0.6.2" +haze = "0.7.1" kotlin = "1.9.23" ksp = "1.9.23-1.0.20" compose = "1.6.10" From 0dd59c9717a8c47ead938c97b73709cdffc71891 Mon Sep 17 00:00:00 2001 From: sanlorng Date: Fri, 14 Jun 2024 18:04:02 +0800 Subject: [PATCH 007/247] [gallery] Fixed `GalleryPage` issues and exclude all design page. --- .../com/konyaco/fluent/gallery/component/GalleryHeader.kt | 6 ++---- .../com/konyaco/fluent/gallery/component/GalleryPage.kt | 1 + .../com/konyaco/fluent/gallery/screen/AllSamplesScreen.kt | 6 ++---- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/GalleryHeader.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/GalleryHeader.kt index 2e65da3d..00936785 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/GalleryHeader.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/GalleryHeader.kt @@ -42,7 +42,6 @@ import com.konyaco.fluent.icons.regular.Document import com.konyaco.fluent.icons.regular.PersonFeedback import fluentdesign.gallery.generated.resources.Res import fluentdesign.gallery.generated.resources.github_logo -import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.painterResource @Composable @@ -68,7 +67,6 @@ fun GalleryHeader( ) } -@OptIn(ExperimentalResourceApi::class) @Composable fun GalleryHeader( title: AnnotatedString, @@ -76,8 +74,8 @@ fun GalleryHeader( documentPath: String? = null, componentPath: String? = null, galleryPath: String? = null, - themeButtonChecked: Boolean = false, controlVisible: Boolean = true, + themeButtonChecked: Boolean = false, onThemeButtonChanged: (Boolean) -> Unit = {} ) { Column(Modifier.padding(top = 32.dp, bottom = 24.dp, start = 32.dp, end = 32.dp)) { @@ -185,7 +183,7 @@ fun GalleryHeader( } if (description.isNotBlank()) { - GalleryDescription(title, Modifier.padding(top = 24.dp)) + GalleryDescription(description, Modifier.padding(top = 24.dp)) } } } diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/GalleryPage.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/GalleryPage.kt index f7ce6479..4f5fc26e 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/GalleryPage.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/GalleryPage.kt @@ -62,6 +62,7 @@ fun GalleryPage( documentPath, componentPath, galleryPath, + true, inverseTheme.value ) { inverseTheme.value = !inverseTheme.value } diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/AllSamplesScreen.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/AllSamplesScreen.kt index 2cec68ed..144774e7 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/AllSamplesScreen.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/AllSamplesScreen.kt @@ -15,11 +15,9 @@ fun AllSamplesScreen(navigator: ComponentNavigator) { LaunchedEffect(flatMapComponents) { val excludeComponents = listOf( _HomeScreenComponent, - _AllSamplesScreenComponent, - Design_guidance_TypographyScreenComponent, - Design_guidance_IconsScreenComponent + _AllSamplesScreenComponent ) - allComponents = flatMapComponents.filter { it !in excludeComponents } + allComponents = flatMapComponents.filter { it !in excludeComponents && !it.group.startsWith("/" + _Design_guidanceComponents.name) } } ComponentIndexScreen( name = "All samples", From b5f6aa3888d6b6f6295e0569ea0355bd9c09d50b Mon Sep 17 00:00:00 2001 From: sanlorng Date: Tue, 18 Jun 2024 21:16:13 +0800 Subject: [PATCH 008/247] [gallery] New Windows window title bar experience and support mica effect again. --- gallery/build.gradle.kts | 2 + .../konyaco/fluent/gallery/MainActivity.kt | 2 +- .../gallery/component/ComponentGroupInfo.kt | 2 +- .../gallery/component/ComponentNavigator.kt | 8 + .../kotlin/com/konyaco/fluent/gallery/Main.kt | 43 +- .../jna/windows/ComposeWindowProcedure.kt | 198 ++++++++ .../jna/windows/SkiaLayerWindowProcedure.kt | 76 +++ .../gallery/jna/windows/User32Extend.kt | 74 +++ .../gallery/jna/windows/WindowProcedure.kt | 5 + .../jna/windows/structure/VersionHelper.kt | 14 + .../jna/windows/structure/WinUserConst.kt | 42 ++ .../jna/windows/structure/WindowMargins.kt | 17 + .../gallery/window/LayoutHitTestOwner.kt | 167 +++++++ .../gallery/window/WindowsWindowFrame.kt | 432 ++++++++++++++++++ gradle/libs.versions.toml | 3 + 15 files changed, 1070 insertions(+), 15 deletions(-) create mode 100644 gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/ComposeWindowProcedure.kt create mode 100644 gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/SkiaLayerWindowProcedure.kt create mode 100644 gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/User32Extend.kt create mode 100644 gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/WindowProcedure.kt create mode 100644 gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/structure/VersionHelper.kt create mode 100644 gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/structure/WinUserConst.kt create mode 100644 gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/structure/WindowMargins.kt create mode 100644 gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/window/LayoutHitTestOwner.kt create mode 100644 gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/window/WindowsWindowFrame.kt diff --git a/gallery/build.gradle.kts b/gallery/build.gradle.kts index 7ce8ed99..cbe1b7d8 100644 --- a/gallery/build.gradle.kts +++ b/gallery/build.gradle.kts @@ -44,6 +44,8 @@ kotlin { dependencies { implementation(compose.preview) implementation(libs.window.styler) + implementation(libs.jna.platform) + implementation(libs.jna) } } val desktopTest by getting diff --git a/gallery/src/androidMain/kotlin/com/konyaco/fluent/gallery/MainActivity.kt b/gallery/src/androidMain/kotlin/com/konyaco/fluent/gallery/MainActivity.kt index fa56237f..47a0feb5 100644 --- a/gallery/src/androidMain/kotlin/com/konyaco/fluent/gallery/MainActivity.kt +++ b/gallery/src/androidMain/kotlin/com/konyaco/fluent/gallery/MainActivity.kt @@ -11,7 +11,7 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { val componentNavigator = rememberComponentNavigator() - BackHandler(componentNavigator.latestBackEntry != null) { + BackHandler(componentNavigator.canNavigateUp) { componentNavigator.navigateUp() } GalleryTheme { diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/ComponentGroupInfo.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/ComponentGroupInfo.kt index 26c11743..a95859f3 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/ComponentGroupInfo.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/ComponentGroupInfo.kt @@ -38,7 +38,7 @@ object ComponentGroupInfo { @ComponentGroup("Flash", index = 9) const val Motion = "Motion" - @ComponentGroup("Navigation", index = 10) + @ComponentGroup("Navigation", index = 10, packageMap = "$screenPackage.navigation") const val Navigation = "Navigation" @ComponentGroup("ArrowSort", index = 11) diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/ComponentNavigator.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/ComponentNavigator.kt index 63867606..6b30896b 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/ComponentNavigator.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/ComponentNavigator.kt @@ -1,6 +1,8 @@ package com.konyaco.fluent.gallery.component import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember @@ -14,6 +16,8 @@ interface ComponentNavigator { val latestBackEntry: ComponentItem? + val canNavigateUp: Boolean + } @Composable @@ -37,6 +41,10 @@ private class ComponentNavigatorImpl(startItem: ComponentItem) : ComponentNaviga } } + override val canNavigateUp: Boolean by derivedStateOf { + backstack.count { it.content != null } > 1 + } + override val currentBackstack: List get() = backstack diff --git a/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/Main.kt b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/Main.kt index f5f64993..d0eb0096 100644 --- a/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/Main.kt +++ b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/Main.kt @@ -7,28 +7,45 @@ import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState -import com.mayakapps.compose.windowstyler.WindowBackdrop -import com.mayakapps.compose.windowstyler.WindowStyle +import com.konyaco.fluent.gallery.component.rememberComponentNavigator +import com.konyaco.fluent.gallery.jna.windows.structure.isWindows10OrLater +import com.konyaco.fluent.gallery.window.WindowsWindowFrame import fluentdesign.gallery.generated.resources.Res import fluentdesign.gallery.generated.resources.icon -import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.painterResource +import org.jetbrains.skiko.hostOs -@OptIn(ExperimentalResourceApi::class) fun main() = application { + val state = rememberWindowState( + position = WindowPosition(Alignment.Center), + size = DpSize(1280.dp, 720.dp) + ) Window( onCloseRequest = ::exitApplication, - state = rememberWindowState(position = WindowPosition(Alignment.Center), size = DpSize(1280.dp, 720.dp)), + state = state, title = "Compose Fluent Design Gallery", icon = painterResource(Res.drawable.icon) ) { - GalleryTheme { - //TODO Make Window transparent. - WindowStyle( - isDarkTheme = LocalStore.current.darkMode, - backdropType = WindowBackdrop.Mica - ) - App() + val supportBackdrop = hostOs.isWindows && isWindows10OrLater() + GalleryTheme(!supportBackdrop) { + if (supportBackdrop) { + val navigator = rememberComponentNavigator() + WindowsWindowFrame( + onCloseRequest = { exitApplication() }, + icon = painterResource(Res.drawable.icon), + title = "Compose Fluent Design Gallery", + backButtonEnabled = navigator.canNavigateUp, + backButtonClick = { navigator.navigateUp() }, + state = state + ) { + App(navigator) + } + } else { + App() + } + } + } -} \ No newline at end of file +} + diff --git a/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/ComposeWindowProcedure.kt b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/ComposeWindowProcedure.kt new file mode 100644 index 00000000..d286ed58 --- /dev/null +++ b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/ComposeWindowProcedure.kt @@ -0,0 +1,198 @@ +package com.konyaco.fluent.gallery.jna.windows + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.ui.awt.ComposeWindow +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.HTBOTTOM +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.HTBOTTOMLEFT +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.HTBOTTOMRIGHT +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.HTLEFT +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.HTRIGHT +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.HTTOP +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.HTTOPLEFT +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.HTTOPRIGHT +import com.konyaco.fluent.gallery.jna.windows.structure.WindowMargins +import com.mayakapps.compose.windowstyler.findSkiaLayer +import com.sun.jna.Native +import com.sun.jna.NativeLibrary +import com.sun.jna.Pointer +import com.sun.jna.platform.win32.WinDef.HWND +import com.sun.jna.platform.win32.WinDef.UINT +import com.sun.jna.platform.win32.WinDef.LPARAM +import com.sun.jna.platform.win32.WinDef.WPARAM +import com.sun.jna.platform.win32.WinDef.LRESULT +import com.sun.jna.platform.win32.BaseTSD.LONG_PTR +import com.sun.jna.platform.win32.WinUser +import com.sun.jna.platform.win32.WinUser.WM_DESTROY +import com.sun.jna.platform.win32.WinUser.WM_SIZE +import com.sun.jna.platform.win32.WinUser.WS_CAPTION +import com.sun.jna.platform.win32.WinUser.WS_SYSMENU +import java.awt.Window + +internal class ComposeWindowProcedure( + private val window: Window, + private val hitTest: (x: Float, y: Float) -> Int, + private val onWindowInsetUpdate: (WindowInsets) -> Unit +) : WindowProcedure { + private val windowPointer = (this.window as? ComposeWindow) + ?.windowHandle + ?.let(::Pointer) + ?: Native.getWindowPointer(this.window) + + val windowHandle = HWND(windowPointer) + + private var hitResult = 1 + + // See https://learn.microsoft.com/en-us/windows/win32/winmsg/about-messages-and-message-queues#system-defined-messages + private val WM_NCCALCSIZE = 0x0083 + private val WM_NCHITTEST = 0x0084 + private val margins = WindowMargins( + leftBorderWidth = 0, + topBorderHeight = 0, + rightBorderWidth = -1, + bottomBorderHeight = -1 + ) + + // The default window procedure to call its methods when the default method behaviour is desired/sufficient + private var defaultWindowProcedure = User32Extend.instance?.setWindowLong(windowHandle, WinUser.GWL_WNDPROC, this) ?: LONG_PTR(-1) + + private var dpi = UINT(0) + private var width = 0 + private var height = 0 + private var frameX = 0 + private var frameY = 0 + private var edgeX = 0 + private var edgeY = 0 + private var padding = 0 + private var isMaximized = User32Extend.instance?.isWindowInMaximized(windowHandle) ?: false + + val skiaLayerProcedure = (window as? ComposeWindow)?.findSkiaLayer()?.let { + SkiaLayerWindowProcedure( + skiaLayer = it, + hitTest = { x, y -> + val horizontalPadding = frameX + val verticalPadding = frameY + // Hit test for resizer border + hitResult = when { + // skip resizer border hit test if window is maximized + isMaximized -> hitTest(x, y) + x <= horizontalPadding && y > verticalPadding && y < height - verticalPadding -> HTLEFT + x <= horizontalPadding && y <= verticalPadding -> HTTOPLEFT + x <= horizontalPadding -> HTBOTTOMLEFT + y <= verticalPadding && x > horizontalPadding && x < width - horizontalPadding -> HTTOP + y <= verticalPadding && x <= horizontalPadding -> HTTOPLEFT + y <= verticalPadding -> HTTOPRIGHT + x >= width - horizontalPadding && y > verticalPadding && y < height - verticalPadding -> HTRIGHT + x >= width - horizontalPadding && y <= verticalPadding -> HTTOPRIGHT + x >= width - horizontalPadding -> HTBOTTOMRIGHT + y >= height - verticalPadding && x > horizontalPadding && x < width - horizontalPadding -> HTBOTTOM + y >= height - verticalPadding && x <= horizontalPadding -> HTBOTTOMLEFT + y >= height - verticalPadding -> HTBOTTOMRIGHT + // else hit test by user + else -> hitTest(x, y) + } + hitResult + } + ) + } + + init { + enableResizability() + enableBorderAndShadow() + } + + override fun callback(hWnd: HWND, uMsg: Int, wParam: WPARAM, lParam: LPARAM): LRESULT { + return when (uMsg) { + // Returns 0 to make the window not draw the non-client area (title bar and border) + // thus effectively making all the window our client area + WM_NCCALCSIZE -> { + if (wParam.toInt() == 0) { + User32Extend.instance?.CallWindowProc(defaultWindowProcedure, hWnd, uMsg, wParam, lParam) ?: LRESULT(0) + } else { + val user32 = User32Extend.instance ?: return LRESULT(0) + dpi = user32.GetDpiForWindow(hWnd) + frameX = user32.GetSystemMetricsForDpi(WinUser.SM_CXFRAME, dpi) + frameY = user32.GetSystemMetricsForDpi(WinUser.SM_CYFRAME, dpi) + edgeX = user32.GetSystemMetricsForDpi(WinUser.SM_CXEDGE, dpi) + edgeY = user32.GetSystemMetricsForDpi(WinUser.SM_CYEDGE, dpi) + padding = user32.GetSystemMetricsForDpi(WinUser.SM_CXPADDEDBORDER, dpi) + isMaximized = user32.isWindowInMaximized(hWnd) + // Edge inset padding for non-client area + onWindowInsetUpdate( + WindowInsets( + left = if (isMaximized) { + frameX + padding + } else { + edgeX + }, + right = if (isMaximized) { + frameX + padding + } else { + edgeX + }, + top = if (isMaximized) { + frameY + padding + } else { + edgeY + }, + bottom = if (isMaximized) { + frameY + padding + } else { + edgeY + } + ) + ) + LRESULT(0) + } + + } + + WM_NCHITTEST -> { + // Hit test result return + return LRESULT(hitResult.toLong()) + } + + WM_DESTROY -> { + User32Extend.instance?.CallWindowProc(defaultWindowProcedure, hWnd, uMsg, wParam, lParam) ?: LRESULT(0) + } + + WM_SIZE -> { + width = lParam.toInt() and 0xFFFF + height = (lParam.toInt() shr 16) and 0xFFFF + User32Extend.instance?.CallWindowProc(defaultWindowProcedure, hWnd, uMsg, wParam, lParam) ?: LRESULT(0) + } + + else -> { + User32Extend.instance?.CallWindowProc(defaultWindowProcedure, hWnd, uMsg, wParam, lParam) ?: LRESULT(0) + } + } + } + + /** + * For this to take effect, also set `resizable` argument of Compose Window to `true`. + */ + private fun enableResizability() { + // Enable window resizing and remove standard caption bar + User32Extend.instance?.updateWindowStyle(windowHandle) { oldStyle -> + (oldStyle or WS_CAPTION) and WS_SYSMENU.inv() + } + } + + /** + * To disable window border and shadow, pass (0, 0, 0, 0) as window margins + * (or, simply, don't call this function). + */ + private fun enableBorderAndShadow() { + val dwmApi = "dwmapi" + .runCatching(NativeLibrary::getInstance) + .onFailure { println("Could not load dwmapi library") } + .getOrNull() + dwmApi + ?.runCatching { getFunction("DwmExtendFrameIntoClientArea") } + ?.onFailure { println("Could not enable window native decorations (border/shadow/rounded corners)") } + ?.getOrNull() + ?.invoke(arrayOf(windowHandle, margins)) + } + + + +} \ No newline at end of file diff --git a/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/SkiaLayerWindowProcedure.kt b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/SkiaLayerWindowProcedure.kt new file mode 100644 index 00000000..4e4c8564 --- /dev/null +++ b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/SkiaLayerWindowProcedure.kt @@ -0,0 +1,76 @@ +package com.konyaco.fluent.gallery.jna.windows + +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.HTCLIENT +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.HTMAXBUTTON +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.HTTRANSPANRENT +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.WM_LBUTTONDOWN +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.WM_LBUTTONUP +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.WM_MOUSEMOVE +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.WM_NCHITTEST +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.WM_NCLBUTTONDOWN +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.WM_NCLBUTTONUP +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.WM_NCMOUSEMOVE +import com.sun.jna.Native +import com.sun.jna.Pointer +import com.sun.jna.platform.win32.BaseTSD +import com.sun.jna.platform.win32.WinDef +import com.sun.jna.platform.win32.WinDef.HWND +import com.sun.jna.platform.win32.WinDef.LRESULT +import com.sun.jna.platform.win32.WinDef.POINT +import com.sun.jna.platform.win32.WinUser +import org.jetbrains.skiko.SkiaLayer + +class SkiaLayerWindowProcedure( + skiaLayer: SkiaLayer, + private val hitTest: (x: Float, y: Float) -> Int +): WindowProcedure { + + private val windowHandle = HWND(Pointer(skiaLayer.windowHandle)) + private val contentHandle = HWND(skiaLayer.canvas.let(Native::getComponentPointer)) + private val defaultWindowProcedure = User32Extend.instance?.setWindowLong(contentHandle, WinUser.GWL_WNDPROC, this) ?: BaseTSD.LONG_PTR(-1) + + private var hitResult = 1 + + override fun callback( + hwnd: HWND, + uMsg: Int, + wParam: WinDef.WPARAM, + lParam: WinDef.LPARAM + ): LRESULT { + + return when(uMsg) { + + WM_NCHITTEST -> { + val x = lParam.toInt() and 0xFFFF + val y = (lParam.toInt() shr 16) and 0xFFFF + val point = POINT(x, y) + User32Extend.instance?.ScreenToClient(windowHandle, point) + hitResult = hitTest(point.x.toFloat(), point.y.toFloat()) + point.clear() + when(hitResult) { + HTCLIENT, HTMAXBUTTON -> LRESULT(hitResult.toLong()) + else -> LRESULT(HTTRANSPANRENT.toLong()) + } + } + + WM_NCMOUSEMOVE -> { + User32Extend.instance?.SendMessage(contentHandle, WM_MOUSEMOVE, wParam, lParam) + LRESULT(0) + } + + WM_NCLBUTTONDOWN -> { + User32Extend.instance?.SendMessage(contentHandle, WM_LBUTTONDOWN, wParam, lParam) + LRESULT(0) + } + + WM_NCLBUTTONUP -> { + User32Extend.instance?.SendMessage(contentHandle, WM_LBUTTONUP, wParam, lParam) + return LRESULT(0) + } + + else -> { + User32Extend.instance?.CallWindowProc(defaultWindowProcedure, hwnd, uMsg, wParam, lParam) ?: LRESULT(0) + } + } + } +} \ No newline at end of file diff --git a/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/User32Extend.kt b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/User32Extend.kt new file mode 100644 index 00000000..49a9f05d --- /dev/null +++ b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/User32Extend.kt @@ -0,0 +1,74 @@ +package com.konyaco.fluent.gallery.jna.windows + +import com.sun.jna.Native +import com.sun.jna.Platform +import com.sun.jna.platform.win32.User32 +import com.sun.jna.platform.win32.WinDef.HWND +import com.sun.jna.platform.win32.WinDef.WPARAM +import com.sun.jna.platform.win32.WinDef.LPARAM +import com.sun.jna.platform.win32.WinDef.LRESULT +import com.sun.jna.platform.win32.WinUser.WindowProc +import com.sun.jna.platform.win32.BaseTSD.LONG_PTR +import com.sun.jna.platform.win32.WinDef.POINT +import com.sun.jna.platform.win32.WinDef.UINT +import com.sun.jna.platform.win32.WinUser +import com.sun.jna.win32.W32APIOptions + +@Suppress("FunctionName") +internal interface User32Extend : User32 { + + fun SetWindowLong(hWnd: HWND, nIndex: Int, wndProc: WindowProc): LONG_PTR + + fun SetWindowLongPtr(hWnd: HWND, nIndex: Int, wndProc: WindowProc): LONG_PTR + + fun CallWindowProc( + proc: LONG_PTR, + hWnd: HWND, + uMsg: Int, + uParam: WPARAM, + lParam: LPARAM + ): LRESULT + + fun GetSystemMetricsForDpi(nIndex: Int, dpi: UINT): Int + + fun GetDpiForWindow(hWnd: HWND): UINT + + fun ScreenToClient(hWnd: HWND, lpPoint: POINT): Boolean + + + companion object { + + val instance by lazy { + runCatching { + Native.load( + "user32", + User32Extend::class.java, + W32APIOptions.DEFAULT_OPTIONS + ) + } + .onFailure { println("Could not load user32 library") } + .getOrNull() + } + } +} + +internal fun User32Extend.setWindowLong(hWnd: HWND, nIndex: Int, procedure: WindowProcedure): LONG_PTR { + return if (Platform.is64Bit()) { + SetWindowLongPtr(hWnd, nIndex, procedure) + } else { + SetWindowLong(hWnd, nIndex, procedure) + } +} + +internal fun User32.isWindowInMaximized(hWnd: HWND): Boolean { + val placement = WinUser.WINDOWPLACEMENT() + val result = GetWindowPlacement(hWnd, placement) + .booleanValue() && placement.showCmd == WinUser.SW_SHOWMAXIMIZED + placement.clear() + return result +} + +internal fun User32.updateWindowStyle(hWnd: HWND, styleBlock: (oldStyle: Int) -> Int) { + val oldStyle = GetWindowLong(hWnd, WinUser.GWL_STYLE) + SetWindowLong(hWnd, WinUser.GWL_STYLE, styleBlock(oldStyle)) +} \ No newline at end of file diff --git a/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/WindowProcedure.kt b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/WindowProcedure.kt new file mode 100644 index 00000000..d0f513a5 --- /dev/null +++ b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/WindowProcedure.kt @@ -0,0 +1,5 @@ +package com.konyaco.fluent.gallery.jna.windows + +import com.sun.jna.platform.win32.WinUser.WindowProc + +typealias WindowProcedure = WindowProc \ No newline at end of file diff --git a/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/structure/VersionHelper.kt b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/structure/VersionHelper.kt new file mode 100644 index 00000000..c5b144ec --- /dev/null +++ b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/structure/VersionHelper.kt @@ -0,0 +1,14 @@ +package com.konyaco.fluent.gallery.jna.windows.structure + +import com.sun.jna.platform.win32.Kernel32 + +val windowsBuildNumber by lazy { + val buildNumber = Kernel32.INSTANCE.GetVersion().high.toInt() + buildNumber +} + +fun isWindows10OrLater() = windowsBuildNumber >= 10240 + +fun isWindows11OrLater() = windowsBuildNumber >= 22000 + +fun isWindows1122H2OrLater() = windowsBuildNumber >= 22621 \ No newline at end of file diff --git a/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/structure/WinUserConst.kt b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/structure/WinUserConst.kt new file mode 100644 index 00000000..f14a05ed --- /dev/null +++ b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/structure/WinUserConst.kt @@ -0,0 +1,42 @@ +package com.konyaco.fluent.gallery.jna.windows.structure + +object WinUserConst { + + // non client area hit test message + val WM_NCHITTEST = 0x0084 + // mouse move message + val WM_MOUSEMOVE = 0x0200 + // left mouse button down message + val WM_LBUTTONDOWN = 0x0201 + // left mouse button up message + val WM_LBUTTONUP = 0x0202 + // non client area mouse move message + val WM_NCMOUSEMOVE = 0x00A0 + // non client area left mouse down message + val WM_NCLBUTTONDOWN = 0x00A1 + // non client area left mouse up message + val WM_NCLBUTTONUP = 0x00A2 + + /** + * [WM_NCHITTEST] Mouse Position Codes + */ + // pass the hit test to parent window + internal val HTTRANSPANRENT = -1 + // no hit test + internal val HTNOWHERE = 0 + // client area + internal val HTCLIENT = 1 + // title bar + internal val HTCAPTION = 2 + // max button + internal val HTMAXBUTTON = 9 + // window edges + internal val HTLEFT = 10 + internal val HTRIGHT = 11 + internal val HTTOP = 12 + internal val HTTOPLEFT = 13 + internal val HTTOPRIGHT = 14 + internal val HTBOTTOM = 15 + internal val HTBOTTOMLEFT = 16 + internal val HTBOTTOMRIGHT = 17 +} \ No newline at end of file diff --git a/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/structure/WindowMargins.kt b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/structure/WindowMargins.kt new file mode 100644 index 00000000..ac80ad57 --- /dev/null +++ b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/structure/WindowMargins.kt @@ -0,0 +1,17 @@ +package com.konyaco.fluent.gallery.jna.windows.structure + +import com.sun.jna.Structure + +// See https://stackoverflow.com/q/62240901 +@Structure.FieldOrder( + "leftBorderWidth", + "rightBorderWidth", + "topBorderHeight", + "bottomBorderHeight" +) +data class WindowMargins( + @JvmField var leftBorderWidth: Int, + @JvmField var rightBorderWidth: Int, + @JvmField var topBorderHeight: Int, + @JvmField var bottomBorderHeight: Int +) : Structure(), Structure.ByReference \ No newline at end of file diff --git a/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/window/LayoutHitTestOwner.kt b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/window/LayoutHitTestOwner.kt new file mode 100644 index 00000000..b7116e44 --- /dev/null +++ b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/window/LayoutHitTestOwner.kt @@ -0,0 +1,167 @@ +@file:Suppress("UNCHECKED_CAST") + +package com.konyaco.fluent.gallery.window + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.node.PointerInputModifierNode +import androidx.compose.ui.scene.ComposeScene +import androidx.compose.ui.util.fastForEachReversed +import androidx.compose.ui.util.packFloats + +@OptIn(InternalComposeUiApi::class) +@Composable +fun rememberLayoutHitTestOwner(): LayoutHitTestOwner { + val scene = getLocalComposeScene()?.current ?: error("no compose scene") + return remember(scene) { + when(scene::class.qualifiedName) { + "androidx.compose.ui.scene.MultiLayerComposeSceneImpl" -> { + MultiLayerLayoutHitTestOwner(scene) + } + "androidx.compose.ui.scene.SingleLayerComposeSceneImpl" -> { + SingleLayerLayoutHitTestOwner(scene) + } + else -> error("unsupported compose scene") + } + } +} + +@OptIn(InternalComposeUiApi::class) +@Stable +private fun getLocalComposeScene(): ProvidableCompositionLocal? { + val classLoader = ComposeScene::class.java.classLoader + val composeSceneClass = classLoader.loadClass("androidx.compose.ui.scene.ComposeScene_skikoKt") + val methodRef = composeSceneClass.getMethod("getLocalComposeScene") + methodRef.trySetAccessible() + return methodRef.invoke(null) as? ProvidableCompositionLocal +} + +interface LayoutHitTestOwner { + + fun hitTest(x: Float, y: Float): Boolean { + return false + } +} + +/* +* reflect implementation for compose 1.6 + */ +internal abstract class ReflectLayoutHitTestOwner: LayoutHitTestOwner { + + @OptIn(InternalComposeUiApi::class) + protected val classLoader = ComposeScene::class.java.classLoader!! + + private val rootNodeOwnerOwnerField = classLoader.loadClass("androidx.compose.ui.node.RootNodeOwner") + .getDeclaredField("owner").apply { + trySetAccessible() + } + + private val ownerRootField = classLoader.loadClass("androidx.compose.ui.node.Owner") + .getDeclaredMethod("getRoot").apply { + trySetAccessible() + } + + private val hitTestResultClass = classLoader.loadClass("androidx.compose.ui.node.HitTestResult") + + private val layoutNodeHitTestMethod = classLoader.loadClass("androidx.compose.ui.node.LayoutNode") + .declaredMethods.first { it.name.startsWith("hitTest-") && it.parameterCount == 4 } + + protected fun getLayoutNode(rootNodeOwner: Any): Any { + val owner = rootNodeOwnerOwnerField.get(rootNodeOwner) + return ownerRootField.invoke(owner) + } + + protected fun Any.layoutNodeHitTest(x: Float, y: Float): Boolean { + val result = hitTestResultClass.getDeclaredConstructor().newInstance() as List + layoutNodeHitTestMethod.invoke(this, packFloats(x, y), result, false, true) + val lastNode = result.lastOrNull() + return lastNode is PointerInputModifierNode + } + + protected class CopiedList( + private val populate: (MutableList) -> Unit + ) : MutableList by mutableListOf() { + inline fun withCopy( + block: (List) -> Unit + ) { + // In case of recursive calls, allocate new list + val copy = if (isEmpty()) this else mutableListOf() + populate(copy) + try { + block(copy) + } finally { + copy.clear() + } + } + } +} + +@OptIn(InternalComposeUiApi::class) +internal class SingleLayerLayoutHitTestOwner(scene: ComposeScene): ReflectLayoutHitTestOwner() { + + private val sceneClass = classLoader.loadClass("androidx.compose.ui.scene.SingleLayerComposeSceneImpl") + + private val mainOwnerRef = sceneClass.getDeclaredMethod("getMainOwner").let { + it.trySetAccessible() + it.invoke(scene) + } + + override fun hitTest(x: Float, y: Float): Boolean { + return getLayoutNode(mainOwnerRef).layoutNodeHitTest(x, y) + } +} + +@OptIn(InternalComposeUiApi::class) +internal class MultiLayerLayoutHitTestOwner(private val scene: ComposeScene): ReflectLayoutHitTestOwner() { + + private val sceneClass = classLoader.loadClass("androidx.compose.ui.scene.MultiLayerComposeSceneImpl") + private val layerClass = sceneClass.declaredClasses.first {it.name == "androidx.compose.ui.scene.MultiLayerComposeSceneImpl\$AttachedComposeSceneLayer" } + + private val mainOwnerRef = sceneClass.getDeclaredField("mainOwner").let { + it.trySetAccessible() + it.get(scene) + } + + private val layersRef = sceneClass.getDeclaredField("layers").let { + it.trySetAccessible() + it.get(scene) + } as MutableList + + private val focusedLayerField = sceneClass.getDeclaredField("focusedLayer").apply { + trySetAccessible() + } + + private val layerOwnerField = layerClass + .getDeclaredField("owner").apply { + trySetAccessible() + } + + private val layerIsInBoundMethod = layerClass + .declaredMethods.first { it.name.startsWith("isInBounds") }.apply { + trySetAccessible() + } + + private val _layers = CopiedList { + for (layer in layersRef) { + it.add(layer) + } + } + + override fun hitTest(x: Float, y: Float): Boolean { + _layers.withCopy { + it.fastForEachReversed { layer -> + if (layerIsInBoundMethod.invoke(layer, packFloats(x, y)) == true) { + return getLayoutNode(layerOwnerField.get(layer)).layoutNodeHitTest(x, y) + } else if (layer == focusedLayerField.get(scene)) { + return false + } + } + } + return getLayoutNode(mainOwnerRef).layoutNodeHitTest(x, y) + } + +} \ No newline at end of file diff --git a/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/window/WindowsWindowFrame.kt b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/window/WindowsWindowFrame.kt new file mode 100644 index 00000000..5c870b5d --- /dev/null +++ b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/window/WindowsWindowFrame.kt @@ -0,0 +1,432 @@ +package com.konyaco.fluent.gallery.window + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.SizeTransform +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandHorizontally +import androidx.compose.animation.expandIn +import androidx.compose.animation.shrinkHorizontally +import androidx.compose.animation.shrinkOut +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.MutableWindowInsets +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.ComposeWindow +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.text.ExperimentalTextApi +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.FrameWindowScope +import androidx.compose.ui.window.WindowPlacement +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.zIndex +import com.konyaco.fluent.FluentTheme +import com.konyaco.fluent.animation.FluentDuration +import com.konyaco.fluent.animation.FluentEasing +import com.konyaco.fluent.background.BackgroundSizing +import com.konyaco.fluent.background.Layer +import com.konyaco.fluent.component.SubtleButton +import com.konyaco.fluent.component.Text +import com.konyaco.fluent.gallery.jna.windows.ComposeWindowProcedure +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.HTCAPTION +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.HTCLIENT +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.HTMAXBUTTON +import com.konyaco.fluent.gallery.jna.windows.structure.isWindows11OrLater +import com.konyaco.fluent.icons.Icons +import com.konyaco.fluent.icons.regular.ArrowLeft +import com.konyaco.fluent.icons.regular.Dismiss +import com.konyaco.fluent.icons.regular.Square +import com.konyaco.fluent.icons.regular.SquareMultiple +import com.konyaco.fluent.icons.regular.Subtract +import com.konyaco.fluent.scheme.PentaVisualScheme +import com.konyaco.fluent.scheme.VisualStateScheme +import com.konyaco.fluent.scheme.collectVisualState +import com.mayakapps.compose.windowstyler.WindowBackdrop +import com.mayakapps.compose.windowstyler.WindowStyle +import com.mayakapps.compose.windowstyler.findSkiaLayer +import com.sun.jna.platform.win32.User32 +import com.sun.jna.platform.win32.WinDef.HWND +import com.sun.jna.platform.win32.WinUser +import java.awt.Window +import java.awt.event.WindowEvent +import java.awt.event.WindowFocusListener + +@OptIn(ExperimentalLayoutApi::class, ExperimentalTextApi::class) +@Composable +fun FrameWindowScope.WindowsWindowFrame( + onCloseRequest: () -> Unit, + icon: Painter, + title: String, + state: WindowState, + backButtonVisible: Boolean = true, + backButtonEnabled: Boolean = false, + backButtonClick: () -> Unit = {}, + content: @Composable () -> Unit +) { + LaunchedEffect(window) { + window.findSkiaLayer()?.transparency = true + } + WindowStyle( + isDarkTheme = FluentTheme.colors.darkMode, + backdropType = when { + isWindows11OrLater() -> WindowBackdrop.Mica + else -> WindowBackdrop.Solid(FluentTheme.colors.background.mica.baseAlt) + } + ) + + val paddingInset = remember { MutableWindowInsets() } + val maxButtonRect = remember { mutableStateOf(Rect.Zero) } + val captionBarRect = remember { mutableStateOf(Rect.Zero) } + val layoutHitTestOwner = rememberLayoutHitTestOwner() + + val procedure = remember(window) { + ComposeWindowProcedure( + window = window, + hitTest = { x, y -> + when { + maxButtonRect.value.contains(x, y) -> HTMAXBUTTON + captionBarRect.value.contains(x, y) && !layoutHitTestOwner.hitTest( + x, + y + ) -> HTCAPTION + + else -> HTCLIENT + } + }, + onWindowInsetUpdate = { paddingInset.insets = it } + ) + } + Column( + modifier = Modifier.windowInsetsPadding(paddingInset) + ) { + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.height(48.dp) + .onGloballyPositioned { captionBarRect.value = it.boundsInWindow() } + ) { + AnimatedContent( + targetState = backButtonVisible, + transitionSpec = { + ContentTransform( + targetContentEnter = expandHorizontally(), + initialContentExit = shrinkHorizontally(), + sizeTransform = SizeTransform { _, _ -> + tween( + FluentDuration.ShortDuration, + easing = FluentEasing.FastInvokeEasing + ) + } + ) + }, + modifier = Modifier.padding(start = 4.dp) + ) { + if (it) { + val interaction = remember { MutableInteractionSource() } + val isPressed by interaction.collectIsPressedAsState() + val scaleX = animateFloatAsState( + if (isPressed) { + 0.9f + } else { + 1f + } + ) + SubtleButton( + onClick = backButtonClick, + disabled = !backButtonEnabled, + interaction = interaction, + iconOnly = true, + modifier = Modifier.defaultMinSize(36.dp, 36.dp) + ) { + + Text( + text = CaptionButtonIcon.Back.glyph.toString(), + fontFamily = if (!isWindows11OrLater()) { + FontFamily("Segoe MDL2 Assets") + } else { + FontFamily("Segoe Fluent Icons") + }, + modifier = Modifier.graphicsLayer { + this.scaleX = scaleX.value + translationX = (1f - scaleX.value) * 6.dp.toPx() + }, + fontSize = 10.sp + ) + } + } else { + Spacer(modifier = Modifier.width(10.dp).height(36.dp)) + } + } + Image( + painter = icon, + contentDescription = null, + modifier = Modifier.padding(start = 6.dp).size(16.dp) + ) + Text( + text = title, + style = FluentTheme.typography.caption, + modifier = Modifier.padding(start = 16.dp) + ) + Spacer(modifier = Modifier.weight(1f)) + window.CaptionButtonRow( + procedure.windowHandle, + state.placement == WindowPlacement.Maximized, + onCloseRequest = onCloseRequest, + onMaximizeButtonRectUpdate = { + maxButtonRect.value = it + }, + modifier = Modifier.align(Alignment.Top) + ) + } + content() + } +} + +@Composable +fun Window.CaptionButtonRow( + windowHandle: HWND, + isMaximize: Boolean, + onCloseRequest: () -> Unit, + modifier: Modifier = Modifier, + onMaximizeButtonRectUpdate: (Rect) -> Unit +) { + val isActive = remember { mutableStateOf(false) } + DisposableEffect(this) { + val listener = object : WindowFocusListener { + override fun windowGainedFocus(e: WindowEvent?) { + isActive.value = true + } + + override fun windowLostFocus(e: WindowEvent?) { + isActive.value = false + } + } + addWindowFocusListener(listener) + onDispose { + removeWindowFocusListener(listener) + } + } + //Draw the caption button + Row( + modifier = modifier + .zIndex(1f) + ) { + CaptionButton( + onClick = { + User32.INSTANCE.CloseWindow(windowHandle) + }, + icon = CaptionButtonIcon.Minimize, + isActive = isActive.value + ) + CaptionButton( + onClick = { + if (isMaximize) { + User32.INSTANCE.ShowWindow( + windowHandle, + WinUser.SW_RESTORE + ) + } else { + User32.INSTANCE.ShowWindow( + windowHandle, + WinUser.SW_MAXIMIZE + ) + } + }, + icon = if (isMaximize) { + CaptionButtonIcon.Restore + } else { + CaptionButtonIcon.Maximize + }, + isActive = isActive.value, + modifier = Modifier.onGloballyPositioned { + onMaximizeButtonRectUpdate(it.boundsInWindow()) + } + ) + CaptionButton( + icon = CaptionButtonIcon.Close, + onClick = onCloseRequest, + isActive = isActive.value, + colors = CaptionButtonDefaults.closeColors() + ) + } +} + +@OptIn(ExperimentalTextApi::class) +@Composable +fun CaptionButton( + onClick: () -> Unit, + icon: CaptionButtonIcon, + isActive: Boolean, + modifier: Modifier = Modifier, + colors: VisualStateScheme = CaptionButtonDefaults.defaultColors(), + interaction: MutableInteractionSource = remember { MutableInteractionSource() } +) { + val color = colors.schemeFor(interaction.collectVisualState(false)) + Layer( + backgroundSizing = BackgroundSizing.OuterBorderEdge, + border = null, + color = if (isActive) { + color.background + } else { + color.inactiveBackground + }, + contentColor = if (isActive) { + color.foreground + } else { + color.inactiveForeground + }, + modifier = modifier.size(46.dp, 32.dp).clickable( + onClick = onClick, + interactionSource = interaction, + indication = null + ), + shape = RectangleShape + ) { + Text( + text = icon.glyph.toString(), + fontFamily = if (!isWindows11OrLater()) { + FontFamily("Segoe MDL2 Assets") + } else { + FontFamily("Segoe Fluent Icons") + }, + textAlign = TextAlign.Center, + fontSize = 10.sp, + modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center) + ) + } +} + +object CaptionButtonDefaults { + @Composable + @Stable + fun defaultColors( + default: CaptionButtonColor = CaptionButtonColor( + background = FluentTheme.colors.subtleFill.transparent, + foreground = FluentTheme.colors.text.text.primary, + inactiveBackground = FluentTheme.colors.subtleFill.transparent, + inactiveForeground = FluentTheme.colors.text.text.disabled + ), + hovered: CaptionButtonColor = default.copy( + background = FluentTheme.colors.subtleFill.secondary, + inactiveBackground = FluentTheme.colors.subtleFill.secondary, + inactiveForeground = FluentTheme.colors.text.text.primary + ), + pressed: CaptionButtonColor = default.copy( + background = FluentTheme.colors.subtleFill.tertiary, + foreground = FluentTheme.colors.text.text.secondary, + inactiveBackground = FluentTheme.colors.subtleFill.tertiary, + inactiveForeground = FluentTheme.colors.text.text.tertiary + ), + disabled: CaptionButtonColor = default.copy( + foreground = FluentTheme.colors.text.text.disabled, + ), + ) = PentaVisualScheme( + default = default, + hovered = hovered, + pressed = pressed, + disabled = disabled + ) + + @Composable + @Stable + fun closeColors( + default: CaptionButtonColor = CaptionButtonColor( + background = FluentTheme.colors.subtleFill.transparent, + foreground = FluentTheme.colors.text.text.primary, + inactiveBackground = FluentTheme.colors.subtleFill.transparent, + inactiveForeground = FluentTheme.colors.text.text.disabled + ), + hovered: CaptionButtonColor = default.copy( + background = Color(0xFFC42B1C), + foreground = Color.White, + inactiveBackground = Color(0xFFC42B1C), + inactiveForeground = Color.White + ), + pressed: CaptionButtonColor = default.copy( + background = Color(0xFFC42B1C).copy(0.9f), + foreground = Color.White.copy(0.7f), + inactiveBackground = Color(0xFFC42B1C).copy(0.9f), + inactiveForeground = Color.White.copy(0.7f) + ), + disabled: CaptionButtonColor = default.copy( + foreground = FluentTheme.colors.text.text.disabled, + ), + ) = PentaVisualScheme( + default = default, + hovered = hovered, + pressed = pressed, + disabled = disabled + ) +} + +@Stable +data class CaptionButtonColor( + val background: Color, + val foreground: Color, + val inactiveBackground: Color, + val inactiveForeground: Color +) + +enum class CaptionButtonIcon( + val glyph: Char, + val imageVector: ImageVector +) { + Minimize( + glyph = '\uE921', + imageVector = Icons.Default.Subtract + ), + Maximize( + glyph = '\uE922', + imageVector = Icons.Default.Square + ), + Restore( + glyph = '\uE923', + imageVector = Icons.Default.SquareMultiple + ), + Close( + glyph = '\uE8BB', + imageVector = Icons.Default.Dismiss + ), + Back( + glyph = '\uE830', + imageVector = Icons.Default.ArrowLeft + ) +} + +fun Rect.contains(x: Float, y: Float): Boolean { + return x >= left && x < right && y >= top && y < bottom +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ed7a7469..568ac939 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,7 @@ androidGradlePlugin = "8.3.2" androidBuildTools = "31.3.2" windowStyler = "0.3.3-SNAPSHOT" highlights = "0.8.0" +jna = "5.14.0" [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } @@ -24,6 +25,8 @@ android-tools-common = { module = "com.android.tools:common", version.ref = "and android-tools-sdk-common = { module = "com.android.tools:sdk-common", version.ref = "androidBuildTools" } window-styler = { module = "com.mayakapps.compose:window-styler", version.ref = "windowStyler" } highlights = { module = "dev.snipme:highlights", version.ref = "highlights" } +jna-platform = { module = "net.java.dev.jna:jna-platform-jpms", version.ref = "jna" } +jna = { module = "net.java.dev.jna:jna-jpms", version.ref = "jna" } [plugins] kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } From c0918546394dabe4402beb4ff744bf38a7cf3894 Mon Sep 17 00:00:00 2001 From: sanlorng Date: Wed, 19 Jun 2024 02:07:24 +0800 Subject: [PATCH 009/247] [gallery] code clean. --- .../fluent/gallery/jna/windows/ComposeWindowProcedure.kt | 5 ++--- .../fluent/gallery/jna/windows/structure/WinUserConst.kt | 2 ++ .../konyaco/fluent/gallery/window/WindowsWindowFrame.kt | 9 +-------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/ComposeWindowProcedure.kt b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/ComposeWindowProcedure.kt index d286ed58..d0bd68be 100644 --- a/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/ComposeWindowProcedure.kt +++ b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/ComposeWindowProcedure.kt @@ -10,6 +10,8 @@ import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.HTRIGHT import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.HTTOP import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.HTTOPLEFT import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.HTTOPRIGHT +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.WM_NCCALCSIZE +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.WM_NCHITTEST import com.konyaco.fluent.gallery.jna.windows.structure.WindowMargins import com.mayakapps.compose.windowstyler.findSkiaLayer import com.sun.jna.Native @@ -42,9 +44,6 @@ internal class ComposeWindowProcedure( private var hitResult = 1 - // See https://learn.microsoft.com/en-us/windows/win32/winmsg/about-messages-and-message-queues#system-defined-messages - private val WM_NCCALCSIZE = 0x0083 - private val WM_NCHITTEST = 0x0084 private val margins = WindowMargins( leftBorderWidth = 0, topBorderHeight = 0, diff --git a/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/structure/WinUserConst.kt b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/structure/WinUserConst.kt index f14a05ed..c49facf0 100644 --- a/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/structure/WinUserConst.kt +++ b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/jna/windows/structure/WinUserConst.kt @@ -2,6 +2,8 @@ package com.konyaco.fluent.gallery.jna.windows.structure object WinUserConst { + //calculate non client area size message + val WM_NCCALCSIZE = 0x0083 // non client area hit test message val WM_NCHITTEST = 0x0084 // mouse move message diff --git a/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/window/WindowsWindowFrame.kt b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/window/WindowsWindowFrame.kt index 5c870b5d..55f4b5cc 100644 --- a/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/window/WindowsWindowFrame.kt +++ b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/window/WindowsWindowFrame.kt @@ -1,15 +1,12 @@ package com.konyaco.fluent.gallery.window import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ContentTransform import androidx.compose.animation.SizeTransform import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.animation.expandHorizontally -import androidx.compose.animation.expandIn import androidx.compose.animation.shrinkHorizontally -import androidx.compose.animation.shrinkOut import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -36,7 +33,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.awt.ComposeWindow import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape @@ -119,10 +115,7 @@ fun FrameWindowScope.WindowsWindowFrame( hitTest = { x, y -> when { maxButtonRect.value.contains(x, y) -> HTMAXBUTTON - captionBarRect.value.contains(x, y) && !layoutHitTestOwner.hitTest( - x, - y - ) -> HTCAPTION + captionBarRect.value.contains(x, y) && !layoutHitTestOwner.hitTest(x, y) -> HTCAPTION else -> HTCLIENT } From 8fa3e9a77d4857c363a178ebf8344aa7c707dadc Mon Sep 17 00:00:00 2001 From: sanlorng Date: Mon, 24 Jun 2024 11:31:53 +0800 Subject: [PATCH 010/247] [fluent] Update `haze` version to 0.7.2. --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3e268353..47356962 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] activityCompose = "1.8.2" -haze = "0.7.1" +haze = "0.7.2" kotlin = "1.9.23" ksp = "1.9.23-1.0.20" compose = "1.6.10" From 7f742e90601ea54d47a59291c8634be662040ef3 Mon Sep 17 00:00:00 2001 From: sanlorng Date: Fri, 14 Jun 2024 19:19:25 +0800 Subject: [PATCH 011/247] [fluent] Add `Elevation` support . --- .../fluent/background/Elevation.android.kt | 13 ++ .../konyaco/fluent/background/Elevation.kt | 143 ++++++++++++++++++ .../com/konyaco/fluent/background/Layer.kt | 21 ++- .../com/konyaco/fluent/component/Dropdown.kt | 56 ++++--- .../com/konyaco/fluent/component/Flyout.kt | 127 ++++++++++++---- .../fluent/background/Elevation.desktop.kt | 17 +++ .../gallery/screen/design/ElevationScreen.kt | 142 +++++++++++++++++ 7 files changed, 462 insertions(+), 57 deletions(-) create mode 100644 fluent/src/androidMain/kotlin/com/konyaco/fluent/background/Elevation.android.kt create mode 100644 fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Elevation.kt create mode 100644 fluent/src/desktopMain/kotlin/com/konyaco/fluent/background/Elevation.desktop.kt create mode 100644 gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/design/ElevationScreen.kt diff --git a/fluent/src/androidMain/kotlin/com/konyaco/fluent/background/Elevation.android.kt b/fluent/src/androidMain/kotlin/com/konyaco/fluent/background/Elevation.android.kt new file mode 100644 index 00000000..e4961e3e --- /dev/null +++ b/fluent/src/androidMain/kotlin/com/konyaco/fluent/background/Elevation.android.kt @@ -0,0 +1,13 @@ +package com.konyaco.fluent.background + +import android.graphics.BlurMaskFilter +import android.os.Build +import androidx.compose.ui.graphics.Paint + +internal actual fun Paint.applyShadowMaskFilter(radius: Float) { + asFrameworkPaint().maskFilter = BlurMaskFilter(radius, BlurMaskFilter.Blur.NORMAL) +} + +internal actual fun supportFluentElevation(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P +} \ No newline at end of file diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Elevation.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Elevation.kt new file mode 100644 index 00000000..9af5becb --- /dev/null +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Elevation.kt @@ -0,0 +1,143 @@ +package com.konyaco.fluent.background + +import androidx.compose.foundation.border +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.ClipOp +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.addOutline +import androidx.compose.ui.graphics.drawscope.withTransform +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.konyaco.fluent.FluentTheme + +@Composable +fun Modifier.elevation( + elevation: Dp, + shape: Shape, + isDarkTheme: Boolean = FluentTheme.colors.darkMode, +) = when { + elevation.value < 1f -> this + elevation.value < 3f -> border( + width = 1.dp, + brush = if (elevation.value < 2f) { + SolidColor(FluentTheme.colors.stroke.card.default) + } else { + Brush.verticalGradient( + 0.5f to FluentTheme.colors.stroke.control.default, + 0.95f to FluentTheme.colors.stroke.control.secondary, + ) + }, + shape = shape + ) + + else -> platformElevation(shape = shape, elevation = elevation, isDarkTheme = isDarkTheme) +} + +internal fun Modifier.platformElevation( + elevation: Dp, + shape: Shape, + isDarkTheme: Boolean, + borderWidth: Dp = 1.dp, +): Modifier { + if (elevation.value <= 2f) return this + val spotColorOpacity: Float + val ambientColorOpacity: Float + when { + isDarkTheme && elevation.value <= 32 -> { + spotColorOpacity = 0.26f + ambientColorOpacity = 0f + } + + isDarkTheme -> { + spotColorOpacity = 0.37f + ambientColorOpacity = 0.37f + } + + elevation.value <= 32 -> { + spotColorOpacity = minOf(14f, elevation.value + 6) / 100 + ambientColorOpacity = 0f + } + + else -> { + spotColorOpacity = 0.19f + ambientColorOpacity = 0.15f + } + } + val spotColor = Color.Black.copy(alpha = spotColorOpacity.coerceIn(0f, 1f)) + val ambientColor = Color.Black.copy(alpha = ambientColorOpacity.coerceIn(0f, 1f)) + return if (!supportFluentElevation()) { + shadow( + elevation = elevation / 2, + shape = shape, + spotColor = spotColor, + ambientColor = ambientColor + ) + } else { + drawWithCache { + val spotPaint = Paint() + val ambientPaint = Paint() + val spotBlurRadius = 0.5f * elevation.toPx() + val ambientBlurRadius = 0.167f * elevation.toPx() + + spotPaint.color = spotColor + ambientPaint.color = ambientColor + spotPaint.isAntiAlias = false + spotPaint.applyShadowMaskFilter(spotBlurRadius) + ambientPaint.isAntiAlias = false + ambientPaint.applyShadowMaskFilter(ambientBlurRadius) + val borderWidthPx = borderWidth.toPx() + val path = Path().apply { + addOutline( + shape.createOutline( + size = Size(width = size.width - 2 * borderWidthPx, height = size.height - 2 * borderWidthPx), + layoutDirection = layoutDirection, + density = this@drawWithCache + ) + ) + } + onDrawWithContent { + val offsetY = elevation.toPx() * 0.25f + withTransform({ + translate(left = borderWidthPx, top = borderWidthPx) + clipPath(path = path, clipOp = ClipOp.Difference) + translate(left = 0f, top = offsetY) + }) { + drawContext.canvas.drawPath(path, spotPaint) + } + + if (elevation.value > 32f) { + // ambient shadow + withTransform({ + translate(left = 0.5f * borderWidthPx, top = 0.5f * borderWidthPx) + clipPath(path = path, clipOp = ClipOp.Difference) + }) { + drawContext.canvas.drawPath(path, ambientPaint) + } + } + drawContent() + } + } + } +} + +object ElevationDefaults { + val layer: Dp = 0.dp + val control: Dp = 2.dp + val cardRest: Dp = 8.dp + val tooltip: Dp = 16.dp + val flyout: Dp = 32.dp + val dialog: Dp = 128.dp +} + +internal expect fun Paint.applyShadowMaskFilter(radius: Float) + +internal expect fun supportFluentElevation(): Boolean \ No newline at end of file diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Layer.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Layer.kt index a66928c2..9480d019 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Layer.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Layer.kt @@ -13,7 +13,6 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.translate @@ -111,6 +110,7 @@ fun Layer( } } +@Composable private fun Modifier.layer( elevation: Dp, shape: Shape, @@ -119,7 +119,7 @@ private fun Modifier.layer( color: Color ) = then( Modifier - .shadow(elevation = elevation, shape = shape, clip = false) + .elevation(elevation = elevation, shape = shape) .then( if (border != null) { val backgroundShape = @@ -146,12 +146,7 @@ private value class BackgroundPaddingShape(private val borderShape: CornerBasedS override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline { return with(density) { - val circular = borderShape == CircleShape - val paddingPx = when { - circular -> calcCircularPadding(density) - else -> calcPadding(density) - }.toPx() - createInnerOutline(size, density, layoutDirection, paddingPx) + createInnerOutline(size, density, layoutDirection, borderShape.calculateBorderPadding(density)) } } @@ -229,4 +224,14 @@ private fun calcCircularPadding(density: Density): Dp { if (remainder == 0f) 1.dp else (1.dp.toPx() - remainder + 1).toDp() } +} + +internal fun Shape.calculateBorderPadding(density: Density): Float { + val circular = this == CircleShape + return with(density) { + when { + circular -> calcCircularPadding(density) + else -> calcPadding(density) + }.toPx() + } } \ No newline at end of file diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dropdown.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dropdown.kt index 2d370005..96174e6e 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dropdown.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dropdown.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.defaultMinSize @@ -23,8 +24,7 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.platform.LocalDensity @@ -37,12 +37,16 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.window.PopupPositionProvider import androidx.compose.ui.window.PopupProperties +import com.konyaco.fluent.ExperimentalFluentApi import com.konyaco.fluent.FluentTheme +import com.konyaco.fluent.LocalAcrylicPopupEnabled +import com.konyaco.fluent.LocalWindowAcrylicContainer import com.konyaco.fluent.animation.FluentDuration import com.konyaco.fluent.animation.FluentEasing +import com.konyaco.fluent.background.AcrylicDefaults import com.konyaco.fluent.background.BackgroundSizing +import com.konyaco.fluent.background.ElevationDefaults import com.konyaco.fluent.background.Layer -import com.konyaco.fluent.background.Mica @Composable fun DropdownMenu( @@ -98,7 +102,7 @@ internal class DropdownMenuPositionProvider(val density: Density, val offset: Dp val popupToTop = bottomSpace < needSpace && topSpace > needSpace - val y = if(popupToTop) { + val y = if (popupToTop) { anchorBounds.top - needSpace } else { anchorBounds.bottom + gap @@ -111,6 +115,7 @@ internal class DropdownMenuPositionProvider(val density: Density, val offset: Dp } } +@OptIn(ExperimentalFluentApi::class) @Composable internal fun DropdownMenuContent( expandedStates: MutableTransitionState, @@ -125,20 +130,35 @@ internal fun DropdownMenuContent( ), // TODO: If popup direction is upward, the expanding animation should be bottom-to-top. exit = fadeOut(tween(FluentDuration.ShortDuration, easing = FluentEasing.FastDismissEasing)) ) { - Mica(Modifier.shadow(8.dp, FluentTheme.shapes.overlay).clip(FluentTheme.shapes.overlay)) { - // TODO: Dropdown should use Acrylic material. - Layer( - shape = FluentTheme.shapes.overlay, - border = BorderStroke(1.dp, FluentTheme.colors.stroke.surface.flyout), - backgroundSizing = BackgroundSizing.InnerBorderEdge - ) { - Column( - modifier = modifier - .padding(vertical = 4.dp, horizontal = 4.dp) - .width(IntrinsicSize.Max) - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(4.dp), - content = content + val shape = FluentTheme.shapes.overlay + val useAcrylic = LocalAcrylicPopupEnabled.current + Layer( + elevation = ElevationDefaults.flyout, + color = if (!useAcrylic) { + FluentTheme.colors.background.acrylic.default + } else { + Color.Transparent + }, + shape = shape, + border = BorderStroke(1.dp, FluentTheme.colors.stroke.surface.flyout), + backgroundSizing = BackgroundSizing.InnerBorderEdge + ) { + + with(LocalWindowAcrylicContainer.current) { + FlyoutContentLayout( + shape = shape, + contentPadding = PaddingValues(horizontal = 4.dp, vertical = 4.dp), + acrylicEnabled = { useAcrylic && expandedStates.targetState }, + acrylicTint = AcrylicDefaults.tintColor, + content = { + Column( + modifier = modifier + .width(IntrinsicSize.Max) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(4.dp), + content = content + ) + } ) } } diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Flyout.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Flyout.kt index 104bd20f..20f11769 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Flyout.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Flyout.kt @@ -4,11 +4,13 @@ import androidx.compose.animation.* import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.tween import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -16,10 +18,16 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.key.KeyEvent +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.platform.InspectableValue +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.window.PopupProperties @@ -28,10 +36,12 @@ import com.konyaco.fluent.LocalAcrylicPopupEnabled import com.konyaco.fluent.LocalWindowAcrylicContainer import com.konyaco.fluent.animation.FluentDuration import com.konyaco.fluent.animation.FluentEasing +import com.konyaco.fluent.background.AcrylicContainerScope import com.konyaco.fluent.background.AcrylicDefaults import com.konyaco.fluent.background.BackgroundSizing +import com.konyaco.fluent.background.ElevationDefaults import com.konyaco.fluent.background.Layer -import com.konyaco.fluent.background.Mica +import com.konyaco.fluent.background.calculateBorderPadding @Composable fun FlyoutContainer( @@ -225,36 +235,78 @@ internal fun AcrylicPopupContent( } ) ) { - if (!userAcrylic) { - Mica(modifier = Modifier.padding(flyoutPopPaddingFixShadowRender).graphicsLayer { - this.shape = shape - shadowElevation = elevation.toPx() - clip = true - }) { - Layer( - shape = shape, - backgroundSizing = BackgroundSizing.OuterBorderEdge - ) { - Box(modifier = modifier.padding(contentPadding)) { - content() + Layer( + backgroundSizing = BackgroundSizing.InnerBorderEdge, + border = BorderStroke(1.dp, FluentTheme.colors.stroke.surface.flyout), + shape = shape, + elevation = elevation, + color = if (userAcrylic) { + Color.Transparent + } else { + FluentTheme.colors.background.acrylic.default + }, + modifier = modifier + ) { + FlyoutContentLayout( + shape = shape, + contentPadding = contentPadding, + acrylicTint = AcrylicDefaults.tintColor, + acrylicEnabled = { + if (userAcrylic) { + visibleState.targetState || (visibleState.currentState && visibleState.isIdle) + } else { + false } - } + }, + content = content + ) + } + } + } +} + +//Workaround for acrylic PaddingBorder +@OptIn(ExperimentalFluentApi::class) +@Composable +internal fun AcrylicContainerScope.FlyoutContentLayout( + shape: Shape, + acrylicTint: Color, + acrylicEnabled: () -> Boolean, + contentPadding: PaddingValues, + content: @Composable () -> Unit +) { + Layout( + content = { + val acrylicShape = if (shape is RoundedCornerShape) { + with(LocalDensity.current) { + val borderPadding = shape.calculateBorderPadding(this).toDp() + RoundedCornerShape( + topStart = PaddingCornerSize(shape.topStart, borderPadding), + topEnd = PaddingCornerSize(shape.topEnd, borderPadding), + bottomEnd = PaddingCornerSize(shape.bottomEnd, borderPadding), + bottomStart = PaddingCornerSize(shape.bottomStart, borderPadding) + ) } } else { - Box( - modifier = modifier - .border(BorderStroke(1.dp, FluentTheme.colors.stroke.card.default), shape = shape) - .acrylicOverlay( - tint = AcrylicDefaults.tintColor, - enabled = { visibleState.targetState || (visibleState.currentState && visibleState.isIdle) }, - shape = shape - ) - .padding(contentPadding) - .clip(shape) - ) { - content() - } + shape } + Box( + modifier = Modifier.layoutId("placeholder").padding(1.dp).acrylicOverlay( + tint = acrylicTint, + enabled = acrylicEnabled, + shape = acrylicShape + ) + ) + Box(modifier = Modifier.padding(contentPadding).layoutId("content")) { content() } + } + ) { mesurables, constraints -> + val contentPlaceable = mesurables.first { it.layoutId == "content" }.measure(constraints) + val placeholder = mesurables.first { it.layoutId == "placeholder" }.measure( + Constraints.fixed(contentPlaceable.width, contentPlaceable.height) + ) + layout(contentPlaceable.width, contentPlaceable.height) { + placeholder.place(0, 0) + contentPlaceable.place(0, 0) } } } @@ -271,7 +323,7 @@ interface FlyoutContainerScope { } //TODO Remove when shadow can show with animated visibility -internal val flyoutPopPaddingFixShadowRender = 16.dp +internal val flyoutPopPaddingFixShadowRender = 0.dp internal val flyoutDefaultPadding = 8.dp internal fun flyoutEnterSpec() = @@ -299,4 +351,17 @@ internal fun defaultFlyoutEnterPlacementAnimation(placement: FlyoutPlacement): E flyoutEnterSpec() ) } +} + +@Immutable +internal data class PaddingCornerSize(private val size: CornerSize, private val padding: Dp) : + CornerSize, + InspectableValue { + override fun toPx(shapeSize: Size, density: Density) = + with(density) { size.toPx(shapeSize, this) - padding.toPx() } + + override fun toString(): String = size.toString() + + override val valueOverride: Dp + get() = padding } \ No newline at end of file diff --git a/fluent/src/desktopMain/kotlin/com/konyaco/fluent/background/Elevation.desktop.kt b/fluent/src/desktopMain/kotlin/com/konyaco/fluent/background/Elevation.desktop.kt new file mode 100644 index 00000000..6ec7c104 --- /dev/null +++ b/fluent/src/desktopMain/kotlin/com/konyaco/fluent/background/Elevation.desktop.kt @@ -0,0 +1,17 @@ +package com.konyaco.fluent.background + +import androidx.compose.ui.graphics.BlurEffect +import androidx.compose.ui.graphics.Paint +import org.jetbrains.skia.FilterBlurMode +import org.jetbrains.skia.MaskFilter + +internal actual fun Paint.applyShadowMaskFilter(radius: Float) { + asFrameworkPaint().maskFilter = MaskFilter.makeBlur( + mode = FilterBlurMode.NORMAL, + sigma = BlurEffect.convertRadiusToSigma(radius) + ) +} + +internal actual fun supportFluentElevation(): Boolean { + return true +} \ No newline at end of file diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/design/ElevationScreen.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/design/ElevationScreen.kt new file mode 100644 index 00000000..cac0696c --- /dev/null +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/design/ElevationScreen.kt @@ -0,0 +1,142 @@ +package com.konyaco.fluent.gallery.screen.design + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.konyaco.fluent.ExperimentalFluentApi +import com.konyaco.fluent.FluentTheme +import com.konyaco.fluent.FluentThemeConfiguration +import com.konyaco.fluent.background.BackgroundSizing +import com.konyaco.fluent.background.ElevationDefaults +import com.konyaco.fluent.background.Layer +import com.konyaco.fluent.component.Slider +import com.konyaco.fluent.component.Text +import com.konyaco.fluent.gallery.annotation.Component +import com.konyaco.fluent.gallery.annotation.Sample +import com.konyaco.fluent.gallery.component.ComponentPagePath +import com.konyaco.fluent.gallery.component.GalleryPage +import com.konyaco.fluent.source.generated.FluentSourceFile + +@OptIn(ExperimentalFluentApi::class) +@Component(index = 0, icon = "Layer") +@Composable +fun ElevationScreen() { + GalleryPage( + title = "Elevation", + description = "Creating a visual hierarchy of elements in your UI makes the UI easy to scan and conveys what is important to focus on. " + + "Elevation, the act of bringing select elements of your UI forward, is often used to achieve hierarchy.", + componentPath = FluentSourceFile.Elevation, + galleryPath = ComponentPagePath.ElevationScreen, + ) { + Section { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text("Elevation types", style = FluentTheme.typography.bodyStrong) + FluentThemeConfiguration { + Layer( + border = null, + color = Color.Transparent, + backgroundSizing = BackgroundSizing.OuterBorderEdge, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + val items = listOf( + ElevationDefaults::layer.name to ElevationDefaults.layer, + ElevationDefaults::control.name to ElevationDefaults.control, + ElevationDefaults::cardRest.name to ElevationDefaults.cardRest, + ElevationDefaults::tooltip.name to ElevationDefaults.tooltip, + ElevationDefaults::flyout.name to ElevationDefaults.flyout, + ElevationDefaults::dialog.name to ElevationDefaults.dialog + ) + items.forEach { (name, elevation) -> + Column( + modifier = Modifier.weight(1f) + .wrapContentWidth(Alignment.Start), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(name.replaceFirstChar { it.uppercase() }) + ElevationBoxSample( + elevation = elevation, + modifier = Modifier.padding(vertical = 8.dp) + .padding(bottom = 56.dp) + ) + } + } + + } + } + } + } + } + val elevation = remember { mutableStateOf(0) } + Section( + title = "Basic Elevation", + sourceCode = sourceCodeOfElevationBoxSample, + content = { + Box(modifier = Modifier.size(200.dp), contentAlignment = Alignment.Center) { + ElevationBoxSample(elevation.value.dp) + } + }, + options = { + Slider( + value = elevation.value.toFloat(), + onValueChange = { elevation.value = it.toInt() }, + valueRange = 0f..128f, + modifier = Modifier.height(32.dp).width(200.dp) + ) + } + ) + } +} + +@Sample +@Composable +private fun ElevationBoxSample( + elevation: Dp, + modifier: Modifier = Modifier, + shape: Shape = CircleShape +) { + Layer( + backgroundSizing = BackgroundSizing.InnerBorderEdge, + border = BorderStroke( + width = 1.dp, + color = when { + elevation.value > 2f -> FluentTheme.colors.stroke.surface.flyout + elevation.value in 0f..1f -> FluentTheme.colors.stroke.card.default + else -> Color.Transparent + } + ), + color = FluentTheme.colors.background.solid.quaternary, + shape = shape, + elevation = elevation, + modifier = modifier + ) { + Text( + text = "depth\n${elevation.value.toInt()}", + textAlign = TextAlign.Center, + color = FluentTheme.colors.text.text.secondary, + modifier = Modifier.size(boxSize).wrapContentSize(Alignment.Center) + ) + } +} + +private val boxSize = 80.dp \ No newline at end of file From c45108dd32122c47186af626c18754124d5e2100 Mon Sep 17 00:00:00 2001 From: sanlorng Date: Sat, 1 Jun 2024 23:12:50 +0800 Subject: [PATCH 012/247] [fluent] Add `Expander` and `ExpanderItem`. --- .../com/konyaco/fluent/background/Layer.kt | 34 +- .../com/konyaco/fluent/component/Expander.kt | 305 ++++++++++++++++++ .../gallery/component/GallerySection.kt | 225 +++++++------ .../screen/collections/ExpanderScreen.kt | 154 +++++++++ 4 files changed, 610 insertions(+), 108 deletions(-) create mode 100644 fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Expander.kt create mode 100644 gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/collections/ExpanderScreen.kt diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Layer.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Layer.kt index 9480d019..794968bd 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Layer.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Layer.kt @@ -88,6 +88,31 @@ fun Layer( backgroundSizing: BackgroundSizing, elevation: Dp = 0.dp, content: @Composable () -> Unit +) { + Layer( + modifier = modifier, + shape = shape, + color = color, + contentColor = contentColor, + border = border, + backgroundSizing = backgroundSizing, + elevation = elevation, + clipContent = false, + content = content + ) +} + +@Composable +fun Layer( + modifier: Modifier = Modifier, + shape: Shape = RoundedCornerShape(size = 4.dp), + color: Color = FluentTheme.colors.background.layer.default, + contentColor: Color = FluentTheme.colors.text.text.primary, + border: BorderStroke? = BorderStroke(1.dp, FluentTheme.colors.stroke.card.default), + backgroundSizing: BackgroundSizing, + clipContent: Boolean = false, + elevation: Dp = 0.dp, + content: @Composable () -> Unit ) { ProvideTextStyle(FluentTheme.typography.body.copy(color = contentColor)) { CompositionLocalProvider( @@ -100,7 +125,8 @@ fun Layer( shape, border, backgroundSizing, - color + color, + clipContent ), propagateMinConstraints = true ) { @@ -116,7 +142,8 @@ private fun Modifier.layer( shape: Shape, border: BorderStroke?, backgroundSizing: BackgroundSizing, - color: Color + color: Color, + clipContent: Boolean ) = then( Modifier .elevation(elevation = elevation, shape = shape) @@ -130,6 +157,7 @@ private fun Modifier.layer( } Modifier.border(border, shape) .background(color, backgroundShape) + .then(if (clipContent) Modifier.clip(backgroundShape) else Modifier) } else { Modifier.background(color, shape) } @@ -142,7 +170,7 @@ private fun Modifier.layer( */ @Immutable @JvmInline -private value class BackgroundPaddingShape(private val borderShape: CornerBasedShape) : Shape { +internal value class BackgroundPaddingShape(private val borderShape: CornerBasedShape) : Shape { override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline { return with(density) { diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Expander.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Expander.kt new file mode 100644 index 00000000..d5a720c4 --- /dev/null +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Expander.kt @@ -0,0 +1,305 @@ +package com.konyaco.fluent.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.unit.dp +import com.konyaco.fluent.FluentTheme +import com.konyaco.fluent.ProvideTextStyle +import com.konyaco.fluent.animation.FluentDuration +import com.konyaco.fluent.animation.FluentEasing +import com.konyaco.fluent.background.BackgroundSizing +import com.konyaco.fluent.background.Layer +import com.konyaco.fluent.icons.Icons +import com.konyaco.fluent.icons.regular.ChevronDown +import com.konyaco.fluent.scheme.PentaVisualScheme +import com.konyaco.fluent.scheme.VisualStateScheme +import com.konyaco.fluent.scheme.collectVisualState +import com.konyaco.fluent.surface.Card +import com.konyaco.fluent.surface.CardColor + +@Composable +fun Expander( + expanded: Boolean, + onExpandedChanged: (Boolean) -> Unit, + heading: @Composable () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + interactionSource: MutableInteractionSource? = null, + shape: Shape = RoundedCornerShape(4.dp), + icon: (@Composable () -> Unit)? = {}, + caption: @Composable () -> Unit = {}, + training: @Composable () -> Unit = {}, + expandContent: (@Composable ColumnScope.() -> Unit) = {}, +) { + Layer( + backgroundSizing = BackgroundSizing.InnerBorderEdge, + modifier = modifier, + color = Color.Transparent, + shape = shape, + clipContent = true + ) { + Column { + val targetInteractionSource = + interactionSource ?: remember { MutableInteractionSource() } + + ExpanderItemContent( + icon = icon, + heading = heading, + caption = caption, + training = training, + dropdown = { + SubtleButton( + interaction = targetInteractionSource, + onClick = { onExpandedChanged(!expanded) }, + content = { + val degrees by animateFloatAsState(if (expanded) 180f else 0f) + Icon( + imageVector = Icons.Default.ChevronDown, + contentDescription = null, + modifier = Modifier.graphicsLayer { rotationZ = degrees } + ) + }, + iconOnly = true, + disabled = !enabled + ) + }, + modifier = Modifier.padding(top = 1.dp) + .heightIn(ExpanderHeaderHeight) + .background(FluentTheme.colors.background.card.default) + .clickable( + interactionSource = targetInteractionSource, + indication = null, + onClick = { onExpandedChanged(!expanded) }, + enabled = enabled + ) + ) + ExpanderItemSeparator(color = FluentTheme.colors.stroke.card.default) + AnimatedVisibility( + visible = expanded, + enter = expandVertically( + tween(FluentDuration.MediumDuration, 0, FluentEasing.FastInvokeEasing) + ), + exit = shrinkVertically( + tween(FluentDuration.QuickDuration, 0, FluentEasing.SoftDismissEasing) + ) + ) { + Column(modifier = Modifier.padding(bottom = 1.dp)) { + expandContent() + } + } + } + } +} + +@Composable +fun ExpanderItem( + heading: @Composable () -> Unit, + modifier: Modifier = Modifier, + color: Color = FluentTheme.colors.background.card.secondary, + icon: (@Composable () -> Unit)? = {}, + caption: @Composable () -> Unit = {}, + training: @Composable () -> Unit = {}, + dropdown: (@Composable () -> Unit)? = {} +) { + ExpanderItemContent( + icon = icon, + heading = heading, + caption = caption, + training = training, + dropdown = dropdown, + modifier = modifier.background(color) + ) +} + +@Composable +fun CardExpanderItem( + onClick: () -> Unit, + heading: @Composable () -> Unit, + modifier: Modifier = Modifier, + shape: Shape = RoundedCornerShape(4.dp), + enabled: Boolean = true, + colors: VisualStateScheme = ExpanderDefaults.cardExpanderItemColors(), + captionColors: VisualStateScheme = PentaVisualScheme( + default = FluentTheme.colors.text.text.secondary, + hovered = FluentTheme.colors.text.text.secondary, + pressed = FluentTheme.colors.text.text.tertiary, + disabled = FluentTheme.colors.text.text.disabled + ), + interactionSource: MutableInteractionSource? = null, + icon: (@Composable () -> Unit)? = {}, + caption: @Composable () -> Unit = {}, + training: @Composable () -> Unit = {}, + dropdown: (@Composable () -> Unit)? = null, +) { + val targetInteractionSource = interactionSource ?: remember { MutableInteractionSource() } + val captionTextColor = captionColors.schemeFor(targetInteractionSource.collectVisualState(!enabled)) + Card( + onClick = onClick, + modifier = modifier, + disabled = !enabled, + cardColors = colors, + shape = shape, + interactionSource = targetInteractionSource + ) { + ExpanderItemContent( + icon = icon, + heading = heading, + caption = caption, + training = training, + dropdown = dropdown, + modifier = Modifier.heightIn(ExpanderHeaderHeight), + captionTextColor = captionTextColor + ) + } +} + +@Composable +fun CardExpanderItem( + heading: @Composable () -> Unit, + modifier: Modifier = Modifier, + shape: Shape = RoundedCornerShape(4.dp), + color: Color = FluentTheme.colors.background.card.default, + contentColor: Color = FluentTheme.colors.text.text.primary, + captionTextColor: Color = FluentTheme.colors.text.text.secondary, + icon: (@Composable () -> Unit)? = {}, + caption: @Composable () -> Unit = {}, + training: @Composable () -> Unit = {}, + dropdown: (@Composable () -> Unit)? = null +) { + Layer( + modifier = modifier, + backgroundSizing = BackgroundSizing.InnerBorderEdge, + shape = shape, + color = color, + contentColor = contentColor + ) { + ExpanderItemContent( + icon = icon, + heading = heading, + caption = caption, + training = training, + dropdown = dropdown, + modifier = Modifier.heightIn(ExpanderHeaderHeight), + captionTextColor = captionTextColor + ) + } +} + +@Composable +fun ExpanderItemSeparator( + modifier: Modifier = Modifier, + color: Color = FluentTheme.colors.stroke.divider.default +) { + Box( + modifier = modifier.fillMaxWidth().height(1.dp).background(color) + ) +} + +object ExpanderDefaults { + + @Stable + @Composable + fun cardExpanderItemColors( + default: CardColor = CardColor( + fillColor = FluentTheme.colors.background.card.default, + contentColor = FluentTheme.colors.text.text.primary, + borderBrush = SolidColor(FluentTheme.colors.stroke.card.default) + ), + hovered: CardColor = default.copy( + fillColor = FluentTheme.colors.control.secondary, + borderBrush = FluentTheme.colors.borders.control + ), + pressed: CardColor = CardColor( + fillColor = FluentTheme.colors.control.tertiary, + contentColor = FluentTheme.colors.text.text.secondary, + borderBrush = SolidColor(FluentTheme.colors.stroke.control.default) + ), + disabled: CardColor = pressed.copy( + fillColor = FluentTheme.colors.background.card.default, + contentColor = FluentTheme.colors.text.text.disabled, + borderBrush = SolidColor(FluentTheme.colors.stroke.card.default) + ) + ) = PentaVisualScheme(default, hovered, pressed, disabled) +} + +@Composable +internal fun ExpanderItemContent( + modifier: Modifier = Modifier, + captionTextColor: Color = FluentTheme.colors.text.text.secondary, + icon: (@Composable () -> Unit)? = {}, + heading: @Composable () -> Unit = {}, + caption: @Composable () -> Unit = {}, + training: @Composable () -> Unit = {}, + dropdown: (@Composable () -> Unit)? = {} +) { + Row( + modifier = Modifier.then(modifier) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + if (icon != null) { + Box( + modifier = Modifier.widthIn(48.dp).defaultMinSize(16.dp), + contentAlignment = Alignment.Center + ) { + icon() + } + } else { + Spacer(modifier = Modifier.width(16.dp)) + } + Column(modifier = Modifier.padding(vertical = 13.dp)) { + heading() + ProvideTextStyle(FluentTheme.typography.caption.copy(captionTextColor)) { + caption() + } + } + Spacer(modifier = Modifier.weight(1f).height(1.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(end = 8.dp) + ) { + training() + if (dropdown != null) { + Box( + modifier = Modifier.padding(start = 4.dp).defaultMinSize(32.dp), + contentAlignment = Alignment.Center + ) { + dropdown() + } + } else { + Spacer(modifier = Modifier.width(8.dp)) + } + } + } +} + +private val ExpanderHeaderHeight = 62.dp diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/GallerySection.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/GallerySection.kt index ab2b7842..24ae7abf 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/GallerySection.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/GallerySection.kt @@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -34,6 +33,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.konyaco.fluent.Colors import com.konyaco.fluent.ExperimentalFluentApi @@ -43,13 +43,14 @@ import com.konyaco.fluent.animation.FluentDuration import com.konyaco.fluent.animation.FluentEasing import com.konyaco.fluent.background.BackgroundSizing import com.konyaco.fluent.background.Layer +import com.konyaco.fluent.component.ExpanderItem +import com.konyaco.fluent.component.ExpanderItemSeparator import com.konyaco.fluent.component.Icon import com.konyaco.fluent.component.Scrollbar import com.konyaco.fluent.component.SubtleButton import com.konyaco.fluent.component.Text import com.konyaco.fluent.icons.Icons import com.konyaco.fluent.icons.regular.ChevronDown -import com.konyaco.fluent.surface.Card @OptIn(ExperimentalFluentApi::class) @Composable @@ -63,122 +64,136 @@ fun GallerySection( content: @Composable BoxScope.() -> Unit, ) { Column(modifier) { + Text(title, style = FluentTheme.typography.bodyStrong) Spacer(Modifier.height(16.dp)) - FluentThemeConfiguration(colors = colors) { - Layer( - modifier = Modifier.fillMaxWidth().wrapContentHeight(), - shape = RoundedCornerShape( - topStart = FluentTheme.cornerRadius.overlay, - topEnd = FluentTheme.cornerRadius.overlay - ), - color = FluentTheme.colors.background.solid.base, - backgroundSizing = BackgroundSizing.OuterBorderEdge - ) { - Row( - modifier = Modifier.height(IntrinsicSize.Max) - ) { + + Layer( + modifier = Modifier.fillMaxWidth(), + backgroundSizing = BackgroundSizing.InnerBorderEdge, + shape = FluentTheme.shapes.overlay, + clipContent = true, + color = Color.Transparent + ) { + Column { + FluentThemeConfiguration(colors = colors) { Box( - modifier = Modifier.weight(1f) - .defaultMinSize(minHeight = 100.dp) - .padding(16.dp), - contentAlignment = Alignment.CenterStart, - content = content - ) - if (output != null) { - Box( - modifier = Modifier.fillMaxHeight() - .padding(0.dp, 12.dp, 12.dp, 12.dp) - .padding(16.dp) + modifier = Modifier + .background(FluentTheme.colors.background.solid.base) + .fillMaxWidth() + .wrapContentHeight(), + ) { + Row( + modifier = Modifier.height(IntrinsicSize.Max) ) { - Column( - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text("Output:") - output() + Box( + modifier = Modifier.weight(1f) + .defaultMinSize(minHeight = 100.dp) + .padding(16.dp), + contentAlignment = Alignment.CenterStart, + content = content + ) + if (output != null) { + Box( + modifier = Modifier.fillMaxHeight() + .padding(0.dp, 12.dp, 12.dp, 12.dp) + .padding(16.dp) + ) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text("Output:") + output() + } + } + } + if (options != null) { + Spacer( + modifier = Modifier.padding(vertical = 1.dp) + .fillMaxHeight() + .width(1.dp) + .background(FluentTheme.colors.stroke.divider.default) + ) + Column( + modifier = Modifier.fillMaxHeight() + .background(FluentTheme.colors.background.card.default) + .padding(16.dp) + .width(IntrinsicSize.Max), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + options() + } } - } - } - if (options != null) { - Spacer( - modifier = Modifier.padding(vertical = 1.dp) - .fillMaxHeight() - .width(1.dp) - .background(FluentTheme.colors.stroke.divider.default) - ) - Column( - modifier = Modifier.fillMaxHeight() - .background(FluentTheme.colors.background.card.default) - .padding(16.dp) - .width(IntrinsicSize.Max), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - options() } } } - } - } + ExpanderItemSeparator(color = FluentTheme.colors.stroke.card.default) - var sourceCodeExpanded by remember { mutableStateOf(false) } + var sourceCodeExpanded by remember { mutableStateOf(false) } + val interactionSource = remember { MutableInteractionSource() } - val interactionSource = remember { MutableInteractionSource() } - Card( - modifier = Modifier.fillMaxWidth() - .clickable(interactionSource = interactionSource, indication = null, onClick = { - sourceCodeExpanded = !sourceCodeExpanded - }), - shape = RoundedCornerShape( - bottomEnd = if (sourceCodeExpanded) 0.dp else FluentTheme.cornerRadius.overlay, - bottomStart = if (sourceCodeExpanded) 0.dp else FluentTheme.cornerRadius.overlay - ) - ) { - Row(Modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically) { - Text(modifier = Modifier.padding(start = 8.dp).weight(1f), text = "Source Code") - SubtleButton( - onClick = { sourceCodeExpanded = !sourceCodeExpanded }, - interaction = interactionSource, - iconOnly = true - ) { - Icon( - modifier = Modifier.rotate( - animateFloatAsState( - if (sourceCodeExpanded) 180f else 0f, - ).value - ), - imageVector = Icons.Default.ChevronDown, - contentDescription = "Expand source code" + ExpanderItem( + icon = null, + heading = { Text("Source Code") }, + dropdown = { + SubtleButton( + onClick = { sourceCodeExpanded = !sourceCodeExpanded }, + interaction = interactionSource, + iconOnly = true + ) { + Icon( + modifier = Modifier.rotate( + animateFloatAsState( + if (sourceCodeExpanded) 180f else 0f, + ).value + ), + imageVector = Icons.Default.ChevronDown, + contentDescription = "Expand source code" + ) + } + }, + color = FluentTheme.colors.background.card.default, + modifier = Modifier.clickable( + interactionSource = interactionSource, + indication = null, + onClick = { sourceCodeExpanded = !sourceCodeExpanded } ) - } - } - } - AnimatedVisibility( - visible = sourceCodeExpanded, - enter = expandVertically( - tween(FluentDuration.MediumDuration, 0, FluentEasing.FastInvokeEasing) - ), - exit = shrinkVertically( - tween(FluentDuration.QuickDuration, 0, FluentEasing.SoftDismissEasing) - ) - ) { - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape( - bottomEnd = FluentTheme.cornerRadius.overlay, - bottomStart = FluentTheme.cornerRadius.overlay ) - ) { - Column(Modifier.padding(16.dp, 12.dp)) { - Text("Kotlin", style = FluentTheme.typography.bodyStrong) - Spacer(Modifier.height(12.dp)) - val scrollState = rememberScrollState() - Box(Modifier.fillMaxWidth().wrapContentHeight()) { - SourceCode( - modifier = Modifier.horizontalScroll(scrollState), - code = sourceCode + if (sourceCodeExpanded) { + ExpanderItemSeparator(color = FluentTheme.colors.stroke.card.default) + } + AnimatedVisibility( + visible = sourceCodeExpanded, + enter = expandVertically( + tween(FluentDuration.MediumDuration, 0, FluentEasing.FastInvokeEasing) + ), + exit = shrinkVertically( + tween(FluentDuration.QuickDuration, 0, FluentEasing.SoftDismissEasing) + ) + ) { + Column(Modifier.background(FluentTheme.colors.background.card.secondary).padding(bottom = 12.dp)) { + ExpanderItem( + icon = null, + heading = { Text("Kotlin", style = FluentTheme.typography.bodyStrong) }, + color = Color.Transparent, + training = { CopyButton(sourceCode, modifier = Modifier) }, + dropdown = null ) - Box(Modifier.fillMaxWidth().align(Alignment.BottomCenter)) { - Scrollbar(modifier = Modifier.fillMaxWidth(), isVertical = false, adapter = com.konyaco.fluent.component.rememberScrollbarAdapter(scrollState)) + val scrollState = rememberScrollState() + Box(Modifier.padding(horizontal = 16.dp).fillMaxWidth().wrapContentHeight()) { + SourceCode( + modifier = Modifier.horizontalScroll(scrollState), + code = sourceCode + ) + Box(Modifier.fillMaxWidth().align(Alignment.BottomCenter)) { + Scrollbar( + modifier = Modifier.fillMaxWidth(), + isVertical = false, + adapter = com.konyaco.fluent.component.rememberScrollbarAdapter( + scrollState + ) + ) + } } } } diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/collections/ExpanderScreen.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/collections/ExpanderScreen.kt new file mode 100644 index 00000000..77d05f81 --- /dev/null +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/collections/ExpanderScreen.kt @@ -0,0 +1,154 @@ +package com.konyaco.fluent.gallery.screen.collections + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import com.konyaco.fluent.component.CardExpanderItem +import com.konyaco.fluent.component.CheckBox +import com.konyaco.fluent.component.DropDownButton +import com.konyaco.fluent.component.Expander +import com.konyaco.fluent.component.ExpanderItem +import com.konyaco.fluent.component.ExpanderItemSeparator +import com.konyaco.fluent.component.Icon +import com.konyaco.fluent.component.Switcher +import com.konyaco.fluent.component.Text +import com.konyaco.fluent.gallery.annotation.Component +import com.konyaco.fluent.gallery.annotation.Sample +import com.konyaco.fluent.gallery.component.ComponentPagePath +import com.konyaco.fluent.gallery.component.GalleryPage +import com.konyaco.fluent.icons.Icons +import com.konyaco.fluent.icons.regular.ChevronRight +import com.konyaco.fluent.icons.regular.Power +import com.konyaco.fluent.source.generated.FluentSourceFile + +@Component(description = "Control that displays a header and a collapsible content area.") +@Composable +fun ExpanderScreen() { + GalleryPage( + title = "Expander", + description = "An expander control that can be used to create Windows 11 style settings experiences.", + componentPath = FluentSourceFile.Expander, + galleryPath = ComponentPagePath.ExpanderScreen + ) { + Section( + title = "Basic Expander", + sourceCode = sourceCodeOfExpanderSample, + content = { ExpanderSample() } + ) + Section( + title = "Expander without icon", + sourceCode = sourceCodeOfExpanderSampleWithoutIcon, + content = { ExpanderSampleWithoutIcon() } + ) + Section( + title = "Card Expander Item", + sourceCode = sourceCodeOfCardExpanderItemSample, + content = { CardExpanderItemSample() } + ) + val enabled = remember { mutableStateOf(true) } + Section( + title = "Clickable Card Expander Item", + sourceCode = sourceCodeOfClickableCardExpanderItemSample, + content = { ClickableCardExpanderItemSample(enabled = enabled.value) }, + options = { + CheckBox( + checked = enabled.value, + onCheckStateChange = { enabled.value = it }, + label = "Enabled" + ) + } + ) + } +} + +@Sample +@Composable +private fun ExpanderSample() { + val expanded = remember { mutableStateOf(false) } + Expander( + expanded = expanded.value, + onExpandedChanged = { expanded.value = it }, + heading = { Text("Power button functionality") }, + caption = { Text("Adjust what yor power buttons control") }, + icon = { Icon(Icons.Default.Power, null) }, + training = { + val checked = remember { mutableStateOf(true) } + Switcher( + checked = checked.value, + { checked.value = it }, + textBefore = true, + text = if (checked.value) "On" else "Off" + ) + } + ) { + ExpanderItem( + heading = { Text("When I press the power button on battery") }, + training = { DropDownButton(onClick = {}, content = { Text("Sleep") }) } + ) + ExpanderItemSeparator() + ExpanderItem( + heading = { Text("When I press the power button when plugged in") }, + training = { DropDownButton(onClick = {}, content = { Text("Sleep") }) } + ) + } +} + +@Sample +@Composable +private fun ExpanderSampleWithoutIcon() { + val expanded = remember { mutableStateOf(false) } + Expander( + expanded = expanded.value, + onExpandedChanged = { expanded.value = it }, + heading = { Text("Power button functionality") }, + caption = { Text("Adjust what yor power buttons control") }, + icon = null, + training = { + val checked = remember { mutableStateOf(true) } + Switcher( + checked = checked.value, + { checked.value = it }, + textBefore = true, + text = if (checked.value) "On" else "Off" + ) + } + ) { + ExpanderItem( + heading = { Text("When I press the power button on battery") }, + training = { DropDownButton(onClick = {}, content = { Text("Sleep") }) }, + icon = null + ) + } +} + +@Sample +@Composable +private fun CardExpanderItemSample() { + CardExpanderItem( + heading = { Text("Power button functionality") }, + caption = { Text("Adjust what yor power buttons control") }, + icon = { Icon(Icons.Default.Power, null) }, + training = { + val checked = remember { mutableStateOf(true) } + Switcher( + checked = checked.value, + { checked.value = it }, + textBefore = true, + text = if (checked.value) "On" else "Off" + ) + } + ) +} + +@Sample +@Composable +private fun ClickableCardExpanderItemSample(enabled: Boolean = true) { + CardExpanderItem( + heading = { Text("Power button functionality") }, + caption = { Text("Adjust what yor power buttons control") }, + icon = { Icon(Icons.Default.Power, null) }, + dropdown = { Icon(Icons.Default.ChevronRight, null) }, + onClick = {}, + enabled = enabled + ) +} \ No newline at end of file From b6c171eee3ddb72252537121ab13073bdbafae6f Mon Sep 17 00:00:00 2001 From: sanlorng Date: Thu, 13 Jun 2024 19:21:32 +0800 Subject: [PATCH 013/247] [fluent] rename `training` to `trailing`. --- .../com/konyaco/fluent/component/Expander.kt | 20 +++++++++---------- .../gallery/component/GallerySection.kt | 2 +- .../screen/collections/ExpanderScreen.kt | 12 +++++------ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Expander.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Expander.kt index d5a720c4..d8c9c863 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Expander.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Expander.kt @@ -57,7 +57,7 @@ fun Expander( shape: Shape = RoundedCornerShape(4.dp), icon: (@Composable () -> Unit)? = {}, caption: @Composable () -> Unit = {}, - training: @Composable () -> Unit = {}, + trailing: @Composable () -> Unit = {}, expandContent: (@Composable ColumnScope.() -> Unit) = {}, ) { Layer( @@ -75,7 +75,7 @@ fun Expander( icon = icon, heading = heading, caption = caption, - training = training, + trailing = trailing, dropdown = { SubtleButton( interaction = targetInteractionSource, @@ -127,14 +127,14 @@ fun ExpanderItem( color: Color = FluentTheme.colors.background.card.secondary, icon: (@Composable () -> Unit)? = {}, caption: @Composable () -> Unit = {}, - training: @Composable () -> Unit = {}, + trailing: @Composable () -> Unit = {}, dropdown: (@Composable () -> Unit)? = {} ) { ExpanderItemContent( icon = icon, heading = heading, caption = caption, - training = training, + trailing = trailing, dropdown = dropdown, modifier = modifier.background(color) ) @@ -157,7 +157,7 @@ fun CardExpanderItem( interactionSource: MutableInteractionSource? = null, icon: (@Composable () -> Unit)? = {}, caption: @Composable () -> Unit = {}, - training: @Composable () -> Unit = {}, + trailing: @Composable () -> Unit = {}, dropdown: (@Composable () -> Unit)? = null, ) { val targetInteractionSource = interactionSource ?: remember { MutableInteractionSource() } @@ -174,7 +174,7 @@ fun CardExpanderItem( icon = icon, heading = heading, caption = caption, - training = training, + trailing = trailing, dropdown = dropdown, modifier = Modifier.heightIn(ExpanderHeaderHeight), captionTextColor = captionTextColor @@ -192,7 +192,7 @@ fun CardExpanderItem( captionTextColor: Color = FluentTheme.colors.text.text.secondary, icon: (@Composable () -> Unit)? = {}, caption: @Composable () -> Unit = {}, - training: @Composable () -> Unit = {}, + trailing: @Composable () -> Unit = {}, dropdown: (@Composable () -> Unit)? = null ) { Layer( @@ -206,7 +206,7 @@ fun CardExpanderItem( icon = icon, heading = heading, caption = caption, - training = training, + trailing = trailing, dropdown = dropdown, modifier = Modifier.heightIn(ExpanderHeaderHeight), captionTextColor = captionTextColor @@ -258,7 +258,7 @@ internal fun ExpanderItemContent( icon: (@Composable () -> Unit)? = {}, heading: @Composable () -> Unit = {}, caption: @Composable () -> Unit = {}, - training: @Composable () -> Unit = {}, + trailing: @Composable () -> Unit = {}, dropdown: (@Composable () -> Unit)? = {} ) { Row( @@ -287,7 +287,7 @@ internal fun ExpanderItemContent( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(end = 8.dp) ) { - training() + trailing() if (dropdown != null) { Box( modifier = Modifier.padding(start = 4.dp).defaultMinSize(32.dp), diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/GallerySection.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/GallerySection.kt index 24ae7abf..33e2b287 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/GallerySection.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/GallerySection.kt @@ -176,7 +176,7 @@ fun GallerySection( icon = null, heading = { Text("Kotlin", style = FluentTheme.typography.bodyStrong) }, color = Color.Transparent, - training = { CopyButton(sourceCode, modifier = Modifier) }, + trailing = { CopyButton(sourceCode, modifier = Modifier) }, dropdown = null ) val scrollState = rememberScrollState() diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/collections/ExpanderScreen.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/collections/ExpanderScreen.kt index 77d05f81..af9d94d1 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/collections/ExpanderScreen.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/collections/ExpanderScreen.kt @@ -71,7 +71,7 @@ private fun ExpanderSample() { heading = { Text("Power button functionality") }, caption = { Text("Adjust what yor power buttons control") }, icon = { Icon(Icons.Default.Power, null) }, - training = { + trailing = { val checked = remember { mutableStateOf(true) } Switcher( checked = checked.value, @@ -83,12 +83,12 @@ private fun ExpanderSample() { ) { ExpanderItem( heading = { Text("When I press the power button on battery") }, - training = { DropDownButton(onClick = {}, content = { Text("Sleep") }) } + trailing = { DropDownButton(onClick = {}, content = { Text("Sleep") }) } ) ExpanderItemSeparator() ExpanderItem( heading = { Text("When I press the power button when plugged in") }, - training = { DropDownButton(onClick = {}, content = { Text("Sleep") }) } + trailing = { DropDownButton(onClick = {}, content = { Text("Sleep") }) } ) } } @@ -103,7 +103,7 @@ private fun ExpanderSampleWithoutIcon() { heading = { Text("Power button functionality") }, caption = { Text("Adjust what yor power buttons control") }, icon = null, - training = { + trailing = { val checked = remember { mutableStateOf(true) } Switcher( checked = checked.value, @@ -115,7 +115,7 @@ private fun ExpanderSampleWithoutIcon() { ) { ExpanderItem( heading = { Text("When I press the power button on battery") }, - training = { DropDownButton(onClick = {}, content = { Text("Sleep") }) }, + trailing = { DropDownButton(onClick = {}, content = { Text("Sleep") }) }, icon = null ) } @@ -128,7 +128,7 @@ private fun CardExpanderItemSample() { heading = { Text("Power button functionality") }, caption = { Text("Adjust what yor power buttons control") }, icon = { Icon(Icons.Default.Power, null) }, - training = { + trailing = { val checked = remember { mutableStateOf(true) } Switcher( checked = checked.value, From 9b706f8ad40cc399ecbd497ef2b8cdeb1e7212dd Mon Sep 17 00:00:00 2001 From: sanlorng Date: Mon, 23 Sep 2024 19:27:07 +0800 Subject: [PATCH 014/247] Update Expander's shapes --- .../kotlin/com/konyaco/fluent/background/Layer.kt | 2 +- .../kotlin/com/konyaco/fluent/component/Expander.kt | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Layer.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Layer.kt index 794968bd..2f57bd67 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Layer.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Layer.kt @@ -105,7 +105,7 @@ fun Layer( @Composable fun Layer( modifier: Modifier = Modifier, - shape: Shape = RoundedCornerShape(size = 4.dp), + shape: Shape = FluentTheme.shapes.control, color: Color = FluentTheme.colors.background.layer.default, contentColor: Color = FluentTheme.colors.text.text.primary, border: BorderStroke? = BorderStroke(1.dp, FluentTheme.colors.stroke.card.default), diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Expander.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Expander.kt index d8c9c863..c6b57a73 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Expander.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Expander.kt @@ -20,7 +20,6 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue @@ -54,7 +53,7 @@ fun Expander( modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource? = null, - shape: Shape = RoundedCornerShape(4.dp), + shape: Shape = FluentTheme.shapes.control, icon: (@Composable () -> Unit)? = {}, caption: @Composable () -> Unit = {}, trailing: @Composable () -> Unit = {}, @@ -145,7 +144,7 @@ fun CardExpanderItem( onClick: () -> Unit, heading: @Composable () -> Unit, modifier: Modifier = Modifier, - shape: Shape = RoundedCornerShape(4.dp), + shape: Shape = FluentTheme.shapes.control, enabled: Boolean = true, colors: VisualStateScheme = ExpanderDefaults.cardExpanderItemColors(), captionColors: VisualStateScheme = PentaVisualScheme( @@ -186,7 +185,7 @@ fun CardExpanderItem( fun CardExpanderItem( heading: @Composable () -> Unit, modifier: Modifier = Modifier, - shape: Shape = RoundedCornerShape(4.dp), + shape: Shape = FluentTheme.shapes.control, color: Color = FluentTheme.colors.background.card.default, contentColor: Color = FluentTheme.colors.text.text.primary, captionTextColor: Color = FluentTheme.colors.text.text.secondary, From 8773ea81404b995440ffecd11d19f9f3b4a6f14a Mon Sep 17 00:00:00 2001 From: sanlorng Date: Sun, 2 Jun 2024 15:24:54 +0800 Subject: [PATCH 015/247] [fluent] Add `PillButton` and `LiteFilter`. --- .../com/konyaco/fluent/component/Button.kt | 97 ++++++++++- .../konyaco/fluent/component/LiteFilter.kt | 152 ++++++++++++++++++ .../screen/basicinput/LiteFilterScreen.kt | 69 ++++++++ .../screen/basicinput/PillButtonScreen.kt | 78 +++++++++ 4 files changed, 394 insertions(+), 2 deletions(-) create mode 100644 fluent/src/commonMain/kotlin/com/konyaco/fluent/component/LiteFilter.kt create mode 100644 gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/basicinput/LiteFilterScreen.kt create mode 100644 gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/basicinput/PillButtonScreen.kt diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Button.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Button.kt index 35aa6ebc..6102d408 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Button.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Button.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable @@ -419,6 +420,43 @@ fun ToggleSplitButton( ) } +@Composable +fun PillButton( + selected: Boolean, + onSelectedChanged: (Boolean) -> Unit, + modifier: Modifier = Modifier, + interaction: MutableInteractionSource? = null, + disabled: Boolean = false, + colors: VisualStateScheme = if (selected) { + ButtonDefaults.pillButtonSelectedColors() + } else { + ButtonDefaults.pillButtonDefaultColors() + }, + outsideBorder: Boolean = !selected, + content: @Composable RowScope.() -> Unit +) { + val targetInteraction = interaction ?: remember { MutableInteractionSource() } + Button( + modifier = modifier.selectable( + selected = selected, + interactionSource = targetInteraction, + indication = null, + onClick = { onSelectedChanged(!selected) }, + role = Role.Checkbox, + enabled = !disabled + ), + interaction = targetInteraction, + disabled = disabled, + buttonColors = colors, + accentButton = !outsideBorder, + onClick = null, + iconOnly = false, + content = content, + contentArrangement = Arrangement.spacedBy(ButtonDefaults.iconSpacing, Alignment.CenterHorizontally), + shape = CircleShape + ) +} + @Composable private fun Button( modifier: Modifier, @@ -429,10 +467,11 @@ private fun Button( onClick: (() -> Unit)?, iconOnly: Boolean, contentArrangement: Arrangement.Horizontal, - content: @Composable (RowScope.() -> Unit) + shape: Shape = FluentTheme.shapes.control, + content: @Composable (RowScope.() -> Unit), ) { ButtonLayer( - shape = FluentTheme.shapes.control, + shape = shape, displayBorder = true, buttonColors = buttonColors, interaction = interaction, @@ -508,6 +547,8 @@ private fun ButtonLayer( object ButtonDefaults { + val iconSpacing = 8.dp + @Stable @Composable fun buttonColors( @@ -614,6 +655,58 @@ object ButtonDefaults { pressed = pressed, disabled = disabled ) + + @Stable + @Composable + fun pillButtonDefaultColors( + default: ButtonColor = ButtonColor( + fillColor = FluentTheme.colors.control.quaternary, + contentColor = FluentTheme.colors.text.text.primary, + borderBrush = SolidColor(FluentTheme.colors.stroke.control.default) + ), + hovered: ButtonColor = default.copy( + fillColor = FluentTheme.colors.control.secondary, + ), + pressed: ButtonColor = hovered.copy( + contentColor = FluentTheme.colors.text.text.secondary + ), + disabled: ButtonColor = default.copy( + fillColor = FluentTheme.colors.control.disabled, + contentColor = FluentTheme.colors.text.text.disabled + ) + ) = ButtonColorScheme( + default = default, + hovered = hovered, + pressed = pressed, + disabled = disabled + ) + + @Stable + @Composable + fun pillButtonSelectedColors( + default: ButtonColor = ButtonColor( + fillColor = FluentTheme.colors.fillAccent.default, + contentColor = FluentTheme.colors.text.onAccent.primary, + borderBrush = SolidColor(Color.Transparent) + ), + hovered: ButtonColor = default.copy( + fillColor = FluentTheme.colors.fillAccent.secondary, + contentColor = FluentTheme.colors.text.onAccent.primary + ), + pressed: ButtonColor = default.copy( + fillColor = FluentTheme.colors.fillAccent.tertiary, + contentColor = FluentTheme.colors.text.onAccent.secondary + ), + disabled: ButtonColor = default.copy( + fillColor = FluentTheme.colors.fillAccent.disabled, + contentColor = FluentTheme.colors.text.onAccent.disabled + ) + ) = ButtonColorScheme( + default = default, + hovered = hovered, + pressed = pressed, + disabled = disabled + ) } @Composable diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/LiteFilter.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/LiteFilter.kt new file mode 100644 index 00000000..dde97dcd --- /dev/null +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/LiteFilter.kt @@ -0,0 +1,152 @@ +package com.konyaco.fluent.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.gestures.animateScrollBy +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.relocation.BringIntoViewResponder +import androidx.compose.foundation.relocation.bringIntoViewResponder +import androidx.compose.foundation.rememberScrollState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshots.Snapshot +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import com.konyaco.fluent.animation.FluentDuration +import com.konyaco.fluent.icons.Icons +import com.konyaco.fluent.icons.filled.CaretLeft +import com.konyaco.fluent.icons.filled.CaretRight +import kotlinx.coroutines.launch + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun LiteFilter( + modifier: Modifier = Modifier, + state: ScrollState = rememberScrollState(), + content: @Composable RowScope.() -> Unit +) { + Box(modifier = modifier.heightIn(40.dp)) { + val isPreviousVisible = state.canScrollBackward + val isNextVisible = state.canScrollForward + val density = LocalDensity.current + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.clip( + PaddingShape( + clipStart = isPreviousVisible, + clipEnd = isNextVisible + ) + ) + .horizontalScroll(state) + .bringIntoViewResponder(remember(state, density) { LiteFilterBringIntoViewResponder(state, density) }) + .align(Alignment.CenterStart) + ) { + content() + } + + val scope = rememberCoroutineScope() + AnimatedVisibility( + visible = isPreviousVisible, + enter = fadeIn(animationSpec = tween(durationMillis = FluentDuration.ShortDuration)), + exit = fadeOut(animationSpec = tween(durationMillis = FluentDuration.ShortDuration)), + modifier = Modifier.padding(start = 2.dp).align(Alignment.CenterStart) + ) { + SubtleButton( + onClick = { scope.launch { state.animateScrollBy(-state.viewportSize / 3f) } }, + content = { Icon(Icons.Filled.CaretLeft, contentDescription = null) }, + iconOnly = true + ) + } + + AnimatedVisibility( + visible = isNextVisible, + enter = fadeIn(animationSpec = tween(durationMillis = FluentDuration.ShortDuration)), + exit = fadeOut(animationSpec = tween(durationMillis = FluentDuration.ShortDuration)), + modifier = Modifier.padding(end = 2.dp).align(Alignment.CenterEnd) + ) { + SubtleButton( + onClick = { scope.launch { state.animateScrollBy(state.viewportSize / 3f) } }, + content = { Icon(Icons.Filled.CaretRight, contentDescription = null) }, + iconOnly = true + ) + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +private class LiteFilterBringIntoViewResponder( + private val state: ScrollState, + density: Density, +): BringIntoViewResponder { + val startSize = with(density) { 44.dp.toPx() } + val endSize = startSize + + override suspend fun bringChildIntoView(localRect: () -> Rect?) {} + + override fun calculateRectForParent(localRect: Rect): Rect { + return Snapshot.withoutReadObservation { + when { + state.canScrollForward && state.viewportSize - localRect.right - state.value < endSize -> { + return localRect.copy( + right = localRect.right + endSize + ) + } + + state.canScrollBackward && localRect.left < state.value + startSize -> { + return localRect.copy( + left = localRect.left - startSize + ) + } + + else -> localRect + } + } + } +} + +@Stable +private class PaddingShape( + private val clipStart: Boolean, + private val clipEnd: Boolean, + private val startSize: Dp = 44.dp, + private val endSize: Dp = 44.dp +) : Shape { + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + return with(density) { + Outline.Rectangle( + Rect( + left = if (clipStart) startSize.toPx() else 0f, + top = 0f, + right = size.width - if (clipEnd) endSize.toPx() else 0f, + bottom = size.height + ) + ) + } + } +} \ No newline at end of file diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/basicinput/LiteFilterScreen.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/basicinput/LiteFilterScreen.kt new file mode 100644 index 00000000..cb1cc631 --- /dev/null +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/basicinput/LiteFilterScreen.kt @@ -0,0 +1,69 @@ +package com.konyaco.fluent.gallery.screen.basicinput + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import com.konyaco.fluent.component.LiteFilter +import com.konyaco.fluent.component.PillButton +import com.konyaco.fluent.component.Text +import com.konyaco.fluent.gallery.annotation.Component +import com.konyaco.fluent.gallery.annotation.Sample +import com.konyaco.fluent.gallery.component.ComponentPagePath +import com.konyaco.fluent.gallery.component.GalleryPage +import com.konyaco.fluent.source.generated.FluentSourceFile + +@Component(index = 17, description = "An filter container for displaying a list of items.") +@Composable +fun LiteFilterScreen() { + GalleryPage( + title = "LiteFilter", + description = "An filter container for displaying a list of items.", + componentPath = FluentSourceFile.LiteFilter, + galleryPath = ComponentPagePath.LiteFilterScreen + ) { + Section( + title = "LiteFilter", + sourceCode = sourceCodeOfLiteFilterSample, + content = { LiteFilterSample() } + ) + } +} + +@Sample +@Composable +private fun LiteFilterSample() { + val selectedItem = remember { mutableStateOf("") } + LiteFilter { + items().forEach { name -> + PillButton( + selected = selectedItem.value == name, + onSelectedChanged = { + if (selectedItem.value != name) { + selectedItem.value = name + } else { + selectedItem.value = "" + } + }, + ) { + Text(name) + } + } + } +} + +@Stable +private fun items() = listOf( + "All", + "Apps", + "Documents", + "Web", + "People", + "IMG", + "JPG", + "OneDrive", + "SkyDrive", + "Pictures", + "Songs", + "Videos", +) \ No newline at end of file diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/basicinput/PillButtonScreen.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/basicinput/PillButtonScreen.kt new file mode 100644 index 00000000..8596d391 --- /dev/null +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/basicinput/PillButtonScreen.kt @@ -0,0 +1,78 @@ +package com.konyaco.fluent.gallery.screen.basicinput + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import com.konyaco.fluent.component.CheckBox +import com.konyaco.fluent.component.Icon +import com.konyaco.fluent.component.PillButton +import com.konyaco.fluent.component.Text +import com.konyaco.fluent.gallery.annotation.Component +import com.konyaco.fluent.gallery.annotation.Sample +import com.konyaco.fluent.gallery.component.ComponentPagePath +import com.konyaco.fluent.gallery.component.GalleryPage +import com.konyaco.fluent.icons.Icons +import com.konyaco.fluent.icons.regular.Power +import com.konyaco.fluent.source.generated.FluentSourceFile + +@Component(description = "represent content metadata to users. They can be used as static tags, links to genre pages, or content filtering experiences.", index = 14) +@Composable +fun PillButtonScreen() { + GalleryPage( + title = "PillButton", + description = "Pill buttons represent content metadata to users. They can be used as static" + + "tags, links to genre pages, or built into interactive components to create" + + " content filtering experiences.", + componentPath = FluentSourceFile.Button, + galleryPath = ComponentPagePath.PillButtonScreen + ) { + val enabled = remember { mutableStateOf(true) } + Section( + title = "PillButton", + sourceCode = sourceCodeOfPillButtonSample, + content = { + PillButtonSample(enabled.value) + }, + options = { + CheckBox( + checked = enabled.value, + onCheckStateChange = { enabled.value = !enabled.value }, + label = "Enabled" + ) + } + ) + Section( + title = "PillButton with Icon", + sourceCode = sourceCodeOfPillButtonWithIconSample, + content = { + PillButtonWithIconSample() + } + ) + } +} + +@Sample +@Composable +private fun PillButtonSample(enabled: Boolean) { + val selected = remember { mutableStateOf(false) } + PillButton( + selected = selected.value, + onSelectedChanged = { selected.value = !selected.value }, + content = { Text("Close") }, + disabled = !enabled + ) +} + +@Sample +@Composable +private fun PillButtonWithIconSample() { + val selected = remember { mutableStateOf(false) } + PillButton( + selected = selected.value, + onSelectedChanged = { selected.value = !selected.value }, + content = { + Icon(Icons.Default.Power, null) + Text("Close") + } + ) +} \ No newline at end of file From f174097b181519ba8e21fde1b10155f57619517c Mon Sep 17 00:00:00 2001 From: sanlorng Date: Mon, 23 Sep 2024 20:09:37 +0800 Subject: [PATCH 016/247] Fixed Button's compile error --- .../com/konyaco/fluent/component/Button.kt | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Button.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Button.kt index 6102d408..29146089 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Button.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Button.kt @@ -89,15 +89,15 @@ fun Button( content: @Composable RowScope.() -> Unit ) { Button( - modifier, - interaction, - disabled, - buttonColors, - false, - onClick, - iconOnly, - contentArrangement, - content + modifier = modifier, + interaction = interaction, + disabled = disabled, + buttonColors = buttonColors, + accentButton = false, + onClick = onClick, + iconOnly = iconOnly, + contentArrangement = contentArrangement, + content = content ) } @@ -113,15 +113,15 @@ fun AccentButton( content: @Composable RowScope.() -> Unit ) { Button( - modifier, - interaction, - disabled, - buttonColors, - true, - onClick, - iconOnly, - contentArrangement, - content + modifier = modifier, + interaction = interaction, + disabled = disabled, + buttonColors = buttonColors, + accentButton = true, + onClick = onClick, + iconOnly = iconOnly, + contentArrangement = contentArrangement, + content = content ) } @@ -137,15 +137,15 @@ fun SubtleButton( content: @Composable RowScope.() -> Unit ) { Button( - modifier, - interaction, - disabled, - buttonColors, - true, - onClick, - iconOnly, - contentArrangement, - content + modifier = modifier, + interaction = interaction, + disabled = disabled, + buttonColors = buttonColors, + accentButton = true, + onClick = onClick, + iconOnly = iconOnly, + contentArrangement = contentArrangement, + content = content ) } From c16e83db5117a70c9807b3df4d172e456d214efd Mon Sep 17 00:00:00 2001 From: sanlorng Date: Sun, 2 Jun 2024 21:50:50 +0800 Subject: [PATCH 017/247] [fluent] Add `SegmentedControl` and `SegmentedButton`. --- .../fluent/component/SegmentedControl.kt | 278 ++++++++++++++++++ .../basicinput/SegmentedControlScreen.kt | 107 +++++++ 2 files changed, 385 insertions(+) create mode 100644 fluent/src/commonMain/kotlin/com/konyaco/fluent/component/SegmentedControl.kt create mode 100644 gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/basicinput/SegmentedControlScreen.kt diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/SegmentedControl.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/SegmentedControl.kt new file mode 100644 index 00000000..b1bd693a --- /dev/null +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/SegmentedControl.kt @@ -0,0 +1,278 @@ +package com.konyaco.fluent.component + +import androidx.compose.animation.core.animateDp +import androidx.compose.animation.core.tween +import androidx.compose.animation.core.updateTransition +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.geometry.translate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.layout.boundsInParent +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import com.konyaco.fluent.FluentTheme +import com.konyaco.fluent.animation.FluentDuration +import com.konyaco.fluent.animation.FluentEasing +import com.konyaco.fluent.background.BackgroundSizing +import com.konyaco.fluent.background.Layer +import com.konyaco.fluent.scheme.VisualStateScheme +import com.konyaco.fluent.scheme.collectVisualState + +@Composable +fun SegmentedControl( + modifier: Modifier = Modifier, + color: Color = FluentTheme.colors.controlAlt.secondary, + borderStroke: BorderStroke? = BorderStroke( + buttonBorderStrokeWidth, + FluentTheme.colors.stroke.control.default + ), + content: @Composable RowScope.() -> Unit, +) { + Layer( + color = color, + border = borderStroke, + shape = buttonShape, + backgroundSizing = BackgroundSizing.OuterBorderEdge, + modifier = modifier + ) { + Row(content = content) + } +} + +@Composable +fun SegmentedButton( + checked: Boolean, + onCheckedChanged: (Boolean) -> Unit, + colors: VisualStateScheme = if (checked) { + ButtonDefaults.buttonColors() + } else { + ButtonDefaults.subtleButtonColors() + }, + indicator: @Composable () -> Unit = { + HorizontalIndicator( + visible = checked, + modifier = Modifier.padding(bottom = buttonBorderStrokeWidth) + ) + }, + modifier: Modifier = Modifier, + enabled: Boolean = true, + position: SegmentedItemPosition = SegmentedItemPosition.Center, + interactionSource: MutableInteractionSource? = null, + icon: (@Composable () -> Unit)? = null, + text: (@Composable () -> Unit)? = null +) { + val targetInteractionSource = interactionSource ?: remember { MutableInteractionSource() } + val currentColors = colors.schemeFor(targetInteractionSource.collectVisualState(!enabled)) + val shape = if (checked) { + buttonShape + } else { + val padding = when (position) { + SegmentedItemPosition.Start -> PaddingValues( + top = 3.dp, + bottom = 3.dp, + start = 3.dp, + end = 1.dp + ) + + SegmentedItemPosition.Center -> PaddingValues( + horizontal = 1.dp, + vertical = 3.dp + ) + + SegmentedItemPosition.End -> PaddingValues( + top = 3.dp, + bottom = 3.dp, + start = 1.dp, + end = 3.dp + ) + } + PaddingBackgroundShape(2.dp, padding) + } + Layer( + color = currentColors.fillColor, + contentColor = currentColors.contentColor, + border = BorderStroke(buttonBorderStrokeWidth, currentColors.borderBrush), + backgroundSizing = BackgroundSizing.OuterBorderEdge, + modifier = modifier.clickable( + enabled = enabled, + interactionSource = targetInteractionSource, + indication = null, + onClick = { onCheckedChanged(!checked) } + ), + shape = shape, + ) { + HorizontalIndicatorContentLayout( + indicator = indicator, + indicatorVisible = checked, + icon = icon, + text = text, + modifier = Modifier.defaultMinSize(minHeight = buttonMinHeight) + ) + } +} + +enum class SegmentedItemPosition { Start, Center, End } + +@Composable +fun HorizontalIndicator( + modifier: Modifier = Modifier, + visible: Boolean = true, + enabled: Boolean = true, + color: Color = FluentTheme.colors.fillAccent.default, + disabledColor: Color = FluentTheme.colors.fillAccent.disabled +) { + val width by updateTransition(visible).animateDp(transitionSpec = { + if (targetState) tween( + FluentDuration.QuickDuration, + easing = FluentEasing.FastInvokeEasing + ) + else tween(FluentDuration.QuickDuration, easing = FluentEasing.FastDismissEasing) + }, targetValueByState = { if (it) 16.dp else 0.dp }) + Box( + modifier = modifier + .size(width = width, height = 3.dp) + .background( + color = if (enabled) { + color + } else { + disabledColor + }, + shape = CircleShape + ) + ) +} + +@Composable +internal fun HorizontalIndicatorContentLayout( + indicatorVisible: Boolean, + modifier: Modifier = Modifier, + icon: @Composable (() -> Unit)?, + text: @Composable (() -> Unit)?, + indicator: @Composable () -> Unit, +) { + Box( + modifier = modifier.padding(horizontal = 12.dp), + contentAlignment = Alignment.CenterStart + ) { + val indicatorAnchorRect = remember { mutableStateOf(0f) } + val indicatorSize = remember { mutableStateOf(IntSize.Zero) } + val rowOffset = remember { mutableStateOf(0f) } + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.align(Alignment.Center) + .onGloballyPositioned { + rowOffset.value = it.boundsInParent().left + }, + ) { + + val indicatorAnchorModifier = Modifier.onGloballyPositioned { + indicatorAnchorRect.value = it.boundsInParent().center.x + } + icon?.let { + if (indicatorVisible && text == null) { + Box(modifier = indicatorAnchorModifier) { it() } + } else { + it() + } + } + text?.let { + if (indicatorVisible) { + Box(modifier = indicatorAnchorModifier) { it() } + } else { + it() + } + } + + } + Box( + modifier = Modifier + .onSizeChanged { indicatorSize.value = it } + .align(Alignment.BottomStart) + .offset { + IntOffset((rowOffset.value + indicatorAnchorRect.value - indicatorSize.value.width / 2f).toInt(), 0) + } + ) { + indicator() + } + } +} + +@Stable +private class PaddingBackgroundShape(corner: Dp, private val padding: PaddingValues) : Shape { + private val shape = RoundedCornerShape(corner) + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + return with(density) { + val leftPadding = padding.calculateLeftPadding(layoutDirection).toPx() + val topPadding = padding.calculateTopPadding().toPx() + val rightPadding = padding.calculateRightPadding(layoutDirection).toPx() + val bottomPadding = padding.calculateBottomPadding().toPx() + val paddingSize = Size( + size.width - leftPadding - rightPadding, + size.height - topPadding - bottomPadding + ) + when (val oldOutline = shape.createOutline(paddingSize, layoutDirection, density)) { + is Outline.Rectangle -> Outline.Rectangle( + oldOutline.rect.translate( + Offset( + leftPadding, + topPadding + ) + ) + ) + + is Outline.Rounded -> Outline.Rounded( + oldOutline.roundRect.translate( + Offset( + leftPadding, + topPadding + ) + ) + ) + + is Outline.Generic -> Outline.Generic(oldOutline.path.apply { + translate( + Offset( + leftPadding, + topPadding + ) + ) + }) + } + } + } +} \ No newline at end of file diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/basicinput/SegmentedControlScreen.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/basicinput/SegmentedControlScreen.kt new file mode 100644 index 00000000..834e183a --- /dev/null +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/basicinput/SegmentedControlScreen.kt @@ -0,0 +1,107 @@ +package com.konyaco.fluent.gallery.screen.basicinput + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import com.konyaco.fluent.component.Icon +import com.konyaco.fluent.component.SegmentedButton +import com.konyaco.fluent.component.SegmentedControl +import com.konyaco.fluent.component.SegmentedItemPosition +import com.konyaco.fluent.component.Text +import com.konyaco.fluent.gallery.annotation.Component +import com.konyaco.fluent.gallery.annotation.Sample +import com.konyaco.fluent.gallery.component.ComponentPagePath +import com.konyaco.fluent.gallery.component.GalleryPage +import com.konyaco.fluent.icons.Icons +import com.konyaco.fluent.icons.regular.Circle +import com.konyaco.fluent.source.generated.FluentSourceFile + +@Component(index = 15, description = "A common Ul control to configure a view or setting.") +@Composable +fun SegmentedControlScreen() { + GalleryPage( + title = "SegmentedControl", + description = "A common Ul control to configure a view or setting.", + componentPath = FluentSourceFile.SegmentedControl, + galleryPath = ComponentPagePath.SegmentedControlScreen + ) { + Section( + title = "SegmentedControl", + sourceCode = sourceCodeOfSegmentedControlSample, + content = { SegmentedControlSample() } + ) + Section( + title = "SegmentedControl with Icon", + sourceCode = sourceCodeOfSegmentedControlWithIconSample, + content = { SegmentedControlWithIconSample() } + ) + Section( + title = "SegmentedControl Icon Only", + sourceCode = sourceCodeOfSegmentedControlIconOnlySample, + content = { SegmentedControlIconOnlySample() } + ) + } +} + +@Sample +@Composable +private fun SegmentedControlSample() { + val checkedIndex = remember { mutableStateOf(0) } + SegmentedControl { + repeat(count) { index -> + SegmentedButton( + checked = index == checkedIndex.value, + onCheckedChanged = { checkedIndex.value = index }, + position = when (index) { + 0 -> SegmentedItemPosition.Start + count - 1 -> SegmentedItemPosition.End + else -> SegmentedItemPosition.Center + }, + text = { Text("Text") } + ) + } + } +} + +@Sample +@Composable +private fun SegmentedControlWithIconSample() { + val checkedIndex = remember { mutableStateOf(0) } + SegmentedControl { + repeat(count) { index -> + SegmentedButton( + checked = index == checkedIndex.value, + onCheckedChanged = { checkedIndex.value = index }, + position = when (index) { + 0 -> SegmentedItemPosition.Start + count - 1 -> SegmentedItemPosition.End + else -> SegmentedItemPosition.Center + }, + text = { Text("Text") }, + icon = { Icon(Icons.Default.Circle, null) } + ) + } + } +} + +@Sample +@Composable +private fun SegmentedControlIconOnlySample() { + val checkedIndex = remember { mutableStateOf(0) } + SegmentedControl { + repeat(count) { index -> + SegmentedButton( + checked = index == checkedIndex.value, + onCheckedChanged = { checkedIndex.value = index }, + position = when (index) { + 0 -> SegmentedItemPosition.Start + count - 1 -> SegmentedItemPosition.End + else -> SegmentedItemPosition.Center + }, + icon = { Icon(Icons.Default.Circle, null) } + ) + } + } +} + +private const val count = 5 \ No newline at end of file From 554777137342f8c25828f28e5f832e5ef0ac7eb5 Mon Sep 17 00:00:00 2001 From: sanlorng Date: Mon, 23 Sep 2024 20:33:53 +0800 Subject: [PATCH 018/247] Update SegmentedButton --- .../com/konyaco/fluent/component/Button.kt | 4 +- .../fluent/component/SegmentedControl.kt | 48 +++---------------- 2 files changed, 8 insertions(+), 44 deletions(-) diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Button.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Button.kt index 29146089..629d3094 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Button.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Button.kt @@ -723,5 +723,5 @@ private fun AnimatedDropDownIcon(interaction: MutableInteractionSource) { ) } -private val buttonMinHeight = 32.dp -private val buttonBorderStrokeWidth = 1.dp \ No newline at end of file +internal val buttonMinHeight = 32.dp +internal val buttonBorderStrokeWidth = 1.dp \ No newline at end of file diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/SegmentedControl.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/SegmentedControl.kt index b1bd693a..136b4d50 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/SegmentedControl.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/SegmentedControl.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape @@ -21,7 +20,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -31,13 +29,8 @@ import androidx.compose.ui.geometry.translate import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.layout.boundsInParent -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.konyaco.fluent.FluentTheme @@ -61,7 +54,7 @@ fun SegmentedControl( Layer( color = color, border = borderStroke, - shape = buttonShape, + shape = FluentTheme.shapes.control, backgroundSizing = BackgroundSizing.OuterBorderEdge, modifier = modifier ) { @@ -94,7 +87,7 @@ fun SegmentedButton( val targetInteractionSource = interactionSource ?: remember { MutableInteractionSource() } val currentColors = colors.schemeFor(targetInteractionSource.collectVisualState(!enabled)) val shape = if (checked) { - buttonShape + FluentTheme.shapes.control } else { val padding = when (position) { SegmentedItemPosition.Start -> PaddingValues( @@ -133,7 +126,6 @@ fun SegmentedButton( ) { HorizontalIndicatorContentLayout( indicator = indicator, - indicatorVisible = checked, icon = icon, text = text, modifier = Modifier.defaultMinSize(minHeight = buttonMinHeight) @@ -174,7 +166,6 @@ fun HorizontalIndicator( @Composable internal fun HorizontalIndicatorContentLayout( - indicatorVisible: Boolean, modifier: Modifier = Modifier, icon: @Composable (() -> Unit)?, text: @Composable (() -> Unit)?, @@ -184,44 +175,16 @@ internal fun HorizontalIndicatorContentLayout( modifier = modifier.padding(horizontal = 12.dp), contentAlignment = Alignment.CenterStart ) { - val indicatorAnchorRect = remember { mutableStateOf(0f) } - val indicatorSize = remember { mutableStateOf(IntSize.Zero) } - val rowOffset = remember { mutableStateOf(0f) } Row( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.align(Alignment.Center) - .onGloballyPositioned { - rowOffset.value = it.boundsInParent().left - }, ) { - - val indicatorAnchorModifier = Modifier.onGloballyPositioned { - indicatorAnchorRect.value = it.boundsInParent().center.x - } - icon?.let { - if (indicatorVisible && text == null) { - Box(modifier = indicatorAnchorModifier) { it() } - } else { - it() - } - } - text?.let { - if (indicatorVisible) { - Box(modifier = indicatorAnchorModifier) { it() } - } else { - it() - } - } - + icon?.invoke() + text?.invoke() } Box( - modifier = Modifier - .onSizeChanged { indicatorSize.value = it } - .align(Alignment.BottomStart) - .offset { - IntOffset((rowOffset.value + indicatorAnchorRect.value - indicatorSize.value.width / 2f).toInt(), 0) - } + modifier = Modifier.align(Alignment.BottomCenter) ) { indicator() } @@ -272,6 +235,7 @@ private class PaddingBackgroundShape(corner: Dp, private val padding: PaddingVal ) ) }) + else -> oldOutline } } } From 6a9f80bfcdf729aca037b683824f1a91a2093243 Mon Sep 17 00:00:00 2001 From: sanlorng Date: Wed, 5 Jun 2024 22:08:40 +0800 Subject: [PATCH 019/247] [fluent] Add `TextBoxButton`. --- .../com/konyaco/fluent/component/Button.kt | 2 +- .../konyaco/fluent/component/TextBoxButton.kt | 259 ++++++++++++++++++ .../com/konyaco/fluent/component/TextField.kt | 227 +++++++++++++-- .../kotlin/com/konyaco/fluent/gallery/App.kt | 6 + 4 files changed, 473 insertions(+), 21 deletions(-) create mode 100644 fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TextBoxButton.kt diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Button.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Button.kt index 629d3094..0d825368 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Button.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Button.kt @@ -513,7 +513,7 @@ private fun Button( common interaction layer for button and split button. */ @Composable -private fun ButtonLayer( +internal fun ButtonLayer( shape: Shape, buttonColors: VisualStateScheme, interaction: MutableInteractionSource, diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TextBoxButton.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TextBoxButton.kt new file mode 100644 index 00000000..b2469ba4 --- /dev/null +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TextBoxButton.kt @@ -0,0 +1,259 @@ +package com.konyaco.fluent.component + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.focusProperties +import androidx.compose.ui.input.pointer.PointerIcon +import androidx.compose.ui.input.pointer.pointerHoverIcon +import androidx.compose.ui.semantics.Role +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 com.konyaco.fluent.icons.Icons +import com.konyaco.fluent.icons.regular.ArrowRight +import com.konyaco.fluent.icons.regular.ChevronDown +import com.konyaco.fluent.icons.regular.ChevronUp +import com.konyaco.fluent.icons.regular.Dismiss +import com.konyaco.fluent.icons.regular.Eye +import com.konyaco.fluent.icons.regular.Search +import com.konyaco.fluent.scheme.VisualStateScheme +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@Composable +fun TextBoxButton( + modifier: Modifier = Modifier, + enabled: Boolean = true, + onClick: (() -> Unit)? = null, + colors: VisualStateScheme = ButtonDefaults.subtleButtonColors(), + outsideBorder: Boolean = false, + interactionSource: MutableInteractionSource? = null, + focusable: Boolean = false, + content: @Composable RowScope.() -> Unit +) { + val interaction = interactionSource ?: remember { MutableInteractionSource() } + ButtonLayer( + shape = RoundedCornerShape(4.dp), + displayBorder = true, + buttonColors = colors, + interaction = interaction, + disabled = !enabled, + accentButton = !outsideBorder, + modifier = modifier.defaultMinSize(minWidth = 28.dp, minHeight = 24.dp) + ) { + Row( + Modifier + .then( + if (onClick != null) { + Modifier.focusProperties { canFocus = focusable } + .clickable( + onClick = onClick, + interactionSource = interaction, + indication = null, + enabled = enabled + ).pointerHoverIcon(PointerIcon.Default, !enabled) + + } else { + Modifier + } + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + content = content + ) + } +} + +@Composable +fun ToggleTextButton( + checked: Boolean, + onCheckedChanged: ((Boolean) -> Unit), + modifier: Modifier = Modifier, + enabled: Boolean = true, + colors: VisualStateScheme = if (checked) { + ButtonDefaults.accentButtonColors() + } else { + ButtonDefaults.subtleButtonColors() + }, + outsideBorder: Boolean = checked, + interactionSource: MutableInteractionSource? = null, + focusable: Boolean = false, + content: @Composable RowScope.() -> Unit +) { + val interaction = interactionSource ?: remember { MutableInteractionSource() } + TextBoxButton( + enabled = enabled, + onClick = null, + colors = colors, + outsideBorder = outsideBorder, + interactionSource = interaction, + focusable = focusable, + content = content, + modifier = modifier + .focusProperties { canFocus = focusable } + .pointerHoverIcon(PointerIcon.Default, !enabled) + .selectable( + selected = checked, + onClick = { onCheckedChanged(!checked) }, + enabled = enabled, + role = Role.Checkbox, + interactionSource = interaction, + indication = null + ) + ) +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun RepeatTextBoxButton( + onClick: (() -> Unit), + modifier: Modifier = Modifier, + enabled: Boolean = true, + delay: Long = 200, + interval: Long = 50, + colors: VisualStateScheme = ButtonDefaults.subtleButtonColors(), + outsideBorder: Boolean = false, + interactionSource: MutableInteractionSource? = null, + focusable: Boolean = false, + content: @Composable RowScope.() -> Unit +) { + val interaction = interactionSource ?: remember { MutableInteractionSource() } + val pressed = interaction.collectIsPressedAsState() + val scope = rememberCoroutineScope() + + TextBoxButton( + modifier = modifier + .focusProperties { canFocus = focusable } + .pointerHoverIcon(PointerIcon.Default, !enabled) + .combinedClickable( + interactionSource = interaction, + indication = null, + enabled = enabled, + onClick = onClick, + onLongClick = { + onClick() + scope.launch { + delay(delay) + do { + onClick() + delay(interval) + } while (pressed.value) + } + }, + onDoubleClick = { + onClick() + onClick() + } + ), + interactionSource = interaction, + enabled = enabled, + colors = colors, + outsideBorder = outsideBorder, + onClick = null, + content = content + ) +} + +object TextBoxButtonDefaults { + + val iconFontSmallSize = 12.sp + + val iconVectorSmallSize = 12.dp + + val iconFontMediumSize = 16.sp + + val iconVectorMediumSize = 16.dp + + @Composable + fun SearchIcon(fontSize: TextUnit = iconFontSmallSize, vectorSize: Dp = iconVectorSmallSize) { + FontIcon( + glyph = '\uF78B', + vector = Icons.Default.Search, + contentDescription = "Search", + iconSize = fontSize, + vectorSize = vectorSize + ) + } + + @Composable + fun ClearIcon(fontSize: TextUnit = iconFontSmallSize, vectorSize: Dp = iconVectorSmallSize) { + FontIcon( + glyph = '\uE624', + vector = Icons.Default.Dismiss, + contentDescription = "Clear", + iconSize = fontSize, + vectorSize = vectorSize + ) + } + + @Composable + fun RevealPasswordIcon( + fontSize: TextUnit = iconFontSmallSize, + vectorSize: Dp = iconVectorSmallSize + ) { + FontIcon( + glyph = '\uF78D', + vector = Icons.Default.Eye, + contentDescription = "Reveal Password", + iconSize = fontSize, + vectorSize = vectorSize + ) + } + + @Composable + fun ArrowRightIcon( + fontSize: TextUnit = iconFontSmallSize, + vectorSize: Dp = iconVectorSmallSize + ) { + FontIcon( + glyph = '\uE64D', + vector = Icons.Default.ArrowRight, + contentDescription = "Arrow Right", + iconSize = fontSize, + vectorSize = vectorSize + ) + } + + @Composable + fun ChevronUpIcon( + fontSize: TextUnit = iconFontSmallSize, + vectorSize: Dp = iconVectorSmallSize + ) { + FontIcon( + glyph = '\uE70E', + vector = Icons.Default.ChevronUp, + contentDescription = "Chevron Up", + iconSize = fontSize, + vectorSize = vectorSize + ) + } + + @Composable + fun ChevronDownIcon( + fontSize: TextUnit = iconFontSmallSize, + vectorSize: Dp = iconVectorSmallSize + ) { + FontIcon( + glyph = '\uE70F', + vector = Icons.Default.ChevronDown, + contentDescription = "Chevron Down", + iconSize = fontSize, + vectorSize = vectorSize + ) + } +} \ No newline at end of file diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TextField.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TextField.kt index b48e0ed5..940610b7 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TextField.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TextField.kt @@ -4,12 +4,14 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions @@ -29,12 +31,16 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.konyaco.fluent.FluentTheme +import com.konyaco.fluent.LocalContentAlpha import com.konyaco.fluent.LocalContentColor import com.konyaco.fluent.LocalTextStyle import com.konyaco.fluent.background.BackgroundSizing @@ -55,19 +61,18 @@ fun TextField( keyboardActions: KeyboardActions = KeyboardActions(), maxLines: Int = Int.MAX_VALUE, header: (@Composable () -> Unit)? = null, + leadingIcon: (@Composable () -> Unit)? = null, + trailing: (@Composable RowScope.() -> Unit)? = null, placeholder: (@Composable () -> Unit)? = null, + isClearable: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - colors: TextFieldColorScheme = TextFieldDefaults.defaultTextFieldColors() + colors: TextFieldColorScheme = TextFieldDefaults.defaultTextFieldColors(), + shape: Shape = FluentTheme.shapes.control ) { val color = colors.schemeFor(interactionSource.collectVisualState(!enabled, focusFirst = true)) - Column(modifier) { - if (header != null) { - header() - Spacer(Modifier.height(8.dp)) - } + HeaderContainer(header = header, modifier = modifier) { BasicTextField( - modifier = modifier.defaultMinSize(64.dp, 32.dp) - .clip(FluentTheme.shapes.control), + modifier = modifier.textFieldModifier(shape), value = value, onValueChange = onValueChange, textStyle = LocalTextStyle.current.copy(color = color.contentColor), @@ -87,7 +92,72 @@ fun TextField( innerTextField = innerTextField, value = value.text, enabled = enabled, - placeholder = placeholder + placeholder = placeholder, + leadingIcon = leadingIcon, + onClearClick = if (isClearable) { + { onValueChange(TextFieldValue("")) } + } else { + null + }, + trailing = trailing + ) + } + ) + } +} + +@Composable +fun TextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + readOnly: Boolean = false, + singleLine: Boolean = false, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions(), + maxLines: Int = Int.MAX_VALUE, + header: (@Composable () -> Unit)? = null, + leadingIcon: (@Composable () -> Unit)? = null, + trailing: (@Composable RowScope.() -> Unit)? = null, + placeholder: (@Composable () -> Unit)? = null, + isClearable: Boolean = true, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + colors: TextFieldColorScheme = TextFieldDefaults.defaultTextFieldColors(), + shape: Shape = FluentTheme.shapes.control +) { + val color = colors.schemeFor(interactionSource.collectVisualState(!enabled, focusFirst = true)) + HeaderContainer(header = header, modifier = modifier) { + BasicTextField( + modifier = modifier.textFieldModifier(shape), + value = value, + onValueChange = onValueChange, + textStyle = LocalTextStyle.current.copy(color = color.contentColor), + enabled = enabled, + readOnly = readOnly, + singleLine = singleLine, + visualTransformation = visualTransformation, + maxLines = maxLines, + keyboardActions = keyboardActions, + cursorBrush = color.cursorBrush, + keyboardOptions = keyboardOptions, + interactionSource = interactionSource, + decorationBox = { innerTextField -> + TextFieldDefaults.DecorationBox( + color = color, + interactionSource = interactionSource, + innerTextField = innerTextField, + value = value, + enabled = enabled, + placeholder = placeholder, + leadingIcon = leadingIcon, + onClearClick = if (isClearable) { + { onValueChange("") } + } else { + null + }, + trailing = trailing ) } ) @@ -132,6 +202,42 @@ object TextFieldDefaults { disabled = disabled ) + @Deprecated( + "Use DecorationBox instead", ReplaceWith( + "DecorationBox(" + + "value = value," + + "interactionSource = interactionSource," + + "enabled = enabled," + + "color = color," + + "modifier = modifier," + + "placeholder = placeholder," + + "innerTextField = innerTextField," + + "leadingIcon = null" + + ")" + ) + ) + @Composable + fun DecorationBox( + value: String, + interactionSource: MutableInteractionSource, + enabled: Boolean, + color: TextFieldColor, + modifier: Modifier = Modifier.drawBottomLine(enabled, color, interactionSource), + placeholder: (@Composable () -> Unit)?, + innerTextField: @Composable () -> Unit, + ) = DecorationBox( + value = value, + interactionSource = interactionSource, + enabled = enabled, + color = color, + modifier = modifier, + placeholder = placeholder, + innerTextField = innerTextField, + leadingIcon = null, + onClearClick = null, + trailing = null + ) + @Composable fun DecorationBox( value: String, @@ -139,9 +245,13 @@ object TextFieldDefaults { enabled: Boolean, color: TextFieldColor, modifier: Modifier = Modifier.drawBottomLine(enabled, color, interactionSource), + onClearClick: (() -> Unit)? = null, placeholder: (@Composable () -> Unit)?, + leadingIcon: (@Composable () -> Unit)?, + trailing: (@Composable RowScope.() -> Unit)?, innerTextField: @Composable () -> Unit, ) { + Layer( modifier = modifier.hoverable(interactionSource), shape = FluentTheme.shapes.control, @@ -149,17 +259,44 @@ object TextFieldDefaults { border = BorderStroke(1.dp, color.borderBrush), backgroundSizing = BackgroundSizing.OuterBorderEdge ) { - Box( - Modifier.offset(y = (-1).dp).padding(start = 12.dp, end = 12.dp, top = 4.dp, bottom = 3.dp), - Alignment.CenterStart + Row( + horizontalArrangement = TextFieldContentArrangement, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(top = 4.dp, bottom = 4.dp) ) { - innerTextField() - if (value.isEmpty() && placeholder != null) { - CompositionLocalProvider( - LocalContentColor provides color.placeholderColor, - LocalTextStyle provides LocalTextStyle.current.copy(color = color.placeholderColor) + if (leadingIcon != null) { + Box(modifier = Modifier.padding(start = 16.dp)) { + leadingIcon() + } + } + Box( + modifier = Modifier.weight(1f, fill = false).padding(horizontal = 12.dp), + Alignment.CenterStart + ) { + innerTextField() + if (value.isEmpty() && placeholder != null) { + CompositionLocalProvider( + LocalContentColor provides color.placeholderColor, + LocalTextStyle provides LocalTextStyle.current.copy(color = color.placeholderColor) + ) { + placeholder() + } + } + } + val isFocused = interactionSource.collectIsFocusedAsState() + val hasClearButton = onClearClick != null && isFocused.value && value.isNotEmpty() + if (trailing != null || hasClearButton) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.padding(end = 4.dp) ) { - placeholder() + if (hasClearButton) { + TextBoxButton( + enabled = enabled, + onClick = onClearClick + ) { TextBoxButtonDefaults.ClearIcon() } + } + trailing?.invoke(this) } } } @@ -180,7 +317,32 @@ data class TextFieldColor( ) @Composable -private fun Modifier.drawBottomLine(enabled: Boolean, color: TextFieldColor, interactionSource: MutableInteractionSource): Modifier { +private fun HeaderContainer( + header: (@Composable () -> Unit)?, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + Column(modifier = modifier) { + if (header != null) { + CompositionLocalProvider( + LocalTextStyle provides FluentTheme.typography.body, + LocalContentColor provides FluentTheme.colors.text.text.primary, + LocalContentAlpha provides FluentTheme.colors.text.text.primary.alpha + ) { + header() + } + Spacer(Modifier.height(8.dp)) + } + content() + } +} + +@Composable +private fun Modifier.drawBottomLine( + enabled: Boolean, + color: TextFieldColor, + interactionSource: MutableInteractionSource +): Modifier { val isFocused by interactionSource.collectIsFocusedAsState() return if (enabled) { val height by rememberUpdatedState(with(LocalDensity.current) { @@ -196,4 +358,29 @@ private fun Modifier.drawBottomLine(enabled: Boolean, color: TextFieldColor, int ) } } else this +} + +@Stable +internal fun Modifier.textFieldModifier(shape: Shape = FluentTheme.shapes.control) = + defaultMinSize(64.dp, 32.dp).clip(shape) + +@Stable +private object TextFieldContentArrangement : Arrangement.Horizontal { + override fun Density.arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) { + with(Arrangement.Start) { + arrange(totalSize, sizes, layoutDirection, outPositions) + } + if (sizes.size < 2) return + if (layoutDirection == LayoutDirection.Rtl) { + outPositions[outPositions.lastIndex] = 0 + } else { + outPositions[outPositions.lastIndex] = totalSize - sizes.last() + } + + } } \ No newline at end of file diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/App.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/App.kt index c20e4211..57e443ee 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/App.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/App.kt @@ -31,6 +31,8 @@ import com.konyaco.fluent.component.NavigationItemSeparator import com.konyaco.fluent.component.SideNav import com.konyaco.fluent.component.SideNavItem import com.konyaco.fluent.component.Text +import com.konyaco.fluent.component.TextBoxButton +import com.konyaco.fluent.component.TextBoxButtonDefaults import com.konyaco.fluent.component.TextField import com.konyaco.fluent.gallery.component.ComponentItem import com.konyaco.fluent.gallery.component.ComponentNavigator @@ -70,6 +72,10 @@ fun App( value = textFieldValue, onValueChange = { textFieldValue = it }, placeholder = { Text("Search") }, + trailing = { + TextBoxButton(onClick = {}) { TextBoxButtonDefaults.SearchIcon() } + }, + isClearable = true, modifier = Modifier.fillMaxWidth().focusHandle() ) }, From 369888f1930d62b084012a5fa4efca5aab7d8f80 Mon Sep 17 00:00:00 2001 From: sanlorng Date: Thu, 6 Jun 2024 13:22:29 +0800 Subject: [PATCH 020/247] [fluent] Add `AutoSuggestBox`. --- .../component/AutoSuggestBox.android.kt | 43 ++++ .../fluent/component/AutoSuggestBox.kt | 187 ++++++++++++++++++ .../com/konyaco/fluent/component/SideNav.kt | 10 +- .../com/konyaco/fluent/component/TextField.kt | 12 +- .../component/AutoSuggestBox.desktop.kt | 29 +++ .../kotlin/com/konyaco/fluent/gallery/App.kt | 100 +++++++++- .../screen/text/AutoSuggestBoxScreen.kt | 88 +++++++++ 7 files changed, 450 insertions(+), 19 deletions(-) create mode 100644 fluent/src/androidMain/kotlin/com/konyaco/fluent/component/AutoSuggestBox.android.kt create mode 100644 fluent/src/commonMain/kotlin/com/konyaco/fluent/component/AutoSuggestBox.kt create mode 100644 fluent/src/desktopMain/kotlin/com/konyaco/fluent/component/AutoSuggestBox.desktop.kt create mode 100644 gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/text/AutoSuggestBoxScreen.kt diff --git a/fluent/src/androidMain/kotlin/com/konyaco/fluent/component/AutoSuggestBox.android.kt b/fluent/src/androidMain/kotlin/com/konyaco/fluent/component/AutoSuggestBox.android.kt new file mode 100644 index 00000000..09e4c2da --- /dev/null +++ b/fluent/src/androidMain/kotlin/com/konyaco/fluent/component/AutoSuggestBox.android.kt @@ -0,0 +1,43 @@ +package com.konyaco.fluent.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.toComposeRect +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.toSize +import kotlin.math.max +import kotlin.math.roundToInt + +@Composable +internal actual fun rememberSuggestFlyoutCalculateMaxHeight(padding: Dp): (anchorCoordinates: LayoutCoordinates) -> Int { + val config = LocalConfiguration.current + val view = LocalView.current + val verticalMargin = with(LocalDensity.current) { padding.roundToPx() } + return remember(config, view) { + { + val windowBounds = android.graphics.Rect().let { rect -> + view.getWindowVisibleDisplayFrame(rect) + rect.toComposeRect() + } + val anchorBounds = Rect(it.positionInWindow(), it.size.toSize()) + val marginedWindowTop = windowBounds.top + verticalMargin + val marginedWindowBottom = windowBounds.bottom - verticalMargin + val availableHeight = + if (anchorBounds.top > windowBounds.bottom || anchorBounds.bottom < windowBounds.top) { + (marginedWindowBottom - marginedWindowTop).roundToInt() + } else { + val heightAbove = anchorBounds.top - marginedWindowTop + val heightBelow = marginedWindowBottom - anchorBounds.bottom + max(heightAbove, heightBelow).roundToInt() + } + + max(availableHeight, 0) + } + } +} \ No newline at end of file diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/AutoSuggestBox.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/AutoSuggestBox.kt new file mode 100644 index 00000000..9bd28c10 --- /dev/null +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/AutoSuggestBox.kt @@ -0,0 +1,187 @@ +package com.konyaco.fluent.component + +import androidx.compose.animation.expandVertically +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.waitForUpOrCancellation +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.layout +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.constrainHeight +import androidx.compose.ui.unit.constrainWidth +import androidx.compose.ui.unit.dp +import com.konyaco.fluent.CompactMode + +@Composable +fun AutoSuggestionBox( + expanded: Boolean, + onExpandedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + content: @Composable AutoSuggestBoxScope.() -> Unit +) { + + var anchorCoordinates by remember { mutableStateOf(null) } + var anchorWidth by remember { mutableIntStateOf(0) } + var flyoutMaxHeight by remember { mutableIntStateOf(0) } + + val calculateMaxHeight = rememberSuggestFlyoutCalculateMaxHeight(flyoutPopPaddingFixShadowRender + flyoutDefaultPadding) + + val focusRequester = remember { FocusRequester() } + + val autoSuggestBoxScopeImpl = remember(calculateMaxHeight, onExpandedChange) { + + object : AutoSuggestBoxScope { + + override fun Modifier.suggestFlyoutAnchor(): Modifier = this.onGloballyPositioned { + anchorCoordinates = it + anchorWidth = it.size.width + flyoutMaxHeight = calculateMaxHeight(it) + }.pointerInput(onExpandedChange) { + awaitEachGesture { + awaitFirstDown(pass = PointerEventPass.Initial) + val upEvent = waitForUpOrCancellation(pass = PointerEventPass.Initial) + if (upEvent != null) { + onExpandedChange(!expanded) + } + } + }.focusRequester(focusRequester) + + override fun Modifier.suggestFlyoutSize(matchTextFieldWidth: Boolean): Modifier = + this.layout { measurable, constraints -> + val flyoutWidth = constraints.constrainWidth(anchorWidth) + val flyoutConstraints = constraints.copy( + maxHeight = constraints.constrainHeight(flyoutMaxHeight), + minWidth = if (matchTextFieldWidth) flyoutWidth else constraints.minWidth, + maxWidth = if (matchTextFieldWidth) flyoutWidth else constraints.maxWidth, + ) + val placeable = measurable.measure(flyoutConstraints) + layout(placeable.width, placeable.height) { + placeable.place(0, 0) + } + } + + } + } + Box(modifier = modifier) { + autoSuggestBoxScopeImpl.content() + } + SideEffect { + if (expanded) focusRequester.requestFocus() + } +} + +object AutoSuggestBoxDefaults { + + @Stable + fun textFieldShape(expanded: Boolean): Shape { + return if (expanded) RoundedCornerShape( + topStart = 4.dp, + topEnd = 4.dp + ) else RoundedCornerShape(4.dp) + } + + @Composable + fun suggestFlyout( + expanded: Boolean, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(vertical = 3.dp), + content: @Composable () -> Unit + ) { + //TODO Flyout animation + BasicFlyout( + visible = expanded, + onDismissRequest = onDismissRequest, + modifier = modifier, + enterPlacementAnimation = { expandVertically(flyoutEnterSpec()) { it } }, + shape = RoundedCornerShape( + topStart = 0.dp, + topEnd = 0.dp, + bottomStart = 8.dp, + bottomEnd = 8.dp + ), + positionProvider = rememberFlyoutPositionProvider( + initialPlacement = FlyoutPlacement.Bottom, + paddingToAnchor = PaddingValues() + ), + contentPadding = contentPadding, + content = content + ) + } + + @Composable + fun suggestFlyout( + expanded: Boolean, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + state: LazyListState = rememberLazyListState(), + contentPadding: PaddingValues = PaddingValues(vertical = 3.dp), + compactMode: Boolean = false, + itemsContent: LazyListScope.() -> Unit + ) { + BasicFlyout( + visible = expanded, + onDismissRequest = onDismissRequest, + modifier = modifier, + enterPlacementAnimation = { expandVertically(flyoutEnterSpec()) { it } }, + shape = RoundedCornerShape( + topStart = 0.dp, + topEnd = 0.dp, + bottomStart = 8.dp, + bottomEnd = 8.dp + ), + positionProvider = rememberFlyoutPositionProvider( + initialPlacement = FlyoutPlacement.Bottom, + paddingToAnchor = PaddingValues() + ), + contentPadding = PaddingValues(), + content = { + CompactMode(enabled = compactMode) { + val adapter = rememberScrollbarAdapter(state) + ScrollbarContainer( + adapter = adapter + ) { + LazyColumn( + contentPadding = contentPadding, + content = itemsContent, + state = state, + ) + } + + } + } + ) + } + +} + +interface AutoSuggestBoxScope { + fun Modifier.suggestFlyoutAnchor(): Modifier + + fun Modifier.suggestFlyoutSize(matchTextFieldWidth: Boolean = true): Modifier +} + +@Composable +internal expect fun rememberSuggestFlyoutCalculateMaxHeight(padding: Dp): (anchorCoordinates: LayoutCoordinates) -> Int \ No newline at end of file diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/SideNav.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/SideNav.kt index 2d49f93d..4c5a6956 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/SideNav.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/SideNav.kt @@ -84,7 +84,7 @@ fun SideNav( expanded: Boolean, onExpandStateChange: (Boolean) -> Unit, title: @Composable (() -> Unit) = {}, - autoSuggestionBox: (@Composable AutoSuggestionBoxScope.() -> Unit)? = null, + autoSuggestionBox: (@Composable NavigationAutoSuggestBoxScope.() -> Unit)? = null, footer: @Composable (() -> Unit)? = null, content: @Composable () -> Unit ) { @@ -128,7 +128,7 @@ fun SideNav( FocusRequester() } val autoSuggestionBoxScope = remember(focusRequester) { - AutoSuggestionBoxScopeImpl(focusRequester) + NavigationAutoSuggestBoxScopeImpl(focusRequester) } Box( contentAlignment = Alignment.TopStart, @@ -398,7 +398,7 @@ interface IndicatorScope { fun Modifier.indicatorOffset(visible: () -> Boolean): Modifier } -interface AutoSuggestionBoxScope { +interface NavigationAutoSuggestBoxScope { fun Modifier.focusHandle(): Modifier } @@ -426,9 +426,9 @@ private object SideNavigationIndicatorScope: IndicatorScope { } //TODO TopNavigationIndicatorScope -internal class AutoSuggestionBoxScopeImpl( +internal class NavigationAutoSuggestBoxScopeImpl( private val focusRequest: FocusRequester -) : AutoSuggestionBoxScope { +) : NavigationAutoSuggestBoxScope { override fun Modifier.focusHandle() = focusRequester(focusRequest) } diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TextField.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TextField.kt index 940610b7..81aa81c1 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TextField.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TextField.kt @@ -99,7 +99,8 @@ fun TextField( } else { null }, - trailing = trailing + trailing = trailing, + shape = shape ) } ) @@ -157,7 +158,8 @@ fun TextField( } else { null }, - trailing = trailing + trailing = trailing, + shape = shape ) } ) @@ -235,7 +237,8 @@ object TextFieldDefaults { innerTextField = innerTextField, leadingIcon = null, onClearClick = null, - trailing = null + trailing = null, + shape = FluentTheme.shapes.control ) @Composable @@ -245,6 +248,7 @@ object TextFieldDefaults { enabled: Boolean, color: TextFieldColor, modifier: Modifier = Modifier.drawBottomLine(enabled, color, interactionSource), + shape: Shape, onClearClick: (() -> Unit)? = null, placeholder: (@Composable () -> Unit)?, leadingIcon: (@Composable () -> Unit)?, @@ -254,7 +258,7 @@ object TextFieldDefaults { Layer( modifier = modifier.hoverable(interactionSource), - shape = FluentTheme.shapes.control, + shape = shape, color = color.fillColor, border = BorderStroke(1.dp, color.borderBrush), backgroundSizing = BackgroundSizing.OuterBorderEdge diff --git a/fluent/src/desktopMain/kotlin/com/konyaco/fluent/component/AutoSuggestBox.desktop.kt b/fluent/src/desktopMain/kotlin/com/konyaco/fluent/component/AutoSuggestBox.desktop.kt new file mode 100644 index 00000000..19f0ac33 --- /dev/null +++ b/fluent/src/desktopMain/kotlin/com/konyaco/fluent/component/AutoSuggestBox.desktop.kt @@ -0,0 +1,29 @@ +package com.konyaco.fluent.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalWindowInfo +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.toIntRect +import kotlin.math.max + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +internal actual fun rememberSuggestFlyoutCalculateMaxHeight(padding: Dp): (anchorCoordinates: LayoutCoordinates) -> Int { + val windowInfo = LocalWindowInfo.current + val density = LocalDensity.current + val verticalMarginInPx = with(LocalDensity.current) { padding.roundToPx() } + return remember(windowInfo, density) { + { + val boundsInWindow = it.boundsInWindow() + val visibleWindowBounds = windowInfo.containerSize.toIntRect() + val heightAbove = boundsInWindow.top - visibleWindowBounds.top + val heightBelow = visibleWindowBounds.height - boundsInWindow.bottom + max(heightAbove, heightBelow).toInt() - verticalMarginInPx + } + } +} \ No newline at end of file diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/App.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/App.kt index 57e443ee..93b2e1be 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/App.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/App.kt @@ -7,42 +7,61 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp +import com.konyaco.fluent.CompactMode import com.konyaco.fluent.FluentTheme import com.konyaco.fluent.animation.FluentDuration import com.konyaco.fluent.animation.FluentEasing +import com.konyaco.fluent.component.AutoSuggestBoxDefaults +import com.konyaco.fluent.component.AutoSuggestionBox +import com.konyaco.fluent.component.Flyout import com.konyaco.fluent.component.Icon +import com.konyaco.fluent.component.ListItem import com.konyaco.fluent.component.NavigationItemSeparator +import com.konyaco.fluent.component.ScrollbarContainer import com.konyaco.fluent.component.SideNav import com.konyaco.fluent.component.SideNavItem import com.konyaco.fluent.component.Text import com.konyaco.fluent.component.TextBoxButton import com.konyaco.fluent.component.TextBoxButtonDefaults import com.konyaco.fluent.component.TextField +import com.konyaco.fluent.component.rememberScrollbarAdapter import com.konyaco.fluent.gallery.component.ComponentItem import com.konyaco.fluent.gallery.component.ComponentNavigator import com.konyaco.fluent.gallery.component.components import com.konyaco.fluent.gallery.component.rememberComponentNavigator +import com.konyaco.fluent.gallery.component.flatMapComponents import com.konyaco.fluent.gallery.screen.settings.SettingsScreen import com.konyaco.fluent.icons.Icons import com.konyaco.fluent.icons.regular.Settings import com.konyaco.fluent.surface.Card +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.map +@OptIn(FlowPreview::class) @Composable fun App( navigator: ComponentNavigator = rememberComponentNavigator(components.first()) @@ -62,22 +81,83 @@ fun App( var textFieldValue by remember { mutableStateOf(TextFieldValue()) } + //TODO Remove flyout open speed up workaround + Box { + val expandedInternalPopup = remember { mutableStateOf(true) } + Flyout(expandedInternalPopup.value, {}) {} + LaunchedEffect(expandedInternalPopup) { + delay(500) + expandedInternalPopup.value = false + } + } SideNav( modifier = Modifier.fillMaxHeight(), expanded = expanded, onExpandStateChange = { expanded = it }, title = { Text("Controls") }, autoSuggestionBox = { - TextField( - value = textFieldValue, - onValueChange = { textFieldValue = it }, - placeholder = { Text("Search") }, - trailing = { - TextBoxButton(onClick = {}) { TextBoxButtonDefaults.SearchIcon() } - }, - isClearable = true, - modifier = Modifier.fillMaxWidth().focusHandle() - ) + var expandedSuggestion by remember { mutableStateOf(false) } + AutoSuggestionBox( + expanded = expandedSuggestion, + onExpandedChange = { expandedSuggestion = it } + ) { + TextField( + value = textFieldValue, + onValueChange = { textFieldValue = it }, + placeholder = { Text("Search") }, + trailing = { + TextBoxButton(onClick = {}) { TextBoxButtonDefaults.SearchIcon() } + }, + isClearable = true, + shape = AutoSuggestBoxDefaults.textFieldShape(expandedSuggestion), + modifier = Modifier.fillMaxWidth().focusHandle().suggestFlyoutAnchor() + ) + val searchResult = remember(flatMapComponents) { + snapshotFlow { + textFieldValue.text + }.debounce { if (it.isBlank()) 0 else 200 } + .map { + flatMapComponents.filter { item -> + item.name.contains( + it, + ignoreCase = true + ) || item.description.contains(it, ignoreCase = true) + } + } + }.collectAsState(flatMapComponents) + AutoSuggestBoxDefaults.suggestFlyout( + expanded = expandedSuggestion, + onDismissRequest = { expandedSuggestion = false }, + contentPadding = PaddingValues(), + modifier = Modifier.suggestFlyoutSize() + ) { + + CompactMode(false) { + val state = rememberLazyListState() + val adapter = rememberScrollbarAdapter(state) + ScrollbarContainer( + adapter = adapter + ) { + LazyColumn(contentPadding = PaddingValues(vertical = 3.dp), state = state) { + items( + items = searchResult.value, + contentType = { "Item" }, + key = { it.hashCode().toString() } + ) { + ListItem( + onClick = { + navigator.navigate(it) + expandedSuggestion = false + }, + text = { Text(it.name, maxLines = 1) }, + modifier = Modifier.fillMaxWidth() + ) + } + } + } + } + } + } }, footer = { NavigationItem(navigator.latestBackEntry, navigator::navigate, settingItem) diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/text/AutoSuggestBoxScreen.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/text/AutoSuggestBoxScreen.kt new file mode 100644 index 00000000..2d24ab5c --- /dev/null +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/text/AutoSuggestBoxScreen.kt @@ -0,0 +1,88 @@ +package com.konyaco.fluent.gallery.screen.text + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.konyaco.fluent.component.AutoSuggestBoxDefaults +import com.konyaco.fluent.component.AutoSuggestionBox +import com.konyaco.fluent.component.ListItem +import com.konyaco.fluent.component.Text +import com.konyaco.fluent.component.TextField +import com.konyaco.fluent.gallery.annotation.Component +import com.konyaco.fluent.gallery.annotation.Sample +import com.konyaco.fluent.gallery.component.ComponentPagePath +import com.konyaco.fluent.gallery.component.GalleryPage +import com.konyaco.fluent.gallery.component.flatMapComponents +import com.konyaco.fluent.source.generated.FluentSourceFile +import kotlinx.coroutines.flow.map + +@Component(index = 0, description = "A control to provide suggestions as a user is typing.") +@Composable +fun AutoSuggestBoxScreen() { + GalleryPage( + title = "AutoSuggestBox", + description = "A text control that makes suggestions to users as they type. " + + "The app is notified when text has been changed by the user " + + "and is responsible for providing relevant suggestions for this control to display.", + componentPath = FluentSourceFile.AutoSuggestBox, + galleryPath = ComponentPagePath.AutoSuggestBoxScreen + ) { + Section( + title = "Basic AutoSuggestBox Sample", + sourceCode = sourceCodeOfBasicAutoSuggestBoxSample, + content = { BasicAutoSuggestBoxSample() } + ) + } +} + +@Sample +@Composable +private fun BasicAutoSuggestBoxSample() { + var expanded by remember { mutableStateOf(false) } + var keyword by remember { mutableStateOf("") } + AutoSuggestionBox( + expanded = expanded, + onExpandedChange = { expanded = it } + ) { + TextField( + value = keyword, + onValueChange = { keyword = it }, + shape = AutoSuggestBoxDefaults.textFieldShape(expanded), + modifier = Modifier.widthIn(300.dp).suggestFlyoutAnchor() + ) + val searchResult = remember(flatMapComponents) { + snapshotFlow { keyword }.map { + flatMapComponents.filter { item -> + item.name.contains( + it, + ignoreCase = true + ) || item.description.contains(it, ignoreCase = true) + } + } + }.collectAsState(flatMapComponents) + + AutoSuggestBoxDefaults.suggestFlyout( + expanded = expanded, + onDismissRequest = { expanded = false }, + itemsContent = { + items(items = searchResult.value) { + ListItem( + onClick = { expanded = false }, + text = { Text(it.name, maxLines = 1) }, + modifier = Modifier.fillMaxWidth() + ) + } + }, + modifier = Modifier.suggestFlyoutSize() + ) + } +} \ No newline at end of file From 12adeabf4843ac8637f2825e5fd6e12978eec560 Mon Sep 17 00:00:00 2001 From: sanlorng Date: Thu, 13 Jun 2024 19:17:12 +0800 Subject: [PATCH 021/247] [gallery] update auto suggest box flyout. --- .../kotlin/com/konyaco/fluent/gallery/App.kt | 47 ++++++------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/App.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/App.kt index 93b2e1be..286ac0a5 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/App.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/App.kt @@ -7,15 +7,12 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -29,7 +26,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp -import com.konyaco.fluent.CompactMode import com.konyaco.fluent.FluentTheme import com.konyaco.fluent.animation.FluentDuration import com.konyaco.fluent.animation.FluentEasing @@ -39,14 +35,12 @@ import com.konyaco.fluent.component.Flyout import com.konyaco.fluent.component.Icon import com.konyaco.fluent.component.ListItem import com.konyaco.fluent.component.NavigationItemSeparator -import com.konyaco.fluent.component.ScrollbarContainer import com.konyaco.fluent.component.SideNav import com.konyaco.fluent.component.SideNavItem import com.konyaco.fluent.component.Text import com.konyaco.fluent.component.TextBoxButton import com.konyaco.fluent.component.TextBoxButtonDefaults import com.konyaco.fluent.component.TextField -import com.konyaco.fluent.component.rememberScrollbarAdapter import com.konyaco.fluent.gallery.component.ComponentItem import com.konyaco.fluent.gallery.component.ComponentNavigator import com.konyaco.fluent.gallery.component.components @@ -128,35 +122,24 @@ fun App( AutoSuggestBoxDefaults.suggestFlyout( expanded = expandedSuggestion, onDismissRequest = { expandedSuggestion = false }, - contentPadding = PaddingValues(), - modifier = Modifier.suggestFlyoutSize() - ) { - - CompactMode(false) { - val state = rememberLazyListState() - val adapter = rememberScrollbarAdapter(state) - ScrollbarContainer( - adapter = adapter + modifier = Modifier.suggestFlyoutSize(), + itemsContent = { + items( + items = searchResult.value, + contentType = { "Item" }, + key = { it.hashCode().toString() } ) { - LazyColumn(contentPadding = PaddingValues(vertical = 3.dp), state = state) { - items( - items = searchResult.value, - contentType = { "Item" }, - key = { it.hashCode().toString() } - ) { - ListItem( - onClick = { - navigator.navigate(it) - expandedSuggestion = false - }, - text = { Text(it.name, maxLines = 1) }, - modifier = Modifier.fillMaxWidth() - ) - } - } + ListItem( + onClick = { + navigator.navigate(it) + expandedSuggestion = false + }, + text = { Text(it.name, maxLines = 1) }, + modifier = Modifier.fillMaxWidth() + ) } } - } + ) } }, footer = { From 4f2d085f391251924920d42e0ec5cc13f4858fd6 Mon Sep 17 00:00:00 2001 From: sanlorng Date: Mon, 23 Sep 2024 20:54:18 +0800 Subject: [PATCH 022/247] Fixed TextField compile error --- .../commonMain/kotlin/com/konyaco/fluent/component/TextField.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TextField.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TextField.kt index 81aa81c1..d96483a3 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TextField.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TextField.kt @@ -365,7 +365,7 @@ private fun Modifier.drawBottomLine( } @Stable -internal fun Modifier.textFieldModifier(shape: Shape = FluentTheme.shapes.control) = +internal fun Modifier.textFieldModifier(shape: Shape) = defaultMinSize(64.dp, 32.dp).clip(shape) @Stable From 1300f93dd7fc7b81fa09983d1cf326eac65de652 Mon Sep 17 00:00:00 2001 From: sanlorng Date: Mon, 23 Sep 2024 21:07:11 +0800 Subject: [PATCH 023/247] Update TextField shape const --- .../com/konyaco/fluent/component/AutoSuggestBox.kt | 12 +++++++----- .../com/konyaco/fluent/component/TextBoxButton.kt | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/AutoSuggestBox.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/AutoSuggestBox.kt index 9bd28c10..f2720032 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/AutoSuggestBox.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/AutoSuggestBox.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.unit.constrainHeight import androidx.compose.ui.unit.constrainWidth import androidx.compose.ui.unit.dp import com.konyaco.fluent.CompactMode +import com.konyaco.fluent.FluentTheme @Composable fun AutoSuggestionBox( @@ -94,12 +95,13 @@ fun AutoSuggestionBox( object AutoSuggestBoxDefaults { + @Composable @Stable fun textFieldShape(expanded: Boolean): Shape { return if (expanded) RoundedCornerShape( - topStart = 4.dp, - topEnd = 4.dp - ) else RoundedCornerShape(4.dp) + topStart = FluentTheme.cornerRadius.control, + topEnd = FluentTheme.cornerRadius.control, + ) else FluentTheme.shapes.control } @Composable @@ -149,8 +151,8 @@ object AutoSuggestBoxDefaults { shape = RoundedCornerShape( topStart = 0.dp, topEnd = 0.dp, - bottomStart = 8.dp, - bottomEnd = 8.dp + bottomStart = FluentTheme.cornerRadius.overlay, + bottomEnd = FluentTheme.cornerRadius.overlay ), positionProvider = rememberFlyoutPositionProvider( initialPlacement = FlyoutPlacement.Bottom, diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TextBoxButton.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TextBoxButton.kt index b2469ba4..45f8f9a9 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TextBoxButton.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TextBoxButton.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.selection.selectable -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -24,6 +23,7 @@ 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 com.konyaco.fluent.FluentTheme import com.konyaco.fluent.icons.Icons import com.konyaco.fluent.icons.regular.ArrowRight import com.konyaco.fluent.icons.regular.ChevronDown @@ -48,7 +48,7 @@ fun TextBoxButton( ) { val interaction = interactionSource ?: remember { MutableInteractionSource() } ButtonLayer( - shape = RoundedCornerShape(4.dp), + shape = FluentTheme.shapes.control, displayBorder = true, buttonColors = colors, interaction = interaction, From 486904216a279bd17ffde17896e9bd90da137c9d Mon Sep 17 00:00:00 2001 From: sanlorng Date: Mon, 24 Jun 2024 11:07:14 +0800 Subject: [PATCH 024/247] [fluent] add `TabView`. --- .../com/konyaco/fluent/component/TabView.kt | 784 ++++++++++++++++++ .../screen/navigation/TabViewScreen.kt | 96 +++ .../gallery/screen/settings/SettingsScreen.kt | 33 + 3 files changed, 913 insertions(+) create mode 100644 fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TabView.kt create mode 100644 gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/navigation/TabViewScreen.kt diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TabView.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TabView.kt new file mode 100644 index 00000000..02e0e680 --- /dev/null +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TabView.kt @@ -0,0 +1,784 @@ +package com.konyaco.fluent.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.animateScrollBy +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyListItemInfo +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.relocation.BringIntoViewResponder +import androidx.compose.foundation.relocation.bringIntoViewResponder +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.runtime.snapshots.Snapshot +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.boundsInParent +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex +import com.konyaco.fluent.ExperimentalFluentApi +import com.konyaco.fluent.FluentTheme +import com.konyaco.fluent.LocalContentAlpha +import com.konyaco.fluent.LocalContentColor +import com.konyaco.fluent.LocalTextStyle +import com.konyaco.fluent.icons.Icons +import com.konyaco.fluent.icons.filled.CaretLeft +import com.konyaco.fluent.icons.filled.CaretRight +import com.konyaco.fluent.icons.regular.Add +import com.konyaco.fluent.icons.regular.Dismiss +import com.konyaco.fluent.scheme.PentaVisualScheme +import com.konyaco.fluent.scheme.VisualStateScheme +import com.konyaco.fluent.scheme.collectVisualState +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch + +@Composable +fun TabRow( + selectedKey: () -> Any, + modifier: Modifier = Modifier, + state: LazyListState = rememberLazyListState(), + header: @Composable () -> Unit = {}, + footer: @Composable () -> Unit = {}, + borderColor: Color = TabViewDefaults.defaultBorderColor, + scrollActionButtonColors: VisualStateScheme = TabViewDefaults.scrollActionColors(), + content: LazyListScope.() -> Unit +) { + val rowRect = remember { mutableStateOf(Rect.Zero) } + val containerWidth = remember { mutableIntStateOf(0) } + Box( + contentAlignment = Alignment.BottomStart + ) { + val padding = 8.dp + Row( + verticalAlignment = Alignment.Bottom, + modifier = modifier + .zIndex(1f) + .clip(RectangleShape) + .onSizeChanged { containerWidth.value = it.width } + ) { + + val selectedItem = remember { + derivedStateOf { + state.layoutInfo.visibleItemsInfo.firstOrNull { it.key == selectedKey() } + } + } + Box(modifier = Modifier + .zIndex(2f) + .height(2.dp) + .drawTabRowBorder( + borderColor = borderColor, + containerWidth = { containerWidth.value }, + rowRect = { rowRect.value }, + selectedItem = { selectedItem.value } + ) + ) + Box(modifier = Modifier.widthIn(padding)) { + header() + } + val displayScrollController by remember { + derivedStateOf { state.canScrollForward || state.canScrollBackward } + } + val coroutineScope = rememberCoroutineScope() + AnimatedVisibility(displayScrollController) { + TabScrollActionButton( + onClick = { coroutineScope.launch { state.animateScrollBy(-state.layoutInfo.viewportSize.width / 3f) } }, + glyph = '\uEDD9', + vector = Icons.Filled.CaretLeft, + enabled = state.canScrollBackward, + colors = scrollActionButtonColors, + modifier = Modifier.padding(end = 4.dp) + ) + } + LazyRow( + state = state, + content = content, + contentPadding = PaddingValues(horizontal = 4.dp), + modifier = Modifier + .onGloballyPositioned { rowRect.value = it.boundsInParent() } + .weight(1f) + .height(TabViewHeight) + .zIndex(1f) + ) + AnimatedVisibility(displayScrollController) { + TabScrollActionButton( + onClick = { coroutineScope.launch { state.animateScrollBy(state.layoutInfo.viewportSize.width / 3f) } }, + glyph = '\uEDDA', + vector = Icons.Filled.CaretRight, + enabled = state.canScrollForward, + colors = scrollActionButtonColors, + modifier = Modifier.padding(start = 4.dp) + ) + } + Box(modifier = Modifier.widthIn(padding)) { + footer() + } + } + } +} + +@Composable +fun TabItem( + selected: Boolean, + onSelectedChanged: (Boolean) -> Unit, + text: @Composable () -> Unit, + modifier: Modifier = Modifier, + endDividerVisible: Boolean = !selected, + interactionSource: MutableInteractionSource? = null, + colors: VisualStateScheme = if (selected) { + TabViewDefaults.selectedItemColors() + } else { + TabViewDefaults.defaultItemColors() + }, + icon: @Composable () -> Unit = {}, + trailing: @Composable () -> Unit = {} +) { + TabItem( + selected = selected, + onSelectedChanged = onSelectedChanged, + modifier = modifier, + endDividerVisible = endDividerVisible, + interactionSource = interactionSource, + colors = colors, + content = { + Row( + horizontalArrangement = TabItemContentArrangement, + verticalAlignment = Alignment.CenterVertically + ) { + icon() + text() + trailing() + } + } + ) +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun TabItem( + selected: Boolean, + onSelectedChanged: (Boolean) -> Unit, + modifier: Modifier = Modifier, + endDividerVisible: Boolean = !selected, + interactionSource: MutableInteractionSource? = null, + colors: VisualStateScheme = if (selected) { + TabViewDefaults.selectedItemColors() + } else { + TabViewDefaults.defaultItemColors() + }, + content: @Composable () -> Unit +) { + val targetInteractionSource = interactionSource ?: remember { MutableInteractionSource() } + + val direction = LocalLayoutDirection.current + val color = colors.schemeFor(targetInteractionSource.collectVisualState(false)) + val density = LocalDensity.current + val selectedValue = rememberUpdatedState(selected) + Box( + contentAlignment = Alignment.CenterStart, + propagateMinConstraints = true, + modifier = modifier + .then( + if (selected) { + Modifier.drawTabViewItemBorder( + color.borderColor, + TabViewSelectedShape(false), + direction + ) + } else { + Modifier + } + ) + .bringIntoViewResponder( + remember(density, selectedValue) { + TabItemBringIntoViewResponder(density) { selectedValue.value } + } + ) + .clickable( + indication = null, + interactionSource = targetInteractionSource + ) { onSelectedChanged(!selected) } + .background( + color = color.fillColor, + shape = if (selected) { + TabViewSelectedShape(isInner = true) + } else { + RoundedCornerShape(topStart = TopRadius, topEnd = TopRadius) + } + ) + .heightIn(TabViewHeight) + .zIndex(if (selected) 1f else 0f) + ) { + Box( + contentAlignment = Alignment.CenterStart, + propagateMinConstraints = true, + modifier = Modifier + .heightIn(TabViewHeight) + .padding(start = 8.dp, end = 4.dp) + ) { + CompositionLocalProvider( + LocalTextStyle provides if (selected) { + FluentTheme.typography.bodyStrong + } else { + FluentTheme.typography.body + }, + LocalContentColor provides color.contentColor, + LocalContentAlpha provides color.contentColor.alpha + ) { + content() + } + } + if (endDividerVisible) { + Box( + modifier = Modifier + .wrapContentSize(Alignment.CenterEnd) + .size(1.dp, 16.dp) + .background(color.endDividerColor) + ) + } + } + +} + +@Stable +data class TabViewItemColor( + val borderColor: Color, + val fillColor: Color, + val contentColor: Color, + val endDividerColor: Color = Color.Transparent +) + +object TabViewDefaults { + + val defaultBorderColor: Color + @Composable + get() = FluentTheme.colors.stroke.card.default + + @Stable + @Composable + fun defaultItemColors( + default: TabViewItemColor = TabViewItemColor( + borderColor = defaultBorderColor, + fillColor = Color.Transparent, + contentColor = FluentTheme.colors.text.text.secondary, + endDividerColor = FluentTheme.colors.stroke.divider.default + ), + hovered: TabViewItemColor = default.copy( + endDividerColor = Color.Transparent, + fillColor = FluentTheme.colors.background.layerOnMicaBaseAlt.secondary + ), + pressed: TabViewItemColor = default.copy( + endDividerColor = Color.Transparent, + fillColor = FluentTheme.colors.background.layerOnMicaBaseAlt.default, + contentColor = FluentTheme.colors.text.text.tertiary + ), + disabled: TabViewItemColor = default + ): VisualStateScheme = PentaVisualScheme( + default = default, + hovered = hovered, + pressed = pressed, + disabled = disabled + ) + + @Stable + @Composable + fun selectedItemColors( + default: TabViewItemColor = TabViewItemColor( + borderColor = defaultBorderColor, + fillColor = FluentTheme.colors.background.solid.tertiary, + contentColor = FluentTheme.colors.text.text.primary, + endDividerColor = Color.Transparent + ), + hovered: TabViewItemColor = default, + pressed: TabViewItemColor = default, + disabled: TabViewItemColor = default + ): VisualStateScheme = PentaVisualScheme( + default = default, + hovered = hovered, + pressed = pressed, + disabled = disabled + ) + + @Stable + @Composable + fun defaultItemTitleBarColors( + default: TabViewItemColor = TabViewItemColor( + borderColor = defaultBorderColor, + fillColor = FluentTheme.colors.background.layerOnMicaBaseAlt.transparent, + contentColor = FluentTheme.colors.text.text.secondary, + endDividerColor = FluentTheme.colors.stroke.divider.default + ), + hovered: TabViewItemColor = default.copy( + endDividerColor = Color.Transparent, + fillColor = FluentTheme.colors.background.layerOnMicaBaseAlt.secondary + ), + pressed: TabViewItemColor = default.copy( + endDividerColor = Color.Transparent, + fillColor = FluentTheme.colors.background.layerOnMicaBaseAlt.default, + contentColor = FluentTheme.colors.text.text.tertiary + ), + disabled: TabViewItemColor = default + ): VisualStateScheme = PentaVisualScheme( + default = default, + hovered = hovered, + pressed = pressed, + disabled = disabled + ) + + @Stable + @Composable + fun selectedItemTitleBarColors( + default: TabViewItemColor = TabViewItemColor( + borderColor = FluentTheme.colors.stroke.card.default, + fillColor = FluentTheme.colors.background.layerOnMicaBaseAlt.default, + contentColor = FluentTheme.colors.text.text.primary, + endDividerColor = Color.Transparent + ), + hovered: TabViewItemColor = default, + pressed: TabViewItemColor = default, + disabled: TabViewItemColor = default + ): VisualStateScheme = PentaVisualScheme( + default = default, + hovered = hovered, + pressed = pressed, + disabled = disabled + ) + + @Stable + @Composable + fun scrollActionColors() = ButtonDefaults.subtleButtonColors( + default = ButtonColor( + fillColor = FluentTheme.colors.subtleFill.transparent, + contentColor = FluentTheme.colors.text.text.secondary, + borderBrush = SolidColor(Color.Transparent) + ) + ) + + @Composable + fun TabCloseButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + padding: PaddingValues = PaddingValues(start = 4.dp), + enabled: Boolean = true, + colors: VisualStateScheme = ButtonDefaults.subtleButtonColors() + ) { + Button( + onClick = onClick, + content = { + FontIcon( + glyph = '\uE624', + vector = Icons.Default.Dismiss, + iconSize = 12.sp, + contentDescription = null + ) + }, + iconOnly = true, + buttonColors = colors, + disabled = !enabled, + modifier = modifier + .padding(padding) + .heightIn(TabViewHeight) + .wrapContentHeight(Alignment.CenterVertically) + .defaultMinSize(minWidth = TabButtonSize.width, minHeight = TabButtonSize.height), + ) + } + + @Composable + fun TabAddButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + padding: PaddingValues = PaddingValues(start = 4.dp), + enabled: Boolean = true, + colors: VisualStateScheme = ButtonDefaults.subtleButtonColors() + ) { + Button( + onClick = onClick, + content = { + FontIcon( + glyph = '\uE710', + vector = Icons.Default.Add, + contentDescription = null + ) + }, + iconOnly = true, + buttonColors = colors, + disabled = !enabled, + modifier = modifier + .padding(padding) + .heightIn(TabViewHeight) + .wrapContentHeight(Alignment.CenterVertically) + .defaultMinSize(minWidth = TabButtonSize.width, minHeight = TabButtonSize.height), + ) + } + +} + +@ExperimentalFluentApi +@Composable +fun rememberTabItemEndDividerController( + state: LazyListState, + selectedKey: () -> Any +): TabItemEndDividerController { + val controller = remember(state, selectedKey) { + TabItemEndDividerController(state, selectedKey) + } + controller.collectState() + return controller +} + +@ExperimentalFluentApi +class TabItemEndDividerController( + private val state: LazyListState, + private val selectedKey: () -> Any, +) { + private val hoveredItems = mutableStateMapOf() + private var hideDividerItems by mutableStateOf(emptySet()) + + @Composable + internal fun collectState() { + LaunchedEffect(state) { + combine( + snapshotFlow { hoveredItems.toMap() }, + snapshotFlow { selectedKey() }, + snapshotFlow { state.layoutInfo.visibleItemsInfo } + ) { hoveredItems, selectedKey, itemsInfo -> + buildSet { + itemsInfo.forEachIndexed { index, lazyListItemInfo -> + if (lazyListItemInfo.key == selectedKey || hoveredItems.any { it.key == lazyListItemInfo.key }) { + val previousKey = itemsInfo.getOrNull(index - 1)?.key + previousKey?.let(this::add) + } + } + } + + }.collectLatest { + hideDividerItems = it + } + } + } + + @Composable + fun attach(key: Any, interactionSource: InteractionSource): Boolean { + val isHovered = interactionSource.collectIsHoveredAsState() + LaunchedEffect(interactionSource) { + snapshotFlow { + isHovered.value + }.collectLatest { hovered -> + if (hovered) { + hoveredItems[key] = 0 + } else { + hoveredItems.remove(key) + } + } + } + DisposableEffect(interactionSource) { + onDispose { + hoveredItems.remove(key) + } + } + val state = remember { mutableStateOf(true) } + LaunchedEffect(hideDividerItems) { + state.value = !hideDividerItems.contains(key) + } + return state.value + } +} + +@Composable +private fun TabScrollActionButton( + onClick: () -> Unit, + glyph: Char, + vector: ImageVector, + modifier: Modifier = Modifier, + enabled: Boolean = false, + colors: VisualStateScheme +) { + RepeatButton( + onClick = onClick, + content = { + FontIcon( + glyph = glyph, + vector = vector, + contentDescription = null, + iconSize = 8.sp, + vectorSize = 14.dp, + modifier = Modifier + ) + }, + iconOnly = true, + buttonColors = colors, + disabled = !enabled, + modifier = modifier + .heightIn(TabViewHeight) + .wrapContentHeight(Alignment.CenterVertically) + .defaultMinSize(minWidth = TabButtonSize.width, minHeight = TabButtonSize.height), + ) +} + +@Immutable +@Stable +private class TabViewSelectedShape(val isInner: Boolean) : Shape { + + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density + ): Outline { + return with(density) { + Outline.Generic(Path().apply { + val strokePadding = StrokeSize.toPx() / 2f + val innerPadding = if (isInner) strokePadding else 0f + val radius = BottomRadius.toPx() - innerPadding + val topRadius = TopRadius.toPx() - innerPadding + val topPadding = StrokeSize.toPx() + innerPadding + val horizontalOffset = radius - StrokeSize.toPx() / 2f - innerPadding + + moveTo(-horizontalOffset, size.height - strokePadding + innerPadding / 2) + cubicTo( + x1 = radius - horizontalOffset, + y1 = size.height - strokePadding, + x2 = radius - horizontalOffset, + y2 = size.height - radius - strokePadding, + x3 = radius - horizontalOffset, + y3 = size.height - radius - strokePadding + ) + lineTo(radius - horizontalOffset, topRadius + topPadding) + cubicTo( + x1 = radius - horizontalOffset, + y1 = topPadding, + x2 = radius - horizontalOffset + topRadius, + y2 = topPadding, + x3 = radius - horizontalOffset + topRadius, + y3 = topPadding + ) + lineTo(size.width - radius - topRadius + horizontalOffset, topPadding) + cubicTo( + x1 = size.width - radius + horizontalOffset, + y1 = topPadding, + x2 = size.width - radius + horizontalOffset, + y2 = topPadding + topRadius, + x3 = size.width - radius + horizontalOffset, + y3 = topRadius + topPadding + ) + lineTo(size.width - radius + horizontalOffset, size.height - radius - strokePadding) + cubicTo( + x1 = size.width - radius + horizontalOffset, + y1 = size.height - strokePadding + innerPadding / 2, + x2 = size.width + horizontalOffset, + y2 = size.height - strokePadding + innerPadding / 2, + x3 = size.width + horizontalOffset, + y3 = size.height - strokePadding + innerPadding / 2 + ) + lineTo(size.width + horizontalOffset, size.height + strokePadding * 2) + lineTo(0f - horizontalOffset, size.height + strokePadding * 2) + lineTo(0f - horizontalOffset, size.height) + close() + }) + } + } +} + + +@Stable +private fun Modifier.drawTabRowBorder( + borderColor: Color, + containerWidth: () -> Int, + rowRect: () -> Rect, + selectedItem: () -> LazyListItemInfo?, +) = drawWithCache { + val path = Path() + val strokeSizePx = StrokeSize.toPx() + onDrawWithContent { + path.rewind() + path.apply { + moveTo(strokeSizePx / 2, size.height - strokeSizePx) + val currentItem = selectedItem() + val itemPadding = BottomRadius.toPx() + if (currentItem != null) { + val rowRectValue = rowRect() + val currentItemOffset = (rowRectValue.left + currentItem.offset) + lineTo( + (currentItemOffset).coerceIn( + rowRectValue.left, + rowRectValue.right + ), + size.height - strokeSizePx / 2 + ) + moveTo( + (currentItemOffset + currentItem.size + 2 * itemPadding).coerceIn( + rowRectValue.left, + rowRectValue.right + ), + size.height - strokeSizePx / 2 + ) + lineTo( + containerWidth() - strokeSizePx / 2, + size.height - strokeSizePx / 2 + ) + + } else { + lineTo( + containerWidth() - strokeSizePx / 2, + size.height - strokeSizePx / 2 + ) + } + } + drawPath( + path = path, + color = borderColor, + style = Stroke(strokeSizePx) + ) + drawContent() + } +} + +@Stable +private fun Modifier.drawTabViewItemBorder( + color: Color, + shape: Shape, + direction: LayoutDirection, +) = drawWithCache { + onDrawWithContent { + drawContent() + val outline = shape.createOutline(size, direction, this) + val strokeSize = 1.dp.toPx() + when (outline) { + is Outline.Rectangle -> { + drawRect( + color = color, + style = Stroke(strokeSize), + size = outline.rect.size, + topLeft = outline.rect.topLeft + ) + } + + is Outline.Rounded -> { + drawRoundRect( + color = color, + topLeft = Offset(outline.roundRect.top, outline.roundRect.left), + size = Size( + width = outline.roundRect.width, + height = outline.roundRect.height + ), + cornerRadius = outline.roundRect.topLeftCornerRadius, + style = Stroke(strokeSize) + ) + } + + is Outline.Generic -> { + drawPath(outline.path, color, style = Stroke(strokeSize)) + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Stable +private class TabItemBringIntoViewResponder( + density: Density, + val selected: () -> Boolean, +) : BringIntoViewResponder { + val paddingSize = with(density) { BottomRadius.toPx() } + + override suspend fun bringChildIntoView(localRect: () -> Rect?) {} + + override fun calculateRectForParent(localRect: Rect): Rect { + return Snapshot.withoutReadObservation { + if (selected()) { + localRect.copy( + left = localRect.left - paddingSize, + right = localRect.right + paddingSize + ) + } else { + localRect + } + } + } +} + +private val StrokeSize = 1.dp +private val TopRadius = 8.dp +private val BottomRadius = 4.dp +private val TabButtonSize = DpSize(32.dp, 24.dp) + +//TODO combine TextBoxContentArrangement +@Stable +private object TabItemContentArrangement : Arrangement.Horizontal { + + override val spacing: Dp = 8.dp + + override fun Density.arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) { + with(Arrangement.Start) { + arrange(totalSize, sizes, layoutDirection, outPositions) + } + if (sizes.size < 2) return + if (layoutDirection == LayoutDirection.Rtl) { + outPositions[outPositions.lastIndex] = 0 + } else { + outPositions[outPositions.lastIndex] = totalSize - sizes.last() + } + } +} + +private val TabViewHeight = 32.dp \ No newline at end of file diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/navigation/TabViewScreen.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/navigation/TabViewScreen.kt new file mode 100644 index 00000000..98ece603 --- /dev/null +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/navigation/TabViewScreen.kt @@ -0,0 +1,96 @@ +package com.konyaco.fluent.gallery.screen.navigation + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.konyaco.fluent.ExperimentalFluentApi +import com.konyaco.fluent.component.TabRow +import com.konyaco.fluent.component.TabItem +import com.konyaco.fluent.component.TabViewDefaults +import com.konyaco.fluent.component.Text +import com.konyaco.fluent.component.rememberTabItemEndDividerController +import com.konyaco.fluent.gallery.annotation.Component +import com.konyaco.fluent.gallery.annotation.Sample +import com.konyaco.fluent.gallery.component.ComponentPagePath +import com.konyaco.fluent.gallery.component.GalleryPage +import com.konyaco.fluent.source.generated.FluentSourceFile + +@Component(index = 1, description = "A control that displays a collection of tabs that can be used to display several documents.") +@Composable +fun TabViewScreen() { + GalleryPage( + title = "TabView", + description = "TabView provides the user with a collection of tabs that can be used to display several documents.", + componentPath = FluentSourceFile.TabView, + galleryPath = ComponentPagePath.TabViewScreen + ) { + Section( + title = "Basic TabView", + sourceCode = sourceCodeOfTabViewSample, + content = { TabViewSample() } + ) + } +} + +@Sample +@OptIn(ExperimentalFluentApi::class, ExperimentalFoundationApi::class) +@Composable +private fun TabViewSample() { + val selectedKey = remember { mutableIntStateOf(0) } + val items = remember { mutableStateListOf(*Array(10) { it }) } + val state = rememberLazyListState() + val endDividerController = rememberTabItemEndDividerController( + selectedKey = { selectedKey.value }, + state = state + ) + TabRow( + state = state, + selectedKey = { selectedKey.value }, + modifier = Modifier.fillMaxWidth() + ) { + + itemsIndexed(items, key = { _, item -> item }) { index, item -> + val interactionSource = remember { MutableInteractionSource() } + TabItem( + selected = item == selectedKey.value, + onSelectedChanged = { selectedKey.value = item }, + endDividerVisible = endDividerController.attach(item, interactionSource), + interactionSource = interactionSource, + text = {Text("Item ${item + 1}")}, + trailing = { + val isHovered = interactionSource.collectIsHoveredAsState() + if (isHovered.value) { + TabViewDefaults.TabCloseButton( + onClick = { + val isSelected = selectedKey.value == item + items.remove(item) + if (isSelected) { + selectedKey.value = items.getOrNull(index) ?: items.getOrNull(index - 1) ?: 0 + } + } + ) + } + }, + modifier = Modifier.widthIn(160.dp).animateItemPlacement(), + ) + } + + item { + TabViewDefaults.TabAddButton( + onClick = { + items.add(items.lastOrNull()?.plus(1) ?: 0) + } + ) + } + } +} \ No newline at end of file diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/settings/SettingsScreen.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/settings/SettingsScreen.kt index 977201a4..d76ed6ab 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/settings/SettingsScreen.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/settings/SettingsScreen.kt @@ -11,6 +11,8 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CutCornerShape @@ -18,6 +20,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -52,6 +55,9 @@ import com.konyaco.fluent.component.ScrollbarContainer import com.konyaco.fluent.component.Slider import com.konyaco.fluent.component.SubtleButton import com.konyaco.fluent.component.Switcher +import com.konyaco.fluent.component.TabItem +import com.konyaco.fluent.component.TabRow +import com.konyaco.fluent.component.TabViewDefaults import com.konyaco.fluent.component.Text import com.konyaco.fluent.component.TextField import com.konyaco.fluent.component.rememberScrollbarAdapter @@ -390,6 +396,33 @@ private fun Content() { ) } } + + val selectedKey = remember { mutableStateOf(0) } + val tabItems = remember { mutableStateListOf(0,1,2,3,4) } + TabRow( + selectedKey = { selectedKey.value }, + borderColor = FluentTheme.colors.stroke.card.default, + ) { + items(tabItems, key = { it }) { index -> + TabItem( + selected = index == selectedKey.value, + onSelectedChanged = { selectedKey.value = index }, + content = { Text(index.toString()) }, + colors = if (index == selectedKey.value) { + TabViewDefaults.selectedItemTitleBarColors() + } else { + TabViewDefaults.defaultItemTitleBarColors() + }, + endDividerVisible = index != selectedKey.value - 1, + modifier = Modifier.widthIn(60.dp) + ) + } + item { + TabViewDefaults.TabAddButton( + onClick = { tabItems.add(tabItems.size) } + ) + } + } } @Composable From 9b682b451f7131e9a40892ada4b559dfb6b8c4b4 Mon Sep 17 00:00:00 2001 From: sanlorng Date: Mon, 23 Sep 2024 21:15:34 +0800 Subject: [PATCH 025/247] Remove TabView's BottomRadius and TopRadius const --- .../com/konyaco/fluent/component/TabView.kt | 67 ++++++++++--------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TabView.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TabView.kt index 02e0e680..1d56ec15 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TabView.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TabView.kt @@ -123,6 +123,7 @@ fun TabRow( .zIndex(2f) .height(2.dp) .drawTabRowBorder( + bottomRadius = FluentTheme.cornerRadius.control, borderColor = borderColor, containerWidth = { containerWidth.value }, rowRect = { rowRect.value }, @@ -230,6 +231,8 @@ fun TabItem( val color = colors.schemeFor(targetInteractionSource.collectVisualState(false)) val density = LocalDensity.current val selectedValue = rememberUpdatedState(selected) + val bottomRadius = FluentTheme.cornerRadius.control + val topRadius = FluentTheme.cornerRadius.overlay Box( contentAlignment = Alignment.CenterStart, propagateMinConstraints = true, @@ -237,17 +240,21 @@ fun TabItem( .then( if (selected) { Modifier.drawTabViewItemBorder( - color.borderColor, - TabViewSelectedShape(false), - direction + color = color.borderColor, + shape = TabViewSelectedShape( + isInner = false, + topRadius = topRadius, + bottomRadius = bottomRadius + ), + direction = direction ) } else { Modifier } ) .bringIntoViewResponder( - remember(density, selectedValue) { - TabItemBringIntoViewResponder(density) { selectedValue.value } + remember(density, selectedValue, bottomRadius) { + TabItemBringIntoViewResponder(density, bottomRadius) { selectedValue.value } } ) .clickable( @@ -257,9 +264,9 @@ fun TabItem( .background( color = color.fillColor, shape = if (selected) { - TabViewSelectedShape(isInner = true) + TabViewSelectedShape(isInner = true, topRadius, bottomRadius) } else { - RoundedCornerShape(topStart = TopRadius, topEnd = TopRadius) + RoundedCornerShape(topStart = topRadius, topEnd = topRadius) } ) .heightIn(TabViewHeight) @@ -573,7 +580,7 @@ private fun TabScrollActionButton( @Immutable @Stable -private class TabViewSelectedShape(val isInner: Boolean) : Shape { +private class TabViewSelectedShape(val isInner: Boolean, val topRadius: Dp, val bottomRadius: Dp) : Shape { override fun createOutline( size: Size, @@ -584,41 +591,41 @@ private class TabViewSelectedShape(val isInner: Boolean) : Shape { Outline.Generic(Path().apply { val strokePadding = StrokeSize.toPx() / 2f val innerPadding = if (isInner) strokePadding else 0f - val radius = BottomRadius.toPx() - innerPadding - val topRadius = TopRadius.toPx() - innerPadding + val bottomRadius = bottomRadius.toPx() - innerPadding + val topRadius = topRadius.toPx() - innerPadding val topPadding = StrokeSize.toPx() + innerPadding - val horizontalOffset = radius - StrokeSize.toPx() / 2f - innerPadding + val horizontalOffset = bottomRadius - StrokeSize.toPx() / 2f - innerPadding moveTo(-horizontalOffset, size.height - strokePadding + innerPadding / 2) cubicTo( - x1 = radius - horizontalOffset, + x1 = bottomRadius - horizontalOffset, y1 = size.height - strokePadding, - x2 = radius - horizontalOffset, - y2 = size.height - radius - strokePadding, - x3 = radius - horizontalOffset, - y3 = size.height - radius - strokePadding + x2 = bottomRadius - horizontalOffset, + y2 = size.height - bottomRadius - strokePadding, + x3 = bottomRadius - horizontalOffset, + y3 = size.height - bottomRadius - strokePadding ) - lineTo(radius - horizontalOffset, topRadius + topPadding) + lineTo(bottomRadius - horizontalOffset, topRadius + topPadding) cubicTo( - x1 = radius - horizontalOffset, + x1 = bottomRadius - horizontalOffset, y1 = topPadding, - x2 = radius - horizontalOffset + topRadius, + x2 = bottomRadius - horizontalOffset + topRadius, y2 = topPadding, - x3 = radius - horizontalOffset + topRadius, + x3 = bottomRadius - horizontalOffset + topRadius, y3 = topPadding ) - lineTo(size.width - radius - topRadius + horizontalOffset, topPadding) + lineTo(size.width - bottomRadius - topRadius + horizontalOffset, topPadding) cubicTo( - x1 = size.width - radius + horizontalOffset, + x1 = size.width - bottomRadius + horizontalOffset, y1 = topPadding, - x2 = size.width - radius + horizontalOffset, + x2 = size.width - bottomRadius + horizontalOffset, y2 = topPadding + topRadius, - x3 = size.width - radius + horizontalOffset, + x3 = size.width - bottomRadius + horizontalOffset, y3 = topRadius + topPadding ) - lineTo(size.width - radius + horizontalOffset, size.height - radius - strokePadding) + lineTo(size.width - bottomRadius + horizontalOffset, size.height - bottomRadius - strokePadding) cubicTo( - x1 = size.width - radius + horizontalOffset, + x1 = size.width - bottomRadius + horizontalOffset, y1 = size.height - strokePadding + innerPadding / 2, x2 = size.width + horizontalOffset, y2 = size.height - strokePadding + innerPadding / 2, @@ -637,6 +644,7 @@ private class TabViewSelectedShape(val isInner: Boolean) : Shape { @Stable private fun Modifier.drawTabRowBorder( + bottomRadius: Dp, borderColor: Color, containerWidth: () -> Int, rowRect: () -> Rect, @@ -649,7 +657,7 @@ private fun Modifier.drawTabRowBorder( path.apply { moveTo(strokeSizePx / 2, size.height - strokeSizePx) val currentItem = selectedItem() - val itemPadding = BottomRadius.toPx() + val itemPadding = bottomRadius.toPx() if (currentItem != null) { val rowRectValue = rowRect() val currentItemOffset = (rowRectValue.left + currentItem.offset) @@ -732,9 +740,10 @@ private fun Modifier.drawTabViewItemBorder( @Stable private class TabItemBringIntoViewResponder( density: Density, + bottomRadius: Dp, val selected: () -> Boolean, ) : BringIntoViewResponder { - val paddingSize = with(density) { BottomRadius.toPx() } + val paddingSize = with(density) { bottomRadius.toPx() } override suspend fun bringChildIntoView(localRect: () -> Rect?) {} @@ -753,8 +762,6 @@ private class TabItemBringIntoViewResponder( } private val StrokeSize = 1.dp -private val TopRadius = 8.dp -private val BottomRadius = 4.dp private val TabButtonSize = DpSize(32.dp, 24.dp) //TODO combine TextBoxContentArrangement From 948c48825aea5f5cc1a57fa524803885d5e19351 Mon Sep 17 00:00:00 2001 From: sanlorng Date: Mon, 23 Sep 2024 21:27:43 +0800 Subject: [PATCH 026/247] [gallery] add 'TabViewWindow' in desktop --- .../navigation/TabViewScreen.android.kt | 8 + .../screen/navigation/TabViewScreen.kt | 7 +- .../navigation/TabViewScreen.desktop.kt | 390 ++++++++++++++++++ 3 files changed, 404 insertions(+), 1 deletion(-) create mode 100644 gallery/src/androidMain/kotlin/com/konyaco/fluent/gallery/screen/navigation/TabViewScreen.android.kt create mode 100644 gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/screen/navigation/TabViewScreen.desktop.kt diff --git a/gallery/src/androidMain/kotlin/com/konyaco/fluent/gallery/screen/navigation/TabViewScreen.android.kt b/gallery/src/androidMain/kotlin/com/konyaco/fluent/gallery/screen/navigation/TabViewScreen.android.kt new file mode 100644 index 00000000..bcdd65f8 --- /dev/null +++ b/gallery/src/androidMain/kotlin/com/konyaco/fluent/gallery/screen/navigation/TabViewScreen.android.kt @@ -0,0 +1,8 @@ +package com.konyaco.fluent.gallery.screen.navigation + +import androidx.compose.runtime.Composable +import com.konyaco.fluent.gallery.component.GalleryPageScope + +@Composable +internal actual fun GalleryPageScope.PlatformTabViewSection() { +} \ No newline at end of file diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/navigation/TabViewScreen.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/navigation/TabViewScreen.kt index 98ece603..a940128a 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/navigation/TabViewScreen.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/navigation/TabViewScreen.kt @@ -23,6 +23,7 @@ import com.konyaco.fluent.gallery.annotation.Component import com.konyaco.fluent.gallery.annotation.Sample import com.konyaco.fluent.gallery.component.ComponentPagePath import com.konyaco.fluent.gallery.component.GalleryPage +import com.konyaco.fluent.gallery.component.GalleryPageScope import com.konyaco.fluent.source.generated.FluentSourceFile @Component(index = 1, description = "A control that displays a collection of tabs that can be used to display several documents.") @@ -39,6 +40,7 @@ fun TabViewScreen() { sourceCode = sourceCodeOfTabViewSample, content = { TabViewSample() } ) + PlatformTabViewSection() } } @@ -93,4 +95,7 @@ private fun TabViewSample() { ) } } -} \ No newline at end of file +} + +@Composable +internal expect fun GalleryPageScope.PlatformTabViewSection() \ No newline at end of file diff --git a/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/screen/navigation/TabViewScreen.desktop.kt b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/screen/navigation/TabViewScreen.desktop.kt new file mode 100644 index 00000000..ca5fb147 --- /dev/null +++ b/gallery/src/desktopMain/kotlin/com/konyaco/fluent/gallery/screen/navigation/TabViewScreen.desktop.kt @@ -0,0 +1,390 @@ +package com.konyaco.fluent.gallery.screen.navigation + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.MutableWindowInsets +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.relocation.BringIntoViewRequester +import androidx.compose.foundation.relocation.bringIntoViewRequester +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowPlacement +import androidx.compose.ui.window.rememberWindowState +import com.konyaco.fluent.ExperimentalFluentApi +import com.konyaco.fluent.FluentTheme +import com.konyaco.fluent.background.BackgroundSizing +import com.konyaco.fluent.background.Layer +import com.konyaco.fluent.component.Button +import com.konyaco.fluent.component.Icon +import com.konyaco.fluent.component.SubtleButton +import com.konyaco.fluent.component.TabItem +import com.konyaco.fluent.component.TabRow +import com.konyaco.fluent.component.TabViewDefaults +import com.konyaco.fluent.component.Text +import com.konyaco.fluent.component.TextField +import com.konyaco.fluent.component.TextFieldColor +import com.konyaco.fluent.component.TextFieldColorScheme +import com.konyaco.fluent.component.rememberTabItemEndDividerController +import com.konyaco.fluent.gallery.LocalStore +import com.konyaco.fluent.gallery.component.GalleryPageScope +import com.konyaco.fluent.gallery.jna.windows.ComposeWindowProcedure +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.HTCAPTION +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.HTCLIENT +import com.konyaco.fluent.gallery.jna.windows.structure.WinUserConst.HTMAXBUTTON +import com.konyaco.fluent.gallery.window.CaptionButtonRow +import com.konyaco.fluent.gallery.window.contains +import com.konyaco.fluent.gallery.window.rememberLayoutHitTestOwner +import com.konyaco.fluent.icons.Icons +import com.konyaco.fluent.icons.regular.ArrowClockwise +import com.konyaco.fluent.icons.regular.ArrowDownload +import com.konyaco.fluent.icons.regular.ArrowLeft +import com.konyaco.fluent.icons.regular.ArrowRight +import com.konyaco.fluent.icons.regular.History +import com.konyaco.fluent.icons.regular.Home +import com.konyaco.fluent.icons.regular.MoreHorizontal +import com.konyaco.fluent.icons.regular.StarLineHorizontal3 +import com.mayakapps.compose.windowstyler.WindowBackdrop +import com.mayakapps.compose.windowstyler.WindowStyle +import com.mayakapps.compose.windowstyler.findSkiaLayer +import fluentdesign.gallery.generated.resources.Res +import fluentdesign.gallery.generated.resources.icon +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.painterResource + +@OptIn(ExperimentalLayoutApi::class, ExperimentalFluentApi::class, ExperimentalFoundationApi::class) +@Composable +internal actual fun GalleryPageScope.PlatformTabViewSection() { + Section( + title = "TabView Window", + sourceCode = "", + content = { + val windowVisible = remember { mutableStateOf(false) } + Button( + onClick = { + windowVisible.value = !windowVisible.value + }, + content = { + Text("Open Window") + } + ) + val windowState = rememberWindowState(width = 1600.dp, height = 1000.dp) + val store = LocalStore.current + Window( + visible = windowVisible.value, + onCloseRequest = { + windowVisible.value = false + }, + state = windowState, + title = "TabView Window", + icon = painterResource(Res.drawable.icon) + ) { + + WindowStyle( + isDarkTheme = FluentTheme.colors.darkMode, + backdropType = WindowBackdrop.Tabbed, + ) + FluentTheme( + useAcrylicPopup = store.enabledAcrylicPopup, + ) { + + LaunchedEffect(window) { + window.findSkiaLayer()?.transparency = true + } + + val paddingInsets = remember { MutableWindowInsets() } + val layoutHitTestOwner = rememberLayoutHitTestOwner() + val maxButtonRect = remember { mutableStateOf(Rect.Zero) } + val captionBarHeight = 40.dp + val captionBarHeightPx = with(LocalDensity.current) { captionBarHeight.toPx() } + val windowProcedure = remember { + ComposeWindowProcedure( + window = window, + onWindowInsetUpdate = { paddingInsets.insets = it }, + hitTest = { x, y -> + when { + maxButtonRect.value.contains(x, y) -> HTMAXBUTTON + y <= captionBarHeightPx && !layoutHitTestOwner.hitTest( + x, + y + ) -> HTCAPTION + + else -> HTCLIENT + } + } + ) + } + Column(modifier = Modifier.windowInsetsPadding(paddingInsets)) { + val selectedIndex = remember { mutableStateOf(1) } + val tabItems = remember { mutableStateListOf(*Array(100) { it + 1 }) } + val state = rememberLazyListState() + val endDividerController = rememberTabItemEndDividerController( + selectedKey = { selectedIndex.value }, + state = state + ) + val coroutineScope = rememberCoroutineScope() + TabRow( + selectedKey = { selectedIndex.value }, + state = state, + footer = { + Row( + modifier = Modifier.heightIn(captionBarHeight) + .wrapContentHeight(Alignment.Top) + .padding(start = 16.dp) + ) { + window.CaptionButtonRow( + windowHandle = windowProcedure.windowHandle, + isMaximize = windowState.placement == WindowPlacement.Maximized, + onCloseRequest = { windowVisible.value = false }, + onMaximizeButtonRectUpdate = { maxButtonRect.value = it } + ) + } + }, + borderColor = FluentTheme.colors.stroke.card.default, + modifier = Modifier.weight(1f) + ) { + items(tabItems, key = { it }) { index -> + val interactionSource = remember { MutableInteractionSource() } + TabItem( + selected = selectedIndex.value == index, + onSelectedChanged = { selectedIndex.value = index }, + text = { + Text("Item ${index + 1}") + }, + trailing = { + val isHovered = interactionSource.collectIsHoveredAsState() + if (isHovered.value) { + TabViewDefaults.TabCloseButton( + onClick = { + val isSelected = selectedIndex.value == index + tabItems.remove(index) + if (isSelected) { + selectedIndex.value = + tabItems.firstOrNull() ?: 0 + } + } + ) + } + }, + colors = if (selectedIndex.value == index) { + TabViewDefaults.selectedItemTitleBarColors() + } else { + TabViewDefaults.defaultItemTitleBarColors() + }, + endDividerVisible = endDividerController.attach( + index, + interactionSource + ), + interactionSource = interactionSource, + modifier = Modifier.animateItemPlacement().widthIn(160.dp) + ) + } + item { + val bringIntoViewRequester = remember { BringIntoViewRequester() } + TabViewDefaults.TabAddButton( + onClick = { + tabItems.add(tabItems.lastOrNull()?.plus(1) ?: 0) + coroutineScope.launch { + delay(500) + bringIntoViewRequester.bringIntoView() + } + }, + modifier = Modifier.bringIntoViewRequester( + bringIntoViewRequester + ) + ) + } + + } + Layer( + border = null, + backgroundSizing = BackgroundSizing.OuterBorderEdge, + modifier = Modifier.weight(1f).fillMaxWidth(), + color = FluentTheme.colors.background.layerOnMicaBaseAlt.default, + shape = RectangleShape + ) { + Column( + modifier = Modifier.fillMaxWidth().fillMaxHeight() + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .heightIn(48.dp) + .padding(horizontal = 8.dp) + ) { + val iconSize = 18.dp + SubtleButton( + onClick = {}, + iconOnly = true + ) { + Icon( + Icons.Default.ArrowLeft, + contentDescription = null, + modifier = Modifier.size(iconSize) + ) + } + SubtleButton( + onClick = {}, + iconOnly = true + ) { + Icon( + Icons.Default.ArrowRight, + contentDescription = null, + modifier = Modifier.size(iconSize) + ) + } + + SubtleButton( + onClick = {}, + iconOnly = true + ) { + Icon( + Icons.Default.ArrowClockwise, + contentDescription = null, + modifier = Modifier.size(iconSize) + ) + } + + SubtleButton( + onClick = {}, + iconOnly = true + ) { + Icon( + Icons.Default.Home, + contentDescription = null, + modifier = Modifier.size(iconSize) + ) + } + + val value = remember { mutableStateOf(TextFieldValue()) } + TextField( + value = value.value, + onValueChange = { value.value = it }, + colors = defaultTextFieldColors(), + placeholder = { Text("Enter url or search keyword") }, + modifier = Modifier.padding(horizontal = 4.dp).weight(1f) + .fillMaxWidth().height(32.dp) + ) + SubtleButton( + onClick = {}, + iconOnly = true + ) { + Icon( + Icons.Default.History, + contentDescription = null, + modifier = Modifier.size(iconSize) + ) + } + + SubtleButton( + onClick = {}, + iconOnly = true + ) { + Icon( + Icons.Default.ArrowDownload, + contentDescription = null, + modifier = Modifier.size(iconSize) + ) + } + SubtleButton( + onClick = {}, + iconOnly = true + ) { + Icon( + Icons.Default.StarLineHorizontal3, + contentDescription = null, + modifier = Modifier.size(iconSize) + ) + } + SubtleButton( + onClick = {}, + iconOnly = true + ) { + Icon( + Icons.Default.MoreHorizontal, + contentDescription = null, + modifier = Modifier.size(iconSize) + ) + } + } + Box( + modifier = Modifier.fillMaxWidth().height(1.dp) + .background(FluentTheme.colors.stroke.divider.default) + ) + } + } + } + } + } + } + ) +} + +@Stable +@Composable +fun defaultTextFieldColors( + default: TextFieldColor = TextFieldColor( + fillColor = FluentTheme.colors.control.default, + contentColor = FluentTheme.colors.text.text.primary, + placeholderColor = FluentTheme.colors.text.text.secondary, + bottomLineFillColor = Color.Transparent, + borderBrush = SolidColor(Color.Transparent), + cursorBrush = SolidColor(FluentTheme.colors.text.text.primary) + ), + focused: TextFieldColor = default.copy( + fillColor = FluentTheme.colors.control.inputActive, + bottomLineFillColor = FluentTheme.colors.fillAccent.default, + borderBrush = SolidColor(FluentTheme.colors.stroke.control.default) + ), + hovered: TextFieldColor = default.copy( + fillColor = FluentTheme.colors.control.secondary + ), + pressed: TextFieldColor = default.copy( + fillColor = FluentTheme.colors.control.inputActive, + borderBrush = SolidColor(FluentTheme.colors.stroke.control.default) + ), + disabled: TextFieldColor = default.copy( + contentColor = FluentTheme.colors.text.text.disabled, + placeholderColor = FluentTheme.colors.text.text.disabled, + bottomLineFillColor = Color.Transparent, + ) +) = TextFieldColorScheme( + default = default, + focused = focused, + hovered = hovered, + pressed = pressed, + disabled = disabled +) \ No newline at end of file From 927c8a2de6bcb94115b918a38d26b61821d1d916 Mon Sep 17 00:00:00 2001 From: sanlorng Date: Mon, 23 Sep 2024 21:46:21 +0800 Subject: [PATCH 027/247] [fluent] Fixed `Dialog` can't gain focus. --- .../commonMain/kotlin/com/konyaco/fluent/component/Dialog.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dialog.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dialog.kt index 01817f0e..1d91af88 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dialog.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dialog.kt @@ -38,6 +38,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupPositionProvider +import androidx.compose.ui.window.PopupProperties import com.konyaco.fluent.FluentTheme import com.konyaco.fluent.LocalContentColor import com.konyaco.fluent.LocalTextStyle @@ -79,6 +80,7 @@ fun FluentDialog( } if (visibleState.currentState || visibleState.targetState) Popup( + properties = PopupProperties(focusable = true), popupPositionProvider = DialogPopupPositionProvider ) { val scrim by animateColorAsState( From fea1757fc7ec731521793ce49a28af98cac23eed Mon Sep 17 00:00:00 2001 From: sanlorng Date: Mon, 23 Sep 2024 22:47:22 +0800 Subject: [PATCH 028/247] [gallery] Fixed text block color incorrect when theme changed. --- .../com/konyaco/fluent/gallery/component/GallerySection.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/GallerySection.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/GallerySection.kt index 33e2b287..8fec9f0a 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/GallerySection.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/GallerySection.kt @@ -77,9 +77,12 @@ fun GallerySection( ) { Column { FluentThemeConfiguration(colors = colors) { - Box( + Layer( + shape = FluentTheme.shapes.intersectionEdge, + color = FluentTheme.colors.background.solid.base, + backgroundSizing = BackgroundSizing.OuterBorderEdge, + border = null, modifier = Modifier - .background(FluentTheme.colors.background.solid.base) .fillMaxWidth() .wrapContentHeight(), ) { From cdefe7369b17ec87d0f741b0c9e8bd4f40dff0eb Mon Sep 17 00:00:00 2001 From: sanlorng Date: Tue, 24 Sep 2024 10:20:03 +0800 Subject: [PATCH 029/247] [fluent] Fixed incorrect elevation level of `Flyout`. --- .../commonMain/kotlin/com/konyaco/fluent/component/Flyout.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Flyout.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Flyout.kt index 20f11769..7d217c94 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Flyout.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Flyout.kt @@ -203,7 +203,7 @@ internal fun FlyoutContent( exitTransition = fadeOut(flyoutExitSpec()), content = content, contentPadding = contentPadding, - elevation = 8.dp, + elevation = ElevationDefaults.flyout, shape = shape, modifier = modifier ) From 70e7d6fe168be76ac1107bd3abe4881fa09d7386 Mon Sep 17 00:00:00 2001 From: sanlorng Date: Sun, 27 Oct 2024 15:33:37 +0800 Subject: [PATCH 030/247] [fluent] Update Build Script --- .../fluent/plugin/build/BuildConfig.kt | 50 ++++++++++++++++++- .../fluent/plugin/build/BuildPlugin.kt | 4 +- gallery/build.gradle.kts | 40 +++++++++++++-- gallery/proguard-rules.desktop.pro | 3 ++ gradle/libs.versions.toml | 2 +- 5 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 gallery/proguard-rules.desktop.pro diff --git a/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildConfig.kt b/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildConfig.kt index 301ac7e0..86ff5d73 100644 --- a/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildConfig.kt +++ b/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildConfig.kt @@ -8,7 +8,55 @@ object BuildConfig { const val packageName = "$group.fluent" - const val libraryVersion = "0.0.1-dev.8" + private const val snapshotLibraryVersion = "0.1.0-SNAPSHOT" + + val isRelease = System.getenv("PROJECT_BUILD_TYPE") == "release" + + val gitTag = Runtime + .getRuntime() + .exec("git describe --abbrev=0 --tags") + .inputReader() + .readLine() + + val relativeCommitCount = Runtime + .getRuntime() + .exec("git describe --tags") + .inputReader() + .readLine() + .removePrefix(gitTag) + .let { + if (it.isNotEmpty()) { + it.split("-")[1].toInt() + } else { + 0 + } + } + + val libraryVersion = when { + isRelease -> gitTag + else -> snapshotLibraryVersion + } + + val integerVersionName = libraryVersion + .removePrefix("v") + .removeSuffix("-SNAPSHOT") + .substringBefore("-dev") + .let { + val parts = it.split(".") + var major = parts.getOrNull(0) ?: "0" + var minor = parts.getOrNull(1) ?: "0" + if (major.startsWith("0")) { + major = "1" + minor = "0" + } + when(parts.size) { + 1, 2 -> "${major}.$minor.$relativeCommitCount" + else -> { + val patchVersion = parts[2].toIntOrNull() ?: 0 + "${major}.${minor}.${patchVersion * 200 + relativeCommitCount}" + } + } + } object Android { const val compileSdkVersion = 34 diff --git a/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildPlugin.kt b/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildPlugin.kt index a71a0c25..29d4a8d7 100644 --- a/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildPlugin.kt +++ b/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildPlugin.kt @@ -67,8 +67,8 @@ class BuildPlugin : Plugin { } repositories { maven { - val releasesUrl ="https://s01.oss.sonatype.org/content/repositories/snapshots/" - val snapshotsUrl = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" + val snapshotsUrl ="https://s01.oss.sonatype.org/content/repositories/snapshots/" + val releasesUrl = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" name = "OSSRH" url = target.uri( if (target.version.toString().endsWith("SNAPSHOT")) releasesUrl diff --git a/gallery/build.gradle.kts b/gallery/build.gradle.kts index cbe1b7d8..fec034dc 100644 --- a/gallery/build.gradle.kts +++ b/gallery/build.gradle.kts @@ -1,3 +1,4 @@ +import com.android.build.api.variant.impl.VariantOutputImpl import com.konyaco.fluent.plugin.build.BuildConfig import com.konyaco.fluent.plugin.build.applyTargets import org.jetbrains.compose.desktop.application.dsl.TargetFormat @@ -60,7 +61,7 @@ android { minSdk = BuildConfig.Android.minSdkVersion targetSdk = BuildConfig.Android.compileSdkVersion versionCode = 1 - versionName = "1.0" + versionName = BuildConfig.libraryVersion vectorDrawables { useSupportLibrary = true } @@ -70,7 +71,36 @@ android { buildTypes { release { isMinifyEnabled = true - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.android.pro") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.android.pro" + ) + val signFile = System.getenv("ANDROID_SIGNING_FILE") + signFile?.let { + val password = System.getenv("ANDROID_SIGNING_PASSWORD") + val keyAlias = System.getenv("ANDROID_SIGNING_KEY_ALIAS") + val keyPassword = System.getenv("ANDROID_SIGNING_KEY_PASSWORD") + signingConfig = signingConfigs.getByName("release").apply { + this.storeFile = file(signFile) + this.storePassword = password + this.keyAlias = keyAlias + this.keyPassword = keyPassword + } + } + } + } + + androidComponents.onVariants { variant -> + variant.outputs.forEach { output -> + if (output is VariantOutputImpl) { + output.apply { + outputFileName.set( + "${variant.applicationId}-" + + "${variant.buildType}-" + + "${versionName.get()}.apk" + ) + } + } } } @@ -89,10 +119,13 @@ android { compose.desktop { application { mainClass = "${BuildConfig.packageName}.gallery.MainKt" + buildTypes.release.proguard { + configurationFiles.from(project.file("proguard-rules.desktop.pro")) + } nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) packageName = "Compose Fluent Design Gallery" - packageVersion = "1.0.0" + packageVersion = BuildConfig.integerVersionName macOS { iconFile.set(project.file("icons/icon.icns")) jvmArgs( @@ -101,6 +134,7 @@ compose.desktop { } windows { iconFile.set(project.file("icons/icon.ico")) + upgradeUuid = "a23572e1-c6fd-4b76-98ec-1e45953eb941" } linux { iconFile.set(project.file("icons/icon.png")) diff --git a/gallery/proguard-rules.desktop.pro b/gallery/proguard-rules.desktop.pro new file mode 100644 index 00000000..331d8d50 --- /dev/null +++ b/gallery/proguard-rules.desktop.pro @@ -0,0 +1,3 @@ +-dontwarn com.sun.jna.internal.Cleaner +-keep class com.sun.jna.** { *; } +-keep class * implements com.sun.jna.** { *; } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3f0e5932..bf2dff49 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ androidGradlePlugin = "8.3.2" androidBuildTools = "31.3.2" windowStyler = "0.3.3-SNAPSHOT" highlights = "0.8.0" -jna = "5.14.0" +jna = "5.13.0" [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } From d7ab9ebc02dded6a7fd24f9c13ca7415073f0a36 Mon Sep 17 00:00:00 2001 From: sanlorng Date: Sun, 27 Oct 2024 16:23:57 +0800 Subject: [PATCH 031/247] [fluent] Update Build Script --- gallery/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gallery/build.gradle.kts b/gallery/build.gradle.kts index fec034dc..455f2661 100644 --- a/gallery/build.gradle.kts +++ b/gallery/build.gradle.kts @@ -80,12 +80,12 @@ android { val password = System.getenv("ANDROID_SIGNING_PASSWORD") val keyAlias = System.getenv("ANDROID_SIGNING_KEY_ALIAS") val keyPassword = System.getenv("ANDROID_SIGNING_KEY_PASSWORD") - signingConfig = signingConfigs.getByName("release").apply { + signingConfig = signingConfigs.register("release") { this.storeFile = file(signFile) this.storePassword = password this.keyAlias = keyAlias this.keyPassword = keyPassword - } + }.get() } } } From 214034838c1c5e17ee23efa706d72aca0aa85c3f Mon Sep 17 00:00:00 2001 From: sanlorng Date: Sun, 27 Oct 2024 19:54:53 +0800 Subject: [PATCH 032/247] [fluent] Update Build Script --- gallery/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gallery/build.gradle.kts b/gallery/build.gradle.kts index 455f2661..1f39f262 100644 --- a/gallery/build.gradle.kts +++ b/gallery/build.gradle.kts @@ -95,7 +95,7 @@ android { if (output is VariantOutputImpl) { output.apply { outputFileName.set( - "${variant.applicationId}-" + + "${variant.applicationId.get()}-" + "${variant.buildType}-" + "${versionName.get()}.apk" ) From bc424c7bfb668aa2355b52dd2213e734d1645c2c Mon Sep 17 00:00:00 2001 From: sanlorng Date: Mon, 28 Oct 2024 08:43:11 +0800 Subject: [PATCH 033/247] [fluent] Update Build Script --- gallery/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gallery/build.gradle.kts b/gallery/build.gradle.kts index 1f39f262..3a09fc08 100644 --- a/gallery/build.gradle.kts +++ b/gallery/build.gradle.kts @@ -96,8 +96,8 @@ android { output.apply { outputFileName.set( "${variant.applicationId.get()}-" + - "${variant.buildType}-" + - "${versionName.get()}.apk" + "${versionName.get()}-" + + "${variant.buildType}.apk" ) } } From 256a0794c3bb700362bdfd6d9e3c6a62a59a7a5c Mon Sep 17 00:00:00 2001 From: sanlorng Date: Mon, 28 Oct 2024 14:48:12 +0800 Subject: [PATCH 034/247] [fluent] Try to fixed javadoc error, see[workaround](https://github.com/gradle/gradle/issues/26091#issuecomment-1722947958) --- .../konyaco/fluent/plugin/build/BuildPlugin.kt | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildPlugin.kt b/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildPlugin.kt index 29d4a8d7..362203fc 100644 --- a/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildPlugin.kt +++ b/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildPlugin.kt @@ -4,20 +4,28 @@ import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.publish.PublishingExtension import org.gradle.api.publish.maven.MavenPublication +import org.gradle.api.publish.maven.tasks.AbstractPublishToMaven import org.gradle.api.tasks.bundling.Jar import org.gradle.kotlin.dsl.create import org.gradle.kotlin.dsl.findByType import org.gradle.kotlin.dsl.withType +import org.gradle.plugins.signing.Sign import org.gradle.plugins.signing.SigningExtension class BuildPlugin : Plugin { override fun apply(target: Project) { - target.allprojects.forEach { - it.afterEvaluate { + target.allprojects.forEach { project -> + project.afterEvaluate { - it.extensions.findByType()?.apply { - setupMavenPublishing(it) - it.extensions.findByType()?.setupSigning(this) + project.extensions.findByType()?.apply { + setupMavenPublishing(project) + project.extensions.findByType()?.let { signing -> + signing.setupSigning(this@apply) + project.tasks.withType().configureEach { + val signingTask = project.tasks.withType() + mustRunAfter(signingTask) + } + } } } } From ee2f7c1b7e07849070d8ac83047e6c7a8d2071c4 Mon Sep 17 00:00:00 2001 From: sanlorng Date: Mon, 28 Oct 2024 14:52:19 +0800 Subject: [PATCH 035/247] [fluent] Try to fixed javadoc error, see[workaround](https://github.com/gradle/gradle/issues/26091#issuecomment-1722947958) --- .../main/java/com/konyaco/fluent/plugin/build/BuildPlugin.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildPlugin.kt b/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildPlugin.kt index 362203fc..ae3941fb 100644 --- a/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildPlugin.kt +++ b/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildPlugin.kt @@ -20,7 +20,7 @@ class BuildPlugin : Plugin { project.extensions.findByType()?.apply { setupMavenPublishing(project) project.extensions.findByType()?.let { signing -> - signing.setupSigning(this@apply) + signing.setupSigning(this) project.tasks.withType().configureEach { val signingTask = project.tasks.withType() mustRunAfter(signingTask) From 3e7268e48a39ef408383ee14a838098c47fc9541 Mon Sep 17 00:00:00 2001 From: sanlorng Date: Mon, 28 Oct 2024 15:23:04 +0800 Subject: [PATCH 036/247] [fluent] add comment for publish workaround --- .../main/java/com/konyaco/fluent/plugin/build/BuildPlugin.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildPlugin.kt b/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildPlugin.kt index ae3941fb..da7e1bf7 100644 --- a/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildPlugin.kt +++ b/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildPlugin.kt @@ -21,6 +21,8 @@ class BuildPlugin : Plugin { setupMavenPublishing(project) project.extensions.findByType()?.let { signing -> signing.setupSigning(this) + + // workaround for publishing with javadoc see https://github.com/gradle/gradle/issues/26091#issuecomment-1722947958 project.tasks.withType().configureEach { val signingTask = project.tasks.withType() mustRunAfter(signingTask) From b14fad930d56af9d73e44312f80c09f29f010fdb Mon Sep 17 00:00:00 2001 From: sanlorng Date: Mon, 28 Oct 2024 17:52:52 +0800 Subject: [PATCH 037/247] [fluent] update maven url --- .../main/java/com/konyaco/fluent/plugin/build/BuildPlugin.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildPlugin.kt b/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildPlugin.kt index da7e1bf7..3e9dda66 100644 --- a/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildPlugin.kt +++ b/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildPlugin.kt @@ -81,8 +81,8 @@ class BuildPlugin : Plugin { val releasesUrl = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" name = "OSSRH" url = target.uri( - if (target.version.toString().endsWith("SNAPSHOT")) releasesUrl - else snapshotsUrl + if (target.version.toString().endsWith("SNAPSHOT")) snapshotsUrl + else releasesUrl ) credentials { username = System.getenv("OSSRH_USERNAME") From 9cec829fea8e618a631bd97e037b629800bbcdf2 Mon Sep 17 00:00:00 2001 From: sanlorng Date: Mon, 28 Oct 2024 18:30:48 +0800 Subject: [PATCH 038/247] [build] Add Gradle snapshot build action when `dev` branch update --- .github/workflows/github_snapshot_build.yml | 107 ++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 .github/workflows/github_snapshot_build.yml diff --git a/.github/workflows/github_snapshot_build.yml b/.github/workflows/github_snapshot_build.yml new file mode 100644 index 00000000..1ee197c1 --- /dev/null +++ b/.github/workflows/github_snapshot_build.yml @@ -0,0 +1,107 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created +# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle + +name: Gradle Snapshot Build + +on: + push: + branches: + - dev + +jobs: + build: + + strategy: + matrix: + type: [PublishLibrary, Android, Windows, Linux, macOS] + include: + - type: PublishLibrary + publish: release + os: macos-latest + - type: Android + android: apk + os: macos-latest + - type: Windows + desktop: msi + os: windows-latest + - type: Linux + desktop: deb + os: ubuntu-latest + - type: macOS + desktop: dmg + os: macos-latest + + runs-on: ${{ matrix.os }} + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: $ {{ github.ref }} + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + # Configure Gradle for optimal use in GitHub Actions, including caching of downloaded dependencies. + # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md + - name: Setup Gradle + uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0 + + - name: Publish Library + if: ${{ matrix.publish }} + run: | + ./gradlew publishToMavenLocal + ./gradlew publish + env: + OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} + OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + + - name: Upload Library Artifact + if: ${{ matrix.publish }} + uses: actions/upload-artifact@v4 + with: + name: Repository-${{ github.run_id }} + path: ~/.m2/repository + + - name: Build Gallery for ${{ matrix.type }} + if: ${{ matrix.android }} + run: | + echo "$ANDROID_KEYSTORE" | base64 --decode > ${{ github.workspace }}/android_sign_key.jks + ./gradlew :gallery:assembleRelease + env: + ANDROID_KEYSTORE: ${{ secrets.ANDROID_KEYSTORE }} + ANDROID_SIGNING_FILE: ${{ github.workspace }}/android_sign_key.jks + ANDROID_SIGNING_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + ANDROID_SIGNING_KEY_ALIAS: ${{ secrets.KEY_ALIAS }} + ANDROID_SIGNING_KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} + + - name: Upload android build artifacts + if: ${{ matrix.android }} + uses: actions/upload-artifact@v4 + with: + name: Gallery-${{ matrix.type }}-${{ github.run_id }} + path: ${{github.workspace}}/gallery/build/outputs/**/*.${{ matrix.android }} + + - name: Build Gallery for ${{ matrix.type }} + if: ${{ matrix.desktop }} + run: | + ./gradlew :gallery:packageReleaseDistributionForCurrentOS + + - name: Upload desktop build artifacts + if: ${{ matrix.desktop }} + uses: actions/upload-artifact@v4 + with: + name: Gallery-${{ matrix.type }}-${{ github.run_id }} + path: ${{github.workspace}}/gallery/build/compose/binaries/main-release/${{ matrix.desktop }}/*.${{ matrix.desktop }} \ No newline at end of file From cc4dfb32d2a1414913f32612817eccced05c8e7a Mon Sep 17 00:00:00 2001 From: sanlorng Date: Mon, 28 Oct 2024 18:35:09 +0800 Subject: [PATCH 039/247] [fluent] Update FluentDialog api shape --- .../commonMain/kotlin/com/konyaco/fluent/component/Dialog.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dialog.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dialog.kt index 1d91af88..0b49b43e 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dialog.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dialog.kt @@ -71,6 +71,7 @@ class DialogSize( fun FluentDialog( visible: Boolean, size: DialogSize = DialogSize.Standard, + properties: PopupProperties = PopupProperties(focusable = false), content: @Composable () -> Unit ) { val visibleState = remember { MutableTransitionState(false) } @@ -80,7 +81,7 @@ fun FluentDialog( } if (visibleState.currentState || visibleState.targetState) Popup( - properties = PopupProperties(focusable = true), + properties = properties, popupPositionProvider = DialogPopupPositionProvider ) { val scrim by animateColorAsState( From 3033df64624bbaf751a7bc69598acae548c41057 Mon Sep 17 00:00:00 2001 From: Sanlorng Date: Mon, 28 Oct 2024 19:12:34 +0800 Subject: [PATCH 040/247] Update github_snapshot_build.yml --- .github/workflows/github_snapshot_build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github_snapshot_build.yml b/.github/workflows/github_snapshot_build.yml index 1ee197c1..ff5bd50e 100644 --- a/.github/workflows/github_snapshot_build.yml +++ b/.github/workflows/github_snapshot_build.yml @@ -44,7 +44,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - ref: $ {{ github.ref }} + ref: ${{ github.ref }} - name: Set up JDK 17 uses: actions/setup-java@v4 with: @@ -104,4 +104,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: Gallery-${{ matrix.type }}-${{ github.run_id }} - path: ${{github.workspace}}/gallery/build/compose/binaries/main-release/${{ matrix.desktop }}/*.${{ matrix.desktop }} \ No newline at end of file + path: ${{github.workspace}}/gallery/build/compose/binaries/main-release/${{ matrix.desktop }}/*.${{ matrix.desktop }} From 5df68f0f0e88758ab0e223a38f8fdd1c07d7655f Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Sat, 2 Nov 2024 03:46:20 +0800 Subject: [PATCH 041/247] Update Kotlin to 2.0.21 and all deps (cherry picked from commit 9c8cb29d834c3d21a0d4beb948c4e16035c07836) --- build.gradle.kts | 1 + fluent-icons-core/build.gradle.kts | 1 + fluent-icons-extended/build.gradle.kts | 1 + fluent/build.gradle.kts | 1 + gallery/build.gradle.kts | 1 + gradle/libs.versions.toml | 23 ++++++++++++----------- source-generated/build.gradle.kts | 1 + 7 files changed, 18 insertions(+), 11 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 8f55291b..357d3824 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,6 +2,7 @@ import com.konyaco.fluent.plugin.build.BuildConfig plugins { alias(libs.plugins.kotlin.multiplatform) apply false + alias(libs.plugins.kotlin.compose) alias(libs.plugins.compose) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.android.application) apply false diff --git a/fluent-icons-core/build.gradle.kts b/fluent-icons-core/build.gradle.kts index 6a675cf1..33b8aa0b 100644 --- a/fluent-icons-core/build.gradle.kts +++ b/fluent-icons-core/build.gradle.kts @@ -3,6 +3,7 @@ import com.konyaco.fluent.plugin.build.applyTargets plugins { alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.kotlin.compose) alias(libs.plugins.compose) alias(libs.plugins.android.library) alias(libs.plugins.ksp) diff --git a/fluent-icons-extended/build.gradle.kts b/fluent-icons-extended/build.gradle.kts index df0b33be..c17e85de 100644 --- a/fluent-icons-extended/build.gradle.kts +++ b/fluent-icons-extended/build.gradle.kts @@ -3,6 +3,7 @@ import com.konyaco.fluent.plugin.build.applyTargets plugins { alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.kotlin.compose) alias(libs.plugins.compose) alias(libs.plugins.android.library) alias(libs.plugins.ksp) diff --git a/fluent/build.gradle.kts b/fluent/build.gradle.kts index eac4f271..d0d4b6fd 100644 --- a/fluent/build.gradle.kts +++ b/fluent/build.gradle.kts @@ -3,6 +3,7 @@ import com.konyaco.fluent.plugin.build.applyTargets plugins { alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.kotlin.compose) alias(libs.plugins.compose) alias(libs.plugins.android.library) alias(libs.plugins.ksp) diff --git a/gallery/build.gradle.kts b/gallery/build.gradle.kts index 3a09fc08..ed9bf32a 100644 --- a/gallery/build.gradle.kts +++ b/gallery/build.gradle.kts @@ -5,6 +5,7 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat plugins { alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.kotlin.compose) alias(libs.plugins.compose) alias(libs.plugins.android.application) alias(libs.plugins.ksp) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bf2dff49..210147ee 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,26 +1,26 @@ [versions] -activityCompose = "1.8.2" -haze = "0.7.2" -kotlin = "1.9.23" -ksp = "1.9.23-1.0.20" -compose = "1.6.10" +activityCompose = "1.9.3" +haze = "0.6.2" +kotlin = "2.0.21" +ksp = "2.0.21-1.0.26" +compose = "1.7.0" androidGradlePlugin = "8.3.2" -androidBuildTools = "31.3.2" +androidBuildTools = "31.7.2" windowStyler = "0.3.3-SNAPSHOT" highlights = "0.8.0" jna = "5.13.0" [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } -androidx-test-junit = "androidx.test.ext:junit:1.1.5" +androidx-test-junit = "androidx.test.ext:junit:1.2.1" haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" } uuid = "com.benasher44:uuid:0.8.2" -ktor-client-java = "io.ktor:ktor-client-java:2.2.1" -jsoup = "org.jsoup:jsoup:1.15.3" -google-guava = "com.google.guava:guava:31.1-jre" -squareup-kotlinpoet = "com.squareup:kotlinpoet:1.12.0" +ktor-client-java = "io.ktor:ktor-client-java:3.0.0" +jsoup = "org.jsoup:jsoup:1.16.2" +google-guava = "com.google.guava:guava:33.2.1-jre" +squareup-kotlinpoet = "com.squareup:kotlinpoet:1.18.1" android-tools-common = { module = "com.android.tools:common", version.ref = "androidBuildTools" } android-tools-sdk-common = { module = "com.android.tools:sdk-common", version.ref = "androidBuildTools" } window-styler = { module = "com.mayakapps.compose:window-styler", version.ref = "windowStyler" } @@ -30,6 +30,7 @@ jna = { module = "net.java.dev.jna:jna-jpms", version.ref = "jna" } [plugins] kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } diff --git a/source-generated/build.gradle.kts b/source-generated/build.gradle.kts index 4ddd01d3..8d2183fa 100644 --- a/source-generated/build.gradle.kts +++ b/source-generated/build.gradle.kts @@ -3,6 +3,7 @@ import com.konyaco.fluent.plugin.build.applyTargets plugins { alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.kotlin.compose) alias(libs.plugins.compose) alias(libs.plugins.android.library) } From 2c46c0f80a025d96e31f3b35882a5bfd2b2a0153 Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Sat, 2 Nov 2024 03:49:30 +0800 Subject: [PATCH 042/247] Use kotlin uuid instead of the library (cherry picked from commit 398b68a1fbbbba8eb67ee35c968549e38cb41f8e) --- fluent/build.gradle.kts | 1 - .../kotlin/com/konyaco/fluent/component/MenuFlyout.kt | 6 ++++-- gradle/libs.versions.toml | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fluent/build.gradle.kts b/fluent/build.gradle.kts index d0d4b6fd..258fe3d8 100644 --- a/fluent/build.gradle.kts +++ b/fluent/build.gradle.kts @@ -22,7 +22,6 @@ kotlin { api(compose.foundation) api(project(":fluent-icons-core")) implementation(compose.uiUtil) - implementation(libs.uuid) implementation(libs.haze) } } diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/MenuFlyout.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/MenuFlyout.kt index d718be49..012eaf2a 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/MenuFlyout.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/MenuFlyout.kt @@ -30,12 +30,13 @@ import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import com.benasher44.uuid.uuid4 import com.konyaco.fluent.FluentTheme import com.konyaco.fluent.animation.FluentDuration import com.konyaco.fluent.animation.FluentEasing import com.konyaco.fluent.scheme.VisualStateScheme import kotlinx.coroutines.delay +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid @Composable fun MenuFlyoutContainer( @@ -242,13 +243,14 @@ fun MenuFlyoutScope.MenuFlyoutItem( private class MenuFlyoutScopeImpl : MenuFlyoutScope { var latestHoveredItem: String? by mutableStateOf(null) + @OptIn(ExperimentalUuidApi::class) @Composable override fun registerHoveredMenuItem( interaction: MutableInteractionSource, onDelayedHoveredChanged: (hovered: Boolean) -> Unit ) { val isHovered = interaction.collectIsHoveredAsState() - val uuid = remember { uuid4().toString() } + val uuid = remember { Uuid.random().toString() } val delayHovered = remember { mutableStateOf(false) } LaunchedEffect(isHovered.value) { if (isHovered.value) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 210147ee..f8831634 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,6 @@ androidx-activity-compose = { module = "androidx.activity:activity-compose", ver androidx-test-junit = "androidx.test.ext:junit:1.2.1" haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" } -uuid = "com.benasher44:uuid:0.8.2" ktor-client-java = "io.ktor:ktor-client-java:3.0.0" jsoup = "org.jsoup:jsoup:1.16.2" From d378e9e56ef2b14743c9f1f5771472405ed75b9b Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Fri, 1 Nov 2024 20:56:22 +0100 Subject: [PATCH 043/247] Set proper gradle properties (cherry picked from commit ae4e7356178697da29d9e588e2c2107fd080d902) --- gradle.properties | 37 +++++++++++++++---------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/gradle.properties b/gradle.properties index e1f07b15..fb06a988 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,24 +1,17 @@ -# Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app's APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn -android.useAndroidX=true -# Kotlin code style for this project: "official" or "obsolete": +#Gradle +org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" +org.gradle.caching=true +org.gradle.configuration-cache=true + +#Kotlin kotlin.code.style=official -# Enables namespacing of each library's R class so that its R class includes only the -# resources declared in the library itself and none from the library's dependencies, -# thereby reducing the size of the R class for that library +kotlin.js.compiler=ir +kotlin.incremental.native=true + +#Compose +org.jetbrains.compose.experimental.uikit.enabled=true +org.jetbrains.compose.experimental.jscanvas.enabled=true + +#Android +android.useAndroidX=true android.nonTransitiveRClass=true -kotlin.mpp.androidSourceSetLayoutVersion=2 \ No newline at end of file From d9c91824d5aadfc277f9e274b0c30d2499e48c21 Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Fri, 1 Nov 2024 22:01:56 +0100 Subject: [PATCH 044/247] Fix toolchain version in java only modules (cherry picked from commit 0ce4ff3c98d4309a8f980ee0f3bee5a3b72b1549) --- gallery-processor/build.gradle.kts | 1 + source-generated-processor/build.gradle.kts | 1 + 2 files changed, 2 insertions(+) diff --git a/gallery-processor/build.gradle.kts b/gallery-processor/build.gradle.kts index 8212d3de..c638f9ae 100644 --- a/gallery-processor/build.gradle.kts +++ b/gallery-processor/build.gradle.kts @@ -8,6 +8,7 @@ group = BuildConfig.group version = BuildConfig.libraryVersion kotlin { + jvmToolchain(BuildConfig.Jvm.jvmToolchainVersion) jvm() sourceSets { val jvmMain by getting { diff --git a/source-generated-processor/build.gradle.kts b/source-generated-processor/build.gradle.kts index ffafa78f..7ace6414 100644 --- a/source-generated-processor/build.gradle.kts +++ b/source-generated-processor/build.gradle.kts @@ -8,6 +8,7 @@ group = BuildConfig.group version = BuildConfig.libraryVersion kotlin { + jvmToolchain(BuildConfig.Jvm.jvmToolchainVersion) jvm() sourceSets { val jvmMain by getting { From e94ee2cd98511ef0e70dd6d561ac90fda65fa2c6 Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Sat, 2 Nov 2024 05:54:54 +0800 Subject: [PATCH 045/247] Use kotlinx-datetime instead of jvm only calendar and avoid jvm only imports in the common (cherry picked from commit 8f6d4f704e04ab62187bd2f788a1cd68f75208be) --- fluent/build.gradle.kts | 1 + .../fluent/component/CalendarView.android.kt | 25 ++++++ .../com/konyaco/fluent/background/Layer.kt | 1 + .../konyaco/fluent/component/CalendarView.kt | 86 +++++++------------ .../component/FlyoutPositionProvider.kt | 1 + .../fluent/component/CalendarView.desktop.kt | 25 ++++++ .../gallery/screen/settings/SettingsScreen.kt | 2 +- gradle/libs.versions.toml | 4 +- 8 files changed, 86 insertions(+), 59 deletions(-) create mode 100644 fluent/src/androidMain/kotlin/com/konyaco/fluent/component/CalendarView.android.kt create mode 100644 fluent/src/desktopMain/kotlin/com/konyaco/fluent/component/CalendarView.desktop.kt diff --git a/fluent/build.gradle.kts b/fluent/build.gradle.kts index 258fe3d8..998576bf 100644 --- a/fluent/build.gradle.kts +++ b/fluent/build.gradle.kts @@ -22,6 +22,7 @@ kotlin { api(compose.foundation) api(project(":fluent-icons-core")) implementation(compose.uiUtil) + implementation(libs.kotlinx.datetime) implementation(libs.haze) } } diff --git a/fluent/src/androidMain/kotlin/com/konyaco/fluent/component/CalendarView.android.kt b/fluent/src/androidMain/kotlin/com/konyaco/fluent/component/CalendarView.android.kt new file mode 100644 index 00000000..500efb77 --- /dev/null +++ b/fluent/src/androidMain/kotlin/com/konyaco/fluent/component/CalendarView.android.kt @@ -0,0 +1,25 @@ +package com.konyaco.fluent.component + +import java.util.Calendar +import java.util.Locale + +internal actual fun getLocalDayOfWeekNames(): List { + val locale = Locale.getDefault() + val calendar = Calendar.getInstance(locale) + val names = calendar.getDisplayNames(Calendar.DAY_OF_WEEK, Calendar.NARROW_STANDALONE, locale) + ?: calendar.getDisplayNames(Calendar.DAY_OF_WEEK, Calendar.SHORT_STANDALONE, locale) + return names.entries.sortedBy { it.value }.map { it.key } +} + +internal actual fun getLocalMonthNames(): List { + val locale = Locale.getDefault() + val calendar = Calendar.getInstance(locale) + val names = calendar.getDisplayNames(Calendar.MONTH, Calendar.SHORT_STANDALONE, locale) + return names.entries.sortedBy { it.value }.map { it.key } +} + +internal actual fun getLocalFirstDayOfWeek(): Int { + val locale = Locale.getDefault() + val calendar = Calendar.getInstance(locale) + return calendar.firstDayOfWeek +} \ No newline at end of file diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Layer.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Layer.kt index 2f57bd67..814adc81 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Layer.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/background/Layer.kt @@ -27,6 +27,7 @@ import com.konyaco.fluent.FluentTheme import com.konyaco.fluent.LocalContentAlpha import com.konyaco.fluent.LocalContentColor import com.konyaco.fluent.ProvideTextStyle +import kotlin.jvm.JvmInline import kotlin.math.sqrt /** diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/CalendarView.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/CalendarView.kt index 33d53295..90ee7f07 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/CalendarView.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/CalendarView.kt @@ -57,8 +57,14 @@ import com.konyaco.fluent.icons.filled.CaretDown import com.konyaco.fluent.icons.filled.CaretUp import com.konyaco.fluent.scheme.PentaVisualScheme import com.konyaco.fluent.scheme.collectVisualState -import java.util.Calendar -import java.util.Date +import kotlinx.datetime.Clock +import kotlinx.datetime.DatePeriod +import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.isoDayNumber +import kotlinx.datetime.minus +import kotlinx.datetime.plus +import kotlinx.datetime.toLocalDateTime /** @@ -482,14 +488,12 @@ private fun Item( } class CalendarDatePickerState { - private val locale = java.util.Locale.getDefault() - private val calendar = Calendar.getInstance(locale) val currentChooseType = mutableStateOf(ChooseType.DAY) val viewHeaderText = mutableStateOf("") - val dayOfWeekNames = mutableStateOf(getDowNames()) - val monthNames = mutableStateOf(getMonthNames()) + val dayOfWeekNames = mutableStateOf(getLocalDayOfWeekNames()) + val monthNames = mutableStateOf(getLocalMonthNames()) enum class ChooseType { YEAR, MONTH, DAY @@ -516,43 +520,15 @@ class CalendarDatePickerState { // TODO: For localization, e.g. In China the start of week is Monday // FIXME: `firstDayOfWeek` should return 2(Monday), but returns 1(Sunday) in Java 17 - val localeStartDayOfWeek = calendar.firstDayOfWeek + val localeStartDayOfWeek = getLocalFirstDayOfWeek() // val localeStartDayOfWeek = 2 - private fun getDowNames(): List { - val names = calendar.getDisplayNames( - Calendar.DAY_OF_WEEK, - Calendar.NARROW_STANDALONE, - locale - ) ?: calendar.getDisplayNames( - Calendar.DAY_OF_WEEK, - Calendar.SHORT_STANDALONE, - locale - ) - val result = MutableList(7) { "" } - - for (entry in names.entries) { - // minus 1 because dof start from 1 - result[entry.value - 1] = entry.key - } - return result - } - - private fun getMonthNames(): List { - val names = calendar.getDisplayNames(Calendar.MONTH, Calendar.SHORT_STANDALONE, locale) - val result = MutableList(12) { "" } - for (entry in names.entries) { - result[entry.value] = entry.key - } - return result - } - init { - val calendar = Calendar.getInstance() - calendar.time = Date() - val year = calendar.get(Calendar.YEAR) - val monthValue = calendar.get(Calendar.MONTH) - val day = calendar.get(Calendar.DAY_OF_MONTH) + val now = Clock.System.now() + val dateTime = now.toLocalDateTime(TimeZone.currentSystemDefault()) + val year = dateTime.year + val monthValue = dateTime.monthNumber - 1 + val day = dateTime.dayOfMonth // currentYear = mutableStateOf(Year(year)) // viewYear = mutableStateOf(Year(year)) // selectedYear = mutableStateOf(Year(year)) @@ -585,9 +561,9 @@ class CalendarDatePickerState { } private fun calculateCandidateDays(year: Int, monthValue: Int) { - val calendar = Calendar.getInstance() - calendar.set(year, monthValue, 1) - val startDayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) // Start at Sunday(1) + val localDate = LocalDate(year, monthValue + 1, 1) + val startDayOfWeek = localDate.dayOfWeek.isoDayNumber % 7 + 1 +// val startDayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) // Start at Sunday(1) // Start day at the first `localeStartDayOfWeek(e.g. Sunday)` before this month // If localeStartDOW = Sunday(1): @@ -603,17 +579,13 @@ class CalendarDatePickerState { startDayOffset += 7 } - val startDayOfYear = calendar.get(Calendar.DAY_OF_YEAR) - startDayOffset + val startDay = localDate.minus(DatePeriod(days = startDayOffset)) // e.g from 4-29 to 6-9 val daysToDisplay = 7 * 6 - val candidateDays = (startDayOfYear until startDayOfYear + daysToDisplay).map { dayOfYear -> - calendar.set(Calendar.DAY_OF_YEAR, dayOfYear) - val monthValue = calendar.get(Calendar.MONTH) - val day = calendar.get(Calendar.DAY_OF_MONTH) - Day(year, monthValue, day) + this.candidateDays.value = List(daysToDisplay) { i -> + startDay.plus(DatePeriod(days = i)).let { Day(it.year, it.monthNumber - 1, it.dayOfMonth) } } - this.candidateDays.value = candidateDays } internal fun toggleChooseType() { @@ -771,18 +743,18 @@ class CalendarDatePickerState { } ChooseType.DAY -> { - val displayNames = - calendar.getDisplayNames(Calendar.MONTH, Calendar.SHORT_STANDALONE, locale) + val displayNames = getLocalMonthNames() val curr = viewMonth.value val monthValue = curr.monthValue val year = curr.year - val name = displayNames.firstNotNullOf { (k, v) -> - if (v == monthValue) k - else null - } + val name = displayNames[monthValue] // TODO: Should be "May 2024" / "2024年 5月" viewHeaderText.value = "$name $year" } } } -} \ No newline at end of file +} + +expect internal fun getLocalDayOfWeekNames(): List +expect internal fun getLocalMonthNames(): List +expect internal fun getLocalFirstDayOfWeek(): Int \ No newline at end of file diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/FlyoutPositionProvider.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/FlyoutPositionProvider.kt index d6e021fc..751cfffd 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/FlyoutPositionProvider.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/FlyoutPositionProvider.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.geometry.center import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.* import androidx.compose.ui.window.PopupPositionProvider +import kotlin.jvm.JvmInline @Composable internal fun rememberFlyoutPositionProvider( diff --git a/fluent/src/desktopMain/kotlin/com/konyaco/fluent/component/CalendarView.desktop.kt b/fluent/src/desktopMain/kotlin/com/konyaco/fluent/component/CalendarView.desktop.kt new file mode 100644 index 00000000..500efb77 --- /dev/null +++ b/fluent/src/desktopMain/kotlin/com/konyaco/fluent/component/CalendarView.desktop.kt @@ -0,0 +1,25 @@ +package com.konyaco.fluent.component + +import java.util.Calendar +import java.util.Locale + +internal actual fun getLocalDayOfWeekNames(): List { + val locale = Locale.getDefault() + val calendar = Calendar.getInstance(locale) + val names = calendar.getDisplayNames(Calendar.DAY_OF_WEEK, Calendar.NARROW_STANDALONE, locale) + ?: calendar.getDisplayNames(Calendar.DAY_OF_WEEK, Calendar.SHORT_STANDALONE, locale) + return names.entries.sortedBy { it.value }.map { it.key } +} + +internal actual fun getLocalMonthNames(): List { + val locale = Locale.getDefault() + val calendar = Calendar.getInstance(locale) + val names = calendar.getDisplayNames(Calendar.MONTH, Calendar.SHORT_STANDALONE, locale) + return names.entries.sortedBy { it.value }.map { it.key } +} + +internal actual fun getLocalFirstDayOfWeek(): Int { + val locale = Locale.getDefault() + val calendar = Calendar.getInstance(locale) + return calendar.firstDayOfWeek +} \ No newline at end of file diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/settings/SettingsScreen.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/settings/SettingsScreen.kt index d76ed6ab..edf3ac2d 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/settings/SettingsScreen.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/settings/SettingsScreen.kt @@ -265,7 +265,7 @@ private fun Controller( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - Text("Scale: %.2f".format(scale)) + Text("Scale: ${scale.toString().take(4)}") val density = LocalDensity.current Button(onClick = { onScaleChange(density.density) }) { Text("Reset") } Switcher(darkMode, text = "Dark Mode", onCheckStateChange = { onDarkModeChange(it) }) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f8831634..44c005c1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,8 @@ compose = "1.7.0" androidGradlePlugin = "8.3.2" androidBuildTools = "31.7.2" windowStyler = "0.3.3-SNAPSHOT" -highlights = "0.8.0" +highlights = "0.9.3" +kotlinx-datetime = "0.6.1" jna = "5.13.0" [libraries] @@ -16,6 +17,7 @@ androidx-test-junit = "androidx.test.ext:junit:1.2.1" haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" } +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } ktor-client-java = "io.ktor:ktor-client-java:3.0.0" jsoup = "org.jsoup:jsoup:1.16.2" google-guava = "com.google.guava:guava:33.2.1-jre" From c393dc787fa8566a394f06133bab81a77b9ac29c Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Fri, 1 Nov 2024 23:55:42 +0100 Subject: [PATCH 046/247] Add wasmJs target (cherry picked from commit 9d9f310ae8a1fecb5d5dd09653923aecab3497e9) --- .../fluent/plugin/build/BuildExtension.kt | 3 + .../fluent/DefaultFontFamily.wasmJs.kt | 9 ++ ...PlatformCompositionLocalProvider.wasmJs.kt | 9 ++ .../fluent/component/CalendarView.wasmJs.kt | 33 ++++++++ .../konyaco/fluent/component/Dialog.wasmJs.kt | 20 +++++ .../fluent/component/FontIcon.wasmJs.kt | 8 ++ .../component/PlatformScrollBar.wasmJs.kt | 82 +++++++++++++++++++ .../konyaco/fluent/component/Popup.wasmJs.kt | 58 +++++++++++++ gallery/build.gradle.kts | 1 + gallery/src/wasmJsMain/kotlin/main.kt | 13 +++ gallery/src/wasmJsMain/resources/index.html | 15 ++++ 11 files changed, 251 insertions(+) create mode 100644 fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/DefaultFontFamily.wasmJs.kt create mode 100644 fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/PlatformCompositionLocalProvider.wasmJs.kt create mode 100644 fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/CalendarView.wasmJs.kt create mode 100644 fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/Dialog.wasmJs.kt create mode 100644 fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/FontIcon.wasmJs.kt create mode 100644 fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/PlatformScrollBar.wasmJs.kt create mode 100644 fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/Popup.wasmJs.kt create mode 100644 gallery/src/wasmJsMain/kotlin/main.kt create mode 100644 gallery/src/wasmJsMain/resources/index.html diff --git a/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildExtension.kt b/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildExtension.kt index c2d206f2..d4f3d608 100644 --- a/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildExtension.kt +++ b/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildExtension.kt @@ -1,11 +1,14 @@ package com.konyaco.fluent.plugin.build +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +@OptIn(ExperimentalWasmDsl::class) fun KotlinMultiplatformExtension.applyTargets(publish: Boolean = true) { jvm("desktop") androidTarget { if (publish) publishLibraryVariants("release") } jvmToolchain(BuildConfig.Jvm.jvmToolchainVersion) + wasmJs { browser() } } \ No newline at end of file diff --git a/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/DefaultFontFamily.wasmJs.kt b/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/DefaultFontFamily.wasmJs.kt new file mode 100644 index 00000000..d8df06b4 --- /dev/null +++ b/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/DefaultFontFamily.wasmJs.kt @@ -0,0 +1,9 @@ +package com.konyaco.fluent + +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.font.FontFamily + +@Composable +actual fun defaultFontFamily(): FontFamily? { + return null +} \ No newline at end of file diff --git a/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/PlatformCompositionLocalProvider.wasmJs.kt b/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/PlatformCompositionLocalProvider.wasmJs.kt new file mode 100644 index 00000000..dd9c8669 --- /dev/null +++ b/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/PlatformCompositionLocalProvider.wasmJs.kt @@ -0,0 +1,9 @@ +package com.konyaco.fluent + +import androidx.compose.runtime.Composable + +@Composable +actual fun PlatformCompositionLocalProvider(content: @Composable () -> Unit) { + content() +} + diff --git a/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/CalendarView.wasmJs.kt b/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/CalendarView.wasmJs.kt new file mode 100644 index 00000000..3e174bde --- /dev/null +++ b/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/CalendarView.wasmJs.kt @@ -0,0 +1,33 @@ +package com.konyaco.fluent.component + +@JsFun("""(style) => { + var format = new Intl.DateTimeFormat(navigator.language, { weekday: style }) + var baseDate = new Date(Date.UTC(2017, 0, 2)) // just a Monday + var weekDays = [] + for (var day = 0; day < 7; day++) { + baseDate.setDate(baseDate.getDate() + day) + weekDays.push(format.format(baseDate)) + } + return weekDays.join(",") +}""") +private external fun getJsLocalDayOfWeekNames(format: String): String + +internal actual fun getLocalDayOfWeekNames() = + getJsLocalDayOfWeekNames("short").split(",") + +@JsFun("""(style) => { + var format = new Intl.DateTimeFormat(navigator.language, { month: style }) + var months = [] + for (var month = 0; month < 12; month++) { + var testDate = new Date(Date.UTC(2000, month, 1, 0, 0, 0)) + months.push(format.format(testDate)) + } + return months.join(",") +}""") +private external fun getJsLocalMonthNames(format: String): String + +internal actual fun getLocalMonthNames(): List = + getJsLocalMonthNames("short").split(",") + +//https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo +internal actual fun getLocalFirstDayOfWeek() = 1 \ No newline at end of file diff --git a/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/Dialog.wasmJs.kt b/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/Dialog.wasmJs.kt new file mode 100644 index 00000000..0f495dd0 --- /dev/null +++ b/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/Dialog.wasmJs.kt @@ -0,0 +1,20 @@ +package com.konyaco.fluent.component + +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.window.PopupPositionProvider + +internal actual val DialogPopupPositionProvider: PopupPositionProvider = DialogPopupPositionProviderImpl + +internal object DialogPopupPositionProviderImpl : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize + ): IntOffset { + return IntOffset.Zero + } +} \ No newline at end of file diff --git a/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/FontIcon.wasmJs.kt b/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/FontIcon.wasmJs.kt new file mode 100644 index 00000000..62808826 --- /dev/null +++ b/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/FontIcon.wasmJs.kt @@ -0,0 +1,8 @@ +package com.konyaco.fluent.component + +import androidx.compose.runtime.Composable + +@Composable +internal actual fun ProvideFontIcon(content: @Composable () -> Unit) { + content() +} \ No newline at end of file diff --git a/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/PlatformScrollBar.wasmJs.kt b/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/PlatformScrollBar.wasmJs.kt new file mode 100644 index 00000000..0a68844b --- /dev/null +++ b/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/PlatformScrollBar.wasmJs.kt @@ -0,0 +1,82 @@ +package com.konyaco.fluent.component + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +actual interface ScrollbarAdapter { + /** + * Scroll offset of the content inside the scrollable component. + * + * For example, a value of `100` could mean the content is scrolled by 100 pixels from the + * start. + */ + actual val scrollOffset: Double + + /** + * The size of the scrollable content, on the scrollable axis. + */ + actual val contentSize: Double + + /** + * The size of the viewport, on the scrollable axis. + */ + actual val viewportSize: Double + + /** + * Instantly jump to [scrollOffset]. + * + * @param scrollOffset target offset to jump to, value will be coerced to the valid + * scroll range. + */ + actual suspend fun scrollTo(scrollOffset: Double) + +} +@Composable +internal actual fun PlatformScrollBar( + isVertical: Boolean, + adapter: ScrollbarAdapter, + modifier: Modifier, + reverseLayout: Boolean, + colors: ScrollbarColors +) { + //TODO Scrollbar browser implementation +} + +@Composable +actual fun rememberScrollbarAdapter( + state: ScrollState +): ScrollbarAdapter { + return EmptyScrollbarAdapter +} + +@Composable +actual fun rememberScrollbarAdapter( + state: LazyListState +): ScrollbarAdapter { + return EmptyScrollbarAdapter +} + +@Composable +actual fun rememberScrollbarAdapter( + state: LazyGridState +): ScrollbarAdapter { + return EmptyScrollbarAdapter +} + +private object EmptyScrollbarAdapter: ScrollbarAdapter { + override val contentSize: Double + get() = 0.0 + + override val scrollOffset: Double + get() = 0.0 + + override val viewportSize: Double + get() = 0.0 + + override suspend fun scrollTo(scrollOffset: Double) { + + } +} \ No newline at end of file diff --git a/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/Popup.wasmJs.kt b/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/Popup.wasmJs.kt new file mode 100644 index 00000000..8079b839 --- /dev/null +++ b/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/Popup.wasmJs.kt @@ -0,0 +1,58 @@ +package com.konyaco.fluent.component + +import androidx.compose.runtime.* +import androidx.compose.ui.input.key.KeyEvent +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.window.PopupPositionProvider +import androidx.compose.ui.window.PopupProperties + +@Composable +internal actual fun Popup( + popupPositionProvider: PopupPositionProvider, + onDismissRequest: (() -> Unit)?, + properties: PopupProperties, + onPreviewKeyEvent: ((KeyEvent) -> Boolean)?, + onKeyEvent: ((KeyEvent) -> Boolean)?, + content: @Composable () -> Unit +) { + val offset = LocalPopupOffset.current + val delegatePopupPositionProvider = remember(popupPositionProvider) { + DelegatePopupPositionProvider({ offset }, popupPositionProvider) + } + androidx.compose.ui.window.Popup(delegatePopupPositionProvider, onDismissRequest, properties) { + CompositionLocalProvider( + LocalPopupOffset provides delegatePopupPositionProvider.currentOffset, + content = content + ) + } +} + +// Workaround for android nested popup position calculate +private val LocalPopupOffset = staticCompositionLocalOf { IntOffset.Zero } + +private class DelegatePopupPositionProvider( + val offset: () -> IntOffset, + val positionProvider: PopupPositionProvider +): PopupPositionProvider { + + var currentOffset by mutableStateOf(IntOffset.Zero) + + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize + ): IntOffset { + return positionProvider.calculatePosition( + anchorBounds.translate(offset()), + windowSize, + layoutDirection, + popupContentSize + ).apply { + currentOffset = this + } + } +} \ No newline at end of file diff --git a/gallery/build.gradle.kts b/gallery/build.gradle.kts index ed9bf32a..f8eff5e5 100644 --- a/gallery/build.gradle.kts +++ b/gallery/build.gradle.kts @@ -13,6 +13,7 @@ plugins { kotlin { applyTargets(publish = false) + wasmJs { binaries.executable() } sourceSets { val commonMain by getting { dependencies { diff --git a/gallery/src/wasmJsMain/kotlin/main.kt b/gallery/src/wasmJsMain/kotlin/main.kt new file mode 100644 index 00000000..c2a85593 --- /dev/null +++ b/gallery/src/wasmJsMain/kotlin/main.kt @@ -0,0 +1,13 @@ +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.CanvasBasedWindow +import com.konyaco.fluent.gallery.App +import com.konyaco.fluent.gallery.GalleryTheme + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + CanvasBasedWindow("Compose Fluent Design Gallery") { + GalleryTheme { + App() + } + } +} diff --git a/gallery/src/wasmJsMain/resources/index.html b/gallery/src/wasmJsMain/resources/index.html new file mode 100644 index 00000000..fd7d72fd --- /dev/null +++ b/gallery/src/wasmJsMain/resources/index.html @@ -0,0 +1,15 @@ + + + + + + Compose Fluent Design Gallery + + + +
+ +
+ + + \ No newline at end of file From 982ae49998761048eb979c2c20ec4b0a4d9bb154 Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Sat, 2 Nov 2024 01:10:35 +0100 Subject: [PATCH 047/247] Refactor gradle properties (cherry picked from commit fcb5d8da1e70c1458d0938f8900b70c5c30749ce) --- gradle.properties | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/gradle.properties b/gradle.properties index fb06a988..97aeb6e8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,16 +1,13 @@ #Gradle -org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" +org.gradle.jvmargs=-Xmx4G org.gradle.caching=true org.gradle.configuration-cache=true +org.gradle.daemon=true +org.gradle.parallel=true #Kotlin kotlin.code.style=official -kotlin.js.compiler=ir -kotlin.incremental.native=true - -#Compose -org.jetbrains.compose.experimental.uikit.enabled=true -org.jetbrains.compose.experimental.jscanvas.enabled=true +kotlin.daemon.jvmargs=-Xmx4G #Android android.useAndroidX=true From 6f1116cabf905300b1a5b37ed543757c6325d5cd Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Sat, 2 Nov 2024 01:11:23 +0100 Subject: [PATCH 048/247] Update gradle and deps (cherry picked from commit b4ce6cab61ec1941481a285a049c1997395fd8d7) --- gradle/libs.versions.toml | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 44c005c1..32ddf6cc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] activityCompose = "1.9.3" -haze = "0.6.2" +haze = "0.7.3" kotlin = "2.0.21" ksp = "2.0.21-1.0.26" compose = "1.7.0" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e411586a..1e2fbf0d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From d6fb9117ab2d690815d1f3a62f1e292456a154a4 Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Sat, 2 Nov 2024 01:11:35 +0100 Subject: [PATCH 049/247] Add js target (cherry picked from commit 3829f3330daa65924bd485765afbe5818927e312) --- .../fluent/plugin/build/BuildExtension.kt | 1 + .../konyaco/fluent/DefaultFontFamily.js.kt | 9 ++ .../PlatformCompositionLocalProvider.js.kt | 9 ++ .../fluent/component/CalendarView.js.kt | 37 +++++++++ .../com/konyaco/fluent/component/Dialog.js.kt | 20 +++++ .../konyaco/fluent/component/FontIcon.js.kt | 8 ++ .../fluent/component/PlatformScrollBar.js.kt | 82 +++++++++++++++++++ .../com/konyaco/fluent/component/Popup.js.kt | 58 +++++++++++++ gallery/build.gradle.kts | 1 + gallery/src/jsMain/kotlin/main.kt | 18 ++++ gallery/src/jsMain/resources/index.html | 21 +++++ gradle.properties | 3 + 12 files changed, 267 insertions(+) create mode 100644 fluent/src/jsMain/kotlin/com/konyaco/fluent/DefaultFontFamily.js.kt create mode 100644 fluent/src/jsMain/kotlin/com/konyaco/fluent/PlatformCompositionLocalProvider.js.kt create mode 100644 fluent/src/jsMain/kotlin/com/konyaco/fluent/component/CalendarView.js.kt create mode 100644 fluent/src/jsMain/kotlin/com/konyaco/fluent/component/Dialog.js.kt create mode 100644 fluent/src/jsMain/kotlin/com/konyaco/fluent/component/FontIcon.js.kt create mode 100644 fluent/src/jsMain/kotlin/com/konyaco/fluent/component/PlatformScrollBar.js.kt create mode 100644 fluent/src/jsMain/kotlin/com/konyaco/fluent/component/Popup.js.kt create mode 100644 gallery/src/jsMain/kotlin/main.kt create mode 100644 gallery/src/jsMain/resources/index.html diff --git a/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildExtension.kt b/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildExtension.kt index d4f3d608..690ea3a3 100644 --- a/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildExtension.kt +++ b/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildExtension.kt @@ -11,4 +11,5 @@ fun KotlinMultiplatformExtension.applyTargets(publish: Boolean = true) { } jvmToolchain(BuildConfig.Jvm.jvmToolchainVersion) wasmJs { browser() } + js { browser() } } \ No newline at end of file diff --git a/fluent/src/jsMain/kotlin/com/konyaco/fluent/DefaultFontFamily.js.kt b/fluent/src/jsMain/kotlin/com/konyaco/fluent/DefaultFontFamily.js.kt new file mode 100644 index 00000000..d8df06b4 --- /dev/null +++ b/fluent/src/jsMain/kotlin/com/konyaco/fluent/DefaultFontFamily.js.kt @@ -0,0 +1,9 @@ +package com.konyaco.fluent + +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.font.FontFamily + +@Composable +actual fun defaultFontFamily(): FontFamily? { + return null +} \ No newline at end of file diff --git a/fluent/src/jsMain/kotlin/com/konyaco/fluent/PlatformCompositionLocalProvider.js.kt b/fluent/src/jsMain/kotlin/com/konyaco/fluent/PlatformCompositionLocalProvider.js.kt new file mode 100644 index 00000000..dd9c8669 --- /dev/null +++ b/fluent/src/jsMain/kotlin/com/konyaco/fluent/PlatformCompositionLocalProvider.js.kt @@ -0,0 +1,9 @@ +package com.konyaco.fluent + +import androidx.compose.runtime.Composable + +@Composable +actual fun PlatformCompositionLocalProvider(content: @Composable () -> Unit) { + content() +} + diff --git a/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/CalendarView.js.kt b/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/CalendarView.js.kt new file mode 100644 index 00000000..b7155dd0 --- /dev/null +++ b/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/CalendarView.js.kt @@ -0,0 +1,37 @@ +package com.konyaco.fluent.component + +internal actual fun getLocalDayOfWeekNames(): List { + val jsFun: String = js( + """ + var format = new Intl.DateTimeFormat(navigator.language, { weekday: 'short' }) + var baseDate = new Date(Date.UTC(2017, 0, 2)) // just a Monday + var weekDays = [] + for (var day = 0; day < 7; day++) { + baseDate.setDate(baseDate.getDate() + day) + weekDays.push(format.format(baseDate)) + } + weekDays.join(",") + """ + ) + + return jsFun.split(",") +} + +internal actual fun getLocalMonthNames(): List { + val jsFun: String = js( + """ + var format = new Intl.DateTimeFormat(navigator.language, { month: 'short' }) + var months = [] + for (var month = 0; month < 12; month++) { + var testDate = new Date(Date.UTC(2000, month, 1, 0, 0, 0)) + months.push(format.format(testDate)) + } + months.join(",") + """ + ) + + return jsFun.split(",") +} + +//https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo +internal actual fun getLocalFirstDayOfWeek() = 1 \ No newline at end of file diff --git a/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/Dialog.js.kt b/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/Dialog.js.kt new file mode 100644 index 00000000..0f495dd0 --- /dev/null +++ b/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/Dialog.js.kt @@ -0,0 +1,20 @@ +package com.konyaco.fluent.component + +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.window.PopupPositionProvider + +internal actual val DialogPopupPositionProvider: PopupPositionProvider = DialogPopupPositionProviderImpl + +internal object DialogPopupPositionProviderImpl : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize + ): IntOffset { + return IntOffset.Zero + } +} \ No newline at end of file diff --git a/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/FontIcon.js.kt b/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/FontIcon.js.kt new file mode 100644 index 00000000..62808826 --- /dev/null +++ b/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/FontIcon.js.kt @@ -0,0 +1,8 @@ +package com.konyaco.fluent.component + +import androidx.compose.runtime.Composable + +@Composable +internal actual fun ProvideFontIcon(content: @Composable () -> Unit) { + content() +} \ No newline at end of file diff --git a/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/PlatformScrollBar.js.kt b/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/PlatformScrollBar.js.kt new file mode 100644 index 00000000..0a68844b --- /dev/null +++ b/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/PlatformScrollBar.js.kt @@ -0,0 +1,82 @@ +package com.konyaco.fluent.component + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +actual interface ScrollbarAdapter { + /** + * Scroll offset of the content inside the scrollable component. + * + * For example, a value of `100` could mean the content is scrolled by 100 pixels from the + * start. + */ + actual val scrollOffset: Double + + /** + * The size of the scrollable content, on the scrollable axis. + */ + actual val contentSize: Double + + /** + * The size of the viewport, on the scrollable axis. + */ + actual val viewportSize: Double + + /** + * Instantly jump to [scrollOffset]. + * + * @param scrollOffset target offset to jump to, value will be coerced to the valid + * scroll range. + */ + actual suspend fun scrollTo(scrollOffset: Double) + +} +@Composable +internal actual fun PlatformScrollBar( + isVertical: Boolean, + adapter: ScrollbarAdapter, + modifier: Modifier, + reverseLayout: Boolean, + colors: ScrollbarColors +) { + //TODO Scrollbar browser implementation +} + +@Composable +actual fun rememberScrollbarAdapter( + state: ScrollState +): ScrollbarAdapter { + return EmptyScrollbarAdapter +} + +@Composable +actual fun rememberScrollbarAdapter( + state: LazyListState +): ScrollbarAdapter { + return EmptyScrollbarAdapter +} + +@Composable +actual fun rememberScrollbarAdapter( + state: LazyGridState +): ScrollbarAdapter { + return EmptyScrollbarAdapter +} + +private object EmptyScrollbarAdapter: ScrollbarAdapter { + override val contentSize: Double + get() = 0.0 + + override val scrollOffset: Double + get() = 0.0 + + override val viewportSize: Double + get() = 0.0 + + override suspend fun scrollTo(scrollOffset: Double) { + + } +} \ No newline at end of file diff --git a/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/Popup.js.kt b/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/Popup.js.kt new file mode 100644 index 00000000..8079b839 --- /dev/null +++ b/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/Popup.js.kt @@ -0,0 +1,58 @@ +package com.konyaco.fluent.component + +import androidx.compose.runtime.* +import androidx.compose.ui.input.key.KeyEvent +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.window.PopupPositionProvider +import androidx.compose.ui.window.PopupProperties + +@Composable +internal actual fun Popup( + popupPositionProvider: PopupPositionProvider, + onDismissRequest: (() -> Unit)?, + properties: PopupProperties, + onPreviewKeyEvent: ((KeyEvent) -> Boolean)?, + onKeyEvent: ((KeyEvent) -> Boolean)?, + content: @Composable () -> Unit +) { + val offset = LocalPopupOffset.current + val delegatePopupPositionProvider = remember(popupPositionProvider) { + DelegatePopupPositionProvider({ offset }, popupPositionProvider) + } + androidx.compose.ui.window.Popup(delegatePopupPositionProvider, onDismissRequest, properties) { + CompositionLocalProvider( + LocalPopupOffset provides delegatePopupPositionProvider.currentOffset, + content = content + ) + } +} + +// Workaround for android nested popup position calculate +private val LocalPopupOffset = staticCompositionLocalOf { IntOffset.Zero } + +private class DelegatePopupPositionProvider( + val offset: () -> IntOffset, + val positionProvider: PopupPositionProvider +): PopupPositionProvider { + + var currentOffset by mutableStateOf(IntOffset.Zero) + + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize + ): IntOffset { + return positionProvider.calculatePosition( + anchorBounds.translate(offset()), + windowSize, + layoutDirection, + popupContentSize + ).apply { + currentOffset = this + } + } +} \ No newline at end of file diff --git a/gallery/build.gradle.kts b/gallery/build.gradle.kts index f8eff5e5..db96fc5d 100644 --- a/gallery/build.gradle.kts +++ b/gallery/build.gradle.kts @@ -14,6 +14,7 @@ plugins { kotlin { applyTargets(publish = false) wasmJs { binaries.executable() } + js { binaries.executable() } sourceSets { val commonMain by getting { dependencies { diff --git a/gallery/src/jsMain/kotlin/main.kt b/gallery/src/jsMain/kotlin/main.kt new file mode 100644 index 00000000..42c00d0c --- /dev/null +++ b/gallery/src/jsMain/kotlin/main.kt @@ -0,0 +1,18 @@ +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.ComposeViewport +import com.konyaco.fluent.gallery.App +import com.konyaco.fluent.gallery.GalleryTheme +import kotlinx.browser.document +import org.jetbrains.skiko.wasm.onWasmReady + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + onWasmReady { + val body = document.body ?: return@onWasmReady + ComposeViewport(body) { + GalleryTheme { + App() + } + } + } +} diff --git a/gallery/src/jsMain/resources/index.html b/gallery/src/jsMain/resources/index.html new file mode 100644 index 00000000..3b4799e4 --- /dev/null +++ b/gallery/src/jsMain/resources/index.html @@ -0,0 +1,21 @@ + + + + + + Compose Fluent Design Gallery + + + + + + diff --git a/gradle.properties b/gradle.properties index 97aeb6e8..0d3bb846 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,3 +12,6 @@ kotlin.daemon.jvmargs=-Xmx4G #Android android.useAndroidX=true android.nonTransitiveRClass=true + +#Compose +org.jetbrains.compose.experimental.jscanvas.enabled=true From f59ef63d146480598fd83899fc581bc87b9a1da0 Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Sat, 2 Nov 2024 01:19:09 +0100 Subject: [PATCH 050/247] Fix day names in browsers (cherry picked from commit 115d3a7b6e2f099603e1266515aa7826d8933af7) --- .../kotlin/com/konyaco/fluent/component/CalendarView.js.kt | 4 ++-- .../com/konyaco/fluent/component/CalendarView.wasmJs.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/CalendarView.js.kt b/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/CalendarView.js.kt index b7155dd0..cd0fbd79 100644 --- a/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/CalendarView.js.kt +++ b/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/CalendarView.js.kt @@ -6,9 +6,9 @@ internal actual fun getLocalDayOfWeekNames(): List { var format = new Intl.DateTimeFormat(navigator.language, { weekday: 'short' }) var baseDate = new Date(Date.UTC(2017, 0, 2)) // just a Monday var weekDays = [] - for (var day = 0; day < 7; day++) { - baseDate.setDate(baseDate.getDate() + day) + for (var day = 0; day < 7; day++) { weekDays.push(format.format(baseDate)) + baseDate.setDate(baseDate.getDate() + 1) } weekDays.join(",") """ diff --git a/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/CalendarView.wasmJs.kt b/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/CalendarView.wasmJs.kt index 3e174bde..ec5a294a 100644 --- a/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/CalendarView.wasmJs.kt +++ b/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/CalendarView.wasmJs.kt @@ -5,8 +5,8 @@ package com.konyaco.fluent.component var baseDate = new Date(Date.UTC(2017, 0, 2)) // just a Monday var weekDays = [] for (var day = 0; day < 7; day++) { - baseDate.setDate(baseDate.getDate() + day) weekDays.push(format.format(baseDate)) + baseDate.setDate(baseDate.getDate() + 1) } return weekDays.join(",") }""") From a01c68c11fd450945e7d1894036cf34ef9789e74 Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Sat, 2 Nov 2024 01:30:09 +0100 Subject: [PATCH 051/247] Fix calendar view days offset (cherry picked from commit 2952227195c588537f9223eff87e0ef116e86340) --- .../kotlin/com/konyaco/fluent/component/CalendarView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/CalendarView.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/CalendarView.kt index 90ee7f07..c6ce1721 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/CalendarView.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/CalendarView.kt @@ -562,7 +562,7 @@ class CalendarDatePickerState { private fun calculateCandidateDays(year: Int, monthValue: Int) { val localDate = LocalDate(year, monthValue + 1, 1) - val startDayOfWeek = localDate.dayOfWeek.isoDayNumber % 7 + 1 + val startDayOfWeek = localDate.dayOfWeek.isoDayNumber + 1 // val startDayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) // Start at Sunday(1) // Start day at the first `localeStartDayOfWeek(e.g. Sunday)` before this month From e4b9d692937d4380f34e6568840dbdccbcfe2766 Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Sat, 2 Nov 2024 01:30:55 +0100 Subject: [PATCH 052/247] Delete unused code and fix java compilation (cherry picked from commit d1cec334b78c39a06d94ea9ada08fb39f36b7f0a) --- .../component/rememberResourcePainter.kt | 20 ------------------- 1 file changed, 20 deletions(-) delete mode 100644 fluent/src/desktopMain/kotlin/com/konyaco/fluent/component/rememberResourcePainter.kt diff --git a/fluent/src/desktopMain/kotlin/com/konyaco/fluent/component/rememberResourcePainter.kt b/fluent/src/desktopMain/kotlin/com/konyaco/fluent/component/rememberResourcePainter.kt deleted file mode 100644 index ca172840..00000000 --- a/fluent/src/desktopMain/kotlin/com/konyaco/fluent/component/rememberResourcePainter.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.konyaco.fluent.component - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.ResourceLoader -import androidx.compose.ui.res.loadSvgPainter - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -fun rememberResourcePainter(resPath: String): Painter { - val density = LocalDensity.current - val svg = remember(density) { - val file = ResourceLoader.Default.load(resPath) - loadSvgPainter(file, density) - } - return svg -} \ No newline at end of file From ccfa74ee6abebe42717b05db50346e70a0e0dea3 Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Sat, 2 Nov 2024 01:32:28 +0100 Subject: [PATCH 053/247] Update .gitignore (cherry picked from commit 08b9cb1f6c1d6e51388fcc420274b893fba4a9f9) --- .gitignore | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 3f7b3d8f..6d890c17 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,15 @@ -.gradle/ -.idea/ -build/ -local.properties +*.iml +.gradle +.idea +.kotlin .DS_Store +build +*/build +captures +.externalNativeBuild +.cxx +local.properties +xcuserdata/ +Pods/ +*.jks +*yarn.lock From 7a7c85e579d91ced30a8161cdf9586a7a2a3bb5b Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Sat, 2 Nov 2024 01:47:54 +0100 Subject: [PATCH 054/247] Fix browser calendar day offset (cherry picked from commit 1d0a13935b343371cbae19412f30a23df9859726) --- .../kotlin/com/konyaco/fluent/component/CalendarView.js.kt | 2 +- .../kotlin/com/konyaco/fluent/component/CalendarView.wasmJs.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/CalendarView.js.kt b/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/CalendarView.js.kt index cd0fbd79..5c439b17 100644 --- a/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/CalendarView.js.kt +++ b/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/CalendarView.js.kt @@ -34,4 +34,4 @@ internal actual fun getLocalMonthNames(): List { } //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo -internal actual fun getLocalFirstDayOfWeek() = 1 \ No newline at end of file +internal actual fun getLocalFirstDayOfWeek() = 2 //the same as jvm \ No newline at end of file diff --git a/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/CalendarView.wasmJs.kt b/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/CalendarView.wasmJs.kt index ec5a294a..c5e94725 100644 --- a/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/CalendarView.wasmJs.kt +++ b/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/CalendarView.wasmJs.kt @@ -30,4 +30,4 @@ internal actual fun getLocalMonthNames(): List = getJsLocalMonthNames("short").split(",") //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo -internal actual fun getLocalFirstDayOfWeek() = 1 \ No newline at end of file +internal actual fun getLocalFirstDayOfWeek() = 2 //the same as jvm \ No newline at end of file From 4dc8678e440986333294a1c1cd3e6f5d72553ce0 Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Sat, 2 Nov 2024 02:21:08 +0100 Subject: [PATCH 055/247] Add iOS target and demo app (cherry picked from commit 045b30c0336337f6cea3fdac6df65f3d7471cb78) --- .../fluent/plugin/build/BuildExtension.kt | 3 + .../konyaco/fluent/DefaultFontFamily.ios.kt | 9 + .../PlatformCompositionLocalProvider.ios.kt | 9 + .../fluent/component/CalendarView.ios.kt | 16 + .../konyaco/fluent/component/Dialog.ios.kt | 20 + .../konyaco/fluent/component/FontIcon.ios.kt | 8 + .../fluent/component/PlatformScrollBar.ios.kt | 82 ++++ .../com/konyaco/fluent/component/Popup.ios.kt | 58 +++ gallery/build.gradle.kts | 12 + gallery/src/iosMain/kotlin/main.kt | 17 + iosApp/iosApp.xcodeproj/project.pbxproj | 356 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../AccentColor.colorset/Contents.json | 11 + iosApp/iosApp/Assets.xcassets/Contents.json | 6 + iosApp/iosApp/Info.plist | 8 + .../Preview Assets.xcassets/Contents.json | 6 + iosApp/iosApp/iosApp.swift | 19 + 18 files changed, 655 insertions(+) create mode 100644 fluent/src/iosMain/kotlin/com/konyaco/fluent/DefaultFontFamily.ios.kt create mode 100644 fluent/src/iosMain/kotlin/com/konyaco/fluent/PlatformCompositionLocalProvider.ios.kt create mode 100644 fluent/src/iosMain/kotlin/com/konyaco/fluent/component/CalendarView.ios.kt create mode 100644 fluent/src/iosMain/kotlin/com/konyaco/fluent/component/Dialog.ios.kt create mode 100644 fluent/src/iosMain/kotlin/com/konyaco/fluent/component/FontIcon.ios.kt create mode 100644 fluent/src/iosMain/kotlin/com/konyaco/fluent/component/PlatformScrollBar.ios.kt create mode 100644 fluent/src/iosMain/kotlin/com/konyaco/fluent/component/Popup.ios.kt create mode 100644 gallery/src/iosMain/kotlin/main.kt create mode 100644 iosApp/iosApp.xcodeproj/project.pbxproj create mode 100644 iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 iosApp/iosApp/Assets.xcassets/Contents.json create mode 100644 iosApp/iosApp/Info.plist create mode 100644 iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 iosApp/iosApp/iosApp.swift diff --git a/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildExtension.kt b/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildExtension.kt index 690ea3a3..ea279ed4 100644 --- a/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildExtension.kt +++ b/build-plugin/src/main/java/com/konyaco/fluent/plugin/build/BuildExtension.kt @@ -12,4 +12,7 @@ fun KotlinMultiplatformExtension.applyTargets(publish: Boolean = true) { jvmToolchain(BuildConfig.Jvm.jvmToolchainVersion) wasmJs { browser() } js { browser() } + iosX64() + iosArm64() + iosSimulatorArm64() } \ No newline at end of file diff --git a/fluent/src/iosMain/kotlin/com/konyaco/fluent/DefaultFontFamily.ios.kt b/fluent/src/iosMain/kotlin/com/konyaco/fluent/DefaultFontFamily.ios.kt new file mode 100644 index 00000000..d8df06b4 --- /dev/null +++ b/fluent/src/iosMain/kotlin/com/konyaco/fluent/DefaultFontFamily.ios.kt @@ -0,0 +1,9 @@ +package com.konyaco.fluent + +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.font.FontFamily + +@Composable +actual fun defaultFontFamily(): FontFamily? { + return null +} \ No newline at end of file diff --git a/fluent/src/iosMain/kotlin/com/konyaco/fluent/PlatformCompositionLocalProvider.ios.kt b/fluent/src/iosMain/kotlin/com/konyaco/fluent/PlatformCompositionLocalProvider.ios.kt new file mode 100644 index 00000000..dd9c8669 --- /dev/null +++ b/fluent/src/iosMain/kotlin/com/konyaco/fluent/PlatformCompositionLocalProvider.ios.kt @@ -0,0 +1,9 @@ +package com.konyaco.fluent + +import androidx.compose.runtime.Composable + +@Composable +actual fun PlatformCompositionLocalProvider(content: @Composable () -> Unit) { + content() +} + diff --git a/fluent/src/iosMain/kotlin/com/konyaco/fluent/component/CalendarView.ios.kt b/fluent/src/iosMain/kotlin/com/konyaco/fluent/component/CalendarView.ios.kt new file mode 100644 index 00000000..4387c279 --- /dev/null +++ b/fluent/src/iosMain/kotlin/com/konyaco/fluent/component/CalendarView.ios.kt @@ -0,0 +1,16 @@ +package com.konyaco.fluent.component + +import platform.Foundation.NSCalendar +import platform.Foundation.NSCalendarIdentifierGregorian +import platform.Foundation.NSDateFormatter + +internal actual fun getLocalDayOfWeekNames(): List { + return NSCalendar(NSCalendarIdentifierGregorian).weekdaySymbols.map { it.toString() } +} + +internal actual fun getLocalMonthNames(): List { + return NSDateFormatter().monthSymbols.map { it.toString().take(3) } //TODO +} + +//https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo +internal actual fun getLocalFirstDayOfWeek() = 2 //the same as jvm \ No newline at end of file diff --git a/fluent/src/iosMain/kotlin/com/konyaco/fluent/component/Dialog.ios.kt b/fluent/src/iosMain/kotlin/com/konyaco/fluent/component/Dialog.ios.kt new file mode 100644 index 00000000..0f495dd0 --- /dev/null +++ b/fluent/src/iosMain/kotlin/com/konyaco/fluent/component/Dialog.ios.kt @@ -0,0 +1,20 @@ +package com.konyaco.fluent.component + +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.window.PopupPositionProvider + +internal actual val DialogPopupPositionProvider: PopupPositionProvider = DialogPopupPositionProviderImpl + +internal object DialogPopupPositionProviderImpl : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize + ): IntOffset { + return IntOffset.Zero + } +} \ No newline at end of file diff --git a/fluent/src/iosMain/kotlin/com/konyaco/fluent/component/FontIcon.ios.kt b/fluent/src/iosMain/kotlin/com/konyaco/fluent/component/FontIcon.ios.kt new file mode 100644 index 00000000..62808826 --- /dev/null +++ b/fluent/src/iosMain/kotlin/com/konyaco/fluent/component/FontIcon.ios.kt @@ -0,0 +1,8 @@ +package com.konyaco.fluent.component + +import androidx.compose.runtime.Composable + +@Composable +internal actual fun ProvideFontIcon(content: @Composable () -> Unit) { + content() +} \ No newline at end of file diff --git a/fluent/src/iosMain/kotlin/com/konyaco/fluent/component/PlatformScrollBar.ios.kt b/fluent/src/iosMain/kotlin/com/konyaco/fluent/component/PlatformScrollBar.ios.kt new file mode 100644 index 00000000..0a68844b --- /dev/null +++ b/fluent/src/iosMain/kotlin/com/konyaco/fluent/component/PlatformScrollBar.ios.kt @@ -0,0 +1,82 @@ +package com.konyaco.fluent.component + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +actual interface ScrollbarAdapter { + /** + * Scroll offset of the content inside the scrollable component. + * + * For example, a value of `100` could mean the content is scrolled by 100 pixels from the + * start. + */ + actual val scrollOffset: Double + + /** + * The size of the scrollable content, on the scrollable axis. + */ + actual val contentSize: Double + + /** + * The size of the viewport, on the scrollable axis. + */ + actual val viewportSize: Double + + /** + * Instantly jump to [scrollOffset]. + * + * @param scrollOffset target offset to jump to, value will be coerced to the valid + * scroll range. + */ + actual suspend fun scrollTo(scrollOffset: Double) + +} +@Composable +internal actual fun PlatformScrollBar( + isVertical: Boolean, + adapter: ScrollbarAdapter, + modifier: Modifier, + reverseLayout: Boolean, + colors: ScrollbarColors +) { + //TODO Scrollbar browser implementation +} + +@Composable +actual fun rememberScrollbarAdapter( + state: ScrollState +): ScrollbarAdapter { + return EmptyScrollbarAdapter +} + +@Composable +actual fun rememberScrollbarAdapter( + state: LazyListState +): ScrollbarAdapter { + return EmptyScrollbarAdapter +} + +@Composable +actual fun rememberScrollbarAdapter( + state: LazyGridState +): ScrollbarAdapter { + return EmptyScrollbarAdapter +} + +private object EmptyScrollbarAdapter: ScrollbarAdapter { + override val contentSize: Double + get() = 0.0 + + override val scrollOffset: Double + get() = 0.0 + + override val viewportSize: Double + get() = 0.0 + + override suspend fun scrollTo(scrollOffset: Double) { + + } +} \ No newline at end of file diff --git a/fluent/src/iosMain/kotlin/com/konyaco/fluent/component/Popup.ios.kt b/fluent/src/iosMain/kotlin/com/konyaco/fluent/component/Popup.ios.kt new file mode 100644 index 00000000..8079b839 --- /dev/null +++ b/fluent/src/iosMain/kotlin/com/konyaco/fluent/component/Popup.ios.kt @@ -0,0 +1,58 @@ +package com.konyaco.fluent.component + +import androidx.compose.runtime.* +import androidx.compose.ui.input.key.KeyEvent +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.window.PopupPositionProvider +import androidx.compose.ui.window.PopupProperties + +@Composable +internal actual fun Popup( + popupPositionProvider: PopupPositionProvider, + onDismissRequest: (() -> Unit)?, + properties: PopupProperties, + onPreviewKeyEvent: ((KeyEvent) -> Boolean)?, + onKeyEvent: ((KeyEvent) -> Boolean)?, + content: @Composable () -> Unit +) { + val offset = LocalPopupOffset.current + val delegatePopupPositionProvider = remember(popupPositionProvider) { + DelegatePopupPositionProvider({ offset }, popupPositionProvider) + } + androidx.compose.ui.window.Popup(delegatePopupPositionProvider, onDismissRequest, properties) { + CompositionLocalProvider( + LocalPopupOffset provides delegatePopupPositionProvider.currentOffset, + content = content + ) + } +} + +// Workaround for android nested popup position calculate +private val LocalPopupOffset = staticCompositionLocalOf { IntOffset.Zero } + +private class DelegatePopupPositionProvider( + val offset: () -> IntOffset, + val positionProvider: PopupPositionProvider +): PopupPositionProvider { + + var currentOffset by mutableStateOf(IntOffset.Zero) + + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize + ): IntOffset { + return positionProvider.calculatePosition( + anchorBounds.translate(offset()), + windowSize, + layoutDirection, + popupContentSize + ).apply { + currentOffset = this + } + } +} \ No newline at end of file diff --git a/gallery/build.gradle.kts b/gallery/build.gradle.kts index db96fc5d..6f78ed0a 100644 --- a/gallery/build.gradle.kts +++ b/gallery/build.gradle.kts @@ -15,6 +15,18 @@ kotlin { applyTargets(publish = false) wasmJs { binaries.executable() } js { binaries.executable() } + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { + it.binaries.framework { + baseName = "ComposeApp" + isStatic = true + } + } + sourceSets { val commonMain by getting { dependencies { diff --git a/gallery/src/iosMain/kotlin/main.kt b/gallery/src/iosMain/kotlin/main.kt new file mode 100644 index 00000000..51013b73 --- /dev/null +++ b/gallery/src/iosMain/kotlin/main.kt @@ -0,0 +1,17 @@ +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.safeContent +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.ui.Modifier +import androidx.compose.ui.window.ComposeUIViewController +import com.konyaco.fluent.gallery.App +import com.konyaco.fluent.gallery.GalleryTheme +import platform.UIKit.UIViewController + +fun MainViewController(): UIViewController = ComposeUIViewController { + Box(Modifier.windowInsetsPadding(WindowInsets.safeContent)) { + GalleryTheme { + App() + } + } +} diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj new file mode 100644 index 00000000..81551922 --- /dev/null +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -0,0 +1,356 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + A93A953B29CC810C00F8E227 /* iosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93A953A29CC810C00F8E227 /* iosApp.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A93A953729CC810C00F8E227 /* FluentDesign.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FluentDesign.app; sourceTree = BUILT_PRODUCTS_DIR; }; + A93A953A29CC810C00F8E227 /* iosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iosApp.swift; sourceTree = ""; }; + A93A953E29CC810D00F8E227 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + A93A954129CC810D00F8E227 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A93A953429CC810C00F8E227 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A93A952E29CC810C00F8E227 = { + isa = PBXGroup; + children = ( + A93A953929CC810C00F8E227 /* iosApp */, + A93A953829CC810C00F8E227 /* Products */, + C4127409AE3703430489E7BC /* Frameworks */, + ); + sourceTree = ""; + }; + A93A953829CC810C00F8E227 /* Products */ = { + isa = PBXGroup; + children = ( + A93A953729CC810C00F8E227 /* FluentDesign.app */, + ); + name = Products; + sourceTree = ""; + }; + A93A953929CC810C00F8E227 /* iosApp */ = { + isa = PBXGroup; + children = ( + A93A953A29CC810C00F8E227 /* iosApp.swift */, + A93A953E29CC810D00F8E227 /* Assets.xcassets */, + A93A954029CC810D00F8E227 /* Preview Content */, + ); + path = iosApp; + sourceTree = ""; + }; + A93A954029CC810D00F8E227 /* Preview Content */ = { + isa = PBXGroup; + children = ( + A93A954129CC810D00F8E227 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + C4127409AE3703430489E7BC /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + A93A953629CC810C00F8E227 /* iosApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = A93A954529CC810D00F8E227 /* Build configuration list for PBXNativeTarget "iosApp" */; + buildPhases = ( + A9D80A052AAB5CDE006C8738 /* ShellScript */, + A93A953329CC810C00F8E227 /* Sources */, + A93A953429CC810C00F8E227 /* Frameworks */, + A93A953529CC810C00F8E227 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = iosApp; + productName = iosApp; + productReference = A93A953729CC810C00F8E227 /* FluentDesign.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A93A952F29CC810C00F8E227 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1420; + LastUpgradeCheck = 1420; + TargetAttributes = { + A93A953629CC810C00F8E227 = { + CreatedOnToolsVersion = 14.2; + }; + }; + }; + buildConfigurationList = A93A953229CC810C00F8E227 /* Build configuration list for PBXProject "iosApp" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = A93A952E29CC810C00F8E227; + productRefGroup = A93A953829CC810C00F8E227 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A93A953629CC810C00F8E227 /* iosApp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A93A953529CC810C00F8E227 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + A9D80A052AAB5CDE006C8738 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "cd \"$SRCROOT/..\"\n./gradlew :gallery:embedAndSignAppleFrameworkForXcode\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A93A953329CC810C00F8E227 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A93A953B29CC810C00F8E227 /* iosApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A93A954329CC810D00F8E227 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + A93A954429CC810D00F8E227 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + A93A954629CC810D00F8E227 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = iosApp/Info.plist; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.konyaco.fluent.gallery.iosApp; + PRODUCT_NAME = FluentDesign; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + A93A954729CC810D00F8E227 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = iosApp/Info.plist; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.konyaco.fluent.gallery.iosApp; + PRODUCT_NAME = FluentDesign; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A93A953229CC810C00F8E227 /* Build configuration list for PBXProject "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A93A954329CC810D00F8E227 /* Debug */, + A93A954429CC810D00F8E227 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A93A954529CC810D00F8E227 /* Build configuration list for PBXNativeTarget "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A93A954629CC810D00F8E227 /* Debug */, + A93A954729CC810D00F8E227 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = A93A952F29CC810C00F8E227 /* Project object */; +} diff --git a/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/iosApp/Assets.xcassets/Contents.json b/iosApp/iosApp/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist new file mode 100644 index 00000000..11845e1d --- /dev/null +++ b/iosApp/iosApp/Info.plist @@ -0,0 +1,8 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + + diff --git a/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json b/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/iosApp/iosApp.swift b/iosApp/iosApp/iosApp.swift new file mode 100644 index 00000000..fbf3ac08 --- /dev/null +++ b/iosApp/iosApp/iosApp.swift @@ -0,0 +1,19 @@ +import UIKit +import ComposeApp + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + window = UIWindow(frame: UIScreen.main.bounds) + if let window = window { + window.rootViewController = MainKt.MainViewController() + window.makeKeyAndVisible() + } + return true + } +} From f6b1fb807c681d1733d09aff465c46083f079047 Mon Sep 17 00:00:00 2001 From: KonYaco Date: Sat, 2 Nov 2024 13:17:03 +0800 Subject: [PATCH 056/247] Fix Calendar first day of week in JS target (cherry picked from commit a17830de7fbc8a22b54c3dca1889cf3cb7f41d37) --- .../com/konyaco/fluent/component/CalendarView.kt | 5 +++++ .../konyaco/fluent/component/CalendarView.js.kt | 14 +++++++++++--- .../fluent/component/CalendarView.wasmJs.kt | 15 +++++++++++---- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/CalendarView.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/CalendarView.kt index c6ce1721..944dc559 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/CalendarView.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/CalendarView.kt @@ -757,4 +757,9 @@ class CalendarDatePickerState { expect internal fun getLocalDayOfWeekNames(): List expect internal fun getLocalMonthNames(): List + +/** + * Get the first day of week + * Sunday(1), Monday(2), ..., Saturday(7) + */ expect internal fun getLocalFirstDayOfWeek(): Int \ No newline at end of file diff --git a/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/CalendarView.js.kt b/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/CalendarView.js.kt index 5c439b17..a7e221e8 100644 --- a/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/CalendarView.js.kt +++ b/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/CalendarView.js.kt @@ -4,7 +4,7 @@ internal actual fun getLocalDayOfWeekNames(): List { val jsFun: String = js( """ var format = new Intl.DateTimeFormat(navigator.language, { weekday: 'short' }) - var baseDate = new Date(Date.UTC(2017, 0, 2)) // just a Monday + var baseDate = new Date(Date.UTC(2017, 0, 1)) // just a Sunday var weekDays = [] for (var day = 0; day < 7; day++) { weekDays.push(format.format(baseDate)) @@ -33,5 +33,13 @@ internal actual fun getLocalMonthNames(): List { return jsFun.split(",") } -//https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo -internal actual fun getLocalFirstDayOfWeek() = 2 //the same as jvm \ No newline at end of file +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo +internal actual fun getLocalFirstDayOfWeek(): Int { + // zh-CN -> 1 -> Monday -> 2 + // en-US -> 7 -> Sunday -> 1 + return js(""" + var weekInfo = new Intl.Locale(navigator.language).getWeekInfo() + var firstDay = weekInfo.firstDay + firstDay % 7 + 1 + """) as Int +} \ No newline at end of file diff --git a/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/CalendarView.wasmJs.kt b/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/CalendarView.wasmJs.kt index c5e94725..57127745 100644 --- a/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/CalendarView.wasmJs.kt +++ b/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/CalendarView.wasmJs.kt @@ -2,9 +2,9 @@ package com.konyaco.fluent.component @JsFun("""(style) => { var format = new Intl.DateTimeFormat(navigator.language, { weekday: style }) - var baseDate = new Date(Date.UTC(2017, 0, 2)) // just a Monday + var baseDate = new Date(Date.UTC(2017, 0, 1)) // just a Sunday var weekDays = [] - for (var day = 0; day < 7; day++) { + for (var day = 0; day < 7; day++) { weekDays.push(format.format(baseDate)) baseDate.setDate(baseDate.getDate() + 1) } @@ -29,5 +29,12 @@ private external fun getJsLocalMonthNames(format: String): String internal actual fun getLocalMonthNames(): List = getJsLocalMonthNames("short").split(",") -//https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo -internal actual fun getLocalFirstDayOfWeek() = 2 //the same as jvm \ No newline at end of file +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo +internal actual fun getLocalFirstDayOfWeek(): Int = getJsLocalFirstDayOfWeek() + +@JsFun("""() => { + var weekInfo = new Intl.Locale(navigator.language).getWeekInfo() + var firstDay = weekInfo.firstDay + return firstDay % 7 + 1 +}""") +private external fun getJsLocalFirstDayOfWeek(): Int \ No newline at end of file From 4e667fc51f41cb94d5e1ec2d1b06125584bae9c3 Mon Sep 17 00:00:00 2001 From: KonYaco Date: Sat, 2 Nov 2024 13:46:06 +0800 Subject: [PATCH 057/247] Add first day of week fallback for Firefox (cherry picked from commit 5802130a1dbe9cd4428eb27ea65686b1747a2661) --- .../fluent/component/CalendarView.js.kt | 129 ++++++++++++++++- .../fluent/component/CalendarView.wasmJs.kt | 132 +++++++++++++++++- 2 files changed, 251 insertions(+), 10 deletions(-) diff --git a/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/CalendarView.js.kt b/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/CalendarView.js.kt index a7e221e8..59878c8b 100644 --- a/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/CalendarView.js.kt +++ b/fluent/src/jsMain/kotlin/com/konyaco/fluent/component/CalendarView.js.kt @@ -37,9 +37,128 @@ internal actual fun getLocalMonthNames(): List { internal actual fun getLocalFirstDayOfWeek(): Int { // zh-CN -> 1 -> Monday -> 2 // en-US -> 7 -> Sunday -> 1 - return js(""" - var weekInfo = new Intl.Locale(navigator.language).getWeekInfo() - var firstDay = weekInfo.firstDay - firstDay % 7 + 1 - """) as Int + val language = js("navigator.language") as String + val result = js(""" + var result = null + try { + var weekInfo = new Intl.Locale(navigator.language).getWeekInfo() + var firstDay = weekInfo.firstDay + result = firstDay % 7 + 1 + } catch(e) { } + result + """) as Int? ?: fallbackGetLocalFistDayOfWeek(language) + return result +} + +private val localeToFirstDayMap by lazy { + mapOf( + "en-US" to 0, // Sunday + "en-GB" to 1, // Monday + "zh-CN" to 1, // Monday + "fr-FR" to 1, // Monday + "de-DE" to 1, // Monday + "es-ES" to 1, // Monday + "it-IT" to 1, // Monday + "ja-JP" to 0, // Sunday + "ko-KR" to 0, // Sunday + "ru-RU" to 1, // Monday + "ar-SA" to 6, // Saturday + "he-IL" to 0, // Sunday + "af-ZA" to 0, // Sunday + "am-ET" to 1, // Monday + "as-IN" to 1, // Monday + "az-Cyrl" to 1, // Monday + "az-Latn" to 1, // Monday + "be-BY" to 1, // Monday + "bn-BD" to 1, // Monday + "bn-IN" to 1, // Monday + "bs-Cyrl" to 1, // Monday + "bs-Latn" to 1, // Monday + "ca-ES" to 1, // Monday + "ce-RU" to 1, // Monday + "cs-CZ" to 1, // Monday + "cy-GB" to 1, // Monday + "da-DK" to 1, // Monday + "de-AT" to 1, // Monday + "de-LI" to 1, // Monday + "de-LU" to 1, // Monday + "el-CY" to 1, // Monday + "el-GR" to 1, // Monday + "en-CA" to 0, // Sunday + "en-IN" to 0, // Sunday + "en-IE" to 1, // Monday + "es-MX" to 1, // Monday + "es-US" to 1, // Monday + "et-EE" to 1, // Monday + "eu-ES" to 1, // Monday + "fa-IR" to 6, // Saturday + "fi-FI" to 1, // Monday + "fr-CA" to 1, // Monday + "ga-IE" to 1, // Monday + "gd-GB" to 1, // Monday + "gl-ES" to 1, // Monday + "gu-IN" to 1, // Monday + "he-IL" to 0, // Sunday + "hi-IN" to 1, // Monday + "hr-HR" to 1, // Monday + "hu-HU" to 1, // Monday + "hy-AM" to 1, // Monday + "id-ID" to 1, // Monday + "is-IS" to 1, // Monday + "it-CH" to 1, // Monday + "iw-IL" to 0, // Sunday + "ja-JP" to 0, // Sunday + "ka-GE" to 1, // Monday + "kk-KZ" to 1, // Monday + "km-KH" to 1, // Monday + "kn-IN" to 1, // Monday + "ko-KR" to 0, // Sunday + "ky-KG" to 1, // Monday + "lo-LA" to 1, // Monday + "lt-LT" to 1, // Monday + "lv-LV" to 1, // Monday + "mk-MK" to 1, // Monday + "ml-IN" to 1, // Monday + "mn-MN" to 1, // Monday + "mr-IN" to 1, // Monday + "ms-MY" to 1, // Monday + "mt-MT" to 1, // Monday + "ne-NP" to 1, // Monday + "nl-BE" to 1, // Monday + "nl-NL" to 1, // Monday + "no-NO" to 1, // Monday + "pa-IN" to 1, // Monday + "pl-PL" to 1, // Monday + "pt-BR" to 1, // Monday + "pt-PT" to 1, // Monday + "ro-RO" to 1, // Monday + "ru-RU" to 1, // Monday + "si-LK" to 1, // Monday + "sk-SK" to 1, // Monday + "sl-SI" to 1, // Monday + "sq-AL" to 1, // Monday + "sr-Cyrl" to 1, // Monday + "sr-Latn" to 1, // Monday + "sv-SE" to 1, // Monday + "sw-KE" to 1, // Monday + "ta-IN" to 1, // Monday + "te-IN" to 1, // Monday + "th-TH" to 1, // Monday + "tr-TR" to 1, // Monday + "uk-UA" to 1, // Monday + "ur-PK" to 1, // Monday + "uz-Cyrl" to 1, // Monday + "vi-VN" to 1, // Monday + "zh-HK" to 1, // Monday + "zh-MO" to 1, // Monday + "zh-SG" to 1, // Monday + "zh-TW" to 1, // Monday + ) +} + +private const val defaultFirstDay = 1 + +// Workaround for Firefox +private fun fallbackGetLocalFistDayOfWeek(locale: String): Int { + return localeToFirstDayMap.getOrElse(locale) { defaultFirstDay } + 1 } \ No newline at end of file diff --git a/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/CalendarView.wasmJs.kt b/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/CalendarView.wasmJs.kt index 57127745..130b9ca3 100644 --- a/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/CalendarView.wasmJs.kt +++ b/fluent/src/wasmJsMain/kotlin/com/konyaco/fluent/component/CalendarView.wasmJs.kt @@ -30,11 +30,133 @@ internal actual fun getLocalMonthNames(): List = getJsLocalMonthNames("short").split(",") // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo -internal actual fun getLocalFirstDayOfWeek(): Int = getJsLocalFirstDayOfWeek() +internal actual fun getLocalFirstDayOfWeek(): Int { + return getJsLocalFirstDayOfWeek() ?: fallbackGetLocalFistDayOfWeek(getJsLanguage()) +} @JsFun("""() => { - var weekInfo = new Intl.Locale(navigator.language).getWeekInfo() - var firstDay = weekInfo.firstDay - return firstDay % 7 + 1 + var result = null + try { + var weekInfo = new Intl.Locale(navigator.language).getWeekInfo() + var firstDay = weekInfo.firstDay + result = firstDay % 7 + 1 + } catch(e) { } + return result }""") -private external fun getJsLocalFirstDayOfWeek(): Int \ No newline at end of file +private external fun getJsLocalFirstDayOfWeek(): Int? + +@JsFun("""() => navigator.language""") +private external fun getJsLanguage(): String + +private val localeToFirstDayMap by lazy { + mapOf( + "en-US" to 0, // Sunday + "en-GB" to 1, // Monday + "zh-CN" to 1, // Monday + "fr-FR" to 1, // Monday + "de-DE" to 1, // Monday + "es-ES" to 1, // Monday + "it-IT" to 1, // Monday + "ja-JP" to 0, // Sunday + "ko-KR" to 0, // Sunday + "ru-RU" to 1, // Monday + "ar-SA" to 6, // Saturday + "he-IL" to 0, // Sunday + "af-ZA" to 0, // Sunday + "am-ET" to 1, // Monday + "as-IN" to 1, // Monday + "az-Cyrl" to 1, // Monday + "az-Latn" to 1, // Monday + "be-BY" to 1, // Monday + "bn-BD" to 1, // Monday + "bn-IN" to 1, // Monday + "bs-Cyrl" to 1, // Monday + "bs-Latn" to 1, // Monday + "ca-ES" to 1, // Monday + "ce-RU" to 1, // Monday + "cs-CZ" to 1, // Monday + "cy-GB" to 1, // Monday + "da-DK" to 1, // Monday + "de-AT" to 1, // Monday + "de-LI" to 1, // Monday + "de-LU" to 1, // Monday + "el-CY" to 1, // Monday + "el-GR" to 1, // Monday + "en-CA" to 0, // Sunday + "en-IN" to 0, // Sunday + "en-IE" to 1, // Monday + "es-MX" to 1, // Monday + "es-US" to 1, // Monday + "et-EE" to 1, // Monday + "eu-ES" to 1, // Monday + "fa-IR" to 6, // Saturday + "fi-FI" to 1, // Monday + "fr-CA" to 1, // Monday + "ga-IE" to 1, // Monday + "gd-GB" to 1, // Monday + "gl-ES" to 1, // Monday + "gu-IN" to 1, // Monday + "he-IL" to 0, // Sunday + "hi-IN" to 1, // Monday + "hr-HR" to 1, // Monday + "hu-HU" to 1, // Monday + "hy-AM" to 1, // Monday + "id-ID" to 1, // Monday + "is-IS" to 1, // Monday + "it-CH" to 1, // Monday + "iw-IL" to 0, // Sunday + "ja-JP" to 0, // Sunday + "ka-GE" to 1, // Monday + "kk-KZ" to 1, // Monday + "km-KH" to 1, // Monday + "kn-IN" to 1, // Monday + "ko-KR" to 0, // Sunday + "ky-KG" to 1, // Monday + "lo-LA" to 1, // Monday + "lt-LT" to 1, // Monday + "lv-LV" to 1, // Monday + "mk-MK" to 1, // Monday + "ml-IN" to 1, // Monday + "mn-MN" to 1, // Monday + "mr-IN" to 1, // Monday + "ms-MY" to 1, // Monday + "mt-MT" to 1, // Monday + "ne-NP" to 1, // Monday + "nl-BE" to 1, // Monday + "nl-NL" to 1, // Monday + "no-NO" to 1, // Monday + "pa-IN" to 1, // Monday + "pl-PL" to 1, // Monday + "pt-BR" to 1, // Monday + "pt-PT" to 1, // Monday + "ro-RO" to 1, // Monday + "ru-RU" to 1, // Monday + "si-LK" to 1, // Monday + "sk-SK" to 1, // Monday + "sl-SI" to 1, // Monday + "sq-AL" to 1, // Monday + "sr-Cyrl" to 1, // Monday + "sr-Latn" to 1, // Monday + "sv-SE" to 1, // Monday + "sw-KE" to 1, // Monday + "ta-IN" to 1, // Monday + "te-IN" to 1, // Monday + "th-TH" to 1, // Monday + "tr-TR" to 1, // Monday + "uk-UA" to 1, // Monday + "ur-PK" to 1, // Monday + "uz-Cyrl" to 1, // Monday + "vi-VN" to 1, // Monday + "zh-HK" to 1, // Monday + "zh-MO" to 1, // Monday + "zh-SG" to 1, // Monday + "zh-TW" to 1, // Monday + ) +} + +private const val defaultFirstDay = 1 + +// Workaround for Firefox +private fun fallbackGetLocalFistDayOfWeek(locale: String): Int { + return localeToFirstDayMap.getOrElse(locale) { defaultFirstDay } + 1 +} \ No newline at end of file From 4d96e9a925fca59371d7d15f3b9b761346b87142 Mon Sep 17 00:00:00 2001 From: KonYaco Date: Sat, 2 Nov 2024 14:58:02 +0800 Subject: [PATCH 058/247] Add workflow to deploy Github Pages (cherry picked from commit 292bf503a941268bccfdf1e5774ade6483f82740) --- .github/workflows/deploy-gh-pages.yml | 54 +++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/deploy-gh-pages.yml diff --git a/.github/workflows/deploy-gh-pages.yml b/.github/workflows/deploy-gh-pages.yml new file mode 100644 index 00000000..52d0f308 --- /dev/null +++ b/.github/workflows/deploy-gh-pages.yml @@ -0,0 +1,54 @@ +name: Deploy Github Pages +on: + push: + branches: [ "master" ] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Run gradle wasmJsBrowserDistribution task + build: + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + - name: Set up Gradle + uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0 + - name: Build wasm target + run: ./gradlew :gallery:wasmJsBrowserDistribution + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # Upload dist + path: ./gallery/build/dist/wasmJs/productionExecutable + + # Single deploy job since we're just deploying + deploy: + needs: build + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 From 7c29aec08816eccf396e548a961f7429c1776ee3 Mon Sep 17 00:00:00 2001 From: KonYaco Date: Sat, 2 Nov 2024 15:11:48 +0800 Subject: [PATCH 059/247] Remove `skiko.js` dependency in wasm target (cherry picked from commit e4cf68145042a01b18cbaf04370ba8d8e0012ebd) --- gallery/src/wasmJsMain/resources/index.html | 1 - 1 file changed, 1 deletion(-) diff --git a/gallery/src/wasmJsMain/resources/index.html b/gallery/src/wasmJsMain/resources/index.html index fd7d72fd..e17b029d 100644 --- a/gallery/src/wasmJsMain/resources/index.html +++ b/gallery/src/wasmJsMain/resources/index.html @@ -4,7 +4,6 @@ Compose Fluent Design Gallery -
From 38ed63adbaa1e5ec76a7bab25cbc09e712187f98 Mon Sep 17 00:00:00 2001 From: KonYaco Date: Sat, 2 Nov 2024 15:53:45 +0800 Subject: [PATCH 060/247] Add browser icon metadata (cherry picked from commit 38014b113756982ee09c6a1d1b1eee6b73102ed1) --- gallery/src/jsMain/resources/index.html | 1 + gallery/src/wasmJsMain/resources/index.html | 1 + 2 files changed, 2 insertions(+) diff --git a/gallery/src/jsMain/resources/index.html b/gallery/src/jsMain/resources/index.html index 3b4799e4..ce998f71 100644 --- a/gallery/src/jsMain/resources/index.html +++ b/gallery/src/jsMain/resources/index.html @@ -4,6 +4,7 @@ Compose Fluent Design Gallery +