Skip to content

Commit

Permalink
Reenable and update Picker after adding support for multiple backends…
Browse files Browse the repository at this point in the history
… (it's so much cleaner now)
  • Loading branch information
stackotter committed Aug 22, 2023
1 parent 66d37b3 commit ca0765f
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 109 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ struct RandomNumberGeneratorApp: App {

Text("Choose a color:")
.padding(.top, 20)
// Picker(of: ColorOption.allCases, selection: state.$colorOption)
Picker(of: ColorOption.allCases, selection: state.$colorOption)
}
.padding(10)
.foregroundColor(state.colorOption?.color ?? .red)
Expand Down
87 changes: 87 additions & 0 deletions Sources/SwiftCrossUI/AppBackend.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CGtk
import Foundation

public protocol AppBackend {
Expand Down Expand Up @@ -89,6 +90,13 @@ public protocol AppBackend {
func setVisibleChild(ofOneOfContainer container: Widget, to child: Widget)

func createSplitView(leadingChild: Widget, trailingChild: Widget) -> Widget

func createPicker(
options: [String], selectedOption: Int?, onChange: @escaping (Int?) -> Void
) -> Widget
func setOptions(ofPicker picker: Widget, to options: [String])
func setSelectedOption(ofPicker picker: Widget, to selectedOption: Int?)
func setOnChange(ofPicker picker: Widget, to onChange: @escaping (Int?) -> Void)
}

public enum InheritedOrientation {
Expand Down Expand Up @@ -389,6 +397,85 @@ public struct GtkBackend: AppBackend {
widget.expandVertically = true
return widget
}

public func createPicker(
options: [String], selectedOption: Int?, onChange: @escaping (Int?) -> Void
) -> Widget {
let optionStrings = options.map({ "\($0)" })
let widget = GtkDropDown(strings: optionStrings)

let options = options
widget.notifySelected = { [weak widget] in
guard let widget = widget else {
return
}

if Int(widget.selected) >= options.count {
onChange(nil)
} else {
onChange(widget.selected)
}
}
return widget
}

public func setOptions(ofPicker picker: Widget, to options: [String]) {
let picker = picker as! GtkDropDown

// Check whether the options need to be updated or not (avoiding unnecessary updates is
// required to prevent an infinite loop caused by the onChange handler)
var hasChanged = false
for index in 0..<options.count {
guard
let item = gtk_string_list_get_string(picker.model, guint(index)),
String(cString: item) == options[index]
else {
hasChanged = true
break
}
}

// picker.model could be longer than options
if gtk_string_list_get_string(picker.model, guint(options.count)) != nil {
hasChanged = true
}

guard hasChanged else {
return
}

picker.model = gtk_string_list_new(
UnsafePointer(
options
.map({ UnsafePointer($0.unsafeUTF8Copy().baseAddress) })
.unsafeCopy()
.baseAddress
)
)
}

public func setSelectedOption(ofPicker picker: Widget, to selectedOption: Int?) {
let picker = picker as! GtkDropDown
if selectedOption != picker.selected {
picker.selected = selectedOption ?? Int(GTK_INVALID_LIST_POSITION)
}
}

public func setOnChange(ofPicker picker: GtkWidget, to onChange: @escaping (Int?) -> Void) {
(picker as! GtkDropDown).notifySelected = { [weak picker] in
guard let widget = picker else {
return
}

let picker = widget as! GtkDropDown

if Int(picker.selected) == Int(GTK_INVALID_LIST_POSITION) {
onChange(nil)
} else {
onChange(picker.selected)
}
}
}
}

extension AppBackend {
Expand Down
155 changes: 47 additions & 108 deletions Sources/SwiftCrossUI/Views/Picker.swift
Original file line number Diff line number Diff line change
@@ -1,108 +1,47 @@
// import CGtk

// // TODO: This Picker implementation should be able to be cleaned
// // up immensely. Especially once ViewContent and View are combined.

// // TODO: Move this elsewhere
// public class Box<T> {
// var wrappedValue: T

// init(_ value: T) {
// wrappedValue = value
// }
// }

// /// Custom view content storage is required to store the previous set
// /// of options to avoid infinite loops caused by repeatedly setting
// /// options. Every time the options change, the selected index gets
// /// updated, and then the view gets updated. If the view update causes
// /// the index to be updated every single time, then an infinite loop
// /// occurs. Using the view content as the children is possible as this
// /// is just a simple way to store persistent data in the view graph.
// public struct PickerViewContent: ViewContent, ViewGraphNodeChildren {
// public typealias Children = Self
// public typealias Content = Self

// var lastOptions: Box<[String]?>

// public var widgets: [AnyWidget] = []

// public init(from content: Content) {
// self = content
// }

// init<Backend: AppBackend>(lastOptions: [String]?, backend: Backend) {
// self.lastOptions = Box(lastOptions)
// }

// // Children will share the same Box as the content, so no need to update.
// public func update<Backend: AppBackend>(with content: Content, backend: Backend) {}
// }

// /// A picker view.
// public struct Picker<Value: Equatable>: View {
// public typealias Content = PickerViewContent

// public var body = PickerViewContent(lastOptions: nil)

// /// The string to be shown in the text view.
// private var options: [Value]
// /// Specifies whether the text should be wrapped if wider than its container.
// private var value: Binding<Value?>

// /// Creates a new text view with the given content.
// public init(of options: [Value], selection value: Binding<Value?>) {
// self.options = options
// self.value = value
// }

// public func asWidget(_ children: PickerViewContent.Children) -> GtkDropDown {
// // TODO: Figure out why it crashes when given an empty array of strings
// let optionStrings = options.map({ "\($0)" })
// let widget = GtkDropDown(strings: optionStrings)
// children.lastOptions.wrappedValue = optionStrings

// let options = options
// widget.notifySelected = { [weak widget] in
// guard let widget = widget else {
// return
// }

// if Int(widget.selected) >= options.count {
// self.value.wrappedValue = nil
// } else {
// self.value.wrappedValue = options[Int(widget.selected)]
// }
// }
// return widget
// }

// public func update(_ widget: GtkDropDown, children: PickerViewContent.Children) {
// let index: Int
// if let selectedOption = value.wrappedValue {
// index =
// options.firstIndex { option in
// return option == selectedOption
// } ?? Int(GTK_INVALID_LIST_POSITION)
// } else {
// index = Int(GTK_INVALID_LIST_POSITION)
// }

// if widget.selected != index {
// widget.selected = index
// }

// let options = options.map({ "\($0)" })
// if options != children.lastOptions.wrappedValue {
// widget.model = gtk_string_list_new(
// UnsafePointer(
// options
// .map({ UnsafePointer($0.unsafeUTF8Copy().baseAddress) })
// .unsafeCopy()
// .baseAddress
// )
// )
// children.lastOptions.wrappedValue = options
// }
// }
// }
import CGtk

/// A picker view.
public struct Picker<Value: Equatable>: View {
public var body = EmptyViewContent()

/// The string to be shown in the text view.
private var options: [Value]
/// Specifies whether the text should be wrapped if wider than its container.
private var value: Binding<Value?>

/// The index of the selected option (if any).
private var selectedOptionIndex: Int? {
return options.firstIndex { option in
return option == value.wrappedValue
}
}

/// Creates a new text view with the given content.
public init(of options: [Value], selection value: Binding<Value?>) {
self.options = options
self.value = value
}

public func asWidget<Backend: AppBackend>(
_ children: EmptyViewContent.Children,
backend: Backend
) -> Backend.Widget {
return backend.createPicker(
options: options.map { "\($0)" },
selectedOption: selectedOptionIndex
) { selectedIndex in
guard let selectedIndex = selectedIndex else {
value.wrappedValue = nil
return
}
value.wrappedValue = options[selectedIndex]
}
}

public func update<Backend: AppBackend>(
_ widget: Backend.Widget, children: EmptyViewContent.Children, backend: Backend
) {
backend.setOptions(ofPicker: widget, to: options.map { "\($0)" })
backend.setSelectedOption(ofPicker: widget, to: selectedOptionIndex)
}
}

0 comments on commit ca0765f

Please sign in to comment.