Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//
// ControlStyles.swift
// AndroidSwiftUICore
//
// Control styles. Unlike a per-node modifier, a style applies to every matching
// control in the subtree — `VStack { … }.buttonStyle(.bordered)` styles all the
// buttons inside it — so the interpreter carries these down as environment
// values rather than reading them off the styled node.
//
// These are the built-in style spellings only; the `ButtonStyle` /
// `ToggleStyle` / … protocols for writing custom styles aren't modeled.
//

/// A style value shared by every control-style modifier: an opaque kind string
/// the interpreter maps to a platform presentation.
public struct _ControlStyle: Sendable, Equatable {
internal let kind: String
internal init(_ kind: String) { self.kind = kind }
}

public extension _ControlStyle {
// button
static let automatic = _ControlStyle("automatic")
static let bordered = _ControlStyle("bordered")
static let borderedProminent = _ControlStyle("borderedProminent")
static let borderless = _ControlStyle("borderless")
static let plain = _ControlStyle("plain")
// picker
static let segmented = _ControlStyle("segmented")
static let menu = _ControlStyle("menu")
static let inline = _ControlStyle("inline")
// toggle
static let `switch` = _ControlStyle("switch")
static let checkbox = _ControlStyle("checkbox")
static let button = _ControlStyle("button")
// text field
static let roundedBorder = _ControlStyle("roundedBorder")
}

/// Emits one style kind; the `kind` names which control it governs, so styles
/// for different controls compose instead of overwriting each other.
public struct _ControlStyleModifier: RenderModifier {
let control: String
let style: _ControlStyle
public var _modifierNode: ModifierNode {
ModifierNode(kind: control, args: ["style": .string(style.kind)])
}
}

