From 049b5b54f885d6679896dc2c66accae4ad3b963d Mon Sep 17 00:00:00 2001 From: david-swift Date: Sat, 9 Aug 2025 10:48:04 +0200 Subject: [PATCH] Add support for text editors --- Sources/Core/View/TextEditor.swift | 105 +++++++++++++++++++++++++++++ Sources/Demo/Page.swift | 7 ++ Sources/Demo/TextEditorDemo.swift | 38 +++++++++++ 3 files changed, 150 insertions(+) create mode 100644 Sources/Core/View/TextEditor.swift create mode 100644 Sources/Demo/TextEditorDemo.swift diff --git a/Sources/Core/View/TextEditor.swift b/Sources/Core/View/TextEditor.swift new file mode 100644 index 0000000..28a5571 --- /dev/null +++ b/Sources/Core/View/TextEditor.swift @@ -0,0 +1,105 @@ +// +// TextEditor.swift +// Adwaita +// +// Created by david-swift on 09.08.25. +// + +import CAdw + +/// A text editor widget. +public typealias TextEditor = TextView + +/// A text editor widget. +public struct TextView: AdwaitaWidget { + + /// The editor's content. + @Binding var text: String + /// The padding between the border and the content. + var padding = 0 + /// The edges affected by the padding. + var paddingEdges: Set = [] + + /// Initialize a text editor. + /// - Parameter text: The editor's content. + public init(text: Binding) { + 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 + } + + /// 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) + } + } + } + + /// 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. + /// - edges: The affected edges. + /// - Returns: The editor. + public func innerPadding(_ padding: Int = 10, edges: Set = .all) -> Self { + var newSelf = self + newSelf.padding = padding + newSelf.paddingEdges = edges + return newSelf + } + +} diff --git a/Sources/Demo/Page.swift b/Sources/Demo/Page.swift index 61f3807..1654bd8 100644 --- a/Sources/Demo/Page.swift +++ b/Sources/Demo/Page.swift @@ -32,6 +32,7 @@ enum Page: String, Identifiable, CaseIterable, Codable, CustomStringConvertible case picture case idle case fixed + case textEditor var id: Self { self @@ -49,6 +50,8 @@ enum Page: String, Identifiable, CaseIterable, Codable, CustomStringConvertible return "Alert Dialog" case .passwordChecker: return "Password Checker" + case .textEditor: + return "Text Editor" default: return rawValue.capitalized } @@ -105,6 +108,8 @@ enum Page: String, Identifiable, CaseIterable, Codable, CustomStringConvertible return "Update UI from an asynchronous context" case .fixed: return "Place widgets in a coordinate system" + case .textEditor: + return "A simple text editor" } } @@ -152,6 +157,8 @@ enum Page: String, Identifiable, CaseIterable, Codable, CustomStringConvertible IdleDemo() case .fixed: FixedDemo() + case .textEditor: + TextEditorDemo() } } // swiftlint:enable cyclomatic_complexity diff --git a/Sources/Demo/TextEditorDemo.swift b/Sources/Demo/TextEditorDemo.swift new file mode 100644 index 0000000..d2572b7 --- /dev/null +++ b/Sources/Demo/TextEditorDemo.swift @@ -0,0 +1,38 @@ +// +// TextEditorDemo.swift +// Adwaita +// +// Created by david-swift on 09.08.25. +// + +// swiftlint:disable missing_docs + +import Adwaita +import Foundation + +struct TextEditorDemo: View { + + @State private var text = "Hello, world!" + + var view: Body { + VStack(spacing: 20) { + TextEditor(text: $text) + .innerPadding(20) + .frame(minHeight: 60) + .card() + VStack { + Text(text) + .selectable() + .wrap() + .hexpand() + .padding(20) + .halign(.fill) + } + .card() + } + .frame(maxWidth: 500) + } + +} + +// swiftlint:enable missing_docs