Skip to content

Commit 687455b

Browse files
committed
feat(platform/macos): display pre-edit inputs of IME
1 parent 7d2a12b commit 687455b

File tree

1 file changed

+269
-0
lines changed

1 file changed

+269
-0
lines changed

src/entry.c

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242

4343
#if defined(__APPLE__)
4444
#import <Cocoa/Cocoa.h>
45+
#import <QuartzCore/QuartzCore.h>
46+
#import <CoreText/CoreText.h>
4547
#import <objc/runtime.h>
4648
#import <objc/message.h>
4749
#elif defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)
@@ -97,6 +99,255 @@ static struct soluna_ime_rect_state g_soluna_ime_rect = { 0.0f, 0.0f, 0.0f, 0.0f
9799

98100
#if defined(__APPLE__)
99101
static bool g_soluna_macos_composition = false;
102+
static NSTextField *g_soluna_ime_label = nil;
103+
static NSString *g_soluna_macos_ime_font_name = nil;
104+
static CGFloat g_soluna_macos_ime_font_size = 14.0f;
105+
static NSView *g_soluna_ime_caret = nil;
106+
107+
static NSString *soluna_current_marked_text(NSView *view);
108+
static NSRange soluna_current_selected_range(NSView *view);
109+
110+
static void
111+
soluna_macos_apply_ime_font(void) {
112+
if (!g_soluna_ime_label) {
113+
return;
114+
}
115+
CGFloat size = g_soluna_macos_ime_font_size > 0.0f ? g_soluna_macos_ime_font_size : 14.0f;
116+
NSFont *font = nil;
117+
if (g_soluna_macos_ime_font_name) {
118+
font = [NSFont fontWithName:g_soluna_macos_ime_font_name size:size];
119+
}
120+
if (font == nil) {
121+
font = [NSFont systemFontOfSize:size];
122+
}
123+
[g_soluna_ime_label setFont:font];
124+
}
125+
126+
static void
127+
soluna_macos_set_ime_font(const char *font_name, float height_px) {
128+
if (g_soluna_macos_ime_font_name) {
129+
[g_soluna_macos_ime_font_name release];
130+
g_soluna_macos_ime_font_name = nil;
131+
}
132+
if (font_name && font_name[0]) {
133+
NSString *converted = [[NSString alloc] initWithUTF8String:font_name];
134+
if (converted) {
135+
g_soluna_macos_ime_font_name = converted;
136+
} else {
137+
[converted release];
138+
}
139+
}
140+
if (height_px > 0.0f) {
141+
g_soluna_macos_ime_font_size = (CGFloat)height_px;
142+
} else {
143+
g_soluna_macos_ime_font_size = 0.0f;
144+
}
145+
soluna_macos_apply_ime_font();
146+
}
147+
148+
static NSView *
149+
soluna_macos_ensure_ime_caret(NSView *view) {
150+
if (g_soluna_ime_caret == nil) {
151+
g_soluna_ime_caret = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 1, 1)];
152+
[g_soluna_ime_caret setWantsLayer:YES];
153+
CALayer *layer = [g_soluna_ime_caret layer];
154+
if (layer) {
155+
layer.backgroundColor = [[NSColor controlAccentColor] CGColor];
156+
}
157+
}
158+
if (g_soluna_ime_caret.superview != view) {
159+
[g_soluna_ime_caret removeFromSuperview];
160+
if (view) {
161+
NSView *relative = g_soluna_ime_label && g_soluna_ime_label.superview == view ? g_soluna_ime_label : nil;
162+
[view addSubview:g_soluna_ime_caret positioned:NSWindowAbove relativeTo:relative];
163+
}
164+
}
165+
return g_soluna_ime_caret;
166+
}
167+
168+
static void
169+
soluna_macos_hide_ime_caret(void) {
170+
if (g_soluna_ime_caret) {
171+
[g_soluna_ime_caret setHidden:YES];
172+
}
173+
}
174+
175+
static void
176+
soluna_macos_position_ime_caret(NSView *view, NSTextField *label, NSAttributedString *attr, NSRange selectedRange) {
177+
if (selectedRange.location == NSNotFound) {
178+
soluna_macos_hide_ime_caret();
179+
return;
180+
}
181+
NSView *caret = soluna_macos_ensure_ime_caret(view);
182+
if (selectedRange.length > 0) {
183+
[caret setHidden:YES];
184+
return;
185+
}
186+
NSUInteger caretIndex = selectedRange.location + selectedRange.length;
187+
NSUInteger textLength = attr.length;
188+
if (caretIndex > textLength) {
189+
caretIndex = textLength;
190+
}
191+
NSRect textRect = [[label cell] drawingRectForBounds:label.bounds];
192+
CGFloat prefixWidth = 0.0f;
193+
CGFloat lineHeight = 0.0f;
194+
CTLineRef line = CTLineCreateWithAttributedString((CFAttributedStringRef)attr);
195+
if (line) {
196+
if (caretIndex > textLength) {
197+
caretIndex = textLength;
198+
}
199+
double caretOffset = CTLineGetOffsetForStringIndex(line, caretIndex, NULL);
200+
prefixWidth = (CGFloat)ceil(caretOffset);
201+
double ascent = 0.0, descent = 0.0, leading = 0.0;
202+
CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
203+
lineHeight = (CGFloat)(ascent + descent + leading);
204+
CFRelease(line);
205+
}
206+
NSRect caretFrame;
207+
caretFrame.size.width = 2.0f;
208+
if (lineHeight <= 0.0f) {
209+
NSFont *font = [label font];
210+
if (font) {
211+
lineHeight = font.ascender - font.descender;
212+
}
213+
if (lineHeight <= 0.0f) {
214+
lineHeight = g_soluna_macos_ime_font_size > 0.0f ? g_soluna_macos_ime_font_size : 14.0f;
215+
}
216+
}
217+
caretFrame.size.height = MAX(1.0f, lineHeight);
218+
caretFrame.origin.x = label.frame.origin.x + textRect.origin.x + prefixWidth;
219+
CGFloat originY = label.frame.origin.y + textRect.origin.y;
220+
CGFloat verticalAdjust = 0.0f;
221+
if (textRect.size.height > caretFrame.size.height) {
222+
verticalAdjust = (textRect.size.height - caretFrame.size.height) * 0.5f;
223+
}
224+
caretFrame.origin.y = originY + verticalAdjust;
225+
[caret setFrame:NSIntegralRect(caretFrame)];
226+
[caret setHidden:NO];
227+
}
228+
229+
static NSRect
230+
soluna_current_caret_local_rect(NSView *view) {
231+
NSRect caret = NSMakeRect(0, 0, 1, 1);
232+
if (g_soluna_ime_rect.valid) {
233+
CGFloat dpi_scale = sapp_dpi_scale();
234+
if (dpi_scale <= 0.0f) {
235+
dpi_scale = 1.0f;
236+
}
237+
CGFloat logical_height = (CGFloat)sapp_height() / dpi_scale;
238+
CGFloat caret_y = logical_height - (g_soluna_ime_rect.y + g_soluna_ime_rect.h);
239+
caret = NSMakeRect(g_soluna_ime_rect.x, caret_y, g_soluna_ime_rect.w, g_soluna_ime_rect.h);
240+
}
241+
return caret;
242+
}
243+
244+
static NSTextField *
245+
soluna_macos_ensure_ime_label(NSView *view) {
246+
if (g_soluna_ime_label == nil) {
247+
g_soluna_ime_label = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 0, 0)];
248+
[g_soluna_ime_label setEditable:NO];
249+
[g_soluna_ime_label setSelectable:NO];
250+
[g_soluna_ime_label setBezeled:NO];
251+
[g_soluna_ime_label setDrawsBackground:NO];
252+
[g_soluna_ime_label setBordered:NO];
253+
[g_soluna_ime_label setBackgroundColor:[NSColor clearColor]];
254+
[g_soluna_ime_label setHidden:YES];
255+
[g_soluna_ime_label setLineBreakMode:NSLineBreakByClipping];
256+
[g_soluna_ime_label setUsesSingleLineMode:YES];
257+
[g_soluna_ime_label setTranslatesAutoresizingMaskIntoConstraints:YES];
258+
soluna_macos_apply_ime_font();
259+
}
260+
if (g_soluna_ime_label.superview != view) {
261+
[g_soluna_ime_label removeFromSuperview];
262+
if (view) {
263+
[view addSubview:g_soluna_ime_label];
264+
}
265+
}
266+
soluna_macos_apply_ime_font();
267+
return g_soluna_ime_label;
268+
}
269+
270+
static void
271+
soluna_macos_hide_ime_label(void) {
272+
if (g_soluna_ime_label) {
273+
[g_soluna_ime_label setHidden:YES];
274+
}
275+
soluna_macos_hide_ime_caret();
276+
}
277+
278+
static void
279+
soluna_macos_position_ime_label(NSView *view, NSString *text, NSRange selectedRange) {
280+
NSTextInputContext *ctx = nil;
281+
if ([view respondsToSelector:@selector(inputContext)]) {
282+
ctx = [view inputContext];
283+
}
284+
if (ctx) {
285+
NSRange ctxRange = [ctx selectedRange];
286+
if (ctxRange.location != NSNotFound) {
287+
selectedRange = ctxRange;
288+
}
289+
}
290+
if (text == nil || text.length == 0) {
291+
soluna_macos_hide_ime_label();
292+
return;
293+
}
294+
NSTextField *label = soluna_macos_ensure_ime_label(view);
295+
if (label == nil) {
296+
return;
297+
}
298+
NSMutableAttributedString *attr = [[[NSMutableAttributedString alloc] initWithString:text] autorelease];
299+
NSRange fullRange = NSMakeRange(0, attr.length);
300+
if (fullRange.length > 0) {
301+
NSFont *labelFont = [label font];
302+
if (labelFont) {
303+
[attr addAttribute:NSFontAttributeName value:labelFont range:fullRange];
304+
}
305+
[attr addAttribute:NSForegroundColorAttributeName value:[NSColor textColor] range:fullRange];
306+
[attr addAttribute:NSUnderlineStyleAttributeName value:@(NSUnderlineStyleSingle) range:fullRange];
307+
}
308+
if (selectedRange.length > 0 && NSMaxRange(selectedRange) <= attr.length) {
309+
[attr addAttribute:NSBackgroundColorAttributeName value:[NSColor controlAccentColor] range:selectedRange];
310+
[attr addAttribute:NSForegroundColorAttributeName value:[NSColor alternateSelectedControlTextColor] range:selectedRange];
311+
}
312+
[label setAttributedStringValue:attr];
313+
NSSize textSize = [[label cell] cellSizeForBounds:NSMakeRect(0, 0, CGFLOAT_MAX, CGFLOAT_MAX)];
314+
CGFloat padding = 6.0f;
315+
textSize.width += padding;
316+
textSize.height += padding;
317+
NSRect caret = soluna_current_caret_local_rect(view);
318+
CGFloat baseline = caret.origin.y + caret.size.height;
319+
CGFloat baselineOffset = [label baselineOffsetFromBottom];
320+
CGFloat frameOriginY = baseline - baselineOffset - textSize.height;
321+
NSRect frame = NSMakeRect(caret.origin.x, frameOriginY, textSize.width, textSize.height);
322+
NSRect bounds = view.bounds;
323+
if (frame.origin.x < bounds.origin.x) {
324+
frame.origin.x = bounds.origin.x;
325+
}
326+
CGFloat maxX = NSMaxX(bounds);
327+
if (NSMaxX(frame) > maxX) {
328+
frame.origin.x = maxX - frame.size.width;
329+
}
330+
if (frame.origin.y < bounds.origin.y) {
331+
frame.origin.y = bounds.origin.y;
332+
}
333+
CGFloat maxY = NSMaxY(bounds);
334+
if (NSMaxY(frame) > maxY) {
335+
frame.origin.y = maxY - frame.size.height;
336+
}
337+
[label setFrame:NSIntegralRect(frame)];
338+
[label setHidden:NO];
339+
soluna_macos_position_ime_caret(view, label, attr, selectedRange);
340+
}
341+
342+
static void
343+
soluna_macos_refresh_ime_label(NSView *view) {
344+
if (g_soluna_ime_label == nil || [g_soluna_ime_label isHidden]) {
345+
return;
346+
}
347+
NSString *text = soluna_current_marked_text(view);
348+
NSRange range = soluna_current_selected_range(view);
349+
soluna_macos_position_ime_label(view, text, range);
350+
}
100351
#endif
101352

102353
struct soluna_message {
@@ -307,17 +558,20 @@ soluna_current_caret_screen_rect(NSView *view) {
307558
soluna_set_event_consumed(self, false);
308559
}
309560
g_soluna_macos_composition = false;
561+
soluna_macos_hide_ime_label();
310562
}
311563

312564
- (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange {
313565
NSString *plain = soluna_plain_string(string);
314566
soluna_store_marked_text(self, plain, selectedRange);
315567
g_soluna_macos_composition = (string != nil);
568+
soluna_macos_position_ime_label(self, plain, selectedRange);
316569
}
317570

318571
- (void)unmarkText {
319572
soluna_store_marked_text(self, nil, NSMakeRange(NSNotFound, 0));
320573
g_soluna_macos_composition = false;
574+
soluna_macos_hide_ime_label();
321575
}
322576

323577
- (NSRange)selectedRange {
@@ -634,6 +888,8 @@ lset_ime_rect(lua_State *L) {
634888
g_soluna_ime_rect.valid = false;
635889
#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)
636890
soluna_win32_apply_ime_rect();
891+
#elif defined(__APPLE__)
892+
soluna_macos_hide_ime_label();
637893
#endif
638894
return 0;
639895
}
@@ -644,6 +900,13 @@ lset_ime_rect(lua_State *L) {
644900
g_soluna_ime_rect.valid = true;
645901
#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)
646902
soluna_win32_apply_ime_rect();
903+
#elif defined(__APPLE__)
904+
if (g_soluna_ime_label && ![g_soluna_ime_label isHidden]) {
905+
NSView *view = [g_soluna_ime_label superview];
906+
if (view) {
907+
soluna_macos_refresh_ime_label(view);
908+
}
909+
}
647910
#endif
648911
#endif
649912
return 0;
@@ -657,6 +920,9 @@ lset_ime_font(lua_State *L) {
657920
if (top == 0) {
658921
#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)
659922
g_soluna_ime_font_valid = FALSE;
923+
#endif
924+
#if defined(__APPLE__)
925+
soluna_macos_set_ime_font(NULL, 0.0f);
660926
#endif
661927
return 0;
662928
}
@@ -676,6 +942,9 @@ lset_ime_font(lua_State *L) {
676942
}
677943
#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)
678944
soluna_win32_set_ime_font(name, size);
945+
#endif
946+
#if defined(__APPLE__)
947+
soluna_macos_set_ime_font(name, size);
679948
#endif
680949
return 0;
681950
}

0 commit comments

Comments
 (0)