diff --git a/Sources/Core/Model/Enumerations/WrapMode.swift b/Sources/Core/Model/Enumerations/WrapMode.swift new file mode 100644 index 0000000..8abb991 --- /dev/null +++ b/Sources/Core/Model/Enumerations/WrapMode.swift @@ -0,0 +1,43 @@ +// +// WrapMode.swift +// Adwaita +// +// Created by mlm on 24.08.25. +// + +import CAdw + +/// Wrap modes for `TextView`/`TextEditor` +public enum WrapMode: GtkWrapMode, RawRepresentable { + + // swiftlint:disable discouraged_none_name + /// GTK_WRAP_NONE + case none + // swiftlint:enable discouraged_none_name + /// GTK_WRAP_CHAR + case char + /// GTK_WRAP_WORD + case word + /// GTK_WRAP_WORD_CHAR + case wordChar + + /// Get the GtkWrapMode. + public var rawValue: GtkWrapMode { + switch self { + case .none: + GTK_WRAP_NONE + case .char: + GTK_WRAP_CHAR + case .word: + GTK_WRAP_WORD + case .wordChar: + GTK_WRAP_WORD_CHAR + } + } + + /// Initialize from the GtkWrapMode. + /// - Parameter rawValue: The GtkWrapMode. + public init?(rawValue: GtkWrapMode) { + nil + } +} diff --git a/Sources/Core/Model/Extensions/GtkWrapMode.swift b/Sources/Core/Model/Extensions/GtkWrapMode.swift new file mode 100644 index 0000000..b99bf11 --- /dev/null +++ b/Sources/Core/Model/Extensions/GtkWrapMode.swift @@ -0,0 +1,19 @@ +// +// GtkWrapMode.swift +// Adwaita +// +// Created by mlm on 24.08.25. +// + +import CAdw + +/// Add ExpressibleByIntegerLiteral conformance to make GtkWrapMode usable as +/// a RawValue in an enum. +extension GtkWrapMode: @retroactive ExpressibleByIntegerLiteral { + + /// Initialize from an integer literal. + public init(integerLiteral value: Int) { + self.init(UInt32(value)) + } + +} diff --git a/Sources/Core/View/TextEditor.swift b/Sources/Core/View/TextEditor.swift index 28a5571..e848e74 100644 --- a/Sources/Core/View/TextEditor.swift +++ b/Sources/Core/View/TextEditor.swift @@ -14,73 +14,75 @@ public typealias TextEditor = TextView public struct TextView: AdwaitaWidget { /// The editor's content. - @Binding var text: String + @BindingProperty( + observe: { _, text, storage in + if let buffer = storage.content["buffer"]?.first { + buffer.connectSignal(name: "changed") { + let currentText = Self.getText(buffer: buffer) + if text.wrappedValue != currentText { + text.wrappedValue = currentText + } + } + } + }, + set: { _, text, storage in + if let buffer = storage.content["buffer"]?.first, Self.getText(buffer: buffer) != text { + gtk_text_buffer_set_text(buffer.opaquePointer?.cast(), text, -1) + } + }, + pointer: Any.self + ) + var text: Binding = .constant("") /// The padding between the border and the content. - var padding = 0 - /// The edges affected by the padding. - var paddingEdges: Set = [] + @Property( + set: { $1.set($0) }, + pointer: OpaquePointer.self + ) + var padding: InnerPadding? + /// The (word) wrap mode used when rendering the text. + @Property( + set: { gtk_text_view_set_wrap_mode($0.cast(), $1.rawValue) }, + pointer: OpaquePointer.self + ) + var wrapMode: WrapMode = .none /// Initialize a text editor. /// - Parameter text: The editor's content. public init(text: Binding) { - self._text = text + self.text = text } - /// Get the editor's view storage. - /// - Parameters: - /// - data: The widget data. - /// - type: The view render data type. - /// - Returns: The view storage. - public func container(data: WidgetData, type: Data.Type) -> ViewStorage { - let buffer = ViewStorage(gtk_text_buffer_new(nil)?.opaque()) - let editor = ViewStorage( - gtk_text_view_new_with_buffer(buffer.opaquePointer?.cast())?.opaque(), - content: ["buffer": [buffer]] - ) - update(editor, data: data, updateProperties: true, type: type) - return editor - } + /// The inner padding of a text view. + struct InnerPadding { - /// Update a view storage to the editor. - /// - Parameters: - /// - storage: The view storage. - /// - data: The widget data. - /// - updateProperties: Whether to update the view's properties. - /// - type: The view render data type. - public func update(_ storage: ViewStorage, data: WidgetData, updateProperties: Bool, type: Data.Type) { - if let buffer = storage.content["buffer"]?.first { - buffer.connectSignal(name: "changed") { - let text = getText(buffer: buffer) - if self.text != text { - self.text = text - } - } - if updateProperties { - if getText(buffer: buffer) != self.text { - gtk_text_buffer_set_text(buffer.opaquePointer?.cast(), text, -1) - } - } - } - if updateProperties { + /// The padding. + var padding: Int + /// The affected edges. + var paddingEdges: Set + + /// Set the inner padding on a text view. + /// - Parameter pointer: The text view. + func set(_ pointer: OpaquePointer) { if paddingEdges.contains(.top) { - gtk_text_view_set_top_margin(storage.opaquePointer?.cast(), padding.cInt) + gtk_text_view_set_top_margin(pointer.cast(), padding.cInt) } if paddingEdges.contains(.bottom) { - gtk_text_view_set_bottom_margin(storage.opaquePointer?.cast(), padding.cInt) + gtk_text_view_set_bottom_margin(pointer.cast(), padding.cInt) } if paddingEdges.contains(.leading) { - gtk_text_view_set_left_margin(storage.opaquePointer?.cast(), padding.cInt) + gtk_text_view_set_left_margin(pointer.cast(), padding.cInt) } if paddingEdges.contains(.trailing) { - gtk_text_view_set_right_margin(storage.opaquePointer?.cast(), padding.cInt) + gtk_text_view_set_right_margin(pointer.cast(), padding.cInt) } } + } /// Get the text view's content. /// - Parameter buffer: The text view's buffer. /// - Returns: The content. - func getText(buffer: ViewStorage) -> String { + static func getText(buffer: ViewStorage) -> String { let startIter: UnsafeMutablePointer = .allocate(capacity: 1) let endIter: UnsafeMutablePointer = .allocate(capacity: 1) gtk_text_buffer_get_start_iter(buffer.opaquePointer?.cast(), startIter) @@ -90,6 +92,34 @@ public struct TextView: AdwaitaWidget { ) } + /// Get the editor's view storage. + /// - Parameters: + /// - data: The widget data. + /// - type: The view render data type. + /// - Returns: The view storage. + public func container(data: WidgetData, type: Data.Type) -> ViewStorage where Data: ViewRenderData { + let buffer = ViewStorage(gtk_text_buffer_new(nil)?.opaque()) + let editor = ViewStorage( + gtk_text_view_new_with_buffer(buffer.opaquePointer?.cast())?.opaque(), + content: ["buffer": [buffer]] + ) + initProperties(editor, data: data, type: type) + update(editor, data: data, updateProperties: true, type: type) + return editor + } + + /// Set the wrapMode for the text view. + /// + /// Available wrap modes are `none`, `char`, `word`, `wordChar`. Please refer to the + /// corresponding `GtkWrapMode` documentation for the details on how they work. + /// + /// - Parameter mode: The `WrapMode` to set. + public func wrapMode(_ mode: WrapMode) -> Self { + var newSelf = self + newSelf.wrapMode = mode + return newSelf + } + /// Add padding between the editor's content and border. /// - Parameters: /// - padding: The padding's value. @@ -97,8 +127,7 @@ public struct TextView: AdwaitaWidget { /// - Returns: The editor. public func innerPadding(_ padding: Int = 10, edges: Set = .all) -> Self { var newSelf = self - newSelf.padding = padding - newSelf.paddingEdges = edges + newSelf.padding = .init(padding: padding, paddingEdges: edges) return newSelf }