Repository navigation
Support ButtonStyle and PrimitiveButtonStyle protocols - #523
gurmeherchawla wants to merge 13 commits into
Conversation
automatic, button, and switch were accidentally typed as ButtonStyle instead of ToggleStyle.
…uration types Introduces PrimitiveButtonStyle protocol and its concrete implementations: DefaultButtonStyle, PlainButtonStyle, BorderlessButtonStyle, BorderedButtonStyle, BorderedProminentButtonStyle, and M3TextButtonStyle. Adds functional ButtonStyleConfiguration and PrimitiveButtonStyleConfiguration replacing the dead stubs in comments, and Button(configuration:) init so styles can delegate rendering via makeBody. Coexists with the existing ButtonStyle bridging struct unchanged.
Tracks the active button style chain in the environment. Each node holds the current style, its parent (the style it overrides), and the modifier that installed it. isBuiltin distinguishes built-in styles that render natively in Compose from custom styles that render via makeBody.
…p StackedButtonStyle Replaces the raw-value ButtonStyle struct with a ButtonStyle protocol matching SwiftUI's API. Updates ButtonStyleModifier to build the StackedButtonStyle chain and EnvironmentValues._buttonStyle to store it. Updates RenderButton to dispatch on the unwrapped style using type checks (is BorderedButtonStyle etc.), and updates shouldRenderListItem/RenderListItem to use the new isListItemStyle helper. Updates Navigation and Picker call sites.
Adds RenderCustomStyleButton which invokes makeBody on custom styles and composes the body with _buttonStyle set to the parent, enabling Button(configuration) delegation. Adds BridgedButtonStyle and BridgedPrimitiveButtonStyle for natively-compiled styles that cross the Skip bridge, with per-button @Environment syncing via ButtonStyleBridgedConfiguration. Adds buttonStyle(isPrimitive:environmentKeys:bridgedMakeBody:) for the bridge entry point.
|
closes #263 |
There was a problem hiding this comment.
Nice work. Keeping the parent style in StackedButtonStyle is a clean way to get the SwiftUI behavior where Button(configuration) renders in the enclosing style. I tested this together with skip-fuse-ui#143 in a real Fuse app on Android. Existing .plain buttons render as before, and custom ButtonStyle bodies render and respond to taps. One change before merge:
1. Add Lite tests for the custom style paths
Nothing in skip-ui exercises RenderCustomStyleButton or the StackedButtonStyle hand-off. In Tests/SkipUITests/SkipUITests.swift, add this next to testButton:
func testCustomButtonStyles() throws {
try testUI(view: {
CustomButtonStyleTestView().accessibilityIdentifier("test-view")
}, eval: { rule in
try check(rule, id: "label", hasText: "Counter: 0")
#if SKIP
// ButtonStyle: makeBody renders around the label and the tap fires the action
rule.onNodeWithTag("styled.body").assertIsDisplayed()
rule.onNodeWithTag("styled").performClick()
try check(rule, id: "label", hasText: "Counter: 1")
// PrimitiveButtonStyle whose Button(configuration) hands off to the outer .bordered style without recursing
rule.onNodeWithTag("primitive").performClick()
try check(rule, id: "label", hasText: "Counter: 11")
// Disabled custom-styled button doesn't fire
rule.onNodeWithTag("disabled").performClick()
try check(rule, id: "label", hasText: "Counter: 11")
#endif
})
}
struct CustomButtonStyleTestView: View {
@State var counter = 0
var body: some View {
VStack {
Text("Counter: \(counter)")
.accessibilityIdentifier("label")
Button("Styled") { counter += 1 }
.buttonStyle(TestButtonStyle())
.accessibilityIdentifier("styled")
VStack {
Button("Primitive") { counter += 10 }
.buttonStyle(TestPrimitiveButtonStyle())
.accessibilityIdentifier("primitive")
}
.buttonStyle(.bordered)
Button("Disabled") { counter += 100 }
.buttonStyle(TestButtonStyle())
.disabled(true)
.accessibilityIdentifier("disabled")
}
}
}
struct TestButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
HStack {
configuration.label
Text(configuration.isPressed ? "pressed" : "idle")
.accessibilityIdentifier("styled.body")
}
.padding()
}
}
struct TestPrimitiveButtonStyle: PrimitiveButtonStyle {
func makeBody(configuration: Configuration) -> some View {
Button(configuration)
.padding()
}
}The primitive case is the important one. If the source guard in ButtonStyleModifier ever regresses, Button(configuration) will render back into the same style and recurse, and this test will hang or overflow the stack instead of passing silently.
Related, but not for this PR
Once styles read isEnabled, one existing SkipUI bug gets more visible. DisabledModifier (in View/AdditionalViewModifiers.swift) sets isEnabled = !disabled instead of combining it with the outer value. An inner .disabled(false), common inside reusable button wrappers, re-enables a subtree that an outer .disabled(true) disabled. SwiftUI combines the two. The fix is one line, which I verified on device:
- $0.setisEnabled(!disabled)
+ $0.setisEnabled($0.isEnabled && !disabled)I'll open that as a separate PR so it doesn't hold this one up.
This fixes a bug where, after the first withAnimation, every later change kept animating on Android
Adds support for ScrollPosition, onScrollGeometryChange and onScrollPhaseChange.
Adds ScrollPosition, scroll geometry, and scroll phase support
Adds document for scroll position, geometry and phase support
…ton-style-protocol
Replaces the raw-value
ButtonStylebridging struct with properButtonStyleandPrimitiveButtonStyleprotocols matching SwiftUI's API, enabling custom button styles viamakeBodyon Android.Labels: enhancement
What changed
ButtonStyle/PrimitiveButtonStyleprotocolsThe old
ButtonStylestruct was a raw-value enum used purely for bridging. It is replaced by two protocols:PrimitiveButtonStyle— for styles that control their own interaction (matches SwiftUI)ButtonStyle— for styles that receiveisPressedstate and defer interaction to the frameworkAll built-in styles (
DefaultButtonStyle,PlainButtonStyle,BorderedButtonStyle,BorderedProminentButtonStyle,BorderlessButtonStyle,M3TextButtonStyle) are now concrete structs conforming toPrimitiveButtonStyle. They continue to render natively in Compose without going throughmakeBody.StackedButtonStyleIntroduced to track the active style chain in the environment. When
Button(configuration)is called inside a custommakeBody, it renders in the next outer style rather than recursing — matching SwiftUI's behaviour. Thesourcereference prevents a modifier from making itself its own parent when it re-evaluates within its own scope.RenderCustomStyleButtonNew rendering path for non-built-in styles. Calls
makeBody, sets_buttonStyleto the parent stack node, then composes the body so that any nestedButton(configuration)delegates outward correctly.Bridge support
BridgedButtonStyleandBridgedPrimitiveButtonStyleallow natively-compiled styles (from skip-fuse-ui) to cross the bridge.ButtonStyleBridgedConfigurationexposesenvironmentSupport(forKey:)so the bridge layer can sync@Environmentvalues declared on the style at each button's Compose position. A newbuttonStyle(isPrimitive:environmentKeys:bridgedMakeBody:)overload is the bridge entry point.Other fixes
ToggleStylestatic properties (automatic,button,switch) were incorrectly typed asButtonStyle— fixed.PickerandNavigationLinklist-item rendering updated to use the newisListItemStylehelper.Related PR
skip-fuse-ui: skiptools/skip-fuse-ui#143