Skip to content

Commit

Permalink
Merge pull request #90 from Konyaco/component/flyout_components
Browse files Browse the repository at this point in the history
[fluent] Add `TooltipBox` and `FlyoutAnchorScope`
  • Loading branch information
Sanlorng authored Feb 4, 2025
2 parents f041d5d + d6f39d2 commit eec43e1
Show file tree
Hide file tree
Showing 18 changed files with 1,198 additions and 572 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<LayoutCoordinates?>(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()
}
}

Expand Down Expand Up @@ -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
override fun Modifier.flyoutSize(matchAnchorWidth: Boolean): Modifier {
return with(scope) { flyoutSize(matchAnchorWidth) }
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -155,7 +165,7 @@ fun ColorPicker(
Spacer(
modifier = Modifier
.fillMaxWidth()
.height(12.dp)
.requiredHeight(12.dp)
.alphaBackground(CircleShape)
.background(
Brush.horizontalGradient(
Expand All @@ -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")
})
}
)
}
Expand Down Expand Up @@ -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)}")
}
)
}
}

Expand Down Expand Up @@ -534,6 +560,38 @@ sealed class ColorSpectrum {

companion object {
val Default: ColorSpectrum get() = Square

@Composable
internal fun LabelPopup(
offsetState: State<IntOffset?>,
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() {
Expand Down Expand Up @@ -644,6 +702,12 @@ sealed class ColorSpectrum {
}
) {
dot()
LabelPopup(
offsetState = offset,
label = {
label(color)
}
)
}
}

Expand Down Expand Up @@ -688,6 +752,7 @@ sealed class ColorSpectrum {

data object Square : ColorSpectrum() {

@OptIn(ExperimentalFluentApi::class)
@Composable
override fun content(
modifier: Modifier,
Expand Down Expand Up @@ -794,6 +859,7 @@ sealed class ColorSpectrum {
}
}


Box {
Box(
modifier = Modifier
Expand All @@ -804,6 +870,7 @@ sealed class ColorSpectrum {
}
) {
dot()
LabelPopup(offsetState = offset, label = { label(color) })
}
}
}
Expand Down
Loading

0 comments on commit eec43e1

Please sign in to comment.