diff --git a/fluent/src/androidMain/kotlin/com/konyaco/fluent/component/AutoSuggestBox.android.kt b/fluent/src/androidMain/kotlin/com/konyaco/fluent/component/Flyout.android.kt similarity index 93% rename from fluent/src/androidMain/kotlin/com/konyaco/fluent/component/AutoSuggestBox.android.kt rename to fluent/src/androidMain/kotlin/com/konyaco/fluent/component/Flyout.android.kt index 09e4c2da..4f8727e8 100644 --- a/fluent/src/androidMain/kotlin/com/konyaco/fluent/component/AutoSuggestBox.android.kt +++ b/fluent/src/androidMain/kotlin/com/konyaco/fluent/component/Flyout.android.kt @@ -15,7 +15,7 @@ import kotlin.math.max import kotlin.math.roundToInt @Composable -internal actual fun rememberSuggestFlyoutCalculateMaxHeight(padding: Dp): (anchorCoordinates: LayoutCoordinates) -> Int { +actual fun rememberFlyoutCalculateMaxHeight(padding: Dp): (LayoutCoordinates) -> Int { val config = LocalConfiguration.current val view = LocalView.current val verticalMargin = with(LocalDensity.current) { padding.roundToPx() } 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 f2720032..7372474d 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/AutoSuggestBox.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/AutoSuggestBox.kt @@ -14,82 +14,41 @@ 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.runtime.rememberUpdatedState 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 +import com.konyaco.fluent.ExperimentalFluentApi import com.konyaco.fluent.FluentTheme +@OptIn(ExperimentalFluentApi::class) @Composable fun AutoSuggestionBox( expanded: Boolean, onExpandedChange: (Boolean) -> Unit, modifier: Modifier = Modifier, - content: @Composable AutoSuggestBoxScope.() -> Unit + content: @Composable FlyoutAnchorScope.() -> 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) - } - } - - } + val flyoutAnchorScope = rememberFlyoutAnchorScope() + val expandedState = rememberUpdatedState(expanded) + val autoSuggestBoxScopeImpl = remember(expandedState, onExpandedChange) { + AutoSuggestBoxScopeImpl( + onExpandedChange = onExpandedChange, + expanded = { expandedState.value }, + scope = flyoutAnchorScope + ) } Box(modifier = modifier) { autoSuggestBoxScopeImpl.content() } SideEffect { - if (expanded) focusRequester.requestFocus() + if (expanded) autoSuggestBoxScopeImpl.focusRequester.requestFocus() } } @@ -179,11 +138,28 @@ object AutoSuggestBoxDefaults { } -interface AutoSuggestBoxScope { - fun Modifier.suggestFlyoutAnchor(): Modifier - - fun Modifier.suggestFlyoutSize(matchTextFieldWidth: Boolean = true): Modifier -} +@ExperimentalFluentApi +private class AutoSuggestBoxScopeImpl( + private val onExpandedChange: (Boolean) -> Unit, + private val expanded: () -> Boolean, + private val scope: FlyoutAnchorScope +): FlyoutAnchorScope { + + val focusRequester = FocusRequester() + + override fun Modifier.flyoutAnchor(): Modifier = with(scope) { + flyoutAnchor().pointerInput(onExpandedChange) { + awaitEachGesture { + awaitFirstDown(pass = PointerEventPass.Initial) + val upEvent = waitForUpOrCancellation(pass = PointerEventPass.Initial) + if (upEvent != null) { + onExpandedChange(!expanded()) + } + } + }.focusRequester(focusRequester) + } -@Composable -internal expect fun rememberSuggestFlyoutCalculateMaxHeight(padding: Dp): (anchorCoordinates: LayoutCoordinates) -> Int \ No newline at end of file + override fun Modifier.flyoutSize(matchAnchorWidth: Boolean): Modifier { + return with(scope) { flyoutSize(matchAnchorWidth) } + } +} \ No newline at end of file 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 31bd89b5..a5d2a136 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/ColorPicker.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/ColorPicker.kt @@ -1,5 +1,6 @@ package com.konyaco.fluent.component +import androidx.compose.animation.core.MutableTransitionState import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -13,6 +14,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable +import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -41,11 +43,17 @@ import androidx.compose.ui.input.pointer.pointerInput 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.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue 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.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.LocalContentColor import com.konyaco.fluent.background.BackgroundSizing @@ -140,7 +148,9 @@ fun ColorPicker( }, track = { }, thumb = { state -> - SliderDefaults.Thumb(state, color = FluentTheme.colors.text.text.primary) + SliderDefaults.Thumb(state, color = FluentTheme.colors.text.text.primary, label = { + Text("Value ${(state.value * 100).roundToInt()}") + }) } ) if (alphaEnabled) { @@ -155,7 +165,7 @@ fun ColorPicker( Spacer( modifier = Modifier .fillMaxWidth() - .height(12.dp) + .requiredHeight(12.dp) .alphaBackground(CircleShape) .background( Brush.horizontalGradient( @@ -167,7 +177,9 @@ fun ColorPicker( }, track = {}, thumb = { state -> - SliderDefaults.Thumb(state, color = FluentTheme.colors.text.text.primary) + SliderDefaults.Thumb(state, color = FluentTheme.colors.text.text.primary, label = { + Text("${(state.value * 100).roundToInt()}% Opacity") + }) } ) } @@ -482,9 +494,23 @@ object ColorPickerDefaults { ) } + @OptIn(ExperimentalFluentApi::class, ExperimentalStdlibApi::class) @Composable fun label(color: Color) { - //TODO Tooltip label + val hexFormat = remember { + HexFormat { + upperCase = true + number { + removeLeadingZeros = false + } + } + } + TooltipBoxDefaults.Tooltip( + visibleState = remember { MutableTransitionState(true) }, + content = { + Text("#${color.value.toHexString(hexFormat).substring(2, 8)}") + } + ) } } @@ -534,6 +560,38 @@ sealed class ColorSpectrum { companion object { val Default: ColorSpectrum get() = Square + + @Composable + internal fun LabelPopup( + offsetState: State, + label: @Composable () -> Unit + ) { + if (offsetState.value != null) { + val padding = with(LocalDensity.current) { 24.dp.toPx() } + Popup( + properties = PopupProperties(focusable = false), + popupPositionProvider = remember(offsetState) { + object : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize + ): IntOffset { + var (offsetX, offsetY) = anchorBounds.topCenter + offsetX += - popupContentSize.width / 2 + offsetY += - popupContentSize.height + return IntOffset( + x = offsetX.coerceIn(0, windowSize.width - popupContentSize.width), + y = offsetY.minus(padding).roundToInt().coerceIn(0, windowSize.height - popupContentSize.height) + ) + } + } + }, + content = label + ) + } + } } data object Round : ColorSpectrum() { @@ -644,6 +702,12 @@ sealed class ColorSpectrum { } ) { dot() + LabelPopup( + offsetState = offset, + label = { + label(color) + } + ) } } @@ -688,6 +752,7 @@ sealed class ColorSpectrum { data object Square : ColorSpectrum() { + @OptIn(ExperimentalFluentApi::class) @Composable override fun content( modifier: Modifier, @@ -794,6 +859,7 @@ sealed class ColorSpectrum { } } + Box { Box( modifier = Modifier @@ -804,6 +870,7 @@ sealed class ColorSpectrum { } ) { dot() + LabelPopup(offsetState = offset, label = { label(color) }) } } } 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 76865c4e..b3d6ec34 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Flyout.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Flyout.kt @@ -12,7 +12,9 @@ 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.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 @@ -24,12 +26,17 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.layout import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.layout.onGloballyPositioned 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.constrainHeight +import androidx.compose.ui.unit.constrainWidth import androidx.compose.ui.unit.dp import androidx.compose.ui.window.PopupProperties import com.konyaco.fluent.ExperimentalFluentApi @@ -75,6 +82,7 @@ fun FlyoutContainer( ) } +@OptIn(ExperimentalFluentApi::class) @Composable internal fun BasicFlyoutContainer( flyout: @Composable FlyoutContainerScope.() -> Unit, @@ -85,13 +93,16 @@ internal fun BasicFlyoutContainer( val flyoutState = remember(initialVisible) { mutableStateOf(initialVisible) } - val flyoutScope = remember(flyoutState) { - FlyoutContainerScopeImpl(flyoutState) - } - Box(modifier = modifier) { - flyoutScope.content() - flyoutScope.flyout() + FlyoutAnchorScope { + val flyoutScope = remember(flyoutState, this) { + FlyoutContainerScopeImpl(flyoutState, this) + } + Box(modifier = modifier) { + flyoutScope.content() + flyoutScope.flyout() + } } + } enum class FlyoutPlacement { @@ -321,17 +332,81 @@ internal data class PaddingCornerSize( get() = padding } -private class FlyoutContainerScopeImpl(visibleState: MutableState) : FlyoutContainerScope { +@OptIn(ExperimentalFluentApi::class) +private class FlyoutContainerScopeImpl( + visibleState: MutableState, + scope: FlyoutAnchorScope, +) : FlyoutContainerScope, FlyoutAnchorScope by scope { override var isFlyoutVisible: Boolean by visibleState } -interface FlyoutContainerScope { +@OptIn(ExperimentalFluentApi::class) +interface FlyoutContainerScope: FlyoutAnchorScope { var isFlyoutVisible: Boolean } +@ExperimentalFluentApi +interface FlyoutAnchorScope { + + fun Modifier.flyoutAnchor(): Modifier + + fun Modifier.flyoutSize(matchAnchorWidth: Boolean = false): Modifier +} + +@ExperimentalFluentApi +@Composable +fun FlyoutAnchorScope( + anchorPadding: Dp = flyoutPopPaddingFixShadowRender + flyoutDefaultPadding, + content: @Composable FlyoutAnchorScope.() -> Unit +) { + content(rememberFlyoutAnchorScope(anchorPadding)) +} + +@ExperimentalFluentApi +@Stable +@Composable +internal fun rememberFlyoutAnchorScope(padding: Dp = flyoutPopPaddingFixShadowRender + flyoutDefaultPadding): FlyoutAnchorScope { + val calculateMaxHeight = rememberFlyoutCalculateMaxHeight(padding) + return remember(calculateMaxHeight) { + FlyoutAnchorScopeImpl(calculateMaxHeight) + } +} + +@ExperimentalFluentApi +private class FlyoutAnchorScopeImpl( + private val calculateMaxHeight: (anchorCoordinates: LayoutCoordinates) -> Int +) : FlyoutAnchorScope { + private var anchorWidth by mutableIntStateOf(0) + private var flyoutMaxHeight by mutableIntStateOf(0) + + override fun Modifier.flyoutAnchor(): Modifier = this.onGloballyPositioned { + anchorWidth = it.size.width + flyoutMaxHeight = calculateMaxHeight(it) + } + + override fun Modifier.flyoutSize(matchAnchorWidth: Boolean): Modifier { + return this.layout { measurable, constraints -> + val flyoutWidth = constraints.constrainWidth(anchorWidth) + val flyoutConstraints = constraints.copy( + maxHeight = constraints.constrainHeight(flyoutMaxHeight), + minWidth = if (matchAnchorWidth) flyoutWidth else constraints.minWidth, + maxWidth = if (matchAnchorWidth) flyoutWidth else constraints.maxWidth, + ) + val placeable = measurable.measure(flyoutConstraints) + layout(placeable.width, placeable.height) { + placeable.place(0, 0) + } + } + } + +} + +@Composable +internal expect fun rememberFlyoutCalculateMaxHeight(padding: Dp): (anchorCoordinates: LayoutCoordinates) -> Int + //TODO Remove when shadow can show with animated visibility internal val flyoutPopPaddingFixShadowRender = 0.dp internal val flyoutDefaultPadding = 8.dp 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 fcdeffc9..dccc3abd 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/MenuFlyout.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/MenuFlyout.kt @@ -12,6 +12,8 @@ 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.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable @@ -122,12 +124,19 @@ internal fun MenuFlyout( onKeyEvent = onKeyEvent, onPreviewKeyEvent = onPreviewKeyEvent ) { - Column( - modifier = Modifier.width(IntrinsicSize.Max) + val state = rememberScrollState() + ScrollbarContainer( + adapter = rememberScrollbarAdapter(state) ) { - val scope = remember { MenuFlyoutScopeImpl() } - scope.content() + Column( + modifier = Modifier.width(IntrinsicSize.Max) + .verticalScroll(state) + ) { + val scope = remember { MenuFlyoutScopeImpl() } + scope.content() + } } + } } diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/NavigationView.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/NavigationView.kt index 8f2b74ec..08c8ba9e 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/NavigationView.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/NavigationView.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalFluentApi::class, ExperimentalFoundationApi::class) + package com.konyaco.fluent.component import androidx.compose.animation.AnimatedVisibility @@ -121,7 +123,6 @@ class NavigationState( val indicatorState = IndicatorState(initialOffset) } -@OptIn(ExperimentalFoundationApi::class) @Composable fun NavigationView( menuItems: NavigationMenuScope.() -> Unit, @@ -217,7 +218,6 @@ fun NavigationView( } } -@OptIn(ExperimentalFluentApi::class, ExperimentalFoundationApi::class) @Composable private fun TopLayout( modifier: Modifier, @@ -269,7 +269,6 @@ private fun TopLayout( ) } -@OptIn(ExperimentalFluentApi::class, ExperimentalFoundationApi::class) @Composable private fun LeftLayout( menuItems: NavigationMenuScope.() -> Unit, @@ -333,7 +332,6 @@ private fun LeftLayout( } } -@OptIn(ExperimentalFluentApi::class, ExperimentalFoundationApi::class) @Composable private fun LeftCollapsedLayout( modifier: Modifier, @@ -500,7 +498,6 @@ private fun LeftCollapsedLayout( } } -@OptIn(ExperimentalFluentApi::class, ExperimentalFoundationApi::class) @Composable private fun LeftCompactLayout( modifier: Modifier, @@ -686,7 +683,6 @@ fun NavigationMenuScope.menuItem( } } -@OptIn(ExperimentalFluentApi::class) @Composable fun NavigationMenuItemScope.MenuItem( selected: Boolean, @@ -723,24 +719,29 @@ fun NavigationMenuItemScope.MenuItem( if (displayMode == NavigationDisplayMode.Top) { var flyoutVisible by remember { mutableStateOf(false) } - TopNavItem( - selected = selected, - onClick = { - onClick(!selected) - flyoutVisible = !flyoutVisible - }, - text = if (isFooter) null else text, - flyoutVisible = flyoutVisible, - onFlyoutVisibleChanged = { flyoutVisible = it }, - indicatorState = indicatorState, - icon = icon, - items = items, - enabled = enabled, - interactionSource = interactionSource, - colors = colors, - indicator = indicator, - badge = badge - ) + TooltipBox( + tooltip = text, + enabled = isFooter + ) { + TopNavItem( + selected = selected, + onClick = { + onClick(!selected) + flyoutVisible = !flyoutVisible + }, + text = if (isFooter) null else text, + icon = icon, + flyoutVisible = flyoutVisible, + onFlyoutVisibleChanged = { flyoutVisible = it }, + indicatorState = indicatorState, + items = items, + enabled = enabled, + interactionSource = interactionSource, + colors = colors, + indicator = indicator, + badge = badge + ) + } } else { val isExpanded = LocalNavigationExpand.current var flyoutVisible by remember(isExpanded) { mutableStateOf(false) } @@ -1035,34 +1036,44 @@ object NavigationDefaults { buttonColors: ButtonColorScheme = ButtonDefaults.subtleButtonColors(), interaction: MutableInteractionSource = remember { MutableInteractionSource() }, animationEnabled: Boolean = true, + expanded: Boolean = LocalNavigationExpand.current, ) { - Button( - onClick = onClick, - interaction = interaction, - icon = { - if (animationEnabled) { - val isPressed by interaction.collectIsPressedAsState() - val scaleX = animateFloatAsState( - targetValue = if (isPressed) 0.6f else 1f, - animationSpec = tween( - durationMillis = FluentDuration.ShortDuration, - easing = FluentEasing.FastInvokeEasing - ) - ) - Box( - content = { icon() }, - modifier = Modifier.graphicsLayer { - this.scaleX = scaleX.value - } - ) - } else { - icon() - } + TooltipBox( + tooltip = { + Text( + text = if (expanded) "Close Navigation" else "Open Navigation" + ) }, - modifier = modifier, - disabled = disabled, - buttonColors = buttonColors - ) + enabled = !disabled + ) { + Button( + onClick = onClick, + interaction = interaction, + icon = { + if (animationEnabled) { + val isPressed by interaction.collectIsPressedAsState() + val scaleX = animateFloatAsState( + targetValue = if (isPressed) 0.6f else 1f, + animationSpec = tween( + durationMillis = FluentDuration.ShortDuration, + easing = FluentEasing.FastInvokeEasing + ) + ) + Box( + content = { icon() }, + modifier = Modifier.graphicsLayer { + this.scaleX = scaleX.value + } + ) + } else { + icon() + } + }, + modifier = modifier, + disabled = disabled, + buttonColors = buttonColors + ) + } } @Composable @@ -1103,38 +1114,45 @@ object NavigationDefaults { interaction: MutableInteractionSource = remember { MutableInteractionSource() }, animationEnabled: Boolean = true, ) { - Button( - onClick = onClick, - iconOnly = true, - interaction = interaction, - content = { - if (animationEnabled) { - val isPressed by interaction.collectIsPressedAsState() - val scaleX = animateFloatAsState( - targetValue = if (isPressed) 0.9f else 1f, - animationSpec = tween( - durationMillis = FluentDuration.ShortDuration, - easing = FluentEasing.FastInvokeEasing - ) - ) - Box( - content = { icon() }, - modifier = Modifier.graphicsLayer { - this.scaleX = scaleX.value - translationX = (1f - scaleX.value) * 6.dp.toPx() - } - ) - } else { - icon() - } + TooltipBox( + tooltip = { + Text(text = "Back") }, - modifier = modifier - .size(44.dp, 40.dp) - .padding(vertical = 2.dp) - .padding(start = 4.dp), - disabled = disabled, - buttonColors = buttonColors - ) + enabled = !disabled + ) { + Button( + onClick = onClick, + iconOnly = true, + interaction = interaction, + content = { + if (animationEnabled) { + val isPressed by interaction.collectIsPressedAsState() + val scaleX = animateFloatAsState( + targetValue = if (isPressed) 0.9f else 1f, + animationSpec = tween( + durationMillis = FluentDuration.ShortDuration, + easing = FluentEasing.FastInvokeEasing + ) + ) + Box( + content = { icon() }, + modifier = Modifier.graphicsLayer { + this.scaleX = scaleX.value + translationX = (1f - scaleX.value) * 6.dp.toPx() + } + ) + } else { + icon() + } + }, + modifier = modifier + .size(44.dp, 40.dp) + .padding(vertical = 2.dp) + .padding(start = 4.dp), + disabled = disabled, + buttonColors = buttonColors + ) + } } } @@ -1380,7 +1398,6 @@ private data class ValueNavigationMenuItemScope( } } -@OptIn(ExperimentalFoundationApi::class) private inline fun IntervalList.forEachItem(action: T.(index: Int) -> Unit) { repeat(size) { val item = get(it) @@ -1388,7 +1405,6 @@ private inline fun IntervalList.forEachItem(action: T.(index: Int) -> Uni } } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun rememberNavigationMenuInterval( content: NavigationMenuScope.() -> Unit @@ -1401,7 +1417,6 @@ private fun rememberNavigationMenuInterval( }.value } -@OptIn(ExperimentalFoundationApi::class) private class NavigationMenuScopeImpl( content: NavigationMenuScope.() -> Unit ) : NavigationMenuScope, LazyLayoutIntervalContent() { @@ -1444,7 +1459,6 @@ private class NavigationMenuScopeImpl( } } -@OptIn(ExperimentalFoundationApi::class) private class NavigationViewMenuInterval( override val key: ((index: Int) -> Any)?, override val type: ((index: Int) -> Any?), @@ -1458,8 +1472,10 @@ internal fun rememberNavigationItemsFlyoutScope( ): MenuFlyoutContainerScope { val expandedState = rememberUpdatedState(expanded) val onExpandedChangedState = rememberUpdatedState(onExpandedChanged) - return remember { - object : MenuFlyoutContainerScope, MenuFlyoutScope by MenuFlyoutScopeImpl() { + val anchorScope = rememberFlyoutAnchorScope() + return remember(anchorScope, expandedState, onExpandedChangedState) { + object : MenuFlyoutContainerScope, MenuFlyoutScope by MenuFlyoutScopeImpl(), + FlyoutAnchorScope by anchorScope { override var isFlyoutVisible: Boolean get() = expandedState.value set(value) { 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 5bcc5ac4..81ec6d1c 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/SideNav.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/SideNav.kt @@ -12,6 +12,7 @@ import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource @@ -225,6 +226,7 @@ fun SideNavItem( ) } +@OptIn(ExperimentalFoundationApi::class) @ExperimentalFluentApi @Composable fun SideNavItem( @@ -259,132 +261,138 @@ fun SideNavItem( val color = colors.schemeFor(interaction.collectVisualState(!enabled)) - Column( - modifier = modifier.indicatorRect(indicatorState, selected) + TooltipBox( + tooltip = { + Row(content = text) + }, + enabled = !expand && enabled ) { - CompositionLocalProvider( - LocalNavigationLevel provides 0, - LocalNavigationExpand provides true + Column( + modifier = modifier.indicatorRect(indicatorState, selected) ) { - MenuFlyout( - visible = flyoutVisible, - onDismissRequest = { onFlyoutVisibleChanged(false) }, - placement = FlyoutPlacement.End + CompositionLocalProvider( + LocalNavigationLevel provides 0, + LocalNavigationExpand provides true ) { - items?.invoke( - rememberNavigationItemsFlyoutScope(flyoutVisible, onFlyoutVisibleChanged) - ) - } - } - Box( - Modifier.height(40.dp) - .widthIn(48.dp) - .fillMaxWidth() - .padding(4.dp, 2.dp) - ) { - val navigationLevelPadding = 28.dp * LocalNavigationLevel.current - Layer( - modifier = Modifier.fillMaxWidth().height(36.dp), - shape = FluentTheme.shapes.control, - color = animateColorAsState( - targetValue = color.fillColor, - animationSpec = tween( - durationMillis = FluentDuration.QuickDuration, - easing = FluentEasing.FastInvokeEasing + MenuFlyout( + visible = flyoutVisible, + onDismissRequest = { onFlyoutVisibleChanged(false) }, + placement = FlyoutPlacement.End + ) { + items?.invoke( + rememberNavigationItemsFlyoutScope(flyoutVisible, onFlyoutVisibleChanged) ) - ).value, - contentColor = color.contentColor, - border = null, - backgroundSizing = BackgroundSizing.OuterBorderEdge + } + } + Box( + Modifier.height(40.dp) + .widthIn(48.dp) + .fillMaxWidth() + .padding(4.dp, 2.dp) ) { - Box( - modifier = Modifier - .clickable( - onClick = { onSelectedChanged(!selected) }, - interactionSource = interaction, - indication = null, - enabled = enabled + val navigationLevelPadding = 28.dp * LocalNavigationLevel.current + Layer( + modifier = Modifier.fillMaxWidth().height(36.dp), + shape = FluentTheme.shapes.control, + color = animateColorAsState( + targetValue = color.fillColor, + animationSpec = tween( + durationMillis = FluentDuration.QuickDuration, + easing = FluentEasing.FastInvokeEasing ) - .padding(start = navigationLevelPadding), - contentAlignment = Alignment.CenterStart + ).value, + contentColor = color.contentColor, + border = null, + backgroundSizing = BackgroundSizing.OuterBorderEdge ) { - Row(verticalAlignment = Alignment.CenterVertically) { - if (icon != null) { - Box( - modifier = Modifier.padding(start = 12.dp).size(16.dp), - contentAlignment = Alignment.Center, - content = { icon() } - ) - } - if (expand) { - Row( - modifier = Modifier - .weight(1f) - .wrapContentWidth(Alignment.Start) - .padding(start = 16.dp, end = 12.dp), - horizontalArrangement = Arrangement.spacedBy( - space = 8.dp, - alignment = Alignment.CenterHorizontally - ), - verticalAlignment = Alignment.CenterVertically, - content = text + Box( + modifier = Modifier + .clickable( + onClick = { onSelectedChanged(!selected) }, + interactionSource = interaction, + indication = null, + enabled = enabled ) - if (badge != null) { + .padding(start = navigationLevelPadding), + contentAlignment = Alignment.CenterStart + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + if (icon != null) { Box( + modifier = Modifier.padding(start = 12.dp).size(16.dp), contentAlignment = Alignment.Center, - modifier = Modifier.padding( - end = if (items != null) { - 4.dp - } else { - 12.dp - } - ) - ) { - badge() - } + content = { icon() } + ) } - if (items != null) { - val rotation by animateFloatAsState( - targetValue = if (expandItems) { - 180f - } else { - 00f - }, - animationSpec = tween( - durationMillis = FluentDuration.ShortDuration, - easing = FluentEasing.FastInvokeEasing - ) + if (expand) { + Row( + modifier = Modifier + .weight(1f) + .wrapContentWidth(Alignment.Start) + .padding(start = 16.dp, end = 12.dp), + horizontalArrangement = Arrangement.spacedBy( + space = 8.dp, + alignment = Alignment.CenterHorizontally + ), + verticalAlignment = Alignment.CenterVertically, + content = text ) + if (badge != null) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.padding( + end = if (items != null) { + 4.dp + } else { + 12.dp + } + ) + ) { + badge() + } + } + if (items != null) { + val rotation by animateFloatAsState( + targetValue = if (expandItems) { + 180f + } else { + 00f + }, + animationSpec = tween( + durationMillis = FluentDuration.ShortDuration, + easing = FluentEasing.FastInvokeEasing + ) + ) - val fraction by animateFloatAsState( - targetValue = if (expand) 1f else 0f, - animationSpec = tween( - durationMillis = FluentDuration.ShortDuration, - easing = FluentEasing.FastInvokeEasing + val fraction by animateFloatAsState( + targetValue = if (expand) 1f else 0f, + animationSpec = tween( + durationMillis = FluentDuration.ShortDuration, + easing = FluentEasing.FastInvokeEasing + ) ) - ) - FontIcon( - glyph = '\uE972', - iconSize = FontIconDefaults.fontSizeSmall, - vector = Icons.Default.ChevronDown, - contentDescription = null, - modifier = Modifier - .padding(start = 2.dp, end = 14.dp) - .wrapContentWidth(Alignment.CenterHorizontally) - .graphicsLayer { - rotationZ = rotation - alpha = if (fraction == 1f) { - 1f - } else { - 0f + FontIcon( + glyph = '\uE972', + iconSize = FontIconDefaults.fontSizeSmall, + vector = Icons.Default.ChevronDown, + contentDescription = null, + modifier = Modifier + .padding(start = 2.dp, end = 14.dp) + .wrapContentWidth(Alignment.CenterHorizontally) + .graphicsLayer { + rotationZ = rotation + alpha = if (fraction == 1f) { + 1f + } else { + 0f + } } - } - ) + ) + } } } - } - if (badge != null && !expand) { + if (badge != null && !expand) { Box( modifier = Modifier.align(Alignment.TopEnd) .padding(top = 2.dp, end = 2.dp) @@ -392,56 +400,56 @@ fun SideNavItem( badge() } } - } - } - Box( - modifier = Modifier - .align(Alignment.CenterStart) - .padding(start = navigationLevelPadding), - content = { - SideNavigationIndicatorScope(indicatorState).indicator(color.indicatorColor) - } - ) - } - - if (items != null) { - AnimatedVisibility( - visible = expandItems && expand, - enter = fadeIn( - animationSpec = tween( - durationMillis = FluentDuration.ShortDuration, - easing = FluentEasing.FastInvokeEasing - ) - ) + expandVertically( - animationSpec = tween( - durationMillis = FluentDuration.ShortDuration, - easing = FluentEasing.FastInvokeEasing - ) - ), - exit = fadeOut( - animationSpec = tween( - durationMillis = FluentDuration.ShortDuration, - easing = FluentEasing.FastInvokeEasing - ) - ) + shrinkVertically( - animationSpec = tween( - durationMillis = FluentDuration.ShortDuration, - easing = FluentEasing.FastInvokeEasing - ) - ), - modifier = Modifier.fillMaxWidth() - ) { - CompositionLocalProvider( - value = LocalNavigationLevel provides LocalNavigationLevel.current + 1, + }} + Box( + modifier = Modifier + .align(Alignment.CenterStart) + .padding(start = navigationLevelPadding), content = { - Column( - content = { - items(FakeMenuFlyoutContainerScope) - } - ) + SideNavigationIndicatorScope(indicatorState).indicator(color.indicatorColor) } ) } + + if (items != null) { + AnimatedVisibility( + visible = expandItems && expand, + enter = fadeIn( + animationSpec = tween( + durationMillis = FluentDuration.ShortDuration, + easing = FluentEasing.FastInvokeEasing + ) + ) + expandVertically( + animationSpec = tween( + durationMillis = FluentDuration.ShortDuration, + easing = FluentEasing.FastInvokeEasing + ) + ), + exit = fadeOut( + animationSpec = tween( + durationMillis = FluentDuration.ShortDuration, + easing = FluentEasing.FastInvokeEasing + ) + ) + shrinkVertically( + animationSpec = tween( + durationMillis = FluentDuration.ShortDuration, + easing = FluentEasing.FastInvokeEasing + ) + ), + modifier = Modifier.fillMaxWidth() + ) { + CompositionLocalProvider( + value = LocalNavigationLevel provides LocalNavigationLevel.current + 1, + content = { + Column( + content = { + items(FakeMenuFlyoutContainerScope) + } + ) + } + ) + } + } } } @@ -560,4 +568,12 @@ internal class NavigationAutoSuggestBoxScopeImpl( private object FakeMenuFlyoutContainerScope : MenuFlyoutContainerScope, MenuFlyoutScope by MenuFlyoutScopeImpl() { override var isFlyoutVisible: Boolean = false + + override fun Modifier.flyoutAnchor(): Modifier { + return this + } + + override fun Modifier.flyoutSize(matchAnchorWidth: Boolean): Modifier { + return this + } } \ No newline at end of file diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Slider.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Slider.kt index 0011f096..350af3b1 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Slider.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Slider.kt @@ -1,9 +1,11 @@ package com.konyaco.fluent.component +import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Canvas +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown @@ -43,6 +45,8 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.util.lerp +import androidx.compose.ui.window.PopupProperties +import com.konyaco.fluent.ExperimentalFluentApi import com.konyaco.fluent.FluentTheme import com.konyaco.fluent.animation.FluentDuration import com.konyaco.fluent.animation.FluentEasing @@ -307,7 +311,7 @@ class SliderState( this.isDragging = false } - private fun snapToNearestTickValue(value: Float): Float { + internal fun snapToNearestTickValue(value: Float): Float { return this.stepFractions .map { lerp(this.valueRange.start, this.valueRange.endInclusive, it) } .minBy { abs(it - value) } @@ -465,9 +469,11 @@ object SliderDefaults { } } + @OptIn(ExperimentalFluentApi::class, ExperimentalFoundationApi::class) @Composable fun Thumb( state: SliderState, + label: @Composable (state: SliderState) -> Unit = { Tooltip(state) }, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, @@ -481,62 +487,86 @@ object SliderDefaults { val hovered by interactionSource.collectIsHoveredAsState() val pressed by interactionSource.collectIsPressedAsState() - Layer( - modifier = modifier - .layout { measurable, constraints -> - val placeable = measurable.measure(constraints.copy(minWidth = 0)) - val width = maxOf(constraints.maxWidth, placeable.width) - val height = maxOf(constraints.maxHeight, placeable.height) - layout(width, height) { - val offset = Alignment.CenterStart.align( - size = IntSize(placeable.width, placeable.height), - space = IntSize(width, height), - layoutDirection = layoutDirection - ) - placeable.place( - x = offset.x + calcThumbOffset( - maxWidth = width, - thumbSize = ThumbSize.toPx(), - padding = 1.dp.toPx(), - fraction = state.rawFraction - ).roundToInt(), - y = offset.y + 0 - ) + FlyoutAnchorScope { + Layer( + modifier = modifier + .flyoutAnchor() + .layout { measurable, constraints -> + val placeable = measurable.measure(constraints.copy(minWidth = 0)) + val width = maxOf(constraints.maxWidth, placeable.width) + val height = maxOf(constraints.maxHeight, placeable.height) + layout(width, height) { + val offset = Alignment.CenterStart.align( + size = IntSize(placeable.width, placeable.height), + space = IntSize(width, height), + layoutDirection = layoutDirection + ) + placeable.place( + x = offset.x + calcThumbOffset( + maxWidth = width, + thumbSize = ThumbSize.toPx(), + padding = 1.dp.toPx(), + fraction = state.rawFraction + ).roundToInt(), + y = offset.y + 0 + ) + } } - } - .requiredSize(ThumbSizeWithBorder) - .hoverable(interactionSource, enabled), - shape = shape, - color = ringColor, - border = border, - backgroundSizing = BackgroundSizing.InnerBorderEdge - ) { - Box(contentAlignment = Alignment.Center) { - // Inner Thumb - Box( - Modifier.size( - animateDpAsState( + .requiredSize(ThumbSizeWithBorder) + .hoverable(interactionSource, enabled), + shape = shape, + color = ringColor, + border = border, + backgroundSizing = BackgroundSizing.InnerBorderEdge + ) { + Box(contentAlignment = Alignment.Center) { + // Inner Thumb + Box( + Modifier.size( + animateDpAsState( + when { + pressed || state.isDragging -> InnerThumbPressedSize + hovered -> InnerThumbHoverSize + else -> InnerThumbSize + }, + tween( + FluentDuration.QuickDuration, + easing = FluentEasing.FastInvokeEasing + ) + ).value + ).background( when { - pressed || state.isDragging -> InnerThumbPressedSize - hovered -> InnerThumbHoverSize - else -> InnerThumbSize - }, - tween( - FluentDuration.QuickDuration, - easing = FluentEasing.FastInvokeEasing + !enabled -> disabledColor + pressed || state.isDragging -> draggingColor + else -> color + }, shape + ) + ) + } + if (state.isDragging) { + Popup( + properties = PopupProperties(focusable = false), + popupPositionProvider = rememberTooltipPositionProvider(state = null), + content = { + TooltipBoxDefaults.Tooltip( + visibleState = remember { MutableTransitionState(true) }, + content = { label(state) }, + modifier = Modifier.flyoutAnchor() ) - ).value - ).background( - when { - !enabled -> disabledColor - pressed || state.isDragging -> draggingColor - else -> color - }, shape + } ) - ) + } } } } + + @Composable + fun Tooltip(state: SliderState, snap: Boolean = state.steps != 0) { + Text( + if (snap) state.snapToNearestTickValue(state.value).toString() + else state.value.toString() + ) + } } private val ThumbSize = 20.dp diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TooltipBox.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TooltipBox.kt new file mode 100644 index 00000000..36cdfafa --- /dev/null +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TooltipBox.kt @@ -0,0 +1,311 @@ +package com.konyaco.fluent.component + +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.BasicTooltipBox +import androidx.compose.foundation.BasicTooltipDefaults +import androidx.compose.foundation.BasicTooltipState +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.MutatorMutex +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.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.runtime.setValue +import androidx.compose.runtime.snapshots.Snapshot +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.center +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.PointerType +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +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.unit.dp +import androidx.compose.ui.unit.plus +import androidx.compose.ui.window.PopupPositionProvider +import com.konyaco.fluent.ExperimentalFluentApi +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.ElevationDefaults +import kotlinx.coroutines.Job +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlin.math.roundToInt + +@OptIn(ExperimentalFoundationApi::class) +@ExperimentalFluentApi +@Composable +fun TooltipBox( + tooltip: @Composable () -> Unit, + state: TooltipState = rememberTooltipState(), + modifier: Modifier = Modifier, + positionProvider: PopupPositionProvider = rememberTooltipPositionProvider(state), + enabled: Boolean = true, + content: @Composable () -> Unit, +) { + FlyoutAnchorScope { + BasicTooltipBox( + state = state, + positionProvider = positionProvider, + enableUserInput = false, + tooltip = { + TooltipBoxDefaults.Tooltip( + visible = state.isVisible, + content = tooltip, + modifier = Modifier.flyoutSize() + ) + }, + modifier = modifier + ) { + Box( + modifier = Modifier + .flyoutAnchor() + .gestureHandle(enabled, state) + ) { + content() + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@ExperimentalFluentApi +@Composable +fun rememberTooltipState( + initialIsVisible: Boolean = false, + isPersistent: Boolean = true, + mutatorMutex: MutatorMutex = BasicTooltipDefaults.GlobalMutatorMutex, +): TooltipState { + return remember(initialIsVisible, isPersistent, mutatorMutex) { + TooltipState(initialIsVisible, isPersistent, mutatorMutex) + } +} + +@OptIn(ExperimentalFoundationApi::class) +private fun Modifier.gestureHandle( + enabled: Boolean, + state: TooltipState +): Modifier { + return if (enabled) { + + then(Modifier.pointerInput(state) { + coroutineScope { + awaitEachGesture { + val longPressTimeout = viewConfiguration.longPressTimeoutMillis + val pass = PointerEventPass.Initial + val change = awaitFirstDown(pass = pass) + val inputType = change.type + if (inputType == PointerType.Touch || inputType == PointerType.Stylus) { + try { + withTimeout(longPressTimeout) { + waitForUpOrCancellation(pass) + } + } catch (_: PointerEventTimeoutCancellationException) { + state.pointerPosition = change.position - if (inputType == PointerType.Touch) { + viewConfiguration.minimumTouchTargetSize.toSize().center + } else { + Offset.Zero + } + state.pointerType = inputType + launch { + state.show(MutatePriority.UserInput) + } + val changes = awaitPointerEvent(pass).changes + for (i in 0 until changes.size) { changes[i].consume() } + } + } + } + } + }.pointerInput(state) { + coroutineScope { + var hoveredJob: Job? = null + awaitEachGesture { + val event = awaitPointerEvent(PointerEventPass.Main) + val longPressTimeout = viewConfiguration.longPressTimeoutMillis + val change = event.changes[0] + if (change.type != PointerType.Mouse) return@awaitEachGesture + if (event.type == PointerEventType.Enter) { + hoveredJob?.cancel() + hoveredJob = launch { + delay(longPressTimeout) + state.show(mutatePriority = MutatePriority.UserInput) + } + } + if (!state.isVisible && event.type != PointerEventType.Exit) { + state.pointerType = change.type + state.pointerPosition = change.position + } else if (event.type == PointerEventType.Exit) { + state.pointerPosition = null + state.pointerType = null + hoveredJob?.cancel() + state.dismiss() + } + } + } + + }) + } else { + this + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun rememberTooltipPositionProvider( + state: TooltipState?, + anchorPadding: Dp = 0.dp +): PopupPositionProvider { + val anchorPadding = with(LocalDensity.current) { (anchorPadding + flyoutPopPaddingFixShadowRender + flyoutDefaultPadding).toPx().roundToInt() } + val mouseOverflowPadding = + with(LocalDensity.current) { defaultMousePadding.toPx().roundToInt() } + return remember(state, anchorPadding, mouseOverflowPadding) { + TooltipPopupPositionProvider(state, anchorPadding, mouseOverflowPadding) + } +} + +@Stable +@ExperimentalFoundationApi +class TooltipState( + initialIsVisible: Boolean = false, + isPersistent: Boolean = true, + mutatorMutex: MutatorMutex = BasicTooltipDefaults.GlobalMutatorMutex, +) : BasicTooltipState by BasicTooltipState( + initialIsVisible = initialIsVisible, + isPersistent = isPersistent, + mutatorMutex = mutatorMutex +) { + var pointerPosition: Offset? by mutableStateOf(null) + internal set + var pointerType: PointerType? by mutableStateOf(null) + internal set +} + +@ExperimentalFluentApi +object TooltipBoxDefaults { + + val iconSpacing = 8.dp + + @Composable + fun Tooltip( + visible: Boolean, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, + ) { + val visibleState = remember { + MutableTransitionState(false) + } + visibleState.targetState = visible + Tooltip( + visibleState = visibleState, + content = content, + modifier = modifier + ) + } + + @Composable + fun Tooltip( + visibleState: MutableTransitionState, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, + ) { + + AcrylicPopupContent( + elevation = ElevationDefaults.tooltip, + visibleState = visibleState, + enterTransition = fadeIn( + tween( + FluentDuration.ShortDuration, + easing = FluentEasing.FastInvokeEasing + ) + ), + exitTransition = fadeOut( + tween( + FluentDuration.QuickDuration, + easing = FluentEasing.FastDismissEasing + ) + ), + shape = FluentTheme.shapes.control, + contentPadding = PaddingValues( + start = 8.dp, + end = 8.dp, + top = 6.dp, + bottom = 8.dp + ), + content = { + ProvideTextStyle( + value = FluentTheme.typography.caption, + content = content + ) + }, + modifier = modifier + ) + } +} + +@OptIn(ExperimentalFoundationApi::class) +internal class TooltipPopupPositionProvider( + private val state: TooltipState?, + private val anchorPadding: Int, + private val mouseOverflowPadding: Int, +) : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize + ): IntOffset { + return Snapshot.withoutReadObservation { + val pointerPosition = state?.pointerPosition + val alignmentPosition = pointerPosition ?: Offset( + x = anchorBounds.width / 2f, + y = 0f + ) + var (offsetX, offsetY) = alignmentPosition + IntOffset( + x = anchorBounds.left - popupContentSize.width / 2, + y = anchorBounds.top - anchorPadding - popupContentSize.height + ) + val topOverflow = -offsetY + + if (topOverflow > 0) { + + if (pointerPosition != null) { + // Fixed pointer overflow dismiss + offsetY = pointerPosition.y + anchorPadding + mouseOverflowPadding + } else { + offsetY -= topOverflow + } + } + + val rightOverflow = offsetX + popupContentSize.width - windowSize.width + if (rightOverflow > 0) { + offsetX -= rightOverflow + } + + offsetX = offsetX.coerceAtLeast(0f) + offsetY = offsetY.coerceAtLeast(0f) + + return IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) + } + } +} + +private val defaultMousePadding = 16.dp \ No newline at end of file diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TopNav.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TopNav.kt index 195ccdd9..9c24952b 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TopNav.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/TopNav.kt @@ -75,28 +75,32 @@ fun TopNav( horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically, overflowAction = { - Box { - MenuFlyout( - visible = expanded, - onDismissRequest = { onExpandedChanged(false) }, - placement = FlyoutPlacement.Auto, - content = { - repeat(overflowItemCount) { index -> - overflowItem(index) - } - } - ) - TopNavItem( - selected = false, - icon = { - FontIcon( - glyph = '\uE712', - vector = Icons.Default.MoreHorizontal, - contentDescription = null, - ) - }, - onClick = { onExpandedChanged(true) } - ) + FlyoutAnchorScope { + Box { + MenuFlyout( + visible = expanded, + onDismissRequest = { onExpandedChanged(false) }, + placement = FlyoutPlacement.Auto, + content = { + repeat(overflowItemCount) { index -> + overflowItem(index) + } + }, + modifier = Modifier.flyoutSize() + ) + TopNavItem( + selected = false, + icon = { + FontIcon( + glyph = '\uE712', + vector = Icons.Default.MoreHorizontal, + contentDescription = null, + ) + }, + onClick = { onExpandedChanged(true) }, + modifier = Modifier.flyoutAnchor() + ) + } } }, content = content @@ -153,7 +157,6 @@ fun TopNavItem( val targetInteractionSource = interactionSource ?: remember { MutableInteractionSource() } val currentColor = colors.schemeFor(targetInteractionSource.collectVisualState(!enabled)) - Layer( color = currentColor.fillColor, contentColor = currentColor.contentColor, @@ -170,53 +173,61 @@ fun TopNavItem( } ) ) { - MenuFlyout( - visible = flyoutVisible && items != null, - onDismissRequest = { - onFlyoutVisibleChanged(false) - }, - placement = FlyoutPlacement.Bottom - ) { - items?.invoke(rememberNavigationItemsFlyoutScope(flyoutVisible, onFlyoutVisibleChanged)) - } - Box { - HorizontalIndicatorContentLayout( - modifier = Modifier.height(40.dp), - text = text, - icon = icon, - trailing = items?.let { - { - val rotation by animateFloatAsState( - if (flyoutVisible) { - 180f - } else { - 00f - } - ) - FontIcon( - glyph = '\uE972', - iconSize = FontIconDefaults.fontSizeSmall, - vector = Icons.Default.ChevronDown, - contentDescription = null, - modifier = Modifier - .graphicsLayer { - rotationZ = rotation + FlyoutAnchorScope { + MenuFlyout( + visible = flyoutVisible && items != null, + onDismissRequest = { + onFlyoutVisibleChanged(false) + }, + placement = FlyoutPlacement.Bottom, + modifier = Modifier.flyoutSize() + ) { + items?.invoke( + rememberNavigationItemsFlyoutScope( + flyoutVisible, + onFlyoutVisibleChanged + ) + ) + } + Box { + HorizontalIndicatorContentLayout( + modifier = Modifier.height(40.dp), + text = text, + icon = icon, + trailing = items?.let { + { + val rotation by animateFloatAsState( + if (flyoutVisible) { + 180f + } else { + 00f } - ) + ) + FontIcon( + glyph = '\uE972', + iconSize = FontIconDefaults.fontSizeSmall, + vector = Icons.Default.ChevronDown, + contentDescription = null, + modifier = Modifier + .graphicsLayer { + rotationZ = rotation + } + ) + } + }, + indicator = { + val scope = TopNavigationIndicatorScope(indicatorState = indicatorState) + scope.indicator(currentColor.indicatorColor) + } + ) + if (badge != null) { + Box( + contentAlignment = Alignment.TopEnd, + modifier = Modifier.matchParentSize() + .padding(top = 4.dp, end = if (iconOnly) 2.dp else 0.dp) + ) { + badge() } - }, - indicator = { - val scope = TopNavigationIndicatorScope(indicatorState = indicatorState) - scope.indicator(currentColor.indicatorColor) - } - ) - if (badge != null) { - Box( - contentAlignment = Alignment.TopEnd, - modifier = Modifier.matchParentSize() - .padding(top = 4.dp, end = if (iconOnly) 2.dp else 0.dp) - ) { - badge() } } } diff --git a/fluent/src/skikoMain/kotlin/com/konyaco/fluent/component/AutoSuggestBox.skiko.kt b/fluent/src/skikoMain/kotlin/com/konyaco/fluent/component/Flyout.skiko.kt similarity index 90% rename from fluent/src/skikoMain/kotlin/com/konyaco/fluent/component/AutoSuggestBox.skiko.kt rename to fluent/src/skikoMain/kotlin/com/konyaco/fluent/component/Flyout.skiko.kt index 19f0ac33..adad8ac2 100644 --- a/fluent/src/skikoMain/kotlin/com/konyaco/fluent/component/AutoSuggestBox.skiko.kt +++ b/fluent/src/skikoMain/kotlin/com/konyaco/fluent/component/Flyout.skiko.kt @@ -13,7 +13,7 @@ import kotlin.math.max @OptIn(ExperimentalComposeUiApi::class) @Composable -internal actual fun rememberSuggestFlyoutCalculateMaxHeight(padding: Dp): (anchorCoordinates: LayoutCoordinates) -> Int { +actual fun rememberFlyoutCalculateMaxHeight(padding: Dp): (LayoutCoordinates) -> Int { val windowInfo = LocalWindowInfo.current val density = LocalDensity.current val verticalMarginInPx = with(LocalDensity.current) { padding.roundToPx() } 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 cfefa1a7..1f297efd 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,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp +import com.konyaco.fluent.ExperimentalFluentApi import com.konyaco.fluent.FluentTheme import com.konyaco.fluent.animation.FluentDuration import com.konyaco.fluent.animation.FluentEasing @@ -62,7 +63,7 @@ import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.map -@OptIn(FlowPreview::class) +@OptIn(FlowPreview::class, ExperimentalFluentApi::class) @Composable fun App( navigator: ComponentNavigator = rememberComponentNavigator(components.first()), @@ -172,7 +173,7 @@ fun App( }, isClearable = true, shape = AutoSuggestBoxDefaults.textFieldShape(expandedSuggestion), - modifier = Modifier.fillMaxWidth().focusHandle().suggestFlyoutAnchor() + modifier = Modifier.fillMaxWidth().focusHandle().flyoutAnchor() ) val searchResult = remember(flatMapComponents) { snapshotFlow { @@ -190,7 +191,7 @@ fun App( AutoSuggestBoxDefaults.suggestFlyout( expanded = expandedSuggestion, onDismissRequest = { expandedSuggestion = false }, - modifier = Modifier.suggestFlyoutSize(), + modifier = Modifier.flyoutSize(matchAnchorWidth = true), itemsContent = { items( items = searchResult.value, diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/CopyButton.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/CopyButton.kt index addcac3b..3c852ba2 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/CopyButton.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/component/CopyButton.kt @@ -1,6 +1,7 @@ package com.konyaco.fluent.gallery.component import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -10,21 +11,25 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.AnnotatedString +import com.konyaco.fluent.ExperimentalFluentApi import com.konyaco.fluent.component.Button import com.konyaco.fluent.component.ButtonColorScheme -import com.konyaco.fluent.component.ButtonColors import com.konyaco.fluent.component.ButtonDefaults import com.konyaco.fluent.component.Icon +import com.konyaco.fluent.component.Text +import com.konyaco.fluent.component.TooltipBox import com.konyaco.fluent.icons.Icons import com.konyaco.fluent.icons.regular.Checkmark import com.konyaco.fluent.icons.regular.Copy import kotlinx.coroutines.delay +@OptIn(ExperimentalFoundationApi::class, ExperimentalFluentApi::class) @Composable fun CopyButton( copyData: String, modifier: Modifier = Modifier, - colors: ButtonColorScheme = ButtonDefaults.buttonColors() + colors: ButtonColorScheme = ButtonDefaults.buttonColors(), + tooltip: String = "Copy to clipboard" ) { var isCopy by remember { mutableStateOf(false) } LaunchedEffect(isCopy) { @@ -34,22 +39,26 @@ fun CopyButton( } } val clipboard = LocalClipboardManager.current - Button( - onClick = { - clipboard.setText(AnnotatedString(copyData)) - isCopy = true - }, - iconOnly = true, - content = { - AnimatedContent(isCopy) { target -> - if (target) { - Icon(Icons.Default.Checkmark, contentDescription = null) - } else { - Icon(Icons.Default.Copy, contentDescription = null) + TooltipBox( + tooltip = { Text(tooltip) } + ){ + Button( + onClick = { + clipboard.setText(AnnotatedString(copyData)) + isCopy = true + }, + iconOnly = true, + content = { + AnimatedContent(isCopy) { target -> + if (target) { + Icon(Icons.Default.Checkmark, contentDescription = null) + } else { + Icon(Icons.Default.Copy, contentDescription = null) + } } - } - }, - buttonColors = colors, - modifier = modifier - ) + }, + buttonColors = colors, + modifier = modifier + ) + } } \ No newline at end of file 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 27ba775b..0a8961ec 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 @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalFoundationApi::class, ExperimentalFluentApi::class) + package com.konyaco.fluent.gallery.component import androidx.compose.foundation.ExperimentalFoundationApi @@ -20,6 +22,7 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.ExperimentalTextApi import androidx.compose.ui.unit.dp +import com.konyaco.fluent.ExperimentalFluentApi import com.konyaco.fluent.FluentTheme import com.konyaco.fluent.component.Button import com.konyaco.fluent.component.DropDownButton @@ -29,6 +32,7 @@ import com.konyaco.fluent.component.Icon import com.konyaco.fluent.component.MenuFlyoutContainer import com.konyaco.fluent.component.Text import com.konyaco.fluent.component.ToggleButton +import com.konyaco.fluent.component.TooltipBox import com.konyaco.fluent.gallery.ProjectUrl import com.konyaco.fluent.icons.Icons import com.konyaco.fluent.icons.filled.BrightnessHigh @@ -86,92 +90,108 @@ fun GalleryHeader( verticalAlignment = Alignment.CenterVertically ) { if (documentPath != null) { - Button( - onClick = { - uriHandler.openUri(ProjectUrl.documentationOf(documentPath)) - }, - content = { - Icon( - Icons.Default.Document, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Text("Documentation") - } - ) + TooltipBox( + tooltip = { Text("Documentation") } + ){ + Button( + onClick = { + uriHandler.openUri(ProjectUrl.documentationOf(documentPath)) + }, + content = { + Icon( + Icons.Default.Document, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Text("Documentation") + } + ) + } } if (componentPath != null && galleryPath != null) { - MenuFlyoutContainer( - flyout = { - HyperlinkButton( - onClick = { - uriHandler.openUri( - ProjectUrl.componentCodeOf(componentPath) - ) - isFlyoutVisible = false - }, - content = { Text("Component") }, - modifier = Modifier.fillMaxWidth() - .padding(horizontal = 5.dp, vertical = 2.dp) - ) - HyperlinkButton( - onClick = { - uriHandler.openUri(ProjectUrl.galleryCodeOf(galleryPath)) - isFlyoutVisible = false - }, - content = { Text("Sample") }, - modifier = Modifier.fillMaxWidth() - .padding(horizontal = 5.dp, vertical = 2.dp) - ) - }, - content = { - DropDownButton( - onClick = { isFlyoutVisible = true }, - content = { - Icon( - painter = painterResource(Res.drawable.github_logo), - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Text("Source") - } - ) - }, - placement = FlyoutPlacement.Bottom, - adaptivePlacement = true - ) + TooltipBox( + tooltip = { Text("Source code of this sample page") } + ){ + MenuFlyoutContainer( + flyout = { + HyperlinkButton( + onClick = { + uriHandler.openUri( + ProjectUrl.componentCodeOf(componentPath) + ) + isFlyoutVisible = false + }, + content = { Text("Component") }, + modifier = Modifier.fillMaxWidth() + .padding(horizontal = 5.dp, vertical = 2.dp) + ) + HyperlinkButton( + onClick = { + uriHandler.openUri(ProjectUrl.galleryCodeOf(galleryPath)) + isFlyoutVisible = false + }, + content = { Text("Sample") }, + modifier = Modifier.fillMaxWidth() + .padding(horizontal = 5.dp, vertical = 2.dp) + ) + }, + content = { + DropDownButton( + onClick = { isFlyoutVisible = true }, + content = { + Icon( + painter = painterResource(Res.drawable.github_logo), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Text("Source") + } + ) + }, + placement = FlyoutPlacement.Bottom, + adaptivePlacement = true + ) + } } Spacer(modifier = Modifier.weight(1f)) if (controlVisible) { - ToggleButton( - checked = themeButtonChecked, - onCheckedChanged = onThemeButtonChanged, - content = { - Icon( - Icons.Filled.BrightnessHigh, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - }, - iconOnly = true, - modifier = Modifier.widthIn(40.dp) - ) + TooltipBox( + tooltip = { Text("Toggle theme") } + ){ + ToggleButton( + checked = themeButtonChecked, + onCheckedChanged = onThemeButtonChanged, + content = { + Icon( + Icons.Filled.BrightnessHigh, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + }, + iconOnly = true, + modifier = Modifier.widthIn(40.dp) + ) + } Spacer( modifier = Modifier.padding(2.dp).height(16.dp).width(1.dp) .background(FluentTheme.colors.stroke.divider.default) ) - Button( - onClick = { uriHandler.openUri(ProjectUrl.FEED_BACK) }, - content = { - Icon( - Icons.Default.PersonFeedback, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - }, - iconOnly = true, - modifier = Modifier.widthIn(40.dp) - ) + TooltipBox( + tooltip = { Text("Feedback") } + ){ + Button( + onClick = { uriHandler.openUri(ProjectUrl.FEED_BACK) }, + content = { + Icon( + Icons.Default.PersonFeedback, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + }, + iconOnly = true, + modifier = Modifier.widthIn(40.dp) + ) + } } } } @@ -182,7 +202,7 @@ fun GalleryHeader( } } -@OptIn(ExperimentalFoundationApi::class, ExperimentalTextApi::class) +@OptIn(ExperimentalTextApi::class) @Composable fun GalleryDescription(description: AnnotatedString, modifier: Modifier = Modifier) { Text( diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/basicinput/SliderScreen.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/basicinput/SliderScreen.kt index d8308324..abaa63fa 100644 --- a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/basicinput/SliderScreen.kt +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/basicinput/SliderScreen.kt @@ -41,12 +41,15 @@ fun SliderScreen() { val (confirmValue, setConfirmValue) = remember { mutableStateOf(0f) } val stepsSliderState = remember { SliderState(0f, 4, setConfirmValue, 0f..100f) } - Section("A Slider with range, steps and tick marks.", sourceCodeOfSliderStepsSample, content = { - SliderStepsSample(stepsSliderState) - }, output = { - Text("value: ${stepsSliderState.value}") - Text("confirmValue: $confirmValue") - }) + Section( + title = "A Slider with range, steps and tick marks.", + sourceCode = sourceCodeOfSliderStepsSample, + content = { SliderStepsSample(stepsSliderState) }, + output = { + Text("value: ${stepsSliderState.value}") + Text("confirmValue: $confirmValue") + } + ) /*Section("A Slider with tick marks.", "") { TodoComponent() }*/ diff --git a/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/status/TooltipScreen.kt b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/status/TooltipScreen.kt new file mode 100644 index 00000000..a66d9858 --- /dev/null +++ b/gallery/src/commonMain/kotlin/com/konyaco/fluent/gallery/screen/status/TooltipScreen.kt @@ -0,0 +1,72 @@ +@file:OptIn(ExperimentalFoundationApi::class, ExperimentalFluentApi::class) + +package com.konyaco.fluent.gallery.screen.status + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import com.konyaco.fluent.ExperimentalFluentApi +import com.konyaco.fluent.component.Button +import com.konyaco.fluent.component.Text +import com.konyaco.fluent.component.TooltipBox +import com.konyaco.fluent.component.rememberTooltipPositionProvider +import com.konyaco.fluent.component.rememberTooltipState +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(description = "Displays information for an element in a pop-up window.") +@Composable +fun TooltipScreen() { + GalleryPage( + title = "Tooltip", + description = "A Tooltip shows more information about a UI element. " + + "You might show information about what the element does, or what the user should do. " + + "The ToolTip is shown when a user hovers over or presses and holds the Ul element.", + componentPath = FluentSourceFile.TooltipBox, + galleryPath = ComponentPagePath.TooltipScreen + ) { + Section( + title = "Basic TooltipBox", + sourceCode = sourceCodeOfBasicTooltipBoxSample, + content = { BasicTooltipBoxSample() } + ) + + Section( + title = "TooltipBox with anchor padding", + sourceCode = sourceCodeOfTooltipBoxWithAnchorPaddingSample, + content = { TooltipBoxWithAnchorPaddingSample() } + ) + } +} + +@Sample +@Composable +private fun BasicTooltipBoxSample() { + TooltipBox( + tooltip = { Text("Simple Tooltip") } + ) { + Button( + onClick = {}, + content = { Text("Button with a simple Tooltip") } + ) + } +} + +@Sample +@Composable +private fun TooltipBoxWithAnchorPaddingSample() { + val state = rememberTooltipState() + TooltipBox( + state = state, + tooltip = { Text("Offset Tooltip.") }, + positionProvider = rememberTooltipPositionProvider( + anchorPadding = (-80).dp, + state = state + ) + ) { + Text("TextBlock with an offset ToolTip.") + } +} \ No newline at end of file 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 index 2d24ab5c..b388ff70 100644 --- 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 @@ -12,6 +12,7 @@ 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.ExperimentalFluentApi import com.konyaco.fluent.component.AutoSuggestBoxDefaults import com.konyaco.fluent.component.AutoSuggestionBox import com.konyaco.fluent.component.ListItem @@ -45,6 +46,7 @@ fun AutoSuggestBoxScreen() { } @Sample +@OptIn(ExperimentalFluentApi::class) @Composable private fun BasicAutoSuggestBoxSample() { var expanded by remember { mutableStateOf(false) } @@ -57,7 +59,7 @@ private fun BasicAutoSuggestBoxSample() { value = keyword, onValueChange = { keyword = it }, shape = AutoSuggestBoxDefaults.textFieldShape(expanded), - modifier = Modifier.widthIn(300.dp).suggestFlyoutAnchor() + modifier = Modifier.widthIn(300.dp).flyoutAnchor() ) val searchResult = remember(flatMapComponents) { snapshotFlow { keyword }.map { @@ -82,7 +84,7 @@ private fun BasicAutoSuggestBoxSample() { ) } }, - modifier = Modifier.suggestFlyoutSize() + modifier = Modifier.flyoutSize(matchAnchorWidth = true) ) } } \ 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 index 9ddb84fe..558e95ff 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 @@ -6,6 +6,7 @@ import androidx.compose.animation.SizeTransform import androidx.compose.animation.core.tween import androidx.compose.animation.expandHorizontally import androidx.compose.animation.shrinkHorizontally +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -48,6 +49,7 @@ 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.ExperimentalFluentApi import com.konyaco.fluent.FluentTheme import com.konyaco.fluent.animation.FluentDuration import com.konyaco.fluent.animation.FluentEasing @@ -55,6 +57,7 @@ import com.konyaco.fluent.background.BackgroundSizing import com.konyaco.fluent.background.Layer import com.konyaco.fluent.component.NavigationDefaults import com.konyaco.fluent.component.Text +import com.konyaco.fluent.component.TooltipBox 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 @@ -262,6 +265,7 @@ fun Window.CaptionButtonRow( } } +@OptIn(ExperimentalFoundationApi::class, ExperimentalFluentApi::class) @Composable fun CaptionButton( onClick: () -> Unit, @@ -272,33 +276,37 @@ fun CaptionButton( 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 + TooltipBox( + tooltip = { Text(icon.name) } ) { - Text( - text = icon.glyph.toString(), - fontFamily = windowsFontFamily(), - textAlign = TextAlign.Center, - fontSize = 10.sp, - modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center) - ) + 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 = windowsFontFamily(), + textAlign = TextAlign.Center, + fontSize = 10.sp, + modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center) + ) + } } }