Skip to content

Commit

Permalink
Add toggleStyle modifier with .button and .switch styles (#69)
Browse files Browse the repository at this point in the history
* Switch now functions as expected, added Switch to GtkCodeGen

* Renamed occurances of backendSwitch to switchWidget

* Work on merging the two toggle types into one with a modifier

* Merged Toggle and Switch into Toggle and added a modifer to change the style

* Reworked ControlsExample to be a lot more visually clean

* Remove Switch.swift as it is no longer needed

* Resolved most requested changes
selectedToggleStyle now a var, formatting issues have been fixed
Private structs no longer private, init property removed, modifier changed to use variable

* One set of pointless parenthesis managed to escape me

* Set onChange in the createSwitch function
  • Loading branch information
NinjaCheetah authored Nov 8, 2023
1 parent ac7de2e commit 6a053a7
Show file tree
Hide file tree
Showing 8 changed files with 323 additions and 41 deletions.
40 changes: 30 additions & 10 deletions Examples/Controls/Controls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import SwiftCrossUI
class ControlsState: Observable {
@Observed var count = 0
@Observed var exampleButtonState = false
@Observed var exampleSwitchState = false
}

@main
Expand All @@ -14,20 +15,39 @@ struct ControlsApp: App {

let state = ControlsState()

let windowProperties = WindowProperties(title: "ControlsApp", resizable: true)
let windowProperties = WindowProperties(
title: "ControlsApp",
resizable: true
)

var body: some View {
VStack(spacing: 5) {
Text("Button")
Button("Click me!") {
state.count += 1
HStack {
VStack {
Text("Button")
Button("Click me!") {
state.count += 1
}
Text("Count: \(state.count)", wrap: false)
Spacer()
}
Text("Count: \(state.count)", wrap: false)
Spacer()
.padding(.bottom, 15)
Text("Toggle (Button Style)")
Toggle("Toggle me!", active: state.$exampleButtonState)
Text("Currently enabled: \(state.exampleButtonState)")
VStack {
Text("Toggle (Button Style)")
Toggle("Toggle me!", active: state.$exampleButtonState)
.toggleStyle(.button)
Text("Currently enabled: \(state.exampleButtonState)")
Spacer()
.padding(.bottom, 10)
Text("Toggle (Switch Style)")
HStack {
Toggle("Toggle me:", active: state.$exampleSwitchState)
.toggleStyle(.switch)
Spacer()
.padding(.bottom, 10)
}
Text("Currently enabled: \(state.exampleSwitchState)")
Spacer()
}
}
.padding(10)
}
Expand Down
153 changes: 153 additions & 0 deletions Sources/Gtk/Generated/Switch.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import CGtk

/// `GtkSwitch` is a "light switch" that has two states: on or off.
///
/// ![An example GtkSwitch](switch.png)
///
/// The user can control which state should be active by clicking the
/// empty area, or by dragging the handle.
///
/// `GtkSwitch` can also handle situations where the underlying state
/// changes with a delay. In this case, the slider position indicates
/// the user's recent change (as indicated by the [property@Gtk.Switch:active]
/// property), and the color indicates whether the underlying state (represented
/// by the [property@Gtk.Switch:state] property) has been updated yet.
///
/// ![GtkSwitch with delayed state change](switch-state.png)
///
/// See [signal@Gtk.Switch::state-set] for details.
///
/// # CSS nodes
///
/// ```
/// switch
/// ├── image
/// ├── image
/// ╰── slider
/// ```
///
/// `GtkSwitch` has four css nodes, the main node with the name switch and
/// subnodes for the slider and the on and off images. Neither of them is
/// using any style classes.
///
/// # Accessibility
///
/// `GtkSwitch` uses the %GTK_ACCESSIBLE_ROLE_SWITCH role.
public class Switch: Widget, Actionable {
/// Creates a new `GtkSwitch` widget.
override public init() {
super.init()
widgetPointer = gtk_switch_new()
}

override func didMoveToParent() {
removeSignals()

super.didMoveToParent()

addSignal(name: "activate") { [weak self] () in
guard let self = self else { return }
self.activate?(self)
}

let handler1:
@convention(c) (UnsafeMutableRawPointer, Bool, UnsafeMutableRawPointer) -> Void =
{ _, value1, data in
SignalBox1<Bool>.run(data, value1)
}

addSignal(name: "state-set", handler: gCallback(handler1)) { [weak self] (_: Bool) in
guard let self = self else { return }
self.stateSet?(self)
}

let handler2:
@convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void =
{ _, value1, data in
SignalBox1<OpaquePointer>.run(data, value1)
}

addSignal(name: "notify::active", handler: gCallback(handler2)) {
[weak self] (_: OpaquePointer) in
guard let self = self else { return }
self.notifyActive?(self)
}

let handler3:
@convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void =
{ _, value1, data in
SignalBox1<OpaquePointer>.run(data, value1)
}

addSignal(name: "notify::state", handler: gCallback(handler3)) {
[weak self] (_: OpaquePointer) in
guard let self = self else { return }
self.notifyState?(self)
}

let handler4:
@convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void =
{ _, value1, data in
SignalBox1<OpaquePointer>.run(data, value1)
}

addSignal(name: "notify::action-name", handler: gCallback(handler4)) {
[weak self] (_: OpaquePointer) in
guard let self = self else { return }
self.notifyActionName?(self)
}

let handler5:
@convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void =
{ _, value1, data in
SignalBox1<OpaquePointer>.run(data, value1)
}

addSignal(name: "notify::action-target", handler: gCallback(handler5)) {
[weak self] (_: OpaquePointer) in
guard let self = self else { return }
self.notifyActionTarget?(self)
}
}

/// Whether the `GtkSwitch` widget is in its on or off state.
@GObjectProperty(named: "active") public var active: Bool

/// The backend state that is controlled by the switch.
///
/// See [signal@Gtk.Switch::state-set] for details.
@GObjectProperty(named: "state") public var state: Bool

@GObjectProperty(named: "action-name") public var actionName: String?

/// Emitted to animate the switch.
///
/// Applications should never connect to this signal,
/// but use the [property@Gtk.Switch:active] property.
public var activate: ((Switch) -> Void)?

/// Emitted to change the underlying state.
///
/// The ::state-set signal is emitted when the user changes the switch
/// position. The default handler keeps the state in sync with the
/// [property@Gtk.Switch:active] property.
///
/// To implement delayed state change, applications can connect to this
/// signal, initiate the change of the underlying state, and call
/// [method@Gtk.Switch.set_state] when the underlying state change is
/// complete. The signal handler should return %TRUE to prevent the
/// default handler from running.
///
/// Visually, the underlying state is represented by the trough color of
/// the switch, while the [property@Gtk.Switch:active] property is
/// represented by the position of the switch.
public var stateSet: ((Switch) -> Void)?

public var notifyActive: ((Switch) -> Void)?

public var notifyState: ((Switch) -> Void)?

public var notifyActionName: ((Switch) -> Void)?

public var notifyActionTarget: ((Switch) -> Void)?
}
23 changes: 21 additions & 2 deletions Sources/GtkBackend/GtkBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ public struct GtkBackend: AppBackend {
}

public func createToggle(
label: String,
active: Bool,
label: String,
active: Bool,
onChange: @escaping (Bool) -> Void
) -> Widget {
let toggle = ToggleButton()
Expand All @@ -188,6 +188,25 @@ public struct GtkBackend: AppBackend {
}
}

public func createSwitch(active: Bool, onChange: @escaping (Bool) -> Void) -> Widget {
let switchWidget = Switch()
switchWidget.active = active
switchWidget.notifyActive = { widget in
onChange(widget.active)
}
return switchWidget
}

public func setIsActive(ofSwitch switchWidget: Widget, to active: Bool) {
(switchWidget as! Gtk.Switch).active = active
}

public func setOnChange(ofSwitch switchWidget: Widget, to onChange: @escaping (Bool) -> Void) {
(switchWidget as! Gtk.Switch).notifyActive = { widget in
onChange(widget.active)
}
}

public func createTextView(content: String, shouldWrap: Bool) -> Widget {
let label = Label(string: content)
label.lineWrapMode = .wordCharacter
Expand Down
2 changes: 1 addition & 1 deletion Sources/GtkCodeGen/GtkCodeGen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ struct GtkCodeGen {
static func generateSources(for gir: GIR, to directory: URL) throws {
let allowListedClasses = [
"Button", "Entry", "Label", "TextView", "Range", "Scale", "Image", "DropDown",
"Picture",
"Picture", "Switch",
]
for class_ in gir.namespace.classes where allowListedClasses.contains(class_.name) {
let source = generateClass(class_, namespace: gir.namespace)
Expand Down
22 changes: 20 additions & 2 deletions Sources/SwiftCrossUI/Backend/AppBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ public protocol AppBackend {
/// The change handler is called whenever the button is toggled on or off.
func setOnChange(ofToggle toggle: Widget, to onChange: @escaping (Bool) -> Void)

/// Creates a switch that is either on or off. Predominantly used by ``Switch``
func createSwitch(active: Bool, onChange: @escaping (Bool) -> Void) -> Widget
/// Sets the state of the switch to active or not.
func setIsActive(ofSwitch switchWidget: Widget, to active: Bool)
/// Sets the change handler of a switch (replaces any existing change handlers).
/// The change handler is called whenever the switch is turned on or off.
func setOnChange(ofSwitch switchWidget: Widget, to onChange: @escaping (Bool) -> Void)

/// Creates a non-editable text view with optional text wrapping. Predominantly used
/// by ``Text``.`
func createTextView(content: String, shouldWrap: Bool) -> Widget
Expand Down Expand Up @@ -386,8 +394,8 @@ extension AppBackend {
}

public func createToggle(
label: String,
active: Bool,
label: String,
active: Bool,
onChange: @escaping (Bool) -> Void
) -> Widget {
todo("createToggle not implemented")
Expand All @@ -399,6 +407,16 @@ extension AppBackend {
todo("setOnChange not implemented")
}

public func createSwitch(active: Bool, onChange: @escaping (Bool) -> Void) -> Widget {
todo("createSwitch not implemented")
}
public func setIsActive(ofSwitch switchWidget: Widget, to active: Bool) {
todo("setIsActive not implemented")
}
public func setOnChange(ofSwitch switchWidget: Widget, to onChange: @escaping (Bool) -> Void) {
todo("setOnChange not implemented")
}

public func createTextView(content: String, shouldWrap: Bool) -> Widget {
todo("createTextView not implemented")
}
Expand Down
57 changes: 31 additions & 26 deletions Sources/SwiftCrossUI/Views/Toggle.swift
Original file line number Diff line number Diff line change
@@ -1,36 +1,41 @@
/// A control that is either on or off.
public struct Toggle: ElementaryView, View {
/// The label to show on the toggle button.
private var label: String
/// Whether the button is active or not.
private var active: Binding<Bool>
public struct Toggle: View {
/// The style of toggle shown.
var selectedToggleStyle: ToggleStyle
/// The label to be shown on or beside the toggle.
var label: String
/// Whether the toggle is active or not.
var active: Binding<Bool>

/// Creates a toggle button that displays a custom label.
/// Creates a toggle that displays a custom label.
public init(_ label: String, active: Binding<Bool>) {
self.selectedToggleStyle = .button
self.label = label
self.active = active
}

public func asWidget<Backend: AppBackend>(
backend: Backend
) -> Backend.Widget {
return backend.createToggle(
label: label,
active: active.wrappedValue,
onChange: { newValue in
self.active.wrappedValue = newValue
}
)
public var body: some View {
switch selectedToggleStyle {
case .switch:
HStack {
Text(label)
ToggleSwitch(active: active)
}
case .button:
ToggleButton(label, active: active)
}
}
}

public func update<Backend: AppBackend>(
_ widget: Backend.Widget,
backend: Backend
) {
backend.setLabel(ofButton: widget, to: label)
backend.setIsActive(ofToggle: widget, to: active.wrappedValue)
backend.setOnChange(ofToggle: widget) { newActiveState in
active.wrappedValue = newActiveState
}
extension Toggle {
/// Sets the style of the toggle.
public func toggleStyle(_ selectedToggleStyle: ToggleStyle) -> Toggle {
var toggle = self
toggle.selectedToggleStyle = selectedToggleStyle
return toggle
}
}

public enum ToggleStyle {
case `switch`
case button
}
Loading

0 comments on commit 6a053a7

Please sign in to comment.