From 00497e6076c6e0a80f7443e8a8628f81696dd3a9 Mon Sep 17 00:00:00 2001 From: Dan Fabulich Date: Thu, 12 Feb 2026 12:40:37 -0800 Subject: [PATCH 1/5] Reimplement Compose AlertLayout in Skip --- Sources/SkipUI/Skip/SkipAlertDialog.kt | 295 ++++++++++++++++++ .../SkipUI/SkipUI/Layout/Presentation.swift | 249 ++++++++++++--- 2 files changed, 495 insertions(+), 49 deletions(-) create mode 100644 Sources/SkipUI/Skip/SkipAlertDialog.kt diff --git a/Sources/SkipUI/Skip/SkipAlertDialog.kt b/Sources/SkipUI/Skip/SkipAlertDialog.kt new file mode 100644 index 00000000..91087f38 --- /dev/null +++ b/Sources/SkipUI/Skip/SkipAlertDialog.kt @@ -0,0 +1,295 @@ +// 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.padding +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import android.util.Log +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.SideEffect +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, + icon: @Composable (() -> Unit)? = null, + title: @Composable (() -> Unit)? = null, + text: @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" }) + .then(Modifier.logLayoutModifier(tag = "SkipAlertDialog")), + propagateMinConstraints = true, + ) { + SkipAlertDialogContent( + buttons = { + SkipAlertDialogFlowRow( + mainAxisSpacing = ButtonsMainAxisSpacing, + crossAxisSpacing = ButtonsCrossAxisSpacing, + ) { + dismissButton?.let { it() } + confirmButton() + } + }, + icon = icon, + title = title, + text = text, + 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)?, + shape: Shape, + containerColor: Color, + tonalElevation: Dp, + buttonContentColor: Color, + iconContentColor: Color, + titleContentColor: Color, + textContentColor: Color, +) { + SideEffect { Log.d("SkipAlertDialog", "containerColor=$containerColor") } + 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 { + SideEffect { Log.d("SkipAlertMessageColor", "textContentColor=$textContentColor") } + ProvideContentColorTextStyle( + contentColor = textContentColor, + textStyle = MaterialTheme.typography.bodyMedium, + ) { + Box( + Modifier.weight(weight = 1f, fill = false) + .padding(TextPadding) + .align(Alignment.Start), + ) { + text() + } + } + } + Box( + modifier = Modifier + .align(Alignment.End) + .then(Modifier.logLayoutModifier(tag = "SkipAlertDialogButtonBox")), + 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.logLayoutModifier(tag = "SkipAlertDialogFlowRow"), + 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() } + } + @Suppress("ListIterator") sequences.add(0, 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..f70a8a71 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,116 @@ 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 basic cases (no text fields, at most one confirm + optional cancel, so at most 2 buttons). + let isBasicCase = textFields.size == 0 && optionRenderables.size <= 2 + if isBasicCase { + let cancelBtn = actionRenderables.firstOrNull { ($0.strip() as? Button)?.role == .cancel } + let confirmRenderable = optionRenderables.firstOrNull() + 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() } + } + SkipAlertDialog( + onDismissRequest: { isPresented.set(false) }, + 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(modifier: Modifier.logLayoutModifier(tag: "SkipAlertConfirmText"), color: color, text: bt?.localizedTextString() ?? "", style: MaterialTheme.typography.labelLarge) + } + } else { + androidx.compose.material3.TextButton(onClick: { isPresented.set(false) }) { + androidx.compose.material3.Text(modifier: Modifier.logLayoutModifier(tag: "SkipAlertOKText"), 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(modifier: Modifier.logLayoutModifier(tag: "SkipAlertCancelText"), 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(modifier: Modifier.logLayoutModifier(tag: "SkipAlertMessageText"), text: messageText!.localizedTextString(), style: MaterialTheme.typography.bodyMedium) + } : nil, + modifier: Modifier.wrapContentHeight().then(context.modifier), + containerColor: MaterialTheme.colorScheme.surfaceContainerHigh + ) + } else { + BasicAlertDialog(onDismissRequest: { isPresented.set(false) }) { + let modifier = Modifier.sizeIn(minWidth: AlertDialogMinWidth, maxWidth: AlertDialogMaxWidth).wrapContentWidth().wrapContentHeight().then(context.modifier) + Surface(modifier: modifier, shape: AlertDialogDefaults.shape, tonalElevation: AlertDialogDefaults.TonalElevation) { + Column(modifier: Modifier.padding(AlertDialogPadding)) { + RenderAlert(title: title, titleResource: titleResource, context: contentContext, isPresented: isPresented, textFields: textFields, actionRenderables: optionRenderables, message: messageText) + } } } } } @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 +603,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 +617,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) - } - } } } From 460c805d93af910af51ee81a8d81822e56183503 Mon Sep 17 00:00:00 2001 From: Dan Fabulich Date: Fri, 13 Feb 2026 07:45:43 -0800 Subject: [PATCH 2/5] neutral buttons --- Sources/SkipUI/Skip/SkipAlertDialog.kt | 5 +++- .../SkipUI/SkipUI/Layout/Presentation.swift | 30 +++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/Sources/SkipUI/Skip/SkipAlertDialog.kt b/Sources/SkipUI/Skip/SkipAlertDialog.kt index 91087f38..86ac953e 100644 --- a/Sources/SkipUI/Skip/SkipAlertDialog.kt +++ b/Sources/SkipUI/Skip/SkipAlertDialog.kt @@ -61,6 +61,7 @@ fun SkipAlertDialog( 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, @@ -86,6 +87,7 @@ fun SkipAlertDialog( mainAxisSpacing = ButtonsMainAxisSpacing, crossAxisSpacing = ButtonsCrossAxisSpacing, ) { + for (btn in neutralButtons) { btn() } dismissButton?.let { it() } confirmButton() } @@ -229,7 +231,8 @@ private fun SkipAlertDialogFlowRow( if (sequences.isNotEmpty()) { crossAxisSpace += with(density) { crossAxisSpacing.roundToPx() } } - @Suppress("ListIterator") sequences.add(0, currentSequence.toList()) + // Append sequences (not prepend) so neutral buttons appear above dismiss/confirm buttons + sequences.add(currentSequence.toList()) crossAxisSizes += currentCrossAxisSize crossAxisPositions += crossAxisSpace diff --git a/Sources/SkipUI/SkipUI/Layout/Presentation.swift b/Sources/SkipUI/SkipUI/Layout/Presentation.swift index f70a8a71..d1f14c15 100644 --- a/Sources/SkipUI/SkipUI/Layout/Presentation.swift +++ b/Sources/SkipUI/SkipUI/Layout/Presentation.swift @@ -486,11 +486,16 @@ final class DisableScrollToDismissConnection : NestedScrollConnection { let tint = (EnvironmentValues.shared._tint ?? Color.accentColor).colorImpl() let flowRowButtonModifier = Modifier.wrapContentWidth().wrapContentHeight() - // Use native Material3-style dialog for basic cases (no text fields, at most one confirm + optional cancel, so at most 2 buttons). - let isBasicCase = textFields.size == 0 && optionRenderables.size <= 2 + // Use native Material3-style dialog for basic cases (no text fields). N buttons supported via neutralButtons. + let isBasicCase = textFields.size == 0 if isBasicCase { let cancelBtn = actionRenderables.firstOrNull { ($0.strip() as? Button)?.role == .cancel } - let confirmRenderable = optionRenderables.firstOrNull() + // 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() @@ -505,8 +510,27 @@ final class DisableScrollToDismissConnection : NestedScrollConnection { 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(modifier: Modifier.logLayoutModifier(tag: "SkipAlertNeutralText"), 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?() }) { From dadd0cb107f23d26904f9d56c291c2f28f281629 Mon Sep 17 00:00:00 2001 From: Dan Fabulich Date: Fri, 13 Feb 2026 08:05:46 -0800 Subject: [PATCH 3/5] support text fields, remove previous implementation --- Sources/SkipUI/Skip/SkipAlertDialog.kt | 13 ++ .../SkipUI/SkipUI/Layout/Presentation.swift | 169 +++++++++--------- 2 files changed, 95 insertions(+), 87 deletions(-) diff --git a/Sources/SkipUI/Skip/SkipAlertDialog.kt b/Sources/SkipUI/Skip/SkipAlertDialog.kt index 86ac953e..04842d0f 100644 --- a/Sources/SkipUI/Skip/SkipAlertDialog.kt +++ b/Sources/SkipUI/Skip/SkipAlertDialog.kt @@ -19,6 +19,7 @@ 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.material3.AlertDialogDefaults @@ -65,6 +66,7 @@ fun SkipAlertDialog( 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, @@ -95,6 +97,7 @@ fun SkipAlertDialog( icon = icon, title = title, text = text, + textFields = textFields, shape = shape, containerColor = containerColor, tonalElevation = tonalElevation, @@ -114,6 +117,7 @@ private fun SkipAlertDialogContent( icon: (@Composable () -> Unit)?, title: (@Composable () -> Unit)?, text: @Composable (() -> Unit)?, + textFields: @Composable (() -> Unit)? = null, shape: Shape, containerColor: Color, tonalElevation: Dp, @@ -168,6 +172,15 @@ private fun SkipAlertDialogContent( } } } + textFields?.let { + Column( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.Start), + ) { + textFields() + } + } Box( modifier = Modifier .align(Alignment.End) diff --git a/Sources/SkipUI/SkipUI/Layout/Presentation.swift b/Sources/SkipUI/SkipUI/Layout/Presentation.swift index d1f14c15..a198b426 100644 --- a/Sources/SkipUI/SkipUI/Layout/Presentation.swift +++ b/Sources/SkipUI/SkipUI/Layout/Presentation.swift @@ -486,99 +486,94 @@ final class DisableScrollToDismissConnection : NestedScrollConnection { let tint = (EnvironmentValues.shared._tint ?? Color.accentColor).colorImpl() let flowRowButtonModifier = Modifier.wrapContentWidth().wrapContentHeight() - // Use native Material3-style dialog for basic cases (no text fields). N buttons supported via neutralButtons. - let isBasicCase = textFields.size == 0 - if isBasicCase { - 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(modifier: Modifier.logLayoutModifier(tag: "SkipAlertNeutralText"), color: tint, text: bt?.localizedTextString() ?? "", style: MaterialTheme.typography.labelLarge) - } + // 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(modifier: Modifier.logLayoutModifier(tag: "SkipAlertNeutralText"), 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(modifier: Modifier.logLayoutModifier(tag: "SkipAlertConfirmText"), color: color, text: bt?.localizedTextString() ?? "", style: MaterialTheme.typography.labelLarge) - } - } else { - androidx.compose.material3.TextButton(onClick: { isPresented.set(false) }) { - androidx.compose.material3.Text(modifier: Modifier.logLayoutModifier(tag: "SkipAlertOKText"), color: tint, text: stringResource(android.R.string.ok), 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(modifier: Modifier.logLayoutModifier(tag: "SkipAlertConfirmText"), color: color, text: bt?.localizedTextString() ?? "", 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(modifier: Modifier.logLayoutModifier(tag: "SkipAlertCancelText"), color: tint, text: bt?.localizedTextString() ?? "", style: MaterialTheme.typography.labelLarge) - } + } else { + androidx.compose.material3.TextButton(onClick: { isPresented.set(false) }) { + androidx.compose.material3.Text(modifier: Modifier.logLayoutModifier(tag: "SkipAlertOKText"), color: tint, text: stringResource(android.R.string.ok), 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(modifier: Modifier.logLayoutModifier(tag: "SkipAlertMessageText"), text: messageText!.localizedTextString(), style: MaterialTheme.typography.bodyMedium) - } : nil, - modifier: Modifier.wrapContentHeight().then(context.modifier), - containerColor: MaterialTheme.colorScheme.surfaceContainerHigh - ) - } else { - BasicAlertDialog(onDismissRequest: { isPresented.set(false) }) { - let modifier = Modifier.sizeIn(minWidth: AlertDialogMinWidth, maxWidth: AlertDialogMaxWidth).wrapContentWidth().wrapContentHeight().then(context.modifier) - Surface(modifier: modifier, shape: AlertDialogDefaults.shape, tonalElevation: AlertDialogDefaults.TonalElevation) { - Column(modifier: Modifier.padding(AlertDialogPadding)) { - RenderAlert(title: title, titleResource: titleResource, context: contentContext, isPresented: isPresented, textFields: textFields, actionRenderables: optionRenderables, message: messageText) + } + }, + 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(modifier: Modifier.logLayoutModifier(tag: "SkipAlertCancelText"), 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(modifier: Modifier.logLayoutModifier(tag: "SkipAlertMessageText"), 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?) { From f5d5e9ff95ba72f838bea3de5305eb68521b1573 Mon Sep 17 00:00:00 2001 From: Dan Fabulich Date: Fri, 13 Feb 2026 08:18:10 -0800 Subject: [PATCH 4/5] support scrolling --- Sources/SkipUI/Skip/SkipAlertDialog.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/SkipUI/Skip/SkipAlertDialog.kt b/Sources/SkipUI/Skip/SkipAlertDialog.kt index 04842d0f..10fbb128 100644 --- a/Sources/SkipUI/Skip/SkipAlertDialog.kt +++ b/Sources/SkipUI/Skip/SkipAlertDialog.kt @@ -22,6 +22,8 @@ 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 @@ -159,13 +161,17 @@ private fun SkipAlertDialogContent( } text?.let { SideEffect { Log.d("SkipAlertMessageColor", "textContentColor=$textContentColor") } + val scrollState = rememberScrollState() ProvideContentColorTextStyle( contentColor = textContentColor, textStyle = MaterialTheme.typography.bodyMedium, ) { Box( - Modifier.weight(weight = 1f, fill = false) + Modifier + .weight(weight = 1f, fill = false) .padding(TextPadding) + .fillMaxWidth() + .verticalScroll(scrollState) .align(Alignment.Start), ) { text() From aedbe4e1806a7922b5d03058a7b44e48cd5988ac Mon Sep 17 00:00:00 2001 From: Dan Fabulich Date: Fri, 13 Feb 2026 08:10:50 -0800 Subject: [PATCH 5/5] strip logging --- Sources/SkipUI/Skip/SkipAlertDialog.kt | 17 +++++------------ Sources/SkipUI/SkipUI/Layout/Presentation.swift | 10 +++++----- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/Sources/SkipUI/Skip/SkipAlertDialog.kt b/Sources/SkipUI/Skip/SkipAlertDialog.kt index 10fbb128..a30481b5 100644 --- a/Sources/SkipUI/Skip/SkipAlertDialog.kt +++ b/Sources/SkipUI/Skip/SkipAlertDialog.kt @@ -27,10 +27,8 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.SideEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -81,8 +79,7 @@ fun SkipAlertDialog( Box( modifier = modifier .sizeIn(minWidth = DialogMinWidth, maxWidth = DialogMaxWidth) - .then(Modifier.semantics { paneTitle = "Dialog" }) - .then(Modifier.logLayoutModifier(tag = "SkipAlertDialog")), + .then(Modifier.semantics { paneTitle = "Dialog" }), propagateMinConstraints = true, ) { SkipAlertDialogContent( @@ -128,7 +125,6 @@ private fun SkipAlertDialogContent( titleContentColor: Color, textContentColor: Color, ) { - SideEffect { Log.d("SkipAlertDialog", "containerColor=$containerColor") } Surface( modifier = modifier, shape = shape, @@ -159,9 +155,8 @@ private fun SkipAlertDialogContent( } } } - text?.let { - SideEffect { Log.d("SkipAlertMessageColor", "textContentColor=$textContentColor") } - val scrollState = rememberScrollState() + text?.let { + val scrollState = rememberScrollState() ProvideContentColorTextStyle( contentColor = textContentColor, textStyle = MaterialTheme.typography.bodyMedium, @@ -188,9 +183,7 @@ private fun SkipAlertDialogContent( } } Box( - modifier = Modifier - .align(Alignment.End) - .then(Modifier.logLayoutModifier(tag = "SkipAlertDialogButtonBox")), + modifier = Modifier.align(Alignment.End), contentAlignment = Alignment.CenterEnd, ) { ProvideContentColorTextStyle( @@ -227,7 +220,7 @@ private fun SkipAlertDialogFlowRow( val density = LocalDensity.current val layoutDirection = LocalLayoutDirection.current Layout( - modifier = Modifier.logLayoutModifier(tag = "SkipAlertDialogFlowRow"), + modifier = Modifier, content = content, ) { measurables, constraints -> val sequences = mutableListOf>() diff --git a/Sources/SkipUI/SkipUI/Layout/Presentation.swift b/Sources/SkipUI/SkipUI/Layout/Presentation.swift index a198b426..8934ddce 100644 --- a/Sources/SkipUI/SkipUI/Layout/Presentation.swift +++ b/Sources/SkipUI/SkipUI/Layout/Presentation.swift @@ -522,7 +522,7 @@ final class DisableScrollToDismissConnection : NestedScrollConnection { 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(modifier: Modifier.logLayoutModifier(tag: "SkipAlertNeutralText"), color: tint, text: bt?.localizedTextString() ?? "", style: MaterialTheme.typography.labelLarge) + androidx.compose.material3.Text(color: tint, text: bt?.localizedTextString() ?? "", style: MaterialTheme.typography.labelLarge) } } } @@ -537,11 +537,11 @@ final class DisableScrollToDismissConnection : NestedScrollConnection { 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(modifier: Modifier.logLayoutModifier(tag: "SkipAlertConfirmText"), color: color, text: bt?.localizedTextString() ?? "", style: MaterialTheme.typography.labelLarge) + 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(modifier: Modifier.logLayoutModifier(tag: "SkipAlertOKText"), color: tint, text: stringResource(android.R.string.ok), style: MaterialTheme.typography.labelLarge) + androidx.compose.material3.Text(color: tint, text: stringResource(android.R.string.ok), style: MaterialTheme.typography.labelLarge) } } }, @@ -552,7 +552,7 @@ final class DisableScrollToDismissConnection : NestedScrollConnection { 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(modifier: Modifier.logLayoutModifier(tag: "SkipAlertCancelText"), color: tint, text: bt?.localizedTextString() ?? "", style: MaterialTheme.typography.labelLarge) + androidx.compose.material3.Text(color: tint, text: bt?.localizedTextString() ?? "", style: MaterialTheme.typography.labelLarge) } } } : nil, @@ -562,7 +562,7 @@ final class DisableScrollToDismissConnection : NestedScrollConnection { androidx.compose.material3.Text(color: Color.primary.colorImpl(), text: stringResource(titleResource!), style: MaterialTheme.typography.headlineSmall) } : nil), text: messageText != nil ? { - androidx.compose.material3.Text(modifier: Modifier.logLayoutModifier(tag: "SkipAlertMessageText"), text: messageText!.localizedTextString(), style: MaterialTheme.typography.bodyMedium) + androidx.compose.material3.Text(text: messageText!.localizedTextString(), style: MaterialTheme.typography.bodyMedium) } : nil, textFields: textFields.size > 0 ? { for textField in textFields {