Skip to content

Commit 17443a2

Browse files
committed
Impl TextEditor (closes #149) & fix macOS text input cmds (cmd+a etc)
1 parent 181eace commit 17443a2

File tree

22 files changed

+1388
-133
lines changed

22 files changed

+1388
-133
lines changed

Examples/Sources/NotesExample/ContentView.swift

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ struct Note: Codable, Equatable, Identifiable {
3434
struct ContentView: View {
3535
let notesFile = URL(fileURLWithPath: "notes.json")
3636

37+
@Environment(\.colorScheme) var colorScheme
38+
39+
var textEditorBackground: Color {
40+
switch colorScheme {
41+
case .light:
42+
Color(0.8, 0.8, 0.8)
43+
case .dark:
44+
Color(0.18, 0.18, 0.18)
45+
}
46+
}
47+
3748
@State var notes: [Note] = [
3849
Note(title: "Hello, world!", content: "Welcome SwiftCrossNotes!"),
3950
Note(
@@ -118,25 +129,22 @@ struct ContentView: View {
118129
}
119130
}
120131
} detail: {
121-
VStack(alignment: .leading) {
122-
if let selectedNote = selectedNote {
123-
VStack(alignment: .leading, spacing: 4) {
124-
Text("Title")
125-
TextField("Title", text: selectedNote.title)
126-
}
132+
ScrollView {
133+
VStack(alignment: .center) {
134+
if let selectedNote = selectedNote {
135+
HStack(spacing: 4) {
136+
Text("Title")
137+
TextField("Title", text: selectedNote.title)
138+
}
127139

128-
VStack(alignment: .leading, spacing: 4) {
129-
Text("Content")
130-
TextField("Content", text: selectedNote.content)
140+
TextEditor(text: selectedNote.content)
141+
.padding()
142+
.background(textEditorBackground)
143+
.cornerRadius(4)
131144
}
132-
} else {
133-
Text("Select a note...")
134145
}
146+
.padding()
135147
}
136-
.frame(maxWidth: 400)
137-
.padding(10)
138-
139-
Spacer()
140148
}
141149
}
142150
}

Examples/Sources/WebViewExample/WebViewApp.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import DefaultBackend
12
import Foundation
23
import SwiftCrossUI
3-
import DefaultBackend
44

55
#if canImport(SwiftBundlerRuntime)
66
import SwiftBundlerRuntime
@@ -21,7 +21,7 @@ struct WebViewApp: App {
2121
TextField("URL", text: $urlInput)
2222
Button("Go") {
2323
guard let url = URL(string: urlInput) else {
24-
return // disabled
24+
return // disabled
2525
}
2626

2727
self.url = url

Package.resolved

Lines changed: 12 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/AppKitBackend/AppKitBackend.swift

Lines changed: 126 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public final class AppKitBackend: AppBackend {
6464
backing: .buffered,
6565
defer: true
6666
)
67-
window.delegate = window.resizeDelegate
67+
window.delegate = window.customDelegate
6868

6969
return window
7070
}
@@ -94,7 +94,7 @@ public final class AppKitBackend: AppBackend {
9494
ofWindow window: Window,
9595
to action: @escaping (SIMD2<Int>) -> Void
9696
) {
97-
window.resizeDelegate.setHandler(action)
97+
window.customDelegate.setHandler(action)
9898
}
9999

100100
public func setTitle(ofWindow window: Window, to title: String) {
@@ -176,6 +176,9 @@ public final class AppKitBackend: AppBackend {
176176
let about = NSMenuItem()
177177
about.submenu = createDefaultAboutMenu()
178178
menuBar.addItem(about)
179+
let edit = NSMenuItem()
180+
edit.submenu = createDefaultEditMenu()
181+
menuBar.addItem(edit)
179182

180183
var helpMenu: NSMenu?
181184
for submenu in submenus {
@@ -230,6 +233,55 @@ public final class AppKitBackend: AppBackend {
230233
return appMenu
231234
}
232235

236+
public static func createDefaultEditMenu() -> NSMenu {
237+
let appMenu = NSMenu(title: "Edit")
238+
let undoItem = appMenu.addItem(
239+
withTitle: "Undo",
240+
action: "undo:",
241+
keyEquivalent: "z"
242+
)
243+
undoItem.keyEquivalentModifierMask = .command
244+
245+
let redoItem = appMenu.addItem(
246+
withTitle: "Redo",
247+
action: "redo:",
248+
keyEquivalent: "z"
249+
)
250+
redoItem.keyEquivalentModifierMask = [.command, .shift]
251+
252+
appMenu.addItem(NSMenuItem.separator())
253+
254+
let cutItem = appMenu.addItem(
255+
withTitle: "Cut",
256+
action: "cut:",
257+
keyEquivalent: "x"
258+
)
259+
cutItem.keyEquivalentModifierMask = .command
260+
261+
let copyItem = appMenu.addItem(
262+
withTitle: "Copy",
263+
action: "copy:",
264+
keyEquivalent: "c"
265+
)
266+
copyItem.keyEquivalentModifierMask = .command
267+
268+
let pasteItem = appMenu.addItem(
269+
withTitle: "Paste",
270+
action: "paste:",
271+
keyEquivalent: "v"
272+
)
273+
pasteItem.keyEquivalentModifierMask = .command
274+
275+
let selectAllItem = appMenu.addItem(
276+
withTitle: "Select all",
277+
action: "selectAll:",
278+
keyEquivalent: "a"
279+
)
280+
selectAllItem.keyEquivalentModifierMask = .command
281+
282+
return appMenu
283+
}
284+
233285
public func setApplicationMenu(_ submenus: [ResolvedMenu.Submenu]) {
234286
let (menuBar, helpMenu) = Self.renderMenuBar(submenus)
235287
NSApplication.shared.mainMenu = menuBar
@@ -448,7 +500,7 @@ public final class AppKitBackend: AppBackend {
448500

449501
public func size(
450502
of text: String,
451-
whenDisplayedIn textView: Widget,
503+
whenDisplayedIn widget: Widget,
452504
proposedFrame: SIMD2<Int>?,
453505
environment: EnvironmentValues
454506
) -> SIMD2<Int> {
@@ -458,7 +510,7 @@ public final class AppKitBackend: AppBackend {
458510
// reaches zero width.
459511
let size = size(
460512
of: text,
461-
whenDisplayedIn: textView,
513+
whenDisplayedIn: widget,
462514
proposedFrame: SIMD2(1, proposedFrame.y),
463515
environment: environment
464516
)
@@ -676,7 +728,9 @@ public final class AppKitBackend: AppBackend {
676728
textField.isEnabled = environment.isEnabled
677729
textField.placeholderString = placeholder
678730
textField.appearance = environment.colorScheme.nsAppearance
679-
textField.font = Self.font(for: environment)
731+
if textField.font != Self.font(for: environment) {
732+
textField.font = Self.font(for: environment)
733+
}
680734
textField.onEdit = { textField in
681735
onChange(textField.stringValue)
682736
}
@@ -709,6 +763,58 @@ public final class AppKitBackend: AppBackend {
709763
textField.stringValue = content
710764
}
711765

766+
public func createTextEditor() -> Widget {
767+
let textEditor = NSObservableTextView()
768+
textEditor.drawsBackground = false
769+
textEditor.delegate = textEditor
770+
textEditor.allowsUndo = true
771+
textEditor.textContainerInset = .zero
772+
textEditor.textContainer?.lineFragmentPadding = 0
773+
return textEditor
774+
}
775+
776+
public func updateTextEditor(
777+
_ textEditor: Widget,
778+
environment: EnvironmentValues,
779+
onChange: @escaping (String) -> Void
780+
) {
781+
let textEditor = textEditor as! NSObservableTextView
782+
textEditor.onEdit = { textView in
783+
onChange(self.getContent(ofTextEditor: textView))
784+
}
785+
if textEditor.font != Self.font(for: environment) {
786+
textEditor.font = Self.font(for: environment)
787+
}
788+
textEditor.appearance = environment.colorScheme.nsAppearance
789+
textEditor.isEditable = environment.isEnabled
790+
791+
if #available(macOS 14, *) {
792+
textEditor.contentType =
793+
switch environment.textContentType {
794+
case .url:
795+
.URL
796+
case .phoneNumber:
797+
.telephoneNumber
798+
case .name:
799+
.name
800+
case .emailAddress:
801+
.emailAddress
802+
case .text, .digits(_), .decimal(_):
803+
nil
804+
}
805+
}
806+
}
807+
808+
public func setContent(ofTextEditor textEditor: Widget, to content: String) {
809+
let textEditor = textEditor as! NSObservableTextView
810+
textEditor.string = content
811+
}
812+
813+
public func getContent(ofTextEditor textEditor: Widget) -> String {
814+
let textEditor = textEditor as! NSObservableTextView
815+
return textEditor.string
816+
}
817+
712818
public func createScrollContainer(for child: Widget) -> Widget {
713819
let scrollView = NSScrollView()
714820

@@ -1756,6 +1862,14 @@ class NSObservableTextField: NSTextField {
17561862
}
17571863
}
17581864

1865+
class NSObservableTextView: NSTextView, NSTextViewDelegate {
1866+
func textDidChange(_ notification: Notification) {
1867+
onEdit?(self)
1868+
}
1869+
1870+
var onEdit: ((NSTextView) -> Void)?
1871+
}
1872+
17591873
// Source: https://gist.github.com/sindresorhus/3580ce9426fff8fafb1677341fca4815
17601874
extension NSControl {
17611875
typealias ActionClosure = ((NSControl) -> Void)
@@ -1870,7 +1984,8 @@ class NSSplitViewResizingDelegate: NSObject, NSSplitViewDelegate {
18701984
}
18711985

18721986
public class NSCustomWindow: NSWindow {
1873-
var resizeDelegate = ResizeDelegate()
1987+
var customDelegate = Delegate()
1988+
var persistentUndoManager = UndoManager()
18741989

18751990
/// Allows the backing scale factor to be overridden. Useful for keeping
18761991
/// UI tests consistent across devices.
@@ -1882,7 +1997,7 @@ public class NSCustomWindow: NSWindow {
18821997
backingScaleFactorOverride ?? super.backingScaleFactor
18831998
}
18841999

1885-
class ResizeDelegate: NSObject, NSWindowDelegate {
2000+
class Delegate: NSObject, NSWindowDelegate {
18862001
var resizeHandler: ((SIMD2<Int>) -> Void)?
18872002

18882003
func setHandler(_ resizeHandler: @escaping (SIMD2<Int>) -> Void) {
@@ -1907,6 +2022,10 @@ public class NSCustomWindow: NSWindow {
19072022

19082023
return frameSize
19092024
}
2025+
2026+
func windowWillReturnUndoManager(_ window: NSWindow) -> UndoManager? {
2027+
(window as! NSCustomWindow).persistentUndoManager
2028+
}
19102029
}
19112030
}
19122031

Sources/Gtk/Generated/Image.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,9 @@ open class Image: Widget {
241241
/// The symbolic size to display icons at.
242242
@GObjectProperty(named: "icon-size") public var iconSize: IconSize
243243

244+
/// The `GdkPaintable` to display.
245+
@GObjectProperty(named: "paintable") public var paintable: OpaquePointer?
246+
244247
/// The size in pixels to display icons at.
245248
///
246249
/// If set to a value != -1, this property overrides the

Sources/Gtk/Generated/Picture.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ open class Picture: Widget {
170170
/// ratio.
171171
@GObjectProperty(named: "keep-aspect-ratio") public var keepAspectRatio: Bool
172172

173+
/// The `GdkPaintable` to be displayed by this `GtkPicture`.
174+
@GObjectProperty(named: "paintable") public var paintable: OpaquePointer?
175+
173176
public var notifyAlternativeText: ((Picture, OpaquePointer) -> Void)?
174177

175178
public var notifyCanShrink: ((Picture, OpaquePointer) -> Void)?

Sources/Gtk/Utility/Pango.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ public class Pango {
1717
/// acts as a suggested width. The text will attempt to take up less than or equal to the proposed
1818
/// width but if the text wrapping strategy doesn't allow the text to become as small as required
1919
/// than it may take up more the proposed width.
20+
///
21+
/// Uses the `PANGO_WRAP_WORD_CHAR` text wrapping mode.
2022
public func getTextSize(
2123
_ text: String,
2224
proposedWidth: Double? = nil,

0 commit comments

Comments
 (0)