@@ -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
17601874extension NSControl {
17611875 typealias ActionClosure = ( ( NSControl ) -> Void )
@@ -1870,7 +1984,8 @@ class NSSplitViewResizingDelegate: NSObject, NSSplitViewDelegate {
18701984}
18711985
18721986public 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
0 commit comments