diff --git a/Sources/SkipUI/SkipUI/Commands/Menu.swift b/Sources/SkipUI/SkipUI/Commands/Menu.swift index 77a602c3..ebd08ac1 100644 --- a/Sources/SkipUI/SkipUI/Commands/Menu.swift +++ b/Sources/SkipUI/SkipUI/Commands/Menu.swift @@ -78,7 +78,7 @@ public final class Menu : View { onLongClick = { toggleMenu() }, onClick = primaryAction ) - ComposeTextButton(label: label, context: context.content(modifier: primaryActionModifier)) + Button.ComposeTextButton(label: label, context: context.content(modifier: primaryActionModifier)) } else { label.Compose(context: contentContext) } diff --git a/Sources/SkipUI/SkipUI/Commands/Search.swift b/Sources/SkipUI/SkipUI/Commands/Search.swift index a3e20dcf..c29cb24e 100644 --- a/Sources/SkipUI/SkipUI/Commands/Search.swift +++ b/Sources/SkipUI/SkipUI/Commands/Search.swift @@ -138,7 +138,7 @@ let searchFieldHeight = 56.0 } }, keyboardOptions: keyboardOptions, keyboardActions: keyboardActions, singleLine: true, colors: colors, shape: Capsule().asComposeShape(density: LocalDensity.current)) AnimatedVisibility(visible: state.isSearching.value == true) { - ComposeTextButton(label: Text(verbatim: stringResource(android.R.string.cancel)), context: contentContext) { + Button.ComposeTextButton(label: Text(verbatim: stringResource(android.R.string.cancel)), context: contentContext) { state.text.wrappedValue = "" focusManager.clearFocus() state.isSearching.value = false diff --git a/Sources/SkipUI/SkipUI/Containers/Navigation.swift b/Sources/SkipUI/SkipUI/Containers/Navigation.swift index 70f49d48..f7054ccc 100644 --- a/Sources/SkipUI/SkipUI/Containers/Navigation.swift +++ b/Sources/SkipUI/SkipUI/Containers/Navigation.swift @@ -886,18 +886,20 @@ public struct NavigationLink : View, ListItemAdapting { #if SKIP @Composable public override func ComposeContent(context: ComposeContext) { - let navigationContext = context.content(modifier: NavigationModifier(context.modifier)) - ComposeTextButton(label: label, context: navigationContext) + Button.ComposeButton(label: label, context: context, isEnabled: isNavigationEnabled(), action: navigationAction()) } @Composable func shouldComposeListItem() -> Bool { - return true + let buttonStyle = EnvironmentValues.shared._buttonStyle + return buttonStyle == nil || buttonStyle == .automatic || buttonStyle == .plain } @Composable func ComposeListItem(context: ComposeContext, contentModifier: Modifier) { - Row(modifier: NavigationModifier(modifier: Modifier).then(contentModifier), horizontalArrangement: Arrangement.spacedBy(8.dp), verticalAlignment: androidx.compose.ui.Alignment.CenterVertically) { + let isEnabled = isNavigationEnabled() + let modifier = Modifier.clickable(onClick: navigationAction(), enabled: isEnabled).then(contentModifier) + Row(modifier: modifier, horizontalArrangement: Arrangement.spacedBy(8.dp), verticalAlignment: androidx.compose.ui.Alignment.CenterVertically) { Box(modifier: Modifier.weight(Float(1.0))) { - // Continue to specialize for list rendering within the NavigationLink (e.g. Label) + // Continue to specialize for list rendering within the content (e.g. Label) label.Compose(context: context.content(composer: ListItemComposer(contentModifier: Modifier))) } Self.ComposeChevron() @@ -909,9 +911,13 @@ public struct NavigationLink : View, ListItemAdapting { Icon(imageVector: isRTL ? Icons.Outlined.KeyboardArrowLeft : Icons.Outlined.KeyboardArrowRight, contentDescription: nil, tint: androidx.compose.ui.graphics.Color.Gray) } - @Composable private func NavigationModifier(modifier: Modifier) -> Modifier { + @Composable private func isNavigationEnabled() -> Bool { + return (value != nil || destination != nil) && EnvironmentValues.shared.isEnabled + } + + @Composable private func navigationAction() -> () -> Void { let navigator = LocalNavigator.current - return modifier.clickable(enabled: (value != nil || destination != nil) && EnvironmentValues.shared.isEnabled) { + return { // Hack to prevent multiple quick taps from pushing duplicate entries let now = CFAbsoluteTimeGetCurrent() guard NavigationLink.lastNavigationTime + NavigationLink.minimumNavigationInterval <= now else { diff --git a/Sources/SkipUI/SkipUI/Controls/Button.swift b/Sources/SkipUI/SkipUI/Controls/Button.swift index 9bd9b2a3..ecc3e7a3 100644 --- a/Sources/SkipUI/SkipUI/Controls/Button.swift +++ b/Sources/SkipUI/SkipUI/Controls/Button.swift @@ -53,6 +53,24 @@ public struct Button : View, ListItemAdapting { #if SKIP @Composable public override func ComposeContent(context: ComposeContext) { + Self.ComposeButton(label: label, context: context, role: role, action: action) + } + + @Composable func shouldComposeListItem() -> Bool { + let buttonStyle = EnvironmentValues.shared._buttonStyle + return buttonStyle == nil || buttonStyle == .automatic || buttonStyle == .plain + } + + @Composable func ComposeListItem(context: ComposeContext, contentModifier: Modifier) { + let isEnabled = EnvironmentValues.shared.isEnabled + let modifier = Modifier.clickable(onClick: action, enabled: isEnabled).then(contentModifier) + Box(modifier: modifier, contentAlignment: androidx.compose.ui.Alignment.CenterStart) { + Self.ComposeTextButton(label: label, context: context, isPlain: EnvironmentValues.shared._buttonStyle == .plain, role: role, isEnabled: isEnabled) + } + } + + /// Compose a button in the current style. + @Composable static func ComposeButton(label: View, context: ComposeContext, role: ButtonRole? = nil, isEnabled: Bool = EnvironmentValues.shared.isEnabled, action: () -> Void) { let buttonStyle = EnvironmentValues.shared._buttonStyle ComposeContainer(modifier: context.modifier) { modifier in switch buttonStyle { @@ -70,7 +88,7 @@ public struct Button : View, ListItemAdapting { EnvironmentValues.shared.setValues { $0.set_placement(placement.union(ViewPlacement.systemTextColor)) } in: { - FilledTonalButton(onClick: action, modifier: modifier, enabled: EnvironmentValues.shared.isEnabled, colors: colors) { + FilledTonalButton(onClick: action, modifier: modifier, enabled: isEnabled, colors: colors) { label.Compose(context: contentContext) } } @@ -88,26 +106,44 @@ public struct Button : View, ListItemAdapting { EnvironmentValues.shared.setValues { $0.set_placement(placement.union(ViewPlacement.systemTextColor)) } in: { - androidx.compose.material3.Button(onClick: action, modifier: modifier, enabled: EnvironmentValues.shared.isEnabled, colors: colors) { + androidx.compose.material3.Button(onClick: action, modifier: modifier, enabled: isEnabled, colors: colors) { label.Compose(context: contentContext) } } case .plain: - ComposeTextButton(label: label, context: context.content(modifier: modifier), role: role, isPlain: true, action: action) + ComposeTextButton(label: label, context: context.content(modifier: modifier), role: role, isPlain: true, isEnabled: isEnabled, action: action) default: - ComposeTextButton(label: label, context: context.content(modifier: modifier), role: role, action: action) + ComposeTextButton(label: label, context: context.content(modifier: modifier), role: role, isEnabled: isEnabled, action: action) } } } - @Composable func shouldComposeListItem() -> Bool { - let buttonStyle = EnvironmentValues.shared._buttonStyle - return buttonStyle == nil || buttonStyle == .automatic || buttonStyle == .plain - } + /// Render a plain-style button. + /// + /// - Parameters: + /// - action: Pass nil if the given modifier already includes `clickable` + @Composable static func ComposeTextButton(label: View, context: ComposeContext, role: ButtonRole? = nil, isPlain: Bool = false, isEnabled: Bool = EnvironmentValues.shared.isEnabled, action: (() -> Void)? = nil) { + var foregroundStyle: ShapeStyle + if role == .destructive { + foregroundStyle = Color.red + } else { + foregroundStyle = EnvironmentValues.shared._foregroundStyle ?? (isPlain ? Color.primary : (EnvironmentValues.shared._tint ?? Color.accentColor)) + } + if !isEnabled { + let disabledAlpha = Double(ContentAlpha.disabled) + foregroundStyle = AnyShapeStyle(foregroundStyle, opacity: disabledAlpha) + } - @Composable func ComposeListItem(context: ComposeContext, contentModifier: Modifier) { - Box(modifier: Modifier.clickable(onClick: action, enabled: EnvironmentValues.shared.isEnabled).then(contentModifier), contentAlignment: androidx.compose.ui.Alignment.CenterStart) { - ComposeTextButton(label: label, context: context, isPlain: EnvironmentValues.shared._buttonStyle == .plain, role: role) + var modifier = context.modifier + if let action { + modifier = modifier.clickable(onClick: action, enabled: isEnabled) + } + let contentContext = context.content(modifier: modifier) + + EnvironmentValues.shared.setValues { + $0.set_foregroundStyle(foregroundStyle) + } in: { + label.Compose(context: contentContext) } } #else @@ -117,35 +153,6 @@ public struct Button : View, ListItemAdapting { #endif } -#if SKIP -/// Render a plain-style button. -@Composable func ComposeTextButton(label: View, context: ComposeContext, role: ButtonRole? = nil, isPlain: Bool = false, action: (() -> Void)? = nil) { - var foregroundStyle: ShapeStyle - if role == .destructive { - foregroundStyle = Color.red - } else { - foregroundStyle = EnvironmentValues.shared._foregroundStyle ?? (isPlain ? Color.primary : (EnvironmentValues.shared._tint ?? Color.accentColor)) - } - let isEnabled = EnvironmentValues.shared.isEnabled - if !isEnabled { - let disabledAlpha = Double(ContentAlpha.disabled) - foregroundStyle = AnyShapeStyle(foregroundStyle, opacity: disabledAlpha) - } - - var modifier = context.modifier - if let action { - modifier = modifier.clickable(onClick: action, enabled: isEnabled) - } - let contentContext = context.content(modifier: modifier) - - EnvironmentValues.shared.setValues { - $0.set_foregroundStyle(foregroundStyle) - } in: { - label.Compose(context: contentContext) - } -} -#endif - public struct ButtonStyle: RawRepresentable, Equatable { public let rawValue: Int diff --git a/Sources/SkipUI/SkipUI/Controls/DatePicker.swift b/Sources/SkipUI/SkipUI/Controls/DatePicker.swift index 2f11e9c6..079a8576 100644 --- a/Sources/SkipUI/SkipUI/Controls/DatePicker.swift +++ b/Sources/SkipUI/SkipUI/Controls/DatePicker.swift @@ -118,7 +118,7 @@ public struct DatePicker : View { if let dateString = dateFormatter?.string(from: date) { let text = Text(verbatim: dateString) if isEnabled { - ComposeTextButton(label: text, context: context) { + Button.ComposeTextButton(label: text, context: context) { isDatePickerPresented.value = true } } else { @@ -129,7 +129,7 @@ public struct DatePicker : View { if let timeString = timeFormatter?.string(from: date) { let text = Text(verbatim: timeString) if isEnabled { - ComposeTextButton(label: text, context: context) { + Button.ComposeTextButton(label: text, context: context) { isTimePickerPresented.value = true } } else { diff --git a/Sources/SkipUI/SkipUI/Controls/Picker.swift b/Sources/SkipUI/SkipUI/Controls/Picker.swift index 4509a12d..a1624a9d 100644 --- a/Sources/SkipUI/SkipUI/Controls/Picker.swift +++ b/Sources/SkipUI/SkipUI/Controls/Picker.swift @@ -62,7 +62,7 @@ public struct Picker : View, ListItemAdapting { }, enabled: EnvironmentValues.shared.isEnabled) ComposeContainer(modifier: modifier, fillWidth: true) { modifier in Row(modifier: modifier, verticalAlignment: androidx.compose.ui.Alignment.CenterVertically) { - ComposeTextButton(label: label, context: contentContext) + Button.ComposeTextButton(label: label, context: contentContext) androidx.compose.foundation.layout.Spacer(modifier: Modifier.width(8.dp)) androidx.compose.foundation.layout.Spacer(modifier: Modifier.weight(Float(1.0))) ComposeSelectedValue(views: views, context: contentContext, style: style, performsAction: false) @@ -88,7 +88,7 @@ public struct Picker : View, ListItemAdapting { if performsAction { let isMenuExpanded = remember { mutableStateOf(false) } Box { - ComposeTextButton(label: selectedValueLabel, context: context) { isMenuExpanded.value = !isMenuExpanded.value } + Button.ComposeTextButton(label: selectedValueLabel, context: context) { isMenuExpanded.value = !isMenuExpanded.value } if isMenu { ComposePickerSelectionMenu(views: views, isExpanded: isMenuExpanded, context: context.content()) }