From 0af386405855448d0da653b7bd9251a04a4167d5 Mon Sep 17 00:00:00 2001 From: ml Date: Sun, 24 Aug 2025 17:21:16 +0200 Subject: [PATCH 1/3] Add WrapMode to TextView/TextEditor --- .../Core/Model/Enumerations/WrapMode.swift | 22 +++++++++++++++++++ .../Core/Model/Extensions/GtkWrapMode.swift | 9 ++++++++ Sources/Core/View/TextEditor.swift | 16 ++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 Sources/Core/Model/Enumerations/WrapMode.swift create mode 100644 Sources/Core/Model/Extensions/GtkWrapMode.swift diff --git a/Sources/Core/Model/Enumerations/WrapMode.swift b/Sources/Core/Model/Enumerations/WrapMode.swift new file mode 100644 index 0000000..f643afc --- /dev/null +++ b/Sources/Core/Model/Enumerations/WrapMode.swift @@ -0,0 +1,22 @@ +import CAdw + +/// Wrap modes for `TextView`/`TextEditor` +public enum WrapMode: GtkWrapMode, RawRepresentable { + case none // GTK_WRAP_NONE + case char // GTK_WRAP_CHAR + case word // GTK_WRAP_WORD + case wordChar // GTK_WRAP_WORD_CHAR + + 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 + } + } + + public init?(rawValue: GtkWrapMode) { + return nil + } +} diff --git a/Sources/Core/Model/Extensions/GtkWrapMode.swift b/Sources/Core/Model/Extensions/GtkWrapMode.swift new file mode 100644 index 0000000..9aeb0d1 --- /dev/null +++ b/Sources/Core/Model/Extensions/GtkWrapMode.swift @@ -0,0 +1,9 @@ +import CAdw + +/// Add ExpressibleByIntegerLiteral conformance to make GtkWrapMode usable as +// a RawValue in an enum. +extension GtkWrapMode: @retroactive ExpressibleByIntegerLiteral { + 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..91ad7db 100644 --- a/Sources/Core/View/TextEditor.swift +++ b/Sources/Core/View/TextEditor.swift @@ -19,6 +19,8 @@ public struct TextView: AdwaitaWidget { var padding = 0 /// The edges affected by the padding. var paddingEdges: Set = [] + /// The (word) wrap mode used when rendering the text. + var wrapMode: WrapMode = .none /// Initialize a text editor. /// - Parameter text: The editor's content. @@ -74,9 +76,23 @@ public struct TextView: AdwaitaWidget { if paddingEdges.contains(.trailing) { gtk_text_view_set_right_margin(storage.opaquePointer?.cast(), padding.cInt) } + // update the wrapping mode + gtk_text_view_set_wrap_mode(storage.opaquePointer?.cast(), wrapMode.rawValue) } } + /// 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 + } + /// Get the text view's content. /// - Parameter buffer: The text view's buffer. /// - Returns: The content. From 87b970d84483582998b3c449562c86332a8da5b1 Mon Sep 17 00:00:00 2001 From: david-swift Date: Tue, 26 Aug 2025 13:33:43 +0200 Subject: [PATCH 2/3] Fix code style --- .../Core/Model/Enumerations/WrapMode.swift | 49 +++++++++++++------ .../Core/Model/Extensions/GtkWrapMode.swift | 18 +++++-- Sources/Core/View/TextEditor.swift | 12 ++--- 3 files changed, 55 insertions(+), 24 deletions(-) diff --git a/Sources/Core/Model/Enumerations/WrapMode.swift b/Sources/Core/Model/Enumerations/WrapMode.swift index f643afc..8abb991 100644 --- a/Sources/Core/Model/Enumerations/WrapMode.swift +++ b/Sources/Core/Model/Enumerations/WrapMode.swift @@ -1,22 +1,43 @@ +// +// WrapMode.swift +// Adwaita +// +// Created by mlm on 24.08.25. +// + import CAdw /// Wrap modes for `TextView`/`TextEditor` public enum WrapMode: GtkWrapMode, RawRepresentable { - case none // GTK_WRAP_NONE - case char // GTK_WRAP_CHAR - case word // GTK_WRAP_WORD - case wordChar // GTK_WRAP_WORD_CHAR - 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 + // 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 + } } - } - public init?(rawValue: GtkWrapMode) { - return nil - } + /// 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 index 9aeb0d1..b99bf11 100644 --- a/Sources/Core/Model/Extensions/GtkWrapMode.swift +++ b/Sources/Core/Model/Extensions/GtkWrapMode.swift @@ -1,9 +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. +/// a RawValue in an enum. extension GtkWrapMode: @retroactive ExpressibleByIntegerLiteral { - public init(integerLiteral value: Int) { - self.init(UInt32(value)) - } + + /// 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 91ad7db..ca6d295 100644 --- a/Sources/Core/View/TextEditor.swift +++ b/Sources/Core/View/TextEditor.swift @@ -81,12 +81,12 @@ public struct TextView: AdwaitaWidget { } } - /// 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. + /// 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 From 4eb58b154c5d04fb44b1904420273ff721766a75 Mon Sep 17 00:00:00 2001 From: david-swift Date: Wed, 27 Aug 2025 13:27:36 +0200 Subject: [PATCH 3/3] Use more declarative approach for text editor --- Sources/Core/View/TextEditor.swift | 131 ++++++++++++++++------------- 1 file changed, 72 insertions(+), 59 deletions(-) diff --git a/Sources/Core/View/TextEditor.swift b/Sources/Core/View/TextEditor.swift index ca6d295..e848e74 100644 --- a/Sources/Core/View/TextEditor.swift +++ b/Sources/Core/View/TextEditor.swift @@ -14,18 +14,82 @@ 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 + } + + /// The inner padding of a text view. + struct InnerPadding { + + /// 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(pointer.cast(), padding.cInt) + } + if paddingEdges.contains(.bottom) { + gtk_text_view_set_bottom_margin(pointer.cast(), padding.cInt) + } + if paddingEdges.contains(.leading) { + gtk_text_view_set_left_margin(pointer.cast(), padding.cInt) + } + if paddingEdges.contains(.trailing) { + 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. + 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) + gtk_text_buffer_get_end_iter(buffer.opaquePointer?.cast(), endIter) + return .init( + cString: gtk_text_buffer_get_text(buffer.opaquePointer?.cast(), startIter, endIter, true.cBool) + ) } /// Get the editor's view storage. @@ -33,54 +97,17 @@ public struct TextView: AdwaitaWidget { /// - data: The widget data. /// - type: The view render data type. /// - Returns: The view storage. - public func container(data: WidgetData, type: Data.Type) -> ViewStorage { + 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 } - /// 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 { - if paddingEdges.contains(.top) { - gtk_text_view_set_top_margin(storage.opaquePointer?.cast(), padding.cInt) - } - if paddingEdges.contains(.bottom) { - gtk_text_view_set_bottom_margin(storage.opaquePointer?.cast(), padding.cInt) - } - if paddingEdges.contains(.leading) { - gtk_text_view_set_left_margin(storage.opaquePointer?.cast(), padding.cInt) - } - if paddingEdges.contains(.trailing) { - gtk_text_view_set_right_margin(storage.opaquePointer?.cast(), padding.cInt) - } - // update the wrapping mode - gtk_text_view_set_wrap_mode(storage.opaquePointer?.cast(), wrapMode.rawValue) - } - } - /// Set the wrapMode for the text view. /// /// Available wrap modes are `none`, `char`, `word`, `wordChar`. Please refer to the @@ -93,19 +120,6 @@ public struct TextView: AdwaitaWidget { return newSelf } - /// Get the text view's content. - /// - Parameter buffer: The text view's buffer. - /// - Returns: The content. - 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) - gtk_text_buffer_get_end_iter(buffer.opaquePointer?.cast(), endIter) - return .init( - cString: gtk_text_buffer_get_text(buffer.opaquePointer?.cast(), startIter, endIter, true.cBool) - ) - } - /// Add padding between the editor's content and border. /// - Parameters: /// - padding: The padding's value. @@ -113,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 }