diff --git a/composeApp/src/commonMain/kotlin/UITest.kt b/composeApp/src/commonMain/kotlin/UITest.kt index af665092..adbf52ee 100644 --- a/composeApp/src/commonMain/kotlin/UITest.kt +++ b/composeApp/src/commonMain/kotlin/UITest.kt @@ -11,14 +11,11 @@ import androidx.compose.foundation.layout.captionBarPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Favorite -import androidx.compose.material.icons.rounded.Home -import androidx.compose.material.icons.rounded.Menu -import androidx.compose.material.icons.rounded.Settings import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -40,16 +37,26 @@ import top.yukonga.miuix.kmp.basic.FloatingActionButton import top.yukonga.miuix.kmp.basic.HorizontalPager import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.IconButton +import top.yukonga.miuix.kmp.basic.ListPopup +import top.yukonga.miuix.kmp.basic.ListPopupDefaults import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior import top.yukonga.miuix.kmp.basic.NavigationBar import top.yukonga.miuix.kmp.basic.NavigationItem +import top.yukonga.miuix.kmp.basic.PopupPositionProvider import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.basic.ScrollBehavior import top.yukonga.miuix.kmp.basic.SmallTopAppBar import top.yukonga.miuix.kmp.basic.TopAppBar import top.yukonga.miuix.kmp.basic.rememberTopAppBarState +import top.yukonga.miuix.kmp.extra.DropdownImpl import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.icons.GitHub +import top.yukonga.miuix.kmp.icon.icons.ImmersionMore +import top.yukonga.miuix.kmp.icon.icons.Info +import top.yukonga.miuix.kmp.icon.icons.More +import top.yukonga.miuix.kmp.icon.icons.NavigatorSwitch +import top.yukonga.miuix.kmp.icon.icons.Settings +import top.yukonga.miuix.kmp.utils.MiuixPopupUtil.Companion.dismissPopup import utils.FPSMonitor @OptIn(FlowPreview::class) @@ -76,9 +83,10 @@ fun UITest( } val items = listOf( - NavigationItem("HomePage", Icons.Rounded.Home), - NavigationItem("DropDown", Icons.Rounded.Favorite), - NavigationItem("Settings", Icons.Rounded.Settings) + NavigationItem("HomePage", MiuixIcons.NavigatorSwitch), + NavigationItem("DropDown", MiuixIcons.Info), + NavigationItem("Settings", MiuixIcons.Settings), + NavigationItem("More", MiuixIcons.More) ) LaunchedEffect(pagerState) { @@ -93,6 +101,11 @@ fun UITest( val showFloatingActionButton = remember { mutableStateOf(true) } val enablePageUserScroll = remember { mutableStateOf(false) } + val isTopPopupExpanded = remember { mutableStateOf(false) } + val showTopPopup = remember { mutableStateOf(false) } + val isBottomPopupExpanded = remember { mutableStateOf(false) } + val showBottomPopup = remember { mutableStateOf(false) } + val uriHandler = LocalUriHandler.current Scaffold( @@ -109,12 +122,45 @@ fun UITest( title = "Miuix", scrollBehavior = currentScrollBehavior, actions = { + if (isTopPopupExpanded.value) { + ListPopup( + show = showTopPopup, + popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider, + alignment = PopupPositionProvider.Align.TopRight, + onDismissRequest = { + isTopPopupExpanded.value = false + } + ) { + LazyColumn { + items(items.take(3).size) { index -> + DropdownImpl( + text = items[index].label, + optionSize = items.take(3).size, + isSelected = items[index] == items[targetPage], + onSelectedIndexChange = { + targetPage = index + coroutineScope.launch { + pagerState.animateScrollToPage(index) + } + dismissPopup(showTopPopup) + isTopPopupExpanded.value = false + }, + textWidthDp = 100.dp, + index = index + ) + } + } + } + showTopPopup.value = true + } IconButton( - modifier = Modifier.padding(end = 12.dp), - onClick = { } + modifier = Modifier.padding(end = 21.dp).size(40.dp), + onClick = { + isTopPopupExpanded.value = true + } ) { Icon( - imageVector = Icons.Rounded.Menu, + imageVector = MiuixIcons.ImmersionMore, contentDescription = "Menu" ) } @@ -125,12 +171,45 @@ fun UITest( title = "Miuix", scrollBehavior = currentScrollBehavior, actions = { + if (isTopPopupExpanded.value) { + ListPopup( + show = showTopPopup, + popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider, + alignment = PopupPositionProvider.Align.TopRight, + onDismissRequest = { + isTopPopupExpanded.value = false + } + ) { + LazyColumn { + items(items.take(3).size) { index -> + DropdownImpl( + text = items[index].label, + optionSize = items.take(3).size, + isSelected = items[index] == items[targetPage], + onSelectedIndexChange = { + targetPage = index + coroutineScope.launch { + pagerState.animateScrollToPage(index) + } + dismissPopup(showTopPopup) + isTopPopupExpanded.value = false + }, + textWidthDp = 100.dp, + index = index + ) + } + } + } + showTopPopup.value = true + } IconButton( - modifier = Modifier.padding(end = 12.dp), - onClick = { } + modifier = Modifier.padding(end = 21.dp).size(40.dp), + onClick = { + isTopPopupExpanded.value = true + } ) { Icon( - imageVector = Icons.Rounded.Menu, + imageVector = MiuixIcons.ImmersionMore, contentDescription = "Menu" ) } @@ -146,13 +225,48 @@ fun UITest( enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically() ) { + if (isBottomPopupExpanded.value) { + ListPopup( + show = showBottomPopup, + popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider, + alignment = PopupPositionProvider.Align.BottomRight, + onDismissRequest = { + isBottomPopupExpanded.value = false + } + ) { + LazyColumn { + items(items.take(3).size) { index -> + DropdownImpl( + text = items[index].label, + optionSize = items.take(3).size, + isSelected = items[index] == items[targetPage], + onSelectedIndexChange = { + targetPage = index + coroutineScope.launch { + pagerState.animateScrollToPage(index) + } + dismissPopup(showBottomPopup) + isBottomPopupExpanded.value = false + }, + textWidthDp = 100.dp, + index = index + ) + } + } + } + showBottomPopup.value = true + } NavigationBar( items = items, selected = targetPage, onClick = { index -> - targetPage = index - coroutineScope.launch { - pagerState.animateScrollToPage(index) + if (index in 0..2) { + targetPage = index + coroutineScope.launch { + pagerState.animateScrollToPage(index) + } + } else { + isBottomPopupExpanded.value = true } } ) diff --git a/composeApp/src/commonMain/kotlin/component/TextComponent.kt b/composeApp/src/commonMain/kotlin/component/TextComponent.kt index c7b89e21..6da801aa 100644 --- a/composeApp/src/commonMain/kotlin/component/TextComponent.kt +++ b/composeApp/src/commonMain/kotlin/component/TextComponent.kt @@ -449,8 +449,7 @@ fun dialog2(showDialog: MutableState) { title = "Dropdown", items = dropdownOptions, selectedIndex = dropdownSelectedOption.value, - onSelectedIndexChange = { newOption -> dropdownSelectedOption.value = newOption }, - defaultWindowInsetsPadding = false + onSelectedIndexChange = { newOption -> dropdownSelectedOption.value = newOption } ) SuperSwitch( title = "Switch", diff --git a/miuix/src/androidMain/kotlin/top/yukonga/miuix/kmp/utils/Utils.android.kt b/miuix/src/androidMain/kotlin/top/yukonga/miuix/kmp/utils/Utils.android.kt index 614749a7..b4db734d 100644 --- a/miuix/src/androidMain/kotlin/top/yukonga/miuix/kmp/utils/Utils.android.kt +++ b/miuix/src/androidMain/kotlin/top/yukonga/miuix/kmp/utils/Utils.android.kt @@ -4,20 +4,29 @@ import android.annotation.SuppressLint import android.os.Build import android.view.RoundedCorner import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalView import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.window.layout.WindowMetricsCalculator +import kotlin.math.max +import kotlin.math.min @Composable actual fun getWindowSize(): WindowSize { + val configuration = LocalConfiguration.current + val screenWidthDp = configuration.screenWidthDp + val screenHeightDp = configuration.screenHeightDp val context = LocalContext.current val windowMetrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(context) val widthPx = windowMetrics.bounds.width() val heightPx = windowMetrics.bounds.height() - return WindowSize(widthPx, heightPx) + return if (screenWidthDp > screenHeightDp) + WindowSize(max(widthPx, heightPx), min(widthPx, heightPx)) + else + WindowSize(min(widthPx, heightPx), max(widthPx, heightPx)) } actual fun platform(): Platform = Platform.Android diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/ListPopup.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/ListPopup.kt new file mode 100644 index 00000000..f5bca37d --- /dev/null +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/ListPopup.kt @@ -0,0 +1,361 @@ +package top.yukonga.miuix.kmp.basic + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.captionBar +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.statusBars +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.ui.AbsoluteAlignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.layout +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +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 top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.utils.BackHandler +import top.yukonga.miuix.kmp.utils.MiuixPopupUtil.Companion.dismissPopup +import top.yukonga.miuix.kmp.utils.MiuixPopupUtil.Companion.showPopup +import top.yukonga.miuix.kmp.utils.SmoothRoundedCornerShape +import top.yukonga.miuix.kmp.utils.getWindowSize + +/** + * A popup with a list of items. + * + * @param show The show state of the [ListPopup]. + * @param popupModifier The modifier to be applied to the [ListPopup]. + * @param popupPositionProvider The [PopupPositionProvider] of the [ListPopup]. + * @param alignment The alignment of the [ListPopup]. + * @param onDismissRequest The callback when the [ListPopup] is dismissed. + * @param content The [Composable] content of the [ListPopup]. + */ +@Composable +fun ListPopup( + show: MutableState, + popupModifier: Modifier = Modifier, + popupPositionProvider: PopupPositionProvider = ListPopupDefaults.DropdownPositionProvider, + alignment: PopupPositionProvider.Align = PopupPositionProvider.Align.Right, + onDismissRequest: (() -> Unit)? = null, + content: @Composable () -> Unit +) { + var offset by remember { mutableStateOf(IntOffset.Zero) } + + val density = LocalDensity.current + val layoutDirection = LocalLayoutDirection.current + val getWindowSize = rememberUpdatedState(getWindowSize()) + var windowSize by remember { mutableStateOf(IntSize(getWindowSize.value.width, getWindowSize.value.height)) } + + var parentBounds by remember { mutableStateOf(IntRect.Zero) } + val windowBounds by rememberUpdatedState(with(density) { + IntRect( + left = WindowInsets.displayCutout.asPaddingValues(density).calculateLeftPadding(layoutDirection).roundToPx(), + top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding().roundToPx(), + right = windowSize.width - + WindowInsets.displayCutout.asPaddingValues(density).calculateRightPadding(layoutDirection).roundToPx(), + bottom = windowSize.height - + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding().roundToPx() - + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding().roundToPx() + ) + }) + var popupContentSize = IntSize.Zero + var popupMargin by remember { mutableStateOf(IntRect.Zero) } + + + var transformOrigin by remember { mutableStateOf(TransformOrigin.Center) } + + if (!listPopupStates.contains(show)) listPopupStates.add(show) + + LaunchedEffect(show.value) { + if (show.value) { + listPopupStates.forEach { state -> if (state != show) state.value = false } + } + } + + BackHandler(enabled = show.value) { + dismissPopup(show) + onDismissRequest?.let { it1 -> it1() } + } + + DisposableEffect(Unit) { + onDispose { + dismissPopup(show) + onDismissRequest?.let { it1 -> it1() } + } + } + + DisposableEffect(popupPositionProvider, alignment) { + val popupMargins = popupPositionProvider.getMargins() + popupMargin = with(density) { + IntRect( + left = popupMargins.calculateLeftPadding(layoutDirection).roundToPx(), + top = popupMargins.calculateTopPadding().roundToPx(), + right = popupMargins.calculateRightPadding(layoutDirection).roundToPx(), + bottom = popupMargins.calculateBottomPadding().roundToPx() + ) + } + if (popupContentSize != IntSize.Zero) { + offset = popupPositionProvider.calculatePosition( + parentBounds, + windowBounds, + layoutDirection, + popupContentSize, + popupMargin, + alignment + ) + } + onDispose {} + } + + if (show.value) { + val dropdownElevation by rememberUpdatedState(with(density) { + 11.dp.toPx() + }) + showPopup( + transformOrigin = { transformOrigin } + ) { + Box( + modifier = popupModifier + .pointerInput(Unit) { + detectTapGestures { + dismissPopup(show) + onDismissRequest?.let { it1 -> it1() } + } + } + .layout { measurable, constraints -> + val placeable = measurable.measure( + constraints.copy( + minWidth = 200.dp.roundToPx(), + minHeight = 50.dp.roundToPx(), + maxHeight = windowBounds.height + ) + ) + popupContentSize = IntSize(placeable.width, placeable.height) + offset = popupPositionProvider.calculatePosition( + parentBounds, + windowBounds, + layoutDirection, + popupContentSize, + popupMargin, + alignment + ) + layout(constraints.maxWidth, constraints.maxHeight) { + placeable.place(offset) + } + } + ) { + Box( + Modifier + .align(AbsoluteAlignment.TopLeft) + .graphicsLayer( + shadowElevation = dropdownElevation, + shape = SmoothRoundedCornerShape(16.dp), + ambientShadowColor = Color.Black.copy(alpha = 0.3f), + spotShadowColor = Color.Black.copy(alpha = 0.3f) + ) + .clip(SmoothRoundedCornerShape(16.dp)) + .background(MiuixTheme.colorScheme.surface) + ) { + content.invoke() + } + } + } + } + + Layout( + content = {}, + modifier = Modifier.onGloballyPositioned { childCoordinates -> + val parentCoordinates = childCoordinates.parentLayoutCoordinates!! + val positionInWindow = parentCoordinates.positionInWindow() + parentBounds = IntRect( + left = positionInWindow.x.toInt(), + top = positionInWindow.y.toInt(), + right = positionInWindow.x.toInt() + parentCoordinates.size.width, + bottom = positionInWindow.y.toInt() + parentCoordinates.size.height + ) + val windowHeightPx = getWindowSize.value.height + val windowWidthPx = getWindowSize.value.width + windowSize = IntSize(windowWidthPx, windowHeightPx) + with(density) { + val xInWindow = if (alignment in listOf( + PopupPositionProvider.Align.Right, + PopupPositionProvider.Align.TopRight, + PopupPositionProvider.Align.BottomRight, + ) + ) + parentBounds.right - popupMargin.right - 64.dp.roundToPx() + else + parentBounds.left + popupMargin.left + 64.dp.roundToPx() + val yInWindow = parentBounds.top + parentBounds.height / 2 - 56.dp.roundToPx() + transformOrigin = TransformOrigin( + xInWindow / windowWidthPx.toFloat(), + yInWindow / windowHeightPx.toFloat() + ) + } + } + ) { _, _ -> + layout(0, 0) {} + } +} + +interface PopupPositionProvider { + /** + * Calculate the position (offset) of Popup + * + * @param anchorBounds Bounds of the anchored (parent) component + * @param windowBounds Bounds of the safe area of window (excluding the [WindowInsets.Companion.statusBars], [WindowInsets.Companion.navigationBars] and [WindowInsets.Companion.captionBar]) + * @param layoutDirection [LayoutDirection] + * @param popupContentSize Actual size of the popup content + * @param popupMargin (Extra) Margins for the popup content. See [PopupPositionProvider.getMargins] + * @param alignment Alignment of the popup (relative to the window). See [PopupPositionProvider.Align] + */ + fun calculatePosition( + anchorBounds: IntRect, + windowBounds: IntRect, + layoutDirection: LayoutDirection, + popupContentSize: IntSize, + popupMargin: IntRect, + alignment: Align + ): IntOffset + + /** + * (Extra) Margins for the popup content. + */ + fun getMargins(): PaddingValues + + /** + * Position relative to the window, not relative to the anchor! + */ + enum class Align { + Left, + Right, + TopLeft, + TopRight, + BottomLeft, + BottomRight + } +} + +object ListPopupDefaults { + val DropdownPositionProvider = object : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowBounds: IntRect, + layoutDirection: LayoutDirection, + popupContentSize: IntSize, + popupMargin: IntRect, + alignment: PopupPositionProvider.Align + ): IntOffset { + val offsetX = if (alignment == PopupPositionProvider.Align.Right) { + anchorBounds.right - popupContentSize.width - popupMargin.right + } else { + anchorBounds.left + popupMargin.left + } + val offsetY = if (windowBounds.bottom - anchorBounds.bottom > popupContentSize.height) { + // Show below + anchorBounds.bottom + popupMargin.bottom + } else if (anchorBounds.top - windowBounds.top > popupContentSize.height) { + // Show above + anchorBounds.top - popupContentSize.height - popupMargin.top + } else { + // Middle + anchorBounds.top + anchorBounds.height / 2 - popupContentSize.height / 2 + } + return IntOffset( + x = offsetX.coerceIn(windowBounds.left, windowBounds.right - popupContentSize.width - popupMargin.right), + y = offsetY.coerceIn(windowBounds.top + popupMargin.top, windowBounds.bottom - popupContentSize.height - popupMargin.bottom) + ) + } + + override fun getMargins(): PaddingValues { + return PaddingValues(horizontal = 0.dp, vertical = 8.dp) + } + } + val ContextMenuPositionProvider = object : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowBounds: IntRect, + layoutDirection: LayoutDirection, + popupContentSize: IntSize, + popupMargin: IntRect, + alignment: PopupPositionProvider.Align + ): IntOffset { + val offsetX: Int + val offsetY: Int + when (alignment) { + PopupPositionProvider.Align.TopLeft -> { + offsetX = anchorBounds.left + popupMargin.left + offsetY = anchorBounds.bottom + popupMargin.top + } + + PopupPositionProvider.Align.TopRight -> { + offsetX = anchorBounds.right - popupContentSize.width - popupMargin.right + offsetY = anchorBounds.bottom + popupMargin.top + } + + PopupPositionProvider.Align.BottomLeft -> { + offsetX = anchorBounds.left + popupMargin.left + offsetY = anchorBounds.top - popupContentSize.height - popupMargin.bottom + } + + PopupPositionProvider.Align.BottomRight -> { + offsetX = anchorBounds.right - popupContentSize.width - popupMargin.right + offsetY = anchorBounds.top - popupContentSize.height - popupMargin.bottom + } + + else -> { + // Fallback + offsetX = if (alignment == PopupPositionProvider.Align.Right) { + anchorBounds.right - popupContentSize.width - popupMargin.right + } else { + anchorBounds.left + popupMargin.left + } + offsetY = if (windowBounds.bottom - anchorBounds.bottom > popupContentSize.height) { + // Show below + anchorBounds.bottom + popupMargin.bottom + } else if (anchorBounds.top - windowBounds.top > popupContentSize.height) { + // Show above + anchorBounds.top - popupContentSize.height - popupMargin.top + } else { + // Middle + anchorBounds.top + anchorBounds.height / 2 - popupContentSize.height / 2 + } + } + } + return IntOffset( + x = offsetX.coerceIn(windowBounds.left, windowBounds.right - popupContentSize.width - popupMargin.right), + y = offsetY.coerceIn(windowBounds.top + popupMargin.top, windowBounds.bottom - popupContentSize.height - popupMargin.bottom) + ) + } + + override fun getMargins(): PaddingValues { + return PaddingValues(horizontal = 20.dp, vertical = 0.dp) + } + } +} + +val listPopupStates = mutableStateListOf>() \ No newline at end of file diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/extra/SuperDropdown.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/extra/SuperDropdown.kt index a67fc255..27aea370 100644 --- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/extra/SuperDropdown.kt +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/extra/SuperDropdown.kt @@ -1,93 +1,53 @@ package top.yukonga.miuix.kmp.extra import androidx.compose.foundation.Image -import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.hoverable -import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.captionBar -import androidx.compose.foundation.layout.displayCutout -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.AbsoluteAlignment import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.BlendModeColorFilter import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.TransformOrigin -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.Layout -import androidx.compose.ui.layout.layout -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.layout.positionInWindow -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import top.yukonga.miuix.kmp.basic.BasicComponent import top.yukonga.miuix.kmp.basic.BasicComponentColors import top.yukonga.miuix.kmp.basic.BasicComponentDefaults -import top.yukonga.miuix.kmp.basic.Box +import top.yukonga.miuix.kmp.basic.ListPopup +import top.yukonga.miuix.kmp.basic.PopupPositionProvider import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.icons.ArrowUpDownIntegrated import top.yukonga.miuix.kmp.icon.icons.Check import top.yukonga.miuix.kmp.interfaces.HoldDownInteraction import top.yukonga.miuix.kmp.theme.MiuixTheme -import top.yukonga.miuix.kmp.utils.BackHandler import top.yukonga.miuix.kmp.utils.MiuixPopupUtil.Companion.dismissPopup -import top.yukonga.miuix.kmp.utils.MiuixPopupUtil.Companion.showPopup -import top.yukonga.miuix.kmp.utils.Platform -import top.yukonga.miuix.kmp.utils.SmoothRoundedCornerShape -import top.yukonga.miuix.kmp.utils.getWindowSize -import top.yukonga.miuix.kmp.utils.platform -import kotlin.math.roundToInt /** * A dropdown with a title and a summary. @@ -96,14 +56,11 @@ import kotlin.math.roundToInt * @param items The options of the [SuperDropdown]. * @param selectedIndex The index of the selected option. * @param modifier The modifier to be applied to the [SuperDropdown]. - * @param popupModifier The modifier to be applied to the popup of the [SuperDropdown]. * @param titleColor The color of the title. * @param summary The summary of the [SuperDropdown]. * @param summaryColor The color of the summary. * @param mode The dropdown show mode of the [SuperDropdown]. - * @param horizontalPadding The horizontal padding of the [SuperDropdown]. * @param insideMargin The margin inside the [SuperDropdown]. - * @param defaultWindowInsetsPadding Whether to apply default window insets padding to the [SuperDropdown]. * @param enabled Whether the [SuperDropdown] is enabled. * @param showValue Whether to show the selected value of the [SuperDropdown]. * @param onSelectedIndexChange The callback when the selected index of the [SuperDropdown] is changed. @@ -114,42 +71,29 @@ fun SuperDropdown( items: List, selectedIndex: Int, modifier: Modifier = Modifier, - popupModifier: Modifier = Modifier, titleColor: BasicComponentColors = BasicComponentDefaults.titleColor(), summary: String? = null, summaryColor: BasicComponentColors = BasicComponentDefaults.summaryColor(), mode: DropDownMode = DropDownMode.Normal, - horizontalPadding: Dp = 0.dp, insideMargin: PaddingValues = BasicComponentDefaults.InsideMargin, - defaultWindowInsetsPadding: Boolean = true, enabled: Boolean = true, showValue: Boolean = true, onSelectedIndexChange: ((Int) -> Unit)?, ) { val interactionSource = remember { MutableInteractionSource() } - val isDropdownPreExpand = remember { mutableStateOf(false) } val isDropdownExpanded = remember { mutableStateOf(false) } + val showPopup = remember { mutableStateOf(false) } val coroutineScope = rememberCoroutineScope() val held = remember { mutableStateOf(null) } val hapticFeedback = LocalHapticFeedback.current val actionColor = if (enabled) MiuixTheme.colorScheme.onSurfaceVariantActions else MiuixTheme.colorScheme.disabledOnSecondaryVariant - var pressInteraction: PressInteraction.Press? = null var alignLeft by rememberSaveable { mutableStateOf(true) } - var componentInnerOffsetXPx by remember { mutableIntStateOf(0) } - var componentInnerOffsetYPx by remember { mutableIntStateOf(0) } - var componentInnerHeightPx by remember { mutableIntStateOf(0) } - var componentInnerWidthPx by remember { mutableIntStateOf(0) } - - val density = LocalDensity.current - val getWindowSize = rememberUpdatedState(getWindowSize()) - val windowHeightPx by rememberUpdatedState(getWindowSize.value.height) - val windowWidthPx by rememberUpdatedState(getWindowSize.value.width) - var transformOrigin by remember { mutableStateOf(TransformOrigin.Center) } DisposableEffect(Unit) { onDispose { - dismissPopup(isDropdownExpanded) + dismissPopup(showPopup) + isDropdownExpanded.value = false } } @@ -164,98 +108,56 @@ fun SuperDropdown( BasicComponent( modifier = modifier - .indication( - interactionSource = interactionSource, - indication = LocalIndication.current - ) - .hoverable( - interactionSource = interactionSource, - enabled = enabled - ) .pointerInput(Unit) { - detectTapGestures( - onPress = { offset -> - if (enabled) { - coroutineScope { - val delayJob = launch { - alignLeft = offset.x < (size.width / 2) - isDropdownPreExpand.value = true - delay( - when (platform()) { - Platform.IOS -> 150 - Platform.Android -> 100 - else -> 0 - } - ) - val press = PressInteraction.Press(offset) - interactionSource.emit(press) - pressInteraction = press - } - val success = tryAwaitRelease() - isDropdownPreExpand.value = false - if (success) { - isDropdownExpanded.value = enabled - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - interactionSource.emit(HoldDownInteraction.Hold().also { - held.value = it - }) - } - if (delayJob.isActive) { - delayJob.cancelAndJoin() - if (success) { - val press = PressInteraction.Press(offset) - val release = PressInteraction.Release(press) - interactionSource.emit(press) - interactionSource.emit(release) - } - } else { - pressInteraction?.let { pressInteraction -> - val endInteraction = if (success) { - PressInteraction.Release(pressInteraction) - } else { - PressInteraction.Cancel(pressInteraction) - } - interactionSource.emit(endInteraction) - } - } - pressInteraction = null + awaitPointerEventScope { + while (enabled) { + val event = awaitPointerEvent() + if (event.type != PointerEventType.Move) { + val eventChange = event.changes.first() + if (eventChange.pressed) { + alignLeft = eventChange.position.x < (size.width / 2) } } } - ) + } }, + interactionSource = interactionSource, insideMargin = insideMargin, title = title, titleColor = titleColor, summary = summary, summaryColor = summaryColor, leftAction = { - if (isDropdownPreExpand.value) { - Layout( - content = {}, - modifier = Modifier.onGloballyPositioned { childCoordinates -> - val parentCoordinates = - childCoordinates.parentLayoutCoordinates ?: return@onGloballyPositioned - val positionInWindow = parentCoordinates.positionInWindow() - componentInnerOffsetXPx = positionInWindow.x.toInt() - componentInnerOffsetYPx = positionInWindow.y.toInt() - componentInnerHeightPx = parentCoordinates.size.height - componentInnerWidthPx = parentCoordinates.size.width - with(density) { - val xInWindow = componentInnerOffsetXPx + if (mode == DropDownMode.AlwaysOnRight || !alignLeft) - componentInnerWidthPx - 64.dp.roundToPx() - else - 64.dp.roundToPx() - val yInWindow = componentInnerOffsetYPx + componentInnerHeightPx / 2 - 56.dp.roundToPx() - transformOrigin = TransformOrigin( - xInWindow / windowWidthPx.toFloat(), - yInWindow / windowHeightPx.toFloat() + if (isDropdownExpanded.value) { + ListPopup( + show = showPopup, + alignment = if ((mode == DropDownMode.AlwaysOnRight || !alignLeft)) + PopupPositionProvider.Align.Right + else + PopupPositionProvider.Align.Left, + onDismissRequest = { + isDropdownExpanded.value = false + } + ) { + LazyColumn { + items(items.size) { index -> + DropdownImpl( + text = items[index], + optionSize = items.size, + isSelected = selectedIndex == index, + onSelectedIndexChange = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + onSelectedIndexChange?.let { it1 -> it1(it) } + dismissPopup(showPopup) + isDropdownExpanded.value = false + }, + textWidthDp = 128.dp, + index = index ) } } - ) { _, _ -> - layout(0, 0) {} } + showPopup.value = true } }, rightActions = { @@ -280,127 +182,19 @@ fun SuperDropdown( contentDescription = null ) }, - enabled = enabled - ) - if (isDropdownExpanded.value) { - if (!dropdownStates.contains(isDropdownExpanded)) dropdownStates.add(isDropdownExpanded) - LaunchedEffect(isDropdownExpanded.value) { - if (isDropdownExpanded.value) { - dropdownStates.forEach { state -> if (state != isDropdownExpanded) state.value = false } - } - } - - val textMeasurer = rememberTextMeasurer() - val textStyle = remember { TextStyle(fontWeight = FontWeight.Medium, fontSize = 16.sp) } - val textWidthDp = remember(items) { items.maxOfOrNull { with(density) { textMeasurer.measure(text = it, style = textStyle).size.width.toDp() } } } - val statusBarPx by rememberUpdatedState( - with(density) { WindowInsets.statusBars.asPaddingValues().calculateTopPadding().toPx() }.roundToInt() - ) - val navigationBarPx by rememberUpdatedState( - with(density) { WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding().toPx() }.roundToInt() - ) - val captionBarPx by rememberUpdatedState( - with(density) { WindowInsets.captionBar.asPaddingValues().calculateBottomPadding().toPx() }.roundToInt() - ) - val dropdownElevation by rememberUpdatedState(with(density) { - 11.dp.toPx() - }) - val insideTopPx by rememberUpdatedState(with(density) { - insideMargin.calculateTopPadding().toPx() - }.roundToInt()) - val insideBottomPx by rememberUpdatedState(with(density) { - insideMargin.calculateBottomPadding().toPx() - }.roundToInt()) - val displayCutoutLeftSize by rememberUpdatedState(with(density) { - WindowInsets.displayCutout.asPaddingValues(density).calculateLeftPadding(LayoutDirection.Ltr).toPx() - }.roundToInt()) - val paddingPx by rememberUpdatedState(with(density) { horizontalPadding.toPx() }.roundToInt()) - - BackHandler(enabled = isDropdownExpanded.value) { - dismissPopup(isDropdownExpanded) - } - - showPopup( - transformOrigin = { transformOrigin }, - content = { - Box( - modifier = if (defaultWindowInsetsPadding) { - popupModifier - .windowInsetsPadding(WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal)) - } else { - popupModifier - } - .pointerInput(Unit) { - detectTapGestures( - onTap = { - dismissPopup(isDropdownExpanded) - } - ) - } - .layout { measurable, constraints -> - val placeable = measurable.measure( - constraints.copy( - minWidth = 200.dp.roundToPx(), - minHeight = 50.dp.roundToPx(), - maxHeight = windowHeightPx - statusBarPx - navigationBarPx - captionBarPx - ) - ) - layout(constraints.maxWidth, constraints.maxHeight) { - val xCoordinate = calculateOffsetXPx( - componentInnerOffsetXPx, - componentInnerWidthPx, - placeable.width, - paddingPx, - displayCutoutLeftSize, - defaultWindowInsetsPadding, - (mode == DropDownMode.AlwaysOnRight || !alignLeft) - ) - val yCoordinate = calculateOffsetYPx( - windowHeightPx, - componentInnerOffsetYPx, - placeable.height, - componentInnerHeightPx, - insideTopPx, - insideBottomPx, - statusBarPx, - navigationBarPx, - captionBarPx - ) - placeable.place(xCoordinate, yCoordinate) - } - } - ) { - LazyColumn( - modifier = Modifier - .align(AbsoluteAlignment.TopLeft) - .graphicsLayer( - shadowElevation = dropdownElevation, - shape = SmoothRoundedCornerShape(16.dp), - ambientShadowColor = Color.Black.copy(alpha = 0.3f), - spotShadowColor = Color.Black.copy(alpha = 0.3f) - ) - .clip(SmoothRoundedCornerShape(16.dp)) - .background(MiuixTheme.colorScheme.surface) - ) { - items(items.size) { index -> - DropdownImpl( - text = items[index], - optionSize = items.size, - isSelected = selectedIndex == index, - onSelectedIndexChange = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - onSelectedIndexChange?.let { it1 -> it1(it) } - dismissPopup(isDropdownExpanded) - }, - textWidthDp = textWidthDp, - index = index - ) - } - } + onClick = { + if (enabled) { + isDropdownExpanded.value = enabled + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + coroutineScope.launch { + interactionSource.emit(HoldDownInteraction.Hold().also { + held.value = it + }) } } - ) - } + }, + enabled = enabled + ) } /** @@ -452,7 +246,7 @@ fun DropdownImpl( .padding(top = additionalTopPadding, bottom = additionalBottomPadding) ) { Text( - modifier = Modifier.width(textWidthDp ?: 50.dp), + modifier = Modifier.width(textWidthDp ?: 128.dp), text = text, fontSize = MiuixTheme.textStyles.body1.fontSize, fontWeight = FontWeight.Medium, @@ -467,76 +261,6 @@ fun DropdownImpl( } } -/** - * Calculate the offset of the dropdown. - * - * @param componentInnerOffsetXPx The offset of the component inside. - * @param componentInnerWidthPx The width of the component inside. - * @param dropdownWidthPx The width of the dropdown. - * @param extraPaddingPx The extra padding of the dropdown. - * @param displayCutoutLeftSizePx The size of the display cutout on the left. - * @param defaultWindowInsetsPadding Whether to apply default window insets padding to the dropdown. - * @param alignRight Whether to align the dropdown to the right. - * @return The offset of the dropdown. - */ -fun calculateOffsetXPx( - componentInnerOffsetXPx: Int, - componentInnerWidthPx: Int, - dropdownWidthPx: Int, - extraPaddingPx: Int, - displayCutoutLeftSizePx: Int, - defaultWindowInsetsPadding: Boolean, - alignRight: Boolean -): Int { - return if (alignRight) { - componentInnerOffsetXPx + componentInnerWidthPx - dropdownWidthPx - extraPaddingPx - if (defaultWindowInsetsPadding) displayCutoutLeftSizePx else 0 - } else { - componentInnerOffsetXPx + extraPaddingPx - if (defaultWindowInsetsPadding) displayCutoutLeftSizePx else 0 - } -} - -/** - * Calculate the offset of the dropdown. - * - * @param windowHeightPx The height of the window. - * @param dropdownOffsetPx The default offset of the dropdown. - * @param dropdownHeightPx The height of the dropdown. - * @param componentHeightPx The height of the component. - * @param insideTopHeightPx The height of the top component inside. - * @param insideBottomHeightPx The height of the bottom component inside. - * @param statusBarPx The height of the status bar padding. - * @param navigationBarPx The height of the navigation bar padding. - * @param captionBarPx The height of the caption bar padding. - * @return The offset of the current dropdown. - */ -fun calculateOffsetYPx( - windowHeightPx: Int, - dropdownOffsetPx: Int, - dropdownHeightPx: Int, - componentHeightPx: Int, - insideTopHeightPx: Int, - insideBottomHeightPx: Int, - statusBarPx: Int, - navigationBarPx: Int, - captionBarPx: Int -): Int { - return (if (windowHeightPx - captionBarPx - navigationBarPx - dropdownOffsetPx - componentHeightPx > dropdownHeightPx) { - // Show below - dropdownOffsetPx + componentHeightPx + insideBottomHeightPx / 2 - } else if (dropdownOffsetPx - statusBarPx > dropdownHeightPx) { - // Show above - dropdownOffsetPx - dropdownHeightPx - insideTopHeightPx / 2 - } else { - // Middle - dropdownOffsetPx + componentHeightPx / 2 - dropdownHeightPx / 2 - }).coerceIn(statusBarPx, windowHeightPx - captionBarPx - navigationBarPx - dropdownHeightPx) -} - -/** - * Only one dropdown is allowed to be displayed at a time. - */ -val dropdownStates = mutableStateListOf>() - /** * The dropdown show mode. */ diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/extra/SuperSpinner.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/extra/SuperSpinner.kt index 412af9e9..39a1cdeb 100644 --- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/extra/SuperSpinner.kt +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/extra/SuperSpinner.kt @@ -1,79 +1,53 @@ package top.yukonga.miuix.kmp.extra import androidx.compose.foundation.Image -import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.hoverable -import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.captionBar -import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.sizeIn -import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.AbsoluteAlignment import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.BlendModeColorFilter import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.TransformOrigin -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.Layout -import androidx.compose.ui.layout.layout import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInWindow -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -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 kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import top.yukonga.miuix.kmp.basic.BasicComponent import top.yukonga.miuix.kmp.basic.BasicComponentColors import top.yukonga.miuix.kmp.basic.BasicComponentDefaults -import top.yukonga.miuix.kmp.basic.Box +import top.yukonga.miuix.kmp.basic.ListPopup +import top.yukonga.miuix.kmp.basic.PopupPositionProvider import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.TextButton import top.yukonga.miuix.kmp.icon.MiuixIcons @@ -81,15 +55,8 @@ import top.yukonga.miuix.kmp.icon.icons.ArrowUpDownIntegrated import top.yukonga.miuix.kmp.icon.icons.Check import top.yukonga.miuix.kmp.interfaces.HoldDownInteraction import top.yukonga.miuix.kmp.theme.MiuixTheme -import top.yukonga.miuix.kmp.utils.BackHandler import top.yukonga.miuix.kmp.utils.MiuixPopupUtil.Companion.dismissDialog import top.yukonga.miuix.kmp.utils.MiuixPopupUtil.Companion.dismissPopup -import top.yukonga.miuix.kmp.utils.MiuixPopupUtil.Companion.showPopup -import top.yukonga.miuix.kmp.utils.Platform -import top.yukonga.miuix.kmp.utils.SmoothRoundedCornerShape -import top.yukonga.miuix.kmp.utils.getWindowSize -import top.yukonga.miuix.kmp.utils.platform -import kotlin.math.roundToInt /** * A spinner component with Miuix style. @@ -98,15 +65,12 @@ import kotlin.math.roundToInt * @param items The list of [SpinnerEntry] to be shown in the [SuperSpinner]. * @param selectedIndex The index of the selected item in the [SuperSpinner]. * @param modifier The [Modifier] to be applied to the [SuperSpinner]. - * @param popupModifier The [Modifier] to be applied to the popup of the [SuperSpinner]. * @param titleColor The color of the title of the [SuperSpinner]. * @param summary The summary of the [SuperSpinner]. * @param summaryColor The color of the summary of the [SuperSpinner]. * @param mode The mode of the [SuperSpinner]. - * @param horizontalPadding The horizontal padding of the [SuperSpinner]. * @param leftAction The action to be shown at the left side of the [SuperSpinner]. * @param insideMargin The [PaddingValues] to be applied inside the [SuperSpinner]. - * @param defaultWindowInsetsPadding Whether to apply the default window insets padding to the [SuperSpinner]. * @param enabled Whether the [SuperSpinner] is enabled. * @param showValue Whether to show the value of the [SuperSpinner]. * @param onSelectedIndexChange The callback to be invoked when the selected index of the [SuperSpinner] is changed. @@ -117,43 +81,30 @@ fun SuperSpinner( items: List, selectedIndex: Int, modifier: Modifier = Modifier, - popupModifier: Modifier = Modifier, titleColor: BasicComponentColors = BasicComponentDefaults.titleColor(), summary: String? = null, summaryColor: BasicComponentColors = BasicComponentDefaults.summaryColor(), mode: SpinnerMode = SpinnerMode.Normal, - horizontalPadding: Dp = 0.dp, leftAction: @Composable (() -> Unit)? = null, insideMargin: PaddingValues = BasicComponentDefaults.InsideMargin, - defaultWindowInsetsPadding: Boolean = true, enabled: Boolean = true, showValue: Boolean = true, onSelectedIndexChange: ((Int) -> Unit)?, ) { val interactionSource = remember { MutableInteractionSource() } - val isDropdownPreExpand = remember { mutableStateOf(false) } val isDropdownExpanded = remember { mutableStateOf(false) } + val showPopup = remember { mutableStateOf(false) } val coroutineScope = rememberCoroutineScope() val held = remember { mutableStateOf(null) } val hapticFeedback = LocalHapticFeedback.current val actionColor = if (enabled) MiuixTheme.colorScheme.onSurfaceVariantActions else MiuixTheme.colorScheme.disabledOnSecondaryVariant - var pressInteraction: PressInteraction.Press? = null var alignLeft by rememberSaveable { mutableStateOf(true) } - var componentInnerOffsetXPx by remember { mutableIntStateOf(0) } - var componentInnerOffsetYPx by remember { mutableIntStateOf(0) } - var componentInnerHeightPx by remember { mutableIntStateOf(0) } - var componentInnerWidthPx by remember { mutableIntStateOf(0) } - - val density = LocalDensity.current - val getWindowSize = rememberUpdatedState(getWindowSize()) - val windowHeightPx by rememberUpdatedState(getWindowSize.value.height) - val windowWidthPx by rememberUpdatedState(getWindowSize.value.width) - var transformOrigin by remember { mutableStateOf(TransformOrigin.Center) } DisposableEffect(Unit) { onDispose { - dismissPopup(isDropdownExpanded) + dismissPopup(showPopup) + isDropdownExpanded.value = false } } @@ -168,98 +119,55 @@ fun SuperSpinner( BasicComponent( modifier = modifier - .indication( - interactionSource = interactionSource, - indication = LocalIndication.current - ) - .hoverable( - interactionSource = interactionSource, - enabled = enabled - ) .pointerInput(Unit) { - detectTapGestures( - onPress = { offset -> - if (enabled) { - coroutineScope { - val delayJob = launch { - alignLeft = offset.x < (size.width / 2) - isDropdownPreExpand.value = true - delay( - when (platform()) { - Platform.IOS -> 150 - Platform.Android -> 100 - else -> 0 - } - ) - val press = PressInteraction.Press(offset) - interactionSource.emit(press) - pressInteraction = press - } - val success = tryAwaitRelease() - isDropdownPreExpand.value = false - if (success) { - isDropdownExpanded.value = enabled - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - interactionSource.emit(HoldDownInteraction.Hold().also { - held.value = it - }) - } - if (delayJob.isActive) { - delayJob.cancelAndJoin() - if (success) { - val press = PressInteraction.Press(offset) - val release = PressInteraction.Release(press) - interactionSource.emit(press) - interactionSource.emit(release) - } - } else { - pressInteraction?.let { pressInteraction -> - val endInteraction = if (success) { - PressInteraction.Release(pressInteraction) - } else { - PressInteraction.Cancel(pressInteraction) - } - interactionSource.emit(endInteraction) - } - } - pressInteraction = null + awaitPointerEventScope { + while (enabled) { + val event = awaitPointerEvent() + if (event.type != PointerEventType.Move) { + val eventChange = event.changes.first() + if (eventChange.pressed) { + alignLeft = eventChange.position.x < (size.width / 2) } } } - ) + } }, + interactionSource = interactionSource, insideMargin = insideMargin, title = title, titleColor = titleColor, summary = summary, summaryColor = summaryColor, leftAction = { - if (isDropdownPreExpand.value) { - Layout( - content = {}, - modifier = Modifier.onGloballyPositioned { childCoordinates -> - val parentCoordinates = - childCoordinates.parentLayoutCoordinates ?: return@onGloballyPositioned - val positionInWindow = parentCoordinates.positionInWindow() - componentInnerOffsetXPx = positionInWindow.x.toInt() - componentInnerOffsetYPx = positionInWindow.y.toInt() - componentInnerHeightPx = parentCoordinates.size.height - componentInnerWidthPx = parentCoordinates.size.width - with(density) { - val xInWindow = componentInnerOffsetXPx + if (mode == SpinnerMode.AlwaysOnRight || !alignLeft) - componentInnerWidthPx - 64.dp.roundToPx() - else - 64.dp.roundToPx() - val yInWindow = componentInnerOffsetYPx + componentInnerHeightPx / 2 - 56.dp.roundToPx() - transformOrigin = TransformOrigin( - xInWindow / windowWidthPx.toFloat(), - yInWindow / windowHeightPx.toFloat() - ) + if (isDropdownExpanded.value) { + ListPopup( + show = showPopup, + alignment = if ((mode == SpinnerMode.AlwaysOnRight || !alignLeft)) + PopupPositionProvider.Align.Right + else + PopupPositionProvider.Align.Left, + onDismissRequest = { + isDropdownExpanded.value = false + } + ) { + LazyColumn { + items(items.size) { index -> + SpinnerItemImpl( + entry = items[index], + entryCount = items.size, + isSelected = selectedIndex == index, + index = index, + dialogMode = false + ) { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + onSelectedIndexChange?.let { it1 -> it1(it) } + dismissPopup(showPopup) + isDropdownExpanded.value = false + } } } - ) { _, _ -> - layout(0, 0) {} } + showPopup.value = true } leftAction?.invoke() }, @@ -285,130 +193,19 @@ fun SuperSpinner( contentDescription = null ) }, - enabled = enabled - ) - - if (isDropdownExpanded.value) { - if (!dropdownStates.contains(isDropdownExpanded)) dropdownStates.add(isDropdownExpanded) - LaunchedEffect(isDropdownExpanded.value) { - if (isDropdownExpanded.value) { - dropdownStates.forEach { state -> if (state != isDropdownExpanded) state.value = false } - } - } - - val statusBarPx by rememberUpdatedState( - with(density) { WindowInsets.statusBars.asPaddingValues().calculateTopPadding().toPx() }.roundToInt() - ) - val navigationBarPx by rememberUpdatedState( - with(density) { WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding().toPx() }.roundToInt() - ) - val captionBarPx by rememberUpdatedState( - with(density) { WindowInsets.captionBar.asPaddingValues().calculateBottomPadding().toPx() }.roundToInt() - ) - val dropdownElevation by rememberUpdatedState(with(density) { - 11.dp.toPx() - }) - val insideTopPx by rememberUpdatedState(with(density) { - insideMargin.calculateTopPadding().toPx() - }.roundToInt()) - val insideBottomPx by rememberUpdatedState(with(density) { - insideMargin.calculateBottomPadding().toPx() - }.roundToInt()) - val displayCutoutLeftSize by rememberUpdatedState(with(density) { - WindowInsets.displayCutout.asPaddingValues(density).calculateLeftPadding(LayoutDirection.Ltr).toPx() - }.roundToInt()) - val paddingPx by rememberUpdatedState(with(density) { - horizontalPadding.toPx() - }.roundToInt()) - - BackHandler(enabled = isDropdownExpanded.value) { - dismissPopup(isDropdownExpanded) - } - - showPopup( - transformOrigin = { transformOrigin }, - content = { - Box( - modifier = if (defaultWindowInsetsPadding) { - popupModifier - .windowInsetsPadding( - WindowInsets.displayCutout.only( - WindowInsetsSides.Horizontal - ) - ) - } else { - popupModifier - } - .pointerInput(Unit) { - detectTapGestures( - onTap = { - dismissPopup(isDropdownExpanded) - } - ) - } - .layout { measurable, constraints -> - val placeable = measurable.measure( - constraints.copy( - minWidth = 200.dp.roundToPx(), - minHeight = 50.dp.roundToPx(), - maxHeight = windowHeightPx - statusBarPx - navigationBarPx - captionBarPx - ) - ) - layout(constraints.maxWidth, constraints.maxHeight) { - val xCoordinate = calculateOffsetXPx( - componentInnerOffsetXPx, - componentInnerWidthPx, - placeable.width, - paddingPx, - displayCutoutLeftSize, - defaultWindowInsetsPadding, - (mode == SpinnerMode.AlwaysOnRight || !alignLeft) - ) - val yCoordinate = calculateOffsetYPx( - windowHeightPx, - componentInnerOffsetYPx, - placeable.height, - componentInnerHeightPx, - insideTopPx, - insideBottomPx, - statusBarPx, - navigationBarPx, - captionBarPx - ) - placeable.place(xCoordinate, yCoordinate) - } - } - ) { - LazyColumn( - modifier = Modifier - .align(AbsoluteAlignment.TopLeft) - .graphicsLayer( - shadowElevation = dropdownElevation, - shape = SmoothRoundedCornerShape(16.dp), - ambientShadowColor = Color.Black.copy(alpha = 0.3f), - spotShadowColor = Color.Black.copy(alpha = 0.3f) - ) - .clip(SmoothRoundedCornerShape(16.dp)) - .background(MiuixTheme.colorScheme.surface) - ) { - items(items.size) { index -> - SpinnerItemImpl( - entry = items[index], - entryCount = items.size, - isSelected = selectedIndex == index, - index = index, - dialogMode = false - ) { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - onSelectedIndexChange?.let { it1 -> it1(it) } - dismissPopup(isDropdownExpanded) - } - } - } + onClick = { + if (enabled) { + isDropdownExpanded.value = enabled + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + coroutineScope.launch { + interactionSource.emit(HoldDownInteraction.Hold().also { + held.value = it + }) } } - ) - } + }, + enabled = enabled + ) } /** @@ -548,30 +345,44 @@ fun SuperSpinner( }, insideMargin = DpSize(0.dp, 24.dp) ) { - Column { - LazyColumn { - items(items.size) { index -> - SpinnerItemImpl( - entry = items[index], - entryCount = items.size, - isSelected = selectedIndex == index, - index = index, - dialogMode = true - ) { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - onSelectedIndexChange?.let { it1 -> it1(it) } - dismissDialog(isDropdownExpanded) + Layout( + content = { + LazyColumn { + items(items.size) { index -> + SpinnerItemImpl( + entry = items[index], + entryCount = items.size, + isSelected = selectedIndex == index, + index = index, + dialogMode = true + ) { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + onSelectedIndexChange?.let { it1 -> it1(it) } + dismissDialog(isDropdownExpanded) + } } } + TextButton( + modifier = Modifier.padding(start = 24.dp, top = 12.dp, end = 24.dp).fillMaxWidth(), + text = dialogButtonString, + minHeight = 50.dp, + onClick = { + dismissDialog(isDropdownExpanded) + } + ) + } + ) { measurables, constraints -> + if (measurables.size != 2) { + layout(0, 0) { } + } + val button = measurables[1].measure(constraints) + val lazyList = measurables[0].measure(constraints.copy( + maxHeight = constraints.maxHeight - button.height + )) + layout(constraints.maxWidth, lazyList.height + button.height) { + lazyList.place(0, 0) + button.place(0, lazyList.height) } - TextButton( - modifier = Modifier.padding(start = 24.dp, top = 12.dp, end = 24.dp).fillMaxWidth(), - text = dialogButtonString, - minHeight = 50.dp, - onClick = { - dismissDialog(isDropdownExpanded) - } - ) } } } diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/icon/icons/ImmersionMore.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/icon/icons/ImmersionMore.kt new file mode 100644 index 00000000..7f32750d --- /dev/null +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/icon/icons/ImmersionMore.kt @@ -0,0 +1,52 @@ +package top.yukonga.miuix.kmp.icon.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp +import top.yukonga.miuix.kmp.icon.MiuixIcons + +val MiuixIcons.ImmersionMore: ImageVector + get() { + if (_immersionMore != null) return _immersionMore!! + _immersionMore = ImageVector.Builder("ImmersionMore", 26.dp, 26.dp, 26.0f, 26.0f).apply { + path( + fill = SolidColor(Color.Black), + pathFillType = NonZero + ) { + moveTo(12.9999f, 14.4054f) + curveTo(12.2237f, 14.4054f, 11.5945f, 13.7762f, 11.5945f, 13.0f) + curveTo(11.5945f, 12.2238f, 12.2237f, 11.5946f, 12.9999f, 11.5946f) + curveTo(13.7761f, 11.5946f, 14.4053f, 12.2238f, 14.4053f, 13.0f) + curveTo(14.4053f, 13.7762f, 13.7761f, 14.4054f, 12.9999f, 14.4054f) + close() + } + path( + fill = SolidColor(Color.Black), + pathFillType = NonZero + ) { + moveTo(12.9999f, 21.4324f) + curveTo(12.2237f, 21.4324f, 11.5945f, 20.8032f, 11.5945f, 20.027f) + curveTo(11.5945f, 19.2508f, 12.2237f, 18.6216f, 12.9999f, 18.6216f) + curveTo(13.7761f, 18.6216f, 14.4053f, 19.2508f, 14.4053f, 20.027f) + curveTo(14.4053f, 20.8032f, 13.7761f, 21.4324f, 12.9999f, 21.4324f) + close() + } + path( + fill = SolidColor(Color.Black), + pathFillType = NonZero + ) { + moveTo(12.9999f, 7.3784f) + curveTo(12.2237f, 7.3784f, 11.5945f, 6.7492f, 11.5945f, 5.973f) + curveTo(11.5945f, 5.1968f, 12.2237f, 4.5676f, 12.9999f, 4.5676f) + curveTo(13.7761f, 4.5676f, 14.4053f, 5.1968f, 14.4053f, 5.973f) + curveTo(14.4053f, 6.7492f, 13.7761f, 7.3784f, 12.9999f, 7.3784f) + close() + } + }.build() + return _immersionMore!! + } + +private var _immersionMore: ImageVector? = null \ No newline at end of file diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/icon/icons/Info.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/icon/icons/Info.kt new file mode 100644 index 00000000..90a30934 --- /dev/null +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/icon/icons/Info.kt @@ -0,0 +1,69 @@ +package top.yukonga.miuix.kmp.icon.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.EvenOdd +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp +import top.yukonga.miuix.kmp.icon.MiuixIcons + +val MiuixIcons.Info: ImageVector + get() { + if (_info != null) return _info!! + _info = ImageVector.Builder("Info", 26.dp, 26.dp, 26.0f, 26.0f).apply { + path( + fill = SolidColor(Color.Black), + pathFillType = EvenOdd + ) { + moveTo(13.0f, 21.5935f) + curveTo(17.7437f, 21.5935f, 21.5892f, 17.748f, 21.5892f, 13.0043f) + curveTo(21.5892f, 8.2606f, 17.7437f, 4.4151f, 13.0f, 4.4151f) + curveTo(8.2563f, 4.4151f, 4.4108f, 8.2606f, 4.4108f, 13.0043f) + curveTo(4.4108f, 17.748f, 8.2563f, 21.5935f, 13.0f, 21.5935f) + close() + moveTo(13.0f, 23.1935f) + curveTo(18.6273f, 23.1935f, 23.1892f, 18.6316f, 23.1892f, 13.0043f) + curveTo(23.1892f, 7.377f, 18.6273f, 2.8151f, 13.0f, 2.8151f) + curveTo(7.3727f, 2.8151f, 2.8108f, 7.377f, 2.8108f, 13.0043f) + curveTo(2.8108f, 18.6316f, 7.3727f, 23.1935f, 13.0f, 23.1935f) + close() + } + path( + fill = SolidColor(Color.Black), + pathFillType = NonZero + ) { + moveTo(14.1f, 8.8298f) + curveTo(14.1f, 9.4373f, 13.6075f, 9.9298f, 13.0f, 9.9298f) + curveTo(12.3925f, 9.9298f, 11.9f, 9.4373f, 11.9f, 8.8298f) + curveTo(11.9f, 8.2222f, 12.3925f, 7.7298f, 13.0f, 7.7298f) + curveTo(13.6075f, 7.7298f, 14.1f, 8.2222f, 14.1f, 8.8298f) + close() + } + path( + fill = SolidColor(Color.Black), + pathFillType = NonZero + ) { + moveTo(13.0f, 11.2433f) + curveTo(12.7786f, 11.2433f, 12.6679f, 11.2433f, 12.5788f, 11.274f) + curveTo(12.4154f, 11.3304f, 12.2871f, 11.4587f, 12.2307f, 11.6221f) + curveTo(12.2f, 11.7112f, 12.2f, 11.8219f, 12.2f, 12.0433f) + verticalLineTo(17.4703f) + curveTo(12.2f, 17.6917f, 12.2f, 17.8024f, 12.2307f, 17.8915f) + curveTo(12.2871f, 18.0548f, 12.4154f, 18.1832f, 12.5788f, 18.2395f) + curveTo(12.6679f, 18.2703f, 12.7786f, 18.2703f, 13.0f, 18.2703f) + curveTo(13.2214f, 18.2703f, 13.3321f, 18.2703f, 13.4212f, 18.2395f) + curveTo(13.5845f, 18.1832f, 13.7129f, 18.0548f, 13.7692f, 17.8915f) + curveTo(13.8f, 17.8024f, 13.8f, 17.6917f, 13.8f, 17.4703f) + lineTo(13.8f, 12.0433f) + curveTo(13.8f, 11.8219f, 13.8f, 11.7112f, 13.7692f, 11.6221f) + curveTo(13.7129f, 11.4587f, 13.5845f, 11.3304f, 13.4212f, 11.274f) + curveTo(13.3321f, 11.2433f, 13.2214f, 11.2433f, 13.0f, 11.2433f) + close() + } + }.build() + return _info!! + } + +private var _info: ImageVector? = null \ No newline at end of file diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/icon/icons/More.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/icon/icons/More.kt new file mode 100644 index 00000000..42107388 --- /dev/null +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/icon/icons/More.kt @@ -0,0 +1,70 @@ +package top.yukonga.miuix.kmp.icon.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.EvenOdd +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp +import top.yukonga.miuix.kmp.icon.MiuixIcons + +val MiuixIcons.More: ImageVector + get() { + if (_more != null) return _more!! + _more = ImageVector.Builder("More", 26.dp, 26.dp, 26.0f, 26.0f).apply { + path( + fill = SolidColor(Color.Black), + pathFillType = EvenOdd + ) { + moveTo(6.9265f, 19.0778f) + curveTo(10.2808f, 22.4321f, 15.7192f, 22.4321f, 19.0735f, 19.0778f) + curveTo(22.4277f, 15.7235f, 22.4277f, 10.2851f, 19.0735f, 6.9308f) + curveTo(15.7192f, 3.5766f, 10.2808f, 3.5766f, 6.9265f, 6.9308f) + curveTo(3.5722f, 10.2851f, 3.5722f, 15.7235f, 6.9265f, 19.0778f) + close() + moveTo(5.7951f, 20.2092f) + curveTo(9.7743f, 24.1883f, 16.2257f, 24.1883f, 20.2048f, 20.2092f) + curveTo(24.184f, 16.23f, 24.184f, 9.7786f, 20.2048f, 5.7995f) + curveTo(16.2257f, 1.8203f, 9.7743f, 1.8203f, 5.7951f, 5.7995f) + curveTo(1.816f, 9.7786f, 1.816f, 16.23f, 5.7951f, 20.2092f) + close() + } + path( + fill = SolidColor(Color.Black), + pathFillType = NonZero + ) { + moveTo(14.0999f, 13.0f) + curveTo(14.0999f, 13.6075f, 13.6074f, 14.1f, 12.9999f, 14.1f) + curveTo(12.3924f, 14.1f, 11.8999f, 13.6075f, 11.8999f, 13.0f) + curveTo(11.8999f, 12.3925f, 12.3924f, 11.9f, 12.9999f, 11.9f) + curveTo(13.6074f, 11.9f, 14.0999f, 12.3925f, 14.0999f, 13.0f) + close() + } + path( + fill = SolidColor(Color.Black), + pathFillType = NonZero + ) { + moveTo(18.3162f, 13.0f) + curveTo(18.3162f, 13.6075f, 17.8237f, 14.1f, 17.2162f, 14.1f) + curveTo(16.6087f, 14.1f, 16.1162f, 13.6075f, 16.1162f, 13.0f) + curveTo(16.1162f, 12.3925f, 16.6087f, 11.9f, 17.2162f, 11.9f) + curveTo(17.8237f, 11.9f, 18.3162f, 12.3925f, 18.3162f, 13.0f) + close() + } + path( + fill = SolidColor(Color.Black), + pathFillType = NonZero + ) { + moveTo(9.8837f, 13.0f) + curveTo(9.8837f, 13.6075f, 9.3912f, 14.1f, 8.7837f, 14.1f) + curveTo(8.1762f, 14.1f, 7.6837f, 13.6075f, 7.6837f, 13.0f) + curveTo(7.6837f, 12.3925f, 8.1762f, 11.9f, 8.7837f, 11.9f) + curveTo(9.3912f, 11.9f, 9.8837f, 12.3925f, 9.8837f, 13.0f) + close() + } + }.build() + return _more!! + } + +private var _more: ImageVector? = null \ No newline at end of file diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/icon/icons/NavigatorSwitch.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/icon/icons/NavigatorSwitch.kt new file mode 100644 index 00000000..8dd89b24 --- /dev/null +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/icon/icons/NavigatorSwitch.kt @@ -0,0 +1,130 @@ +package top.yukonga.miuix.kmp.icon.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.EvenOdd +import androidx.compose.ui.graphics.PathFillType.Companion.NonZero +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp +import top.yukonga.miuix.kmp.icon.MiuixIcons + +val MiuixIcons.NavigatorSwitch: ImageVector + get() { + if (_navigatorSwitch != null) return _navigatorSwitch!! + _navigatorSwitch = ImageVector.Builder("NavigatorSwitch", 26.dp, 26.dp, 26.0f, 26.0f).apply { + path( + fill = SolidColor(Color.Black), + pathFillType = EvenOdd + ) { + moveTo(19.5406f, 4.2162f) + curveTo(20.9407f, 4.2162f, 21.6408f, 4.2162f, 22.1755f, 4.4887f) + curveTo(22.6459f, 4.7284f, 23.0284f, 5.1108f, 23.2681f, 5.5812f) + curveTo(23.5406f, 6.116f, 23.5406f, 6.8161f, 23.5406f, 8.2162f) + verticalLineTo(17.7838f) + curveTo(23.5406f, 19.1839f, 23.5406f, 19.884f, 23.2681f, 20.4187f) + curveTo(23.0284f, 20.8891f, 22.6459f, 21.2716f, 22.1755f, 21.5113f) + curveTo(21.6408f, 21.7838f, 20.9407f, 21.7838f, 19.5406f, 21.7838f) + horizontalLineTo(6.4595f) + curveTo(5.0593f, 21.7838f, 4.3593f, 21.7838f, 3.8245f, 21.5113f) + curveTo(3.3541f, 21.2716f, 2.9716f, 20.8891f, 2.732f, 20.4187f) + curveTo(2.4595f, 19.884f, 2.4595f, 19.1839f, 2.4595f, 17.7838f) + verticalLineTo(8.2162f) + curveTo(2.4595f, 6.8161f, 2.4595f, 6.116f, 2.732f, 5.5812f) + curveTo(2.9716f, 5.1108f, 3.3541f, 4.7284f, 3.8245f, 4.4887f) + curveTo(4.3593f, 4.2162f, 5.0593f, 4.2162f, 6.4595f, 4.2162f) + horizontalLineTo(19.5406f) + close() + moveTo(20.34f, 5.815f) + curveTo(20.9001f, 5.815f, 21.1801f, 5.815f, 21.394f, 5.924f) + curveTo(21.5822f, 6.0198f, 21.7351f, 6.1728f, 21.831f, 6.361f) + curveTo(21.94f, 6.5749f, 21.94f, 6.8549f, 21.94f, 7.415f) + verticalLineTo(18.585f) + curveTo(21.94f, 19.145f, 21.94f, 19.4251f, 21.831f, 19.639f) + curveTo(21.7351f, 19.8271f, 21.5822f, 19.9801f, 21.394f, 20.076f) + curveTo(21.1801f, 20.185f, 20.9001f, 20.185f, 20.34f, 20.185f) + horizontalLineTo(12.746f) + verticalLineTo(5.815f) + lineTo(20.34f, 5.815f) + close() + moveTo(11.146f, 5.815f) + lineTo(5.66f, 5.815f) + curveTo(5.0999f, 5.815f, 4.8199f, 5.815f, 4.606f, 5.924f) + curveTo(4.4179f, 6.0198f, 4.2649f, 6.1728f, 4.169f, 6.361f) + curveTo(4.06f, 6.5749f, 4.06f, 6.8549f, 4.06f, 7.415f) + lineTo(4.06f, 18.585f) + curveTo(4.06f, 19.145f, 4.06f, 19.4251f, 4.169f, 19.639f) + curveTo(4.2649f, 19.8271f, 4.4179f, 19.9801f, 4.606f, 20.076f) + curveTo(4.8199f, 20.185f, 5.0999f, 20.185f, 5.66f, 20.185f) + horizontalLineTo(11.146f) + verticalLineTo(5.815f) + close() + } + path( + fill = SolidColor(Color.Black), + pathFillType = NonZero + ) { + moveTo(9.3492f, 8.1689f) + curveTo(9.3492f, 8.3903f, 9.3492f, 8.501f, 9.3185f, 8.5901f) + curveTo(9.2621f, 8.7534f, 9.1338f, 8.8818f, 8.9704f, 8.9381f) + curveTo(8.8813f, 8.9689f, 8.7706f, 8.9689f, 8.5492f, 8.9689f) + horizontalLineTo(6.6357f) + curveTo(6.4143f, 8.9689f, 6.3036f, 8.9689f, 6.2145f, 8.9381f) + curveTo(6.0512f, 8.8818f, 5.9228f, 8.7534f, 5.8665f, 8.5901f) + curveTo(5.8357f, 8.501f, 5.8357f, 8.3903f, 5.8357f, 8.1689f) + curveTo(5.8357f, 7.9475f, 5.8357f, 7.8368f, 5.8665f, 7.7477f) + curveTo(5.9228f, 7.5844f, 6.0512f, 7.456f, 6.2145f, 7.3996f) + curveTo(6.3036f, 7.3689f, 6.4143f, 7.3689f, 6.6357f, 7.3689f) + horizontalLineTo(8.5492f) + curveTo(8.7706f, 7.3689f, 8.8813f, 7.3689f, 8.9704f, 7.3996f) + curveTo(9.1338f, 7.456f, 9.2621f, 7.5844f, 9.3185f, 7.7477f) + curveTo(9.3492f, 7.8368f, 9.3492f, 7.9475f, 9.3492f, 8.1689f) + close() + } + path( + fill = SolidColor(Color.Black), + pathFillType = NonZero + ) { + moveTo(9.3492f, 11.3311f) + curveTo(9.3492f, 11.5525f, 9.3492f, 11.6632f, 9.3185f, 11.7523f) + curveTo(9.2621f, 11.9156f, 9.1338f, 12.0439f, 8.9704f, 12.1003f) + curveTo(8.8813f, 12.1311f, 8.7706f, 12.1311f, 8.5492f, 12.1311f) + horizontalLineTo(6.6357f) + curveTo(6.4143f, 12.1311f, 6.3036f, 12.1311f, 6.2145f, 12.1003f) + curveTo(6.0512f, 12.0439f, 5.9228f, 11.9156f, 5.8665f, 11.7523f) + curveTo(5.8357f, 11.6632f, 5.8357f, 11.5525f, 5.8357f, 11.3311f) + curveTo(5.8357f, 11.1097f, 5.8357f, 10.999f, 5.8665f, 10.9098f) + curveTo(5.9228f, 10.7465f, 6.0512f, 10.6182f, 6.2145f, 10.5618f) + curveTo(6.3036f, 10.5311f, 6.4143f, 10.5311f, 6.6357f, 10.5311f) + horizontalLineTo(8.5492f) + curveTo(8.7706f, 10.5311f, 8.8813f, 10.5311f, 8.9704f, 10.5618f) + curveTo(9.1338f, 10.6182f, 9.2621f, 10.7465f, 9.3185f, 10.9098f) + curveTo(9.3492f, 10.999f, 9.3492f, 11.1097f, 9.3492f, 11.3311f) + close() + } + path( + fill = SolidColor(Color.Black), + pathFillType = NonZero + ) { + moveTo(5.8357f, 14.4932f) + curveTo(5.8357f, 14.7146f, 5.8357f, 14.8253f, 5.8665f, 14.9144f) + curveTo(5.9228f, 15.0778f, 6.0512f, 15.2061f, 6.2145f, 15.2625f) + curveTo(6.3036f, 15.2932f, 6.4143f, 15.2932f, 6.6357f, 15.2932f) + horizontalLineTo(8.5492f) + curveTo(8.7706f, 15.2932f, 8.8813f, 15.2932f, 8.9704f, 15.2625f) + curveTo(9.1338f, 15.2061f, 9.2621f, 15.0778f, 9.3185f, 14.9144f) + curveTo(9.3492f, 14.8253f, 9.3492f, 14.7146f, 9.3492f, 14.4932f) + curveTo(9.3492f, 14.2718f, 9.3492f, 14.1611f, 9.3185f, 14.072f) + curveTo(9.2621f, 13.9087f, 9.1338f, 13.7803f, 8.9704f, 13.724f) + curveTo(8.8813f, 13.6932f, 8.7706f, 13.6932f, 8.5492f, 13.6932f) + horizontalLineTo(6.6357f) + curveTo(6.4143f, 13.6932f, 6.3036f, 13.6932f, 6.2145f, 13.724f) + curveTo(6.0512f, 13.7803f, 5.9228f, 13.9087f, 5.8665f, 14.072f) + curveTo(5.8357f, 14.1611f, 5.8357f, 14.2718f, 5.8357f, 14.4932f) + close() + } + }.build() + return _navigatorSwitch!! + } + +private var _navigatorSwitch: ImageVector? = null \ No newline at end of file diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/icon/icons/Settings.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/icon/icons/Settings.kt new file mode 100644 index 00000000..cac51b49 --- /dev/null +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/icon/icons/Settings.kt @@ -0,0 +1,112 @@ +package top.yukonga.miuix.kmp.icon.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType.Companion.EvenOdd +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp +import top.yukonga.miuix.kmp.icon.MiuixIcons + +val MiuixIcons.Settings: ImageVector + get() { + if (_settings != null) return _settings!! + _settings = ImageVector.Builder("Settings", 26.dp, 26.dp, 26.0f, 26.0f).apply { + path( + fill = SolidColor(Color.Black), + pathFillType = EvenOdd + ) { + moveTo(20.0365f, 8.1285f) + curveTo(20.303f, 8.2783f, 20.45f, 8.3609f, 20.5555f, 8.4616f) + curveTo(20.5895f, 8.4873f, 20.6073f, 8.5047f, 20.6223f, 8.5212f) + curveTo(20.7082f, 8.6152f, 20.7732f, 8.7264f, 20.8129f, 8.8474f) + curveTo(20.8262f, 8.8881f, 20.8377f, 8.9391f, 20.8458f, 9.1102f) + curveTo(20.8493f, 9.1641f, 20.851f, 9.2243f, 20.8519f, 9.2936f) + curveTo(20.8557f, 9.4564f, 20.8578f, 9.6833f, 20.8578f, 10.0044f) + verticalLineTo(15.9955f) + curveTo(20.8578f, 16.3166f, 20.8557f, 16.5435f, 20.8519f, 16.7063f) + curveTo(20.851f, 16.7757f, 20.8493f, 16.836f, 20.8458f, 16.89f) + curveTo(20.8377f, 17.0609f, 20.8262f, 17.1119f, 20.8129f, 17.1525f) + curveTo(20.7732f, 17.2735f, 20.7082f, 17.3847f, 20.6223f, 17.4788f) + curveTo(20.6073f, 17.4952f, 20.5896f, 17.5125f, 20.5558f, 17.5382f) + curveTo(20.4503f, 17.639f, 20.3033f, 17.7216f, 20.0365f, 17.8715f) + lineTo(13.7838f, 21.3852f) + lineTo(13.7838f, 21.3852f) + curveTo(13.5929f, 21.4925f, 13.4657f, 21.564f, 13.3571f, 21.609f) + curveTo(13.2575f, 21.6569f, 13.2178f, 21.6685f, 13.1835f, 21.6757f) + curveTo(13.0625f, 21.7009f, 12.9375f, 21.7009f, 12.8165f, 21.6757f) + curveTo(12.7822f, 21.6685f, 12.7426f, 21.6569f, 12.6432f, 21.6091f) + curveTo(12.5346f, 21.5641f, 12.4073f, 21.4926f, 12.2162f, 21.3852f) + lineTo(12.2162f, 21.3852f) + lineTo(5.9634f, 17.8715f) + lineTo(5.9634f, 17.8715f) + lineTo(5.9634f, 17.8715f) + curveTo(5.6965f, 17.7215f, 5.5495f, 17.6389f, 5.444f, 17.538f) + curveTo(5.4103f, 17.5125f, 5.3927f, 17.4951f, 5.3777f, 17.4788f) + curveTo(5.2918f, 17.3847f, 5.2269f, 17.2735f, 5.1871f, 17.1525f) + curveTo(5.1739f, 17.1121f, 5.1624f, 17.0614f, 5.1544f, 16.8925f) + curveTo(5.1507f, 16.8368f, 5.1489f, 16.7743f, 5.1481f, 16.7021f) + curveTo(5.1443f, 16.5396f, 5.1422f, 16.3139f, 5.1422f, 15.9955f) + verticalLineTo(10.0044f) + curveTo(5.1422f, 9.686f, 5.1443f, 9.4603f, 5.1481f, 9.2978f) + curveTo(5.1489f, 9.2256f, 5.1507f, 9.1633f, 5.1544f, 9.1077f) + curveTo(5.1624f, 8.9386f, 5.1739f, 8.8879f, 5.1871f, 8.8474f) + curveTo(5.2269f, 8.7264f, 5.2918f, 8.6152f, 5.3777f, 8.5212f) + curveTo(5.3927f, 8.5048f, 5.4104f, 8.4874f, 5.4443f, 8.4618f) + curveTo(5.5498f, 8.361f, 5.6968f, 8.2784f, 5.9634f, 8.1285f) + lineTo(11.8792f, 4.8042f) + lineTo(11.9261f, 4.7778f) + lineTo(12.2161f, 4.6148f) + curveTo(12.4068f, 4.5076f, 12.5339f, 4.4362f, 12.6424f, 4.3912f) + curveTo(12.7424f, 4.3431f, 12.7821f, 4.3314f, 12.8165f, 4.3243f) + curveTo(12.9375f, 4.2991f, 13.0625f, 4.2991f, 13.1835f, 4.3243f) + curveTo(13.2179f, 4.3314f, 13.2577f, 4.3431f, 13.3579f, 4.3913f) + curveTo(13.4663f, 4.4364f, 13.5934f, 4.5078f, 13.7838f, 4.6148f) + lineTo(14.046f, 4.7621f) + lineTo(14.1503f, 4.8207f) + lineTo(20.0365f, 8.1285f) + close() + moveTo(12.4902f, 2.7579f) + curveTo(12.1103f, 2.837f, 11.752f, 3.039f, 11.0355f, 3.443f) + lineTo(5.5778f, 6.5201f) + curveTo(4.8366f, 6.9379f, 4.4661f, 7.1468f, 4.1964f, 7.4421f) + curveTo(3.9578f, 7.7033f, 3.7773f, 8.0122f, 3.667f, 8.3483f) + curveTo(3.5422f, 8.7283f, 3.5422f, 9.1537f, 3.5422f, 10.0044f) + verticalLineTo(15.9955f) + curveTo(3.5422f, 16.8463f, 3.5422f, 17.2717f, 3.667f, 17.6516f) + curveTo(3.7773f, 17.9878f, 3.9578f, 18.2966f, 4.1964f, 18.5579f) + curveTo(4.4661f, 18.8531f, 4.8366f, 19.062f, 5.5778f, 19.4799f) + lineTo(11.0355f, 22.557f) + curveTo(11.752f, 22.9609f, 12.1103f, 23.1629f, 12.4902f, 23.242f) + curveTo(12.8265f, 23.3121f, 13.1735f, 23.3121f, 13.5098f, 23.242f) + curveTo(13.8897f, 23.1629f, 14.248f, 22.9609f, 14.9645f, 22.557f) + lineTo(20.4223f, 19.4799f) + curveTo(21.1634f, 19.062f, 21.5339f, 18.8531f, 21.8036f, 18.5579f) + curveTo(22.0422f, 18.2966f, 22.2227f, 17.9878f, 22.333f, 17.6516f) + curveTo(22.4578f, 17.2717f, 22.4578f, 16.8463f, 22.4578f, 15.9955f) + verticalLineTo(10.0044f) + curveTo(22.4578f, 9.1537f, 22.4578f, 8.7283f, 22.333f, 8.3483f) + curveTo(22.2227f, 8.0122f, 22.0422f, 7.7033f, 21.8036f, 7.4421f) + curveTo(21.5339f, 7.1468f, 21.1634f, 6.9379f, 20.4223f, 6.5201f) + lineTo(14.9645f, 3.443f) + curveTo(14.248f, 3.039f, 13.8897f, 2.837f, 13.5098f, 2.7579f) + curveTo(13.1735f, 2.6879f, 12.8265f, 2.6879f, 12.4902f, 2.7579f) + close() + moveTo(15.6162f, 13.0f) + curveTo(15.6162f, 14.4449f, 14.4449f, 15.6162f, 13.0f, 15.6162f) + curveTo(11.5551f, 15.6162f, 10.3838f, 14.4449f, 10.3838f, 13.0f) + curveTo(10.3838f, 11.5551f, 11.5551f, 10.3838f, 13.0f, 10.3838f) + curveTo(14.4449f, 10.3838f, 15.6162f, 11.5551f, 15.6162f, 13.0f) + close() + moveTo(17.2162f, 13.0f) + curveTo(17.2162f, 15.3286f, 15.3286f, 17.2162f, 13.0f, 17.2162f) + curveTo(10.6714f, 17.2162f, 8.7838f, 15.3286f, 8.7838f, 13.0f) + curveTo(8.7838f, 10.6715f, 10.6714f, 8.7838f, 13.0f, 8.7838f) + curveTo(15.3286f, 8.7838f, 17.2162f, 10.6715f, 17.2162f, 13.0f) + close() + } + }.build() + return _settings!! + } + +private var _settings: ImageVector? = null \ No newline at end of file diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/utils/MiuixPopupUtil.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/utils/MiuixPopupUtil.kt index 9218a9d3..49a2c9a3 100644 --- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/utils/MiuixPopupUtil.kt +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/utils/MiuixPopupUtil.kt @@ -71,13 +71,14 @@ class MiuixPopupUtil { /** * Show a popup. * + * @param transformOrigin The pivot point in terms of fraction of the overall size, + * used for scale transformations. By default it's [TransformOrigin.Center]. * @param content The [Composable] content of the popup. - * @param transformOrigin The pivot point in terms of fraction of the overall size, used for scale transformations. By default it's [TransformOrigin.Center]. */ @Composable fun showPopup( - content: (@Composable () -> Unit)? = null, transformOrigin: (() -> TransformOrigin) = { TransformOrigin.Center }, + content: (@Composable () -> Unit)? = null, ) { if (isPopupShowing.value) return popupTransformOrigin.value = transformOrigin