Skip to content

Commit 114fa9a

Browse files
authored
Merge pull request #55 from PureSwift/feature/control-styles
Add control styles: button, picker, toggle, text field
2 parents d56ba52 + e1234cc commit 114fa9a

5 files changed

Lines changed: 369 additions & 48 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//
2+
// ControlStyles.swift
3+
// AndroidSwiftUICore
4+
//
5+
// Control styles. Unlike a per-node modifier, a style applies to every matching
6+
// control in the subtree — `VStack { … }.buttonStyle(.bordered)` styles all the
7+
// buttons inside it — so the interpreter carries these down as environment
8+
// values rather than reading them off the styled node.
9+
//
10+
// These are the built-in style spellings only; the `ButtonStyle` /
11+
// `ToggleStyle` / … protocols for writing custom styles aren't modeled.
12+
//
13+
14+
/// A style value shared by every control-style modifier: an opaque kind string
15+
/// the interpreter maps to a platform presentation.
16+
public struct _ControlStyle: Sendable, Equatable {
17+
internal let kind: String
18+
internal init(_ kind: String) { self.kind = kind }
19+
}
20+
21+
public extension _ControlStyle {
22+
// button
23+
static let automatic = _ControlStyle("automatic")
24+
static let bordered = _ControlStyle("bordered")
25+
static let borderedProminent = _ControlStyle("borderedProminent")
26+
static let borderless = _ControlStyle("borderless")
27+
static let plain = _ControlStyle("plain")
28+
// picker
29+
static let segmented = _ControlStyle("segmented")
30+
static let menu = _ControlStyle("menu")
31+
static let inline = _ControlStyle("inline")
32+
// toggle
33+
static let `switch` = _ControlStyle("switch")
34+
static let checkbox = _ControlStyle("checkbox")
35+
static let button = _ControlStyle("button")
36+
// text field
37+
static let roundedBorder = _ControlStyle("roundedBorder")
38+
}
39+
40+
/// Emits one style kind; the `kind` names which control it governs, so styles
41+
/// for different controls compose instead of overwriting each other.
42+
public struct _ControlStyleModifier: RenderModifier {
43+
let control: String
44+
let style: _ControlStyle
45+
public var _modifierNode: ModifierNode {
46+
ModifierNode(kind: control, args: ["style": .string(style.kind)])
47+
}
48+
}
49+
50+
public extension View {
51+
52+
func buttonStyle(_ style: _ControlStyle) -> ModifiedContent<Self, _ControlStyleModifier> {
53+
modifier(_ControlStyleModifier(control: "buttonStyle", style: style))
54+
}
55+
56+
func pickerStyle(_ style: _ControlStyle) -> ModifiedContent<Self, _ControlStyleModifier> {
57+
modifier(_ControlStyleModifier(control: "pickerStyle", style: style))
58+
}
59+
60+
func toggleStyle(_ style: _ControlStyle) -> ModifiedContent<Self, _ControlStyleModifier> {
61+
modifier(_ControlStyleModifier(control: "toggleStyle", style: style))
62+
}
63+
64+
func textFieldStyle(_ style: _ControlStyle) -> ModifiedContent<Self, _ControlStyleModifier> {
65+
modifier(_ControlStyleModifier(control: "textFieldStyle", style: style))
66+
}
67+
}

AndroidSwiftUICore/Tests/AndroidSwiftUICoreTests/ControlTests.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,53 @@ struct ControlTests {
148148
#expect(ViewHost(ProgressView(value: 0.5)).evaluate().children.isEmpty)
149149
}
150150

151+
@Test("A control style is emitted on the view it is applied to")
152+
func controlStyleEmission() {
153+
let node = ViewHost(Button("Go") {}.buttonStyle(.bordered)).evaluate()
154+
#expect(node.modifiers.first { $0.kind == "buttonStyle" }?.args["style"] == .string("bordered"))
155+
156+
// each control's style is a distinct kind, so they compose rather than
157+
// overwrite one another
158+
let both = ViewHost(
159+
VStack { Text("x") }
160+
.buttonStyle(.plain)
161+
.pickerStyle(.segmented)
162+
).evaluate()
163+
#expect(both.modifiers.first { $0.kind == "buttonStyle" }?.args["style"] == .string("plain"))
164+
#expect(both.modifiers.first { $0.kind == "pickerStyle" }?.args["style"] == .string("segmented"))
165+
#expect(both.modifiers.first { $0.kind == "toggleStyle" } == nil)
166+
}
167+
168+
@Test("A style set on a container is inherited by the controls inside it")
169+
func controlStyleInheritance() {
170+
// The style rides on the container; the interpreter carries it down as
171+
// an environment value, so the buttons themselves carry no style of
172+
// their own — that inheritance is what makes this different from a
173+
// per-node modifier.
174+
let node = ViewHost(
175+
VStack {
176+
Button("One") {}
177+
Button("Two") {}
178+
}
179+
.buttonStyle(.borderedProminent)
180+
).evaluate()
181+
#expect(node.type == "VStack")
182+
#expect(node.modifiers.first { $0.kind == "buttonStyle" }?.args["style"] == .string("borderedProminent"))
183+
#expect(node.children.count == 2)
184+
for child in node.children {
185+
#expect(child.type == "Button")
186+
#expect(child.modifiers.first { $0.kind == "buttonStyle" } == nil)
187+
}
188+
}
189+
190+
@Test("Every control style spelling reaches its own modifier kind")
191+
func controlStyleKinds() {
192+
let toggle = ViewHost(Toggle("t", isOn: .constant(true)).toggleStyle(.checkbox)).evaluate()
193+
#expect(toggle.modifiers.first { $0.kind == "toggleStyle" }?.args["style"] == .string("checkbox"))
194+
let field = ViewHost(TextField("n", text: .constant("")).textFieldStyle(.plain)).evaluate()
195+
#expect(field.modifiers.first { $0.kind == "textFieldStyle" }?.args["style"] == .string("plain"))
196+
}
197+
151198
@Test("Picker emits tagged children and maps the selection string back")
152199
func picker() {
153200
struct Screen: View {

Demo/App.swiftpm/Sources/Catalog.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ struct CatalogEntry: Identifiable {
3434
CatalogEntry(id: "picker", title: "Picker", screen: AnyCatalogScreen(PickerPlayground())),
3535
CatalogEntry(id: "progress", title: "ProgressView", screen: AnyCatalogScreen(ProgressViewPlayground())),
3636
CatalogEntry(id: "morecontrols", title: "More Controls", screen: AnyCatalogScreen(MoreControlsPlayground())),
37+
CatalogEntry(id: "controlstyle", title: "Control Styles", screen: AnyCatalogScreen(ControlStylePlayground())),
3738
CatalogEntry(id: "stack", title: "Stacks", screen: AnyCatalogScreen(StackPlayground())),
3839
CatalogEntry(id: "frame", title: "Frames", screen: AnyCatalogScreen(FramePlayground())),
3940
CatalogEntry(id: "spacer", title: "Spacer & Divider", screen: AnyCatalogScreen(SpacerDividerPlayground())),
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#if canImport(AndroidSwiftUI)
2+
import AndroidSwiftUI
3+
#else
4+
import SwiftUI
5+
#endif
6+
7+
struct ControlStylePlayground: View {
8+
9+
@State private var flavor = "Vanilla"
10+
@State private var notify = true
11+
@State private var name = ""
12+
13+
private let flavors = ["Vanilla", "Cocoa", "Mint"]
14+
15+
var body: some View {
16+
ScrollView {
17+
VStack(alignment: .leading, spacing: 0) {
18+
Example("buttonStyle") {
19+
VStack(alignment: .leading, spacing: 10) {
20+
Button("automatic") {}
21+
Button("bordered") {}.buttonStyle(.bordered)
22+
Button("borderedProminent") {}.buttonStyle(.borderedProminent)
23+
Button("plain") {}.buttonStyle(.plain)
24+
}
25+
}
26+
Example("Inherited by a container") {
27+
// one style, applied once, styles both buttons
28+
VStack(alignment: .leading, spacing: 10) {
29+
Button("Inherited one") {}
30+
Button("Inherited two") {}
31+
}
32+
.buttonStyle(.bordered)
33+
}
34+
Example("pickerStyle") {
35+
VStack(alignment: .leading, spacing: 14) {
36+
Picker("Menu", selection: $flavor) {
37+
ForEach(flavors, id: \.self) { Text($0).tag($0) }
38+
}
39+
Picker("Segmented", selection: $flavor) {
40+
ForEach(flavors, id: \.self) { Text($0).tag($0) }
41+
}
42+
.pickerStyle(.segmented)
43+
Picker("Inline", selection: $flavor) {
44+
ForEach(flavors, id: \.self) { Text($0).tag($0) }
45+
}
46+
.pickerStyle(.inline)
47+
Text("Chosen: \(flavor)")
48+
}
49+
}
50+
Example("toggleStyle") {
51+
VStack(alignment: .leading, spacing: 10) {
52+
Toggle("switch", isOn: $notify)
53+
Toggle("checkbox", isOn: $notify).toggleStyle(.checkbox)
54+
Toggle("button", isOn: $notify).toggleStyle(.button)
55+
}
56+
}
57+
Example("textFieldStyle") {
58+
VStack(alignment: .leading, spacing: 10) {
59+
TextField("rounded border", text: $name)
60+
TextField("plain", text: $name).textFieldStyle(.plain)
61+
}
62+
}
63+
}
64+
}
65+
.navigationTitle("Control Styles")
66+
}
67+
}

0 commit comments

Comments
 (0)