mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-06 09:55:40 +00:00
app: [macOS] implement IME support
Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
+152
-2
@@ -11,6 +11,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
"gioui.org/io/clipboard"
|
"gioui.org/io/clipboard"
|
||||||
@@ -154,6 +155,23 @@ static void raiseWindow(CFTypeRef windowRef) {
|
|||||||
[window makeKeyAndOrderFront:nil];
|
[window makeKeyAndOrderFront:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static CFTypeRef createInputContext(CFTypeRef clientRef) {
|
||||||
|
@autoreleasepool {
|
||||||
|
id<NSTextInputClient> client = (__bridge id<NSTextInputClient>)clientRef;
|
||||||
|
NSTextInputContext *ctx = [[NSTextInputContext alloc] initWithClient:client];
|
||||||
|
return CFBridgingRetain(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void discardMarkedText(CFTypeRef viewRef) {
|
||||||
|
@autoreleasepool {
|
||||||
|
id<NSTextInputClient> view = (__bridge id<NSTextInputClient>)viewRef;
|
||||||
|
NSTextInputContext *ctx = [NSTextInputContext currentInputContext];
|
||||||
|
if (view == [ctx client]) {
|
||||||
|
[ctx discardMarkedText];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
@@ -344,7 +362,12 @@ func (w *window) SetCursor(name pointer.CursorName) {
|
|||||||
w.cursor = windowSetCursor(w.cursor, name)
|
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) {}
|
func (w *window) ShowTextInput(show bool) {}
|
||||||
|
|
||||||
@@ -470,6 +493,133 @@ func gio_onChangeScreen(view C.CFTypeRef, did uint64) {
|
|||||||
w.displayLink.SetDisplayID(did)
|
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() {
|
func (w *window) draw() {
|
||||||
w.scale = float32(C.getViewBackingScale(w.view))
|
w.scale = float32(C.getViewBackingScale(w.view))
|
||||||
wf, hf := float32(C.viewWidth(w.view)), float32(C.viewHeight(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) {
|
func newOSWindow() (*window, error) {
|
||||||
view := C.gio_createView()
|
view := C.gio_createView()
|
||||||
if view == 0 {
|
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))
|
scale := float32(C.getViewBackingScale(view))
|
||||||
w := &window{
|
w := &window{
|
||||||
|
|||||||
+53
-1
@@ -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]);
|
gio_onMouse((__bridge CFTypeRef)view, typ, [NSEvent pressedMouseButtons], p.x, height - p.y, dx, dy, [event timestamp], [event modifierFlags]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@interface GioView : NSView <CALayerDelegate>
|
@interface GioView : NSView <CALayerDelegate,NSTextInputClient>
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation GioView
|
@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.
|
// Don't pass commands up the responder chain.
|
||||||
// They will end up in a beep.
|
// 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<NSAttributedStringKey> *)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
|
@end
|
||||||
|
|
||||||
// Delegates are weakly referenced from their peers. Nothing
|
// Delegates are weakly referenced from their peers. Nothing
|
||||||
|
|||||||
+5
-2
@@ -149,6 +149,7 @@ func NewWindow(options ...Option) *Window {
|
|||||||
dead: make(chan struct{}),
|
dead: make(chan struct{}),
|
||||||
nocontext: cnf.CustomRenderer,
|
nocontext: cnf.CustomRenderer,
|
||||||
}
|
}
|
||||||
|
w.imeState.compose = key.Range{Start: -1, End: -1}
|
||||||
w.semantic.ids = make(map[router.SemanticID]router.SemanticNode)
|
w.semantic.ids = make(map[router.SemanticID]router.SemanticNode)
|
||||||
w.callbacks.w = w
|
w.callbacks.w = w
|
||||||
go w.run(options)
|
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.Start = adjust(e.Selection.Start)
|
||||||
e.Selection.End = adjust(e.Selection.End)
|
e.Selection.End = adjust(e.Selection.End)
|
||||||
e.compose.Start = adjust(e.compose.Start)
|
if e.compose.Start != -1 {
|
||||||
e.compose.End = adjust(e.compose.End)
|
e.compose.Start = adjust(e.compose.Start)
|
||||||
|
e.compose.End = adjust(e.compose.End)
|
||||||
|
}
|
||||||
s := e.Snippet
|
s := e.Snippet
|
||||||
if r.End < s.Start || r.Start > s.End {
|
if r.End < s.Start || r.Start > s.End {
|
||||||
// Replacement does not overlap snippet.
|
// Replacement does not overlap snippet.
|
||||||
|
|||||||
Reference in New Issue
Block a user