public extension View {

func buttonStyle(_ style: _ControlStyle) -> ModifiedContent<Self, _ControlStyleModifier> {
modifier(_ControlStyleModifier(control: "buttonStyle", style: style))
}

func pickerStyle(_ style: _ControlStyle) -> ModifiedContent<Self, _ControlStyleModifier> {
modifier(_ControlStyleModifier(control: "pickerStyle", style: style))
}

func toggleStyle(_ style: _ControlStyle) -> ModifiedContent<Self, _ControlStyleModifier> {
modifier(_ControlStyleModifier(control: "toggleStyle", style: style))
}

func textFieldStyle(_ style: _ControlStyle) -> ModifiedContent<Self, _ControlStyleModifier> {
modifier(_ControlStyleModifier(control: "textFieldStyle", style: style))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,53 @@ struct ControlTests {
#expect(ViewHost(ProgressView(value: 0.5)).evaluate().children.isEmpty)
}

@Test("A control style is emitted on the view it is applied to")
func controlStyleEmission() {
let node = ViewHost(Button("Go") {}.buttonStyle(.bordered)).evaluate()
#expect(node.modifiers.first { $0.kind == "buttonStyle" }?.args["style"] == .string("bordered"))

// each control's style is a distinct kind, so they compose rather than
// overwrite one another
let both = ViewHost(
VStack { Text("x") }
.buttonStyle(.plain)
.pickerStyle(.segmented)
).evaluate()
#expect(both.modifiers.first { $0.kind == "buttonStyle" }?.args["style"] == .string("plain"))
#expect(both.modifiers.first { $0.kind == "pickerStyle" }?.args["style"] == .string("segmented"))
#expect(both.modifiers.first { $0.kind == "toggleStyle" } == nil)
}

@Test("A style set on a container is inherited by the controls inside it")
func controlStyleInheritance() {
// The style rides on the container; the interpreter carries it down as
// an environment value, so the buttons themselves carry no style of
// their own — that inheritance is what makes this different from a
// per-node modifier.
let node = ViewHost(
VStack {
Button("One") {}
Button("Two") {}
}
.buttonStyle(.borderedProminent)
).evaluate()
#expect(node.type == "VStack")
#expect(node.modifiers.first { $0.kind == "buttonStyle" }?.args["style"] == .string("borderedProminent"))
#expect(node.children.count == 2)
for child in node.children {
#expect(child.type == "Button")
#expect(child.modifiers.first { $0.kind == "buttonStyle" } == nil)
}
}

@Test("Every control style spelling reaches its own modifier kind")
func controlStyleKinds() {
let toggle = ViewHost(Toggle("t", isOn: .constant(true)).toggleStyle(.checkbox)).evaluate()
#expect(toggle.modifiers.first { $0.kind == "toggleStyle" }?.args["style"] == .string("checkbox"))
let field = ViewHost(TextField("n", text: .constant("")).textFieldStyle(.plain)).evaluate()
#expect(field.modifiers.first { $0.kind == "textFieldStyle" }?.args["style"] == .string("plain"))
}

@Test("Picker emits tagged children and maps the selection string back")
func picker() {
struct Screen: View {
Expand Down
1 change: 1 addition & 0 deletions Demo/App.swiftpm/Sources/Catalog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ struct CatalogEntry: Identifiable {
CatalogEntry(id: "picker", title: "Picker", screen: AnyCatalogScreen(PickerPlayground())),
CatalogEntry(id: "progress", title: "ProgressView", screen: AnyCatalogScreen(ProgressViewPlayground())),
CatalogEntry(id: "morecontrols", title: "More Controls", screen: AnyCatalogScreen(MoreControlsPlayground())),
CatalogEntry(id: "controlstyle", title: "Control Styles", screen: AnyCatalogScreen(ControlStylePlayground())),
CatalogEntry(id: "stack", title: "Stacks", screen: AnyCatalogScreen(StackPlayground())),
CatalogEntry(id: "frame", title: "Frames", screen: AnyCatalogScreen(FramePlayground())),
CatalogEntry(id: "spacer", title: "Spacer & Divider", screen: AnyCatalogScreen(SpacerDividerPlayground())),
Expand Down
67 changes: 67 additions & 0 deletions Demo/App.swiftpm/Sources/ControlStylePlaygrounds.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#if canImport(AndroidSwiftUI)
import AndroidSwiftUI
#else
import SwiftUI
#endif

struct ControlStylePlayground: View {

@State private var flavor = "Vanilla"
@State private var notify = true
@State private var name = ""

private let flavors = ["Vanilla", "Cocoa", "Mint"]

var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
Example("buttonStyle") {
VStack(alignment: .leading, spacing: 10) {
Button("automatic") {}
Button("bordered") {}.buttonStyle(.bordered)
Button("borderedProminent") {}.buttonStyle(.borderedProminent)
Button("plain") {}.buttonStyle(.plain)
}
}
Example("Inherited by a container") {
// one style, applied once, styles both buttons
VStack(alignment: .leading, spacing: 10) {
Button("Inherited one") {}
Button("Inherited two") {}
}
.buttonStyle(.bordered)
}
Example("pickerStyle") {
VStack(alignment: .leading, spacing: 14) {
Picker("Menu", selection: $flavor) {
ForEach(flavors, id: \.self) { Text($0).tag($0) }
}
Picker("Segmented", selection: $flavor) {
ForEach(flavors, id: \.self) { Text($0).tag($0) }
}
.pickerStyle(.segmented)
Picker("Inline", selection: $flavor) {
ForEach(flavors, id: \.self) { Text($0).tag($0) }
}
.pickerStyle(.inline)
Text("Chosen: \(flavor)")
}
}
Example("toggleStyle") {
VStack(alignment: .leading, spacing: 10) {
Toggle("switch", isOn: $notify)
Toggle("checkbox", isOn: $notify).toggleStyle(.checkbox)
Toggle("button", isOn: $notify).toggleStyle(.button)
}
}
Example("textFieldStyle") {
VStack(alignment: .leading, spacing: 10) {
TextField("rounded border", text: $name)
TextField("plain", text: $name).textFieldStyle(.plain)
}
}
}
}
.navigationTitle("Control Styles")
}
}
Loading
Loading