diff --git a/Sources/SkipUI/Skip/SkipAlertDialog.kt b/Sources/SkipUI/Skip/SkipAlertDialog.kt new file mode 100644 index 00000000..a30481b5 --- /dev/null +++ b/Sources/SkipUI/Skip/SkipAlertDialog.kt @@ -0,0 +1,310 @@ +// Copyright 2021 The Android Open Source Project +// Copyright 2025 Skip - adapted for skip-ui with package and import fixes +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package skip.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.semantics.paneTitle +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastForEachIndexed +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import kotlin.collections.List +import kotlin.math.max + +/** + * Material3-style AlertDialog implementation for skip-ui. + * Copy of androidx.compose.material3.AlertDialog logic with internal/token dependencies + * replaced by public MaterialTheme/AlertDialogDefaults APIs. + * + * Use this for basic cases (title, optional text, confirm + optional dismiss buttons). + */ +@Composable +fun SkipAlertDialog( + onDismissRequest: () -> Unit, + confirmButton: @Composable () -> Unit, + modifier: Modifier = Modifier, + dismissButton: @Composable (() -> Unit)? = null, + neutralButtons: List<@Composable () -> Unit> = emptyList(), + icon: @Composable (() -> Unit)? = null, + title: @Composable (() -> Unit)? = null, + text: @Composable (() -> Unit)? = null, + textFields: @Composable (() -> Unit)? = null, + shape: Shape = AlertDialogDefaults.shape, + containerColor: Color = AlertDialogDefaults.containerColor, + iconContentColor: Color = AlertDialogDefaults.iconContentColor, + titleContentColor: Color = AlertDialogDefaults.titleContentColor, + textContentColor: Color = AlertDialogDefaults.textContentColor, + tonalElevation: Dp = AlertDialogDefaults.TonalElevation, + properties: DialogProperties = DialogProperties(), +) { + Dialog(onDismissRequest = onDismissRequest, properties = properties) { + Box( + modifier = modifier + .sizeIn(minWidth = DialogMinWidth, maxWidth = DialogMaxWidth) + .then(Modifier.semantics { paneTitle = "Dialog" }), + propagateMinConstraints = true, + ) { + SkipAlertDialogContent( + buttons = { + SkipAlertDialogFlowRow( + mainAxisSpacing = ButtonsMainAxisSpacing, + crossAxisSpacing = ButtonsCrossAxisSpacing, + ) { + for (btn in neutralButtons) { btn() } + dismissButton?.let { it() } + confirmButton() + } + }, + icon = icon, + title = title, + text = text, + textFields = textFields, + shape = shape, + containerColor = containerColor, + tonalElevation = tonalElevation, + buttonContentColor = MaterialTheme.colorScheme.primary, + iconContentColor = iconContentColor, + titleContentColor = titleContentColor, + textContentColor = textContentColor, + ) + } + } +} + +@Composable +private fun SkipAlertDialogContent( + buttons: @Composable () -> Unit, + modifier: Modifier = Modifier, + icon: (@Composable () -> Unit)?, + title: (@Composable () -> Unit)?, + text: @Composable (() -> Unit)?, + textFields: @Composable (() -> Unit)? = null, + shape: Shape, + containerColor: Color, + tonalElevation: Dp, + buttonContentColor: Color, + iconContentColor: Color, + titleContentColor: Color, + textContentColor: Color, +) { + Surface( + modifier = modifier, + shape = shape, + color = containerColor, + tonalElevation = tonalElevation, + ) { + Column(modifier = Modifier.padding(DialogPadding)) { + icon?.let { + CompositionLocalProvider(androidx.compose.material3.LocalContentColor provides iconContentColor) { + Box(Modifier.padding(IconPadding).align(Alignment.CenterHorizontally)) { + icon() + } + } + } + title?.let { + ProvideContentColorTextStyle( + contentColor = titleContentColor, + textStyle = MaterialTheme.typography.headlineSmall, + ) { + Box( + Modifier.padding(TitlePadding) + .align( + if (icon == null) Alignment.Start + else Alignment.CenterHorizontally, + ), + ) { + title() + } + } + } + text?.let { + val scrollState = rememberScrollState() + ProvideContentColorTextStyle( + contentColor = textContentColor, + textStyle = MaterialTheme.typography.bodyMedium, + ) { + Box( + Modifier + .weight(weight = 1f, fill = false) + .padding(TextPadding) + .fillMaxWidth() + .verticalScroll(scrollState) + .align(Alignment.Start), + ) { + text() + } + } + } + textFields?.let { + Column( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.Start), + ) { + textFields() + } + } + Box( + modifier = Modifier.align(Alignment.End), + contentAlignment = Alignment.CenterEnd, + ) { + ProvideContentColorTextStyle( + contentColor = buttonContentColor, + textStyle = MaterialTheme.typography.labelLarge, + content = buttons, + ) + } + } + } +} + +/** Replaces material3 internal ProvideContentColorTextStyle using public CompositionLocals. */ +@Composable +private fun ProvideContentColorTextStyle( + contentColor: Color, + textStyle: TextStyle, + content: @Composable () -> Unit, +) { + CompositionLocalProvider( + androidx.compose.material3.LocalContentColor provides contentColor, + androidx.compose.material3.LocalTextStyle provides androidx.compose.material3.LocalTextStyle.current.merge(textStyle), + ) { + content() + } +} + +@Composable +private fun SkipAlertDialogFlowRow( + mainAxisSpacing: Dp, + crossAxisSpacing: Dp, + content: @Composable () -> Unit, +) { + val density = LocalDensity.current + val layoutDirection = LocalLayoutDirection.current + Layout( + modifier = Modifier, + content = content, + ) { measurables, constraints -> + val sequences = mutableListOf>() + val crossAxisSizes = mutableListOf() + val crossAxisPositions = mutableListOf() + + var mainAxisSpace = 0 + var crossAxisSpace = 0 + + val currentSequence = mutableListOf() + var currentMainAxisSize = 0 + var currentCrossAxisSize = 0 + + fun canAddToCurrentSequence(placeable: Placeable) = + currentSequence.isEmpty() || + currentMainAxisSize + with(density) { mainAxisSpacing.roundToPx() } + placeable.width <= + constraints.maxWidth + + fun startNewSequence() { + if (sequences.isNotEmpty()) { + crossAxisSpace += with(density) { crossAxisSpacing.roundToPx() } + } + // Append sequences (not prepend) so neutral buttons appear above dismiss/confirm buttons + sequences.add(currentSequence.toList()) + crossAxisSizes += currentCrossAxisSize + crossAxisPositions += crossAxisSpace + + crossAxisSpace += currentCrossAxisSize + mainAxisSpace = max(mainAxisSpace, currentMainAxisSize) + + currentSequence.clear() + currentMainAxisSize = 0 + currentCrossAxisSize = 0 + } + + measurables.fastForEach { measurable -> + val placeable = measurable.measure(constraints) + if (!canAddToCurrentSequence(placeable)) startNewSequence() + + if (currentSequence.isNotEmpty()) { + currentMainAxisSize += with(density) { mainAxisSpacing.roundToPx() } + } + currentSequence.add(placeable) + currentMainAxisSize += placeable.width + currentCrossAxisSize = max(currentCrossAxisSize, placeable.height) + } + + if (currentSequence.isNotEmpty()) startNewSequence() + + val mainAxisLayoutSize = max(mainAxisSpace, constraints.minWidth) + val crossAxisLayoutSize = max(crossAxisSpace, constraints.minHeight) + + layout(mainAxisLayoutSize, crossAxisLayoutSize) { + sequences.fastForEachIndexed { i, placeables -> + val childrenMainAxisSizes = + IntArray(placeables.size) { j: Int -> + placeables[j].width + + if (j < placeables.lastIndex) with(density) { mainAxisSpacing.roundToPx() } else 0 + } + val arrangement = Arrangement.End + val mainAxisPositions = IntArray(childrenMainAxisSizes.size) + with(arrangement) { + arrange( + mainAxisLayoutSize, + childrenMainAxisSizes, + layoutDirection, + mainAxisPositions, + ) + } + placeables.fastForEachIndexed { j, placeable -> + placeable.place(x = mainAxisPositions[j], y = crossAxisPositions[i]) + } + } + } + } +} + +private val DialogMinWidth = 280.dp +private val DialogMaxWidth = 560.dp + +private val ButtonsMainAxisSpacing = 8.dp +private val ButtonsCrossAxisSpacing = 12.dp +// Paddings for each of the dialog's parts (match Material3 AlertDialog.kt). +private val DialogPadding = PaddingValues(all = 24.dp) +private val IconPadding = PaddingValues(bottom = 16.dp) +private val TitlePadding = PaddingValues(bottom = 16.dp) +private val TextPadding = PaddingValues(bottom = 24.dp) diff --git a/Sources/SkipUI/SkipUI/Layout/Presentation.swift b/Sources/SkipUI/SkipUI/Layout/Presentation.swift index 43e96f3b..8934ddce 100644 --- a/Sources/SkipUI/SkipUI/Layout/Presentation.swift +++ b/Sources/SkipUI/SkipUI/Layout/Presentation.swift @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredHeightIn @@ -35,6 +36,7 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheetProperties import androidx.compose.material3.SheetValue import androidx.compose.material3.Surface +import androidx.compose.material3.TextButton import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -64,6 +66,11 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogWindowProvider import androidx.core.view.WindowCompat +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.layout +import androidx.compose.ui.platform.LocalLayoutDirection #elseif canImport(CoreGraphics) import struct CoreGraphics.CGFloat #endif @@ -73,6 +80,16 @@ import struct CoreGraphics.CGFloat /// Common corner radius for our overlay presentations. let overlayPresentationCornerRadius = 16.0 +// Material3 AlertDialog padding and spacing (match AlertDialog.kt). +private let AlertDialogPadding = PaddingValues(start: 24.dp, top: 24.dp, end: 24.dp, bottom: 24.dp) +private let AlertIconPadding = PaddingValues(start: 0.dp, top: 0.dp, end: 0.dp, bottom: 16.dp) +private let AlertTitlePadding = PaddingValues(start: 0.dp, top: 0.dp, end: 0.dp, bottom: 16.dp) +private let AlertTextPadding = PaddingValues(start: 0.dp, top: 0.dp, end: 0.dp, bottom: 24.dp) +private let ButtonsMainAxisSpacing = 8.dp +private let ButtonsCrossAxisSpacing = 12.dp +private let AlertDialogMinWidth = 280.dp +private let AlertDialogMaxWidth = 560.dp + // SKIP INSERT: @OptIn(ExperimentalMaterial3Api::class) @Composable func SheetPresentation(isPresented: Binding, isFullScreen: Bool, context: ComposeContext, content: () -> any View, onDismiss: (() -> Void)?) { let interactiveDismissDisabledPreference = rememberSaveable(stateSaver: context.stateSaver as! Saver, Any>) { mutableStateOf(Preference(key: InteractiveDismissDisabledPreferenceKey.self)) } @@ -359,6 +376,90 @@ final class DisableScrollToDismissConnection : NestedScrollConnection { } } +/// Simple clone of FlowRow that arranges its children in a horizontal flow (Material3 AlertDialogFlowRow). +/// Wraps to new rows when needed; confirming actions appear above dismissive actions. +@Composable func AlertDialogFlowRow(mainAxisSpacing: Dp, crossAxisSpacing: Dp, content: @Composable () -> Void) { + let density = LocalDensity.current + let layoutDirection = LocalLayoutDirection.current + let mainAxisSpacingPx = with(density) { mainAxisSpacing.roundToPx() } + let crossAxisSpacingPx = with(density) { crossAxisSpacing.roundToPx() } + Layout(content: content) { measurables, constraints in + var sequences: [[Placeable]] = [] + var crossAxisSizes: [Int] = [] + var crossAxisPositions: [Int] = [] + var mainAxisSpace = 0 + var crossAxisSpace = 0 + var currentSequence: [Placeable] = [] + var currentMainAxisSize = 0 + var currentCrossAxisSize = 0 + func canAddToCurrentSequence(_ placeable: Placeable) -> Bool { + currentSequence.isEmpty || currentMainAxisSize + mainAxisSpacingPx + placeable.width <= constraints.maxWidth + } + func startNewSequence() { + if !sequences.isEmpty { + crossAxisSpace += crossAxisSpacingPx + } + sequences.insert(currentSequence, at: 0) + crossAxisSizes.append(currentCrossAxisSize) + crossAxisPositions.append(crossAxisSpace) + crossAxisSpace += currentCrossAxisSize + mainAxisSpace = max(mainAxisSpace, currentMainAxisSize) + currentSequence = [] + currentMainAxisSize = 0 + currentCrossAxisSize = 0 + } + for measurable in measurables { + let placeable = measurable.measure(constraints) + if !canAddToCurrentSequence(placeable) { + startNewSequence() + } + if !currentSequence.isEmpty { + currentMainAxisSize += mainAxisSpacingPx + } + currentSequence.append(placeable) + currentMainAxisSize += placeable.width + currentCrossAxisSize = max(currentCrossAxisSize, placeable.height) + } + if !currentSequence.isEmpty { + startNewSequence() + } + let mainAxisLayoutSize = max(mainAxisSpace, constraints.minWidth) + let crossAxisLayoutSize = max(crossAxisSpace, constraints.minHeight) + return layout(width: mainAxisLayoutSize, height: crossAxisLayoutSize) { + for i in 0.. 0 { + offset -= mainAxisSpacingPx + } + } + } + for j in 0.., context: ComposeContext, actions: any View, message: (any View)? = nil) { guard isPresented.get() else { @@ -379,46 +480,135 @@ final class DisableScrollToDismissConnection : NestedScrollConnection { $0.strip() as? Text }.firstOrNull() - BasicAlertDialog(onDismissRequest: { isPresented.set(false) }) { - let modifier = Modifier.wrapContentWidth().wrapContentHeight().then(context.modifier) - Surface(modifier: modifier, shape: MaterialTheme.shapes.large, tonalElevation: AlertDialogDefaults.TonalElevation) { - let contentContext = context.content() - let hasTopContent = title != nil || titleResource != nil || message != nil || textFields.size > 0 - Column(modifier: Modifier.padding(top: hasTopContent ? 16.dp : 4.dp, bottom: 4.dp), horizontalAlignment: androidx.compose.ui.Alignment.CenterHorizontally) { - RenderAlert(title: title, titleResource: titleResource, context: contentContext, isPresented: isPresented, textFields: textFields, actionRenderables: optionRenderables, message: messageText) + let contentContext = context.content() + let buttonModifier = Modifier.padding(horizontal: 12.dp, vertical: 8.dp) + let buttonFont = Font.title3 + let tint = (EnvironmentValues.shared._tint ?? Color.accentColor).colorImpl() + let flowRowButtonModifier = Modifier.wrapContentWidth().wrapContentHeight() + + // Use native Material3-style dialog for all cases. N buttons and text fields supported. + let cancelBtn = actionRenderables.firstOrNull { ($0.strip() as? Button)?.role == .cancel } + // Confirm: first destructive, else first non-cancel (primary). Others go to neutralButtons. + let confirmRenderable = optionRenderables.firstOrNull { ($0.strip() as? Button)?.role == .destructive } + ?? optionRenderables.firstOrNull { ($0.strip() as? Button)?.role != .cancel } + let neutralRenderables = optionRenderables.filter { r in + (r.strip() as? Button)?.role != .cancel && r !== confirmRenderable + } + var confirmAction: (() -> Void)? + if let r = confirmRenderable { + let stripped = r.strip() + if let button = stripped as? Button { confirmAction = button.action } + else if let link = stripped as? Link { link.ComposeAction(); confirmAction = link.content.action } + else if let nav = stripped as? NavigationLink { confirmAction = nav.navigationAction() } + } + var dismissAction: (() -> Void)? + if let c = cancelBtn { + let stripped = c.strip() + if let button = stripped as? Button { dismissAction = button.action } + else if let link = stripped as? Link { link.ComposeAction(); dismissAction = link.content.action } + else if let nav = stripped as? NavigationLink { dismissAction = nav.navigationAction() } + } + let neutralButtonsList: kotlin.collections.MutableList<@Composable () -> Void> = mutableListOf() + for r in neutralRenderables { + let renderable = r + var neutralAction: (() -> Void)? + let stripped = renderable.strip() + if let button = stripped as? Button { neutralAction = button.action } + else if let link = stripped as? Link { link.ComposeAction(); neutralAction = link.content.action } + else if let nav = stripped as? NavigationLink { neutralAction = nav.navigationAction() } + neutralButtonsList.add { + androidx.compose.material3.TextButton(onClick: { isPresented.set(false); neutralAction?() }) { + let s = renderable.strip() + let button = s as? Button ?? (s as? Link)?.content + let label = button?.label ?? (s as? NavigationLink)?.label + let bt = label?.Evaluate(context: contentContext, options: 0).mapNotNull { $0.strip() as? Text }.firstOrNull() + androidx.compose.material3.Text(color: tint, text: bt?.localizedTextString() ?? "", style: MaterialTheme.typography.labelLarge) } } } + SkipAlertDialog( + onDismissRequest: { isPresented.set(false) }, + neutralButtons: neutralButtonsList, + confirmButton: { + if let r = confirmRenderable { + androidx.compose.material3.TextButton(onClick: { isPresented.set(false); confirmAction?() }) { + let stripped = r.strip() + let button = stripped as? Button ?? (stripped as? Link)?.content + let label = button?.label ?? (stripped as? NavigationLink)?.label + let bt = label?.Evaluate(context: contentContext, options: 0).mapNotNull { $0.strip() as? Text }.firstOrNull() + let color = button?.role == .destructive ? MaterialTheme.colorScheme.error : tint + androidx.compose.material3.Text(color: color, text: bt?.localizedTextString() ?? "", style: MaterialTheme.typography.labelLarge) + } + } else { + androidx.compose.material3.TextButton(onClick: { isPresented.set(false) }) { + androidx.compose.material3.Text(color: tint, text: stringResource(android.R.string.ok), style: MaterialTheme.typography.labelLarge) + } + } + }, + dismissButton: cancelBtn != nil ? { + if let c = cancelBtn { + androidx.compose.material3.TextButton(onClick: { isPresented.set(false); dismissAction?() }) { + let stripped = c.strip() + let button = stripped as? Button + let label = button?.label + let bt = label?.Evaluate(context: contentContext, options: 0).mapNotNull { $0.strip() as? Text }.firstOrNull() + androidx.compose.material3.Text(color: tint, text: bt?.localizedTextString() ?? "", style: MaterialTheme.typography.labelLarge) + } + } + } : nil, + title: title != nil ? { + androidx.compose.material3.Text(color: Color.primary.colorImpl(), text: title!.localizedTextString(), style: MaterialTheme.typography.headlineSmall) + } : (titleResource != nil ? { + androidx.compose.material3.Text(color: Color.primary.colorImpl(), text: stringResource(titleResource!), style: MaterialTheme.typography.headlineSmall) + } : nil), + text: messageText != nil ? { + androidx.compose.material3.Text(text: messageText!.localizedTextString(), style: MaterialTheme.typography.bodyMedium) + } : nil, + textFields: textFields.size > 0 ? { + for textField in textFields { + let topPadding = textField == textFields.firstOrNull() ? 16.dp : 8.dp + let textFieldContext = contentContext.content(modifier: Modifier.padding(top: topPadding)) + textField.Compose(context: textFieldContext) + } + } : nil, + modifier: Modifier.wrapContentHeight().then(context.modifier), + containerColor: MaterialTheme.colorScheme.surfaceContainerHigh + ) } @Composable func RenderAlert(title: Text?, titleResource: Int? = nil, context: ComposeContext, isPresented: Binding, textFields: kotlin.collections.List, actionRenderables: kotlin.collections.List, message: Text?) { - let padding = 16.dp if let title { - androidx.compose.material3.Text(modifier: Modifier.padding(horizontal: padding, vertical: 8.dp), color: Color.primary.colorImpl(), text: title.localizedTextString(), style: Font.title3.bold().fontImpl(), textAlign: TextAlign.Center) + Box(modifier: Modifier.padding(AlertTitlePadding).fillMaxWidth(), contentAlignment: androidx.compose.ui.Alignment.CenterStart) { + androidx.compose.material3.Text(color: Color.primary.colorImpl(), text: title.localizedTextString(), style: MaterialTheme.typography.headlineSmall) + } } else if let titleResource { - androidx.compose.material3.Text(modifier: Modifier.padding(horizontal: padding, vertical: 8.dp), color: Color.primary.colorImpl(), text: stringResource(titleResource), style: Font.title3.bold().fontImpl(), textAlign: TextAlign.Center) + Box(modifier: Modifier.padding(AlertTitlePadding).fillMaxWidth(), contentAlignment: androidx.compose.ui.Alignment.CenterStart) { + androidx.compose.material3.Text(color: Color.primary.colorImpl(), text: stringResource(titleResource), style: MaterialTheme.typography.headlineSmall) + } } if let message { - androidx.compose.material3.Text(modifier: Modifier.padding(start: padding, end: padding), color: Color.primary.colorImpl(), text: message.localizedTextString(), style: Font.callout.fontImpl(), textAlign: TextAlign.Center) + Box(modifier: Modifier.padding(AlertTextPadding).fillMaxWidth(), contentAlignment: androidx.compose.ui.Alignment.CenterStart) { + androidx.compose.material3.Text(color: Color.primary.colorImpl(), text: message.localizedTextString(), style: Font.callout.fontImpl()) + } } for textField in textFields { let topPadding = textField == textFields.firstOrNull() ? 16.dp : 8.dp - let textFieldContext = context.content(modifier: Modifier.padding(top: topPadding, start: padding, end: padding)) + let textFieldContext = context.content(modifier: Modifier.padding(top: topPadding)) textField.Compose(context: textFieldContext) } - let hasTopContent = title != nil || titleResource != nil || message != nil || textFields.size > 0 - if hasTopContent { - androidx.compose.material3.Divider(modifier: Modifier.padding(top: 16.dp)) - } - - let buttonModifier = Modifier.padding(horizontal: padding, vertical: 12.dp) + let buttonModifier = Modifier.padding(horizontal: 12.dp, vertical: 8.dp) let buttonFont = Font.title3 let tint = (EnvironmentValues.shared._tint ?? Color.accentColor).colorImpl() + let flowRowButtonModifier = Modifier.wrapContentWidth().wrapContentHeight() guard actionRenderables.size > 0 else { - AlertButton(modifier: Modifier.fillMaxWidth(), renderable: nil, isPresented: isPresented) { - androidx.compose.material3.Text(modifier: buttonModifier, color: tint, text: stringResource(android.R.string.ok), style: buttonFont.fontImpl()) + Box(modifier: Modifier.fillMaxWidth(), contentAlignment: androidx.compose.ui.Alignment.CenterEnd) { + AlertDialogFlowRow(mainAxisSpacing: ButtonsMainAxisSpacing, crossAxisSpacing: ButtonsCrossAxisSpacing) { + AlertButton(modifier: flowRowButtonModifier, renderable: nil, isPresented: isPresented) { + androidx.compose.material3.Text(modifier: buttonModifier, color: tint, text: stringResource(android.R.string.ok), style: buttonFont.fontImpl()) + } + } } return } @@ -432,7 +622,7 @@ final class DisableScrollToDismissConnection : NestedScrollConnection { }.firstOrNull() let color = button?.role == .destructive ? Color.red.colorImpl() : tint let style = isCancel ? buttonFont.bold().fontImpl() : buttonFont.fontImpl() - androidx.compose.material3.Text(modifier: buttonModifier, color: color, text: text?.localizedTextString() ?? "", maxLines: 1, style: style) + androidx.compose.material3.Text(modifier: buttonModifier, color: color, text: text?.localizedTextString() ?? "", style: style) } let optionRenderables = actionRenderables.filter { @@ -446,39 +636,19 @@ final class DisableScrollToDismissConnection : NestedScrollConnection { guard let button = $0.strip() as? Button else { return false } return button.role == .cancel } - let cancelCount = cancelButton == nil ? 0 : 1 - if optionRenderables.size + cancelCount == 2 { - // Horizontal layout for two buttons //TODO: Should revert to vertical when text is too long - Row(modifier: Modifier.fillMaxWidth().height(IntrinsicSize.Min)) { - let modifier = Modifier.weight(Float(1.0)) - if let renderable = cancelButton ?? optionRenderables.firstOrNull() { - AlertButton(modifier: modifier, renderable: renderable, isPresented: isPresented) { - buttonContent(renderable, renderable === cancelButton) + Box(modifier: Modifier.fillMaxWidth(), contentAlignment: androidx.compose.ui.Alignment.CenterEnd) { + AlertDialogFlowRow(mainAxisSpacing: ButtonsMainAxisSpacing, crossAxisSpacing: ButtonsCrossAxisSpacing) { + if let cancelButton { + AlertButton(modifier: flowRowButtonModifier, renderable: cancelButton, isPresented: isPresented) { + buttonContent(cancelButton, true) } - androidx.compose.material3.VerticalDivider() } - if let button = optionRenderables.lastOrNull() { - AlertButton(modifier: modifier, renderable: button, isPresented: isPresented) { - buttonContent(button, false) + for actionRenderable in optionRenderables { + AlertButton(modifier: flowRowButtonModifier, renderable: actionRenderable, isPresented: isPresented) { + buttonContent(actionRenderable, false) } } } - } else { - // Vertical layout - let modifier = Modifier.fillMaxWidth() - for actionRenderable in optionRenderables { - AlertButton(modifier: modifier, renderable: actionRenderable, isPresented: isPresented) { - buttonContent(actionRenderable, false) - } - if actionRenderable !== optionRenderables.lastOrNull() || cancelButton != nil { - androidx.compose.material3.Divider() - } - } - if let cancelButton { - AlertButton(modifier: modifier, renderable: cancelButton, isPresented: isPresented) { - buttonContent(cancelButton, true) - } - } } }