diff --git a/app/os_macos.go b/app/os_macos.go index 43958204..aa337a96 100644 --- a/app/os_macos.go +++ b/app/os_macos.go @@ -11,6 +11,7 @@ import ( "runtime" "time" "unicode" + "unicode/utf8" "gioui.org/f32" "gioui.org/io/clipboard" @@ -154,6 +155,23 @@ static void raiseWindow(CFTypeRef windowRef) { [window makeKeyAndOrderFront:nil]; } +static CFTypeRef createInputContext(CFTypeRef clientRef) { + @autoreleasepool { + id client = (__bridge id)clientRef; + NSTextInputContext *ctx = [[NSTextInputContext alloc] initWithClient:client]; + return CFBridgingRetain(ctx); + } +} + +static void discardMarkedText(CFTypeRef viewRef) { + @autoreleasepool { + id view = (__bridge id)viewRef; + NSTextInputContext *ctx = [NSTextInputContext currentInputContext]; + if (view == [ctx client]) { + [ctx discardMarkedText]; + } + } +} */ import "C" @@ -344,7 +362,12 @@ func (w *window) SetCursor(name pointer.CursorName) { w.cursor = windowSetCursor(w.cursor, name) } -func (w *window) EditorStateChanged(old, new editorState) {} +func (w *window) EditorStateChanged(old, new editorState) { + if old != new { + C.discardMarkedText(w.view) + w.w.SetComposingRegion(key.Range{Start: -1, End: -1}) + } +} func (w *window) ShowTextInput(show bool) {} @@ -470,6 +493,133 @@ func gio_onChangeScreen(view C.CFTypeRef, did uint64) { w.displayLink.SetDisplayID(did) } +//export gio_hasMarkedText +func gio_hasMarkedText(view C.CFTypeRef) C.int { + w := mustView(view) + state := w.w.EditorState() + if state.compose.Start != -1 { + return 1 + } + return 0 +} + +//export gio_markedRange +func gio_markedRange(view C.CFTypeRef) C.NSRange { + w := mustView(view) + state := w.w.EditorState() + rng := state.compose + start, end := rng.Start, rng.End + if start == -1 { + return C.NSMakeRange(C.NSNotFound, 0) + } + u16start := state.UTF16Index(start) + return C.NSMakeRange( + C.NSUInteger(u16start), + C.NSUInteger(state.UTF16Index(end)-u16start), + ) +} + +//export gio_selectedRange +func gio_selectedRange(view C.CFTypeRef) C.NSRange { + w := mustView(view) + state := w.w.EditorState() + rng := state.Selection + start, end := rng.Start, rng.End + if start > end { + start, end = end, start + } + u16start := state.UTF16Index(start) + return C.NSMakeRange( + C.NSUInteger(u16start), + C.NSUInteger(state.UTF16Index(end)-u16start), + ) +} + +//export gio_unmarkText +func gio_unmarkText(view C.CFTypeRef) { + w := mustView(view) + w.w.SetComposingRegion(key.Range{Start: -1, End: -1}) +} + +//export gio_setMarkedText +func gio_setMarkedText(view, cstr C.CFTypeRef, selRange C.NSRange, replaceRange C.NSRange) { + w := mustView(view) + str := nsstringToString(cstr) + state := w.w.EditorState() + rng := state.compose + if rng.Start == -1 { + rng = state.Selection + } + if replaceRange.location != C.NSNotFound { + // replaceRange is relative to marked (or selected) text. + offset := state.UTF16Index(rng.Start) + start := state.RunesIndex(int(replaceRange.location) + offset) + end := state.RunesIndex(int(replaceRange.location+replaceRange.length) + offset) + rng = key.Range{ + Start: start, + End: end, + } + } + w.w.EditorReplace(rng, str) + comp := key.Range{ + Start: rng.Start, + End: rng.Start + utf8.RuneCountInString(str), + } + w.w.SetComposingRegion(comp) + + sel := key.Range{Start: comp.End, End: comp.End} + if selRange.location != C.NSNotFound { + // selRange is relative to inserted text. + offset := state.UTF16Index(rng.Start) + start := state.RunesIndex(int(selRange.location) + offset) + end := state.RunesIndex(int(selRange.location+selRange.length) + offset) + sel = key.Range{ + Start: start, + End: end, + } + } + w.w.SetEditorSelection(sel) +} + +//export gio_substringForProposedRange +func gio_substringForProposedRange(view C.CFTypeRef, rng C.NSRange, actual C.NSRangePointer) C.CFTypeRef { + w := mustView(view) + state := w.w.EditorState() + start, end := state.Snippet.Start, state.Snippet.End + if start > end { + start, end = end, start + } + w.w.SetEditorSnippet(key.Range{ + Start: state.RunesIndex(int(rng.location)), + End: state.RunesIndex(int(rng.location + rng.length)), + }) + u16start := state.UTF16Index(start) + actual.location = C.NSUInteger(u16start) + actual.length = C.NSUInteger(state.UTF16Index(end) - u16start) + return stringToNSString(state.Snippet.Text) +} + +//export gio_insertText +func gio_insertText(view, cstr C.CFTypeRef, crng C.NSRange) { + w := mustView(view) + state := w.w.EditorState() + rng := state.compose + if rng.Start == -1 { + rng = state.Selection + } + if crng.location != C.NSNotFound { + rng = key.Range{ + Start: state.RunesIndex(int(crng.location)), + End: state.RunesIndex(int(crng.location + crng.length)), + } + } + str := nsstringToString(cstr) + w.w.EditorReplace(rng, str) + w.w.SetComposingRegion(key.Range{Start: -1, End: -1}) + pos := rng.Start + utf8.RuneCountInString(str) + w.w.SetEditorSelection(key.Range{Start: pos, End: pos}) +} + func (w *window) draw() { w.scale = float32(C.getViewBackingScale(w.view)) wf, hf := float32(C.viewWidth(w.view)), float32(C.viewHeight(w.view)) @@ -592,7 +742,7 @@ func newWindow(win *callbacks, options []Option) error { func newOSWindow() (*window, error) { view := C.gio_createView() if view == 0 { - return nil, errors.New("CreateWindow: failed to create view") + return nil, errors.New("newOSWindows: failed to create view") } scale := float32(C.getViewBackingScale(view)) w := &window{ diff --git a/app/os_macos.m b/app/os_macos.m index 6b92abce..6ec2e8a0 100644 --- a/app/os_macos.m +++ b/app/os_macos.m @@ -64,7 +64,7 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo gio_onMouse((__bridge CFTypeRef)view, typ, [NSEvent pressedMouseButtons], p.x, height - p.y, dx, dy, [event timestamp], [event modifierFlags]); } -@interface GioView : NSView +@interface GioView : NSView @end @implementation GioView @@ -131,6 +131,58 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo // Don't pass commands up the responder chain. // They will end up in a beep. } + +- (BOOL)hasMarkedText { + int res = gio_hasMarkedText((__bridge CFTypeRef)self); + return res ? YES : NO; +} +- (NSRange)markedRange { + return gio_markedRange((__bridge CFTypeRef)self); +} +- (NSRange)selectedRange { + return gio_selectedRange((__bridge CFTypeRef)self); +} +- (void)unmarkText { + gio_unmarkText((__bridge CFTypeRef)self); +} +- (void)setMarkedText:(id)string + selectedRange:(NSRange)selRange + replacementRange:(NSRange)replaceRange { + NSString *str; + // string is either an NSAttributedString or an NSString. + if ([string isKindOfClass:[NSAttributedString class]]) { + str = [string string]; + } else { + str = string; + } + gio_setMarkedText((__bridge CFTypeRef)self, (__bridge CFTypeRef)str, selRange, replaceRange); +} +- (NSArray *)validAttributesForMarkedText { + return nil; +} +- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range + actualRange:(NSRangePointer)actualRange { + NSString *str = CFBridgingRelease(gio_substringForProposedRange((__bridge CFTypeRef)self, range, actualRange)); + return [[NSAttributedString alloc] initWithString:str attributes:nil]; +} +- (void)insertText:(id)string + replacementRange:(NSRange)replaceRange { + NSString *str; + // string is either an NSAttributedString or an NSString. + if ([string isKindOfClass:[NSAttributedString class]]) { + str = [string string]; + } else { + str = string; + } + gio_insertText((__bridge CFTypeRef)self, (__bridge CFTypeRef)str, replaceRange); +} +- (NSUInteger)characterIndexForPoint:(NSPoint)point { + return NSNotFound; +} +- (NSRect)firstRectForCharacterRange:(NSRange)range + actualRange:(NSRangePointer)actualRange { + return NSZeroRect; +} @end // Delegates are weakly referenced from their peers. Nothing diff --git a/app/window.go b/app/window.go index 6eaac58a..b544af13 100644 --- a/app/window.go +++ b/app/window.go @@ -149,6 +149,7 @@ func NewWindow(options ...Option) *Window { dead: make(chan struct{}), nocontext: cnf.CustomRenderer, } + w.imeState.compose = key.Range{Start: -1, End: -1} w.semantic.ids = make(map[router.SemanticID]router.SemanticNode) w.callbacks.w = w go w.run(options) @@ -521,8 +522,10 @@ func (e *editorState) Replace(r key.Range, text string) { } e.Selection.Start = adjust(e.Selection.Start) e.Selection.End = adjust(e.Selection.End) - e.compose.Start = adjust(e.compose.Start) - e.compose.End = adjust(e.compose.End) + if e.compose.Start != -1 { + e.compose.Start = adjust(e.compose.Start) + e.compose.End = adjust(e.compose.End) + } s := e.Snippet if r.End < s.Start || r.Start > s.End { // Replacement does not overlap snippet.