Skip to content

Commit 3fb2a68

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

File tree

1 file changed

+259
-0
lines changed

1 file changed

+259
-0
lines changed

src/entry.c

Lines changed: 259 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,245 @@ 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+
if (text == nil || text.length == 0) {
281+
soluna_macos_hide_ime_label();
282+
return;
283+
}
284+
NSTextField *label = soluna_macos_ensure_ime_label(view);
285+
if (label == nil) {
286+
return;
287+
}
288+
NSMutableAttributedString *attr = [[[NSMutableAttributedString alloc] initWithString:text] autorelease];
289+
NSRange fullRange = NSMakeRange(0, attr.length);
290+
if (fullRange.length > 0) {
291+
NSFont *labelFont = [label font];
292+
if (labelFont) {
293+
[attr addAttribute:NSFontAttributeName value:labelFont range:fullRange];
294+
}
295+
[attr addAttribute:NSForegroundColorAttributeName value:[NSColor textColor] range:fullRange];
296+
[attr addAttribute:NSUnderlineStyleAttributeName value:@(NSUnderlineStyleSingle) range:fullRange];
297+
}
298+
if (selectedRange.length > 0 && NSMaxRange(selectedRange) <= attr.length) {
299+
[attr addAttribute:NSBackgroundColorAttributeName value:[NSColor controlAccentColor] range:selectedRange];
300+
[attr addAttribute:NSForegroundColorAttributeName value:[NSColor alternateSelectedControlTextColor] range:selectedRange];
301+
}
302+
[label setAttributedStringValue:attr];
303+
NSSize textSize = [[label cell] cellSizeForBounds:NSMakeRect(0, 0, CGFLOAT_MAX, CGFLOAT_MAX)];
304+
CGFloat padding = 6.0f;
305+
textSize.width += padding;
306+
textSize.height += padding;
307+
NSRect caret = soluna_current_caret_local_rect(view);
308+
CGFloat baseline = caret.origin.y + caret.size.height;
309+
CGFloat baselineOffset = [label baselineOffsetFromBottom];
310+
CGFloat frameOriginY = baseline - baselineOffset - textSize.height;
311+
NSRect frame = NSMakeRect(caret.origin.x, frameOriginY, textSize.width, textSize.height);
312+
NSRect bounds = view.bounds;
313+
if (frame.origin.x < bounds.origin.x) {
314+
frame.origin.x = bounds.origin.x;
315+
}
316+
CGFloat maxX = NSMaxX(bounds);
317+
if (NSMaxX(frame) > maxX) {
318+
frame.origin.x = maxX - frame.size.width;
319+
}
320+
if (frame.origin.y < bounds.origin.y) {
321+
frame.origin.y = bounds.origin.y;
322+
}
323+
CGFloat maxY = NSMaxY(bounds);
324+
if (NSMaxY(frame) > maxY) {
325+
frame.origin.y = maxY - frame.size.height;
326+
}
327+
[label setFrame:NSIntegralRect(frame)];
328+
[label setHidden:NO];
329+
soluna_macos_position_ime_caret(view, label, attr, selectedRange);
330+
}
331+
332+
static void
333+
soluna_macos_refresh_ime_label(NSView *view) {
334+
if (g_soluna_ime_label == nil || [g_soluna_ime_label isHidden]) {
335+
return;
336+
}
337+
NSString *text = soluna_current_marked_text(view);
338+
NSRange range = soluna_current_selected_range(view);
339+
soluna_macos_position_ime_label(view, text, range);
340+
}
100341
#endif
101342

102343
struct soluna_message {
@@ -307,17 +548,20 @@ soluna_current_caret_screen_rect(NSView *view) {
307548
soluna_set_event_consumed(self, false);
308549
}
309550
g_soluna_macos_composition = false;
551+
soluna_macos_hide_ime_label();
310552
}
311553

312554
- (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange {
313555
NSString *plain = soluna_plain_string(string);
314556
soluna_store_marked_text(self, plain, selectedRange);
315557
g_soluna_macos_composition = (string != nil);
558+
soluna_macos_position_ime_label(self, plain, selectedRange);
316559
}
317560

318561
- (void)unmarkText {
319562
soluna_store_marked_text(self, nil, NSMakeRange(NSNotFound, 0));
320563
g_soluna_macos_composition = false;
564+
soluna_macos_hide_ime_label();
321565
}
322566

323567
- (NSRange)selectedRange {
@@ -634,6 +878,8 @@ lset_ime_rect(lua_State *L) {
634878
g_soluna_ime_rect.valid = false;
635879
#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)
636880
soluna_win32_apply_ime_rect();
881+
#elif defined(__APPLE__)
882+
soluna_macos_hide_ime_label();
637883
#endif
638884
return 0;
639885
}
@@ -644,6 +890,13 @@ lset_ime_rect(lua_State *L) {
644890
g_soluna_ime_rect.valid = true;
645891
#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)
646892
soluna_win32_apply_ime_rect();
893+
#elif defined(__APPLE__)
894+
if (g_soluna_ime_label && ![g_soluna_ime_label isHidden]) {
895+
NSView *view = [g_soluna_ime_label superview];
896+
if (view) {
897+
soluna_macos_refresh_ime_label(view);
898+
}
899+
}
647900
#endif
648901
#endif
649902
return 0;
@@ -657,6 +910,9 @@ lset_ime_font(lua_State *L) {
657910
if (top == 0) {
658911
#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)
659912
g_soluna_ime_font_valid = FALSE;
913+
#endif
914+
#if defined(__APPLE__)
915+
soluna_macos_set_ime_font(NULL, 0.0f);
660916
#endif
661917
return 0;
662918
}
@@ -676,6 +932,9 @@ lset_ime_font(lua_State *L) {
676932
}
677933
#if defined(_MSC_VER) || defined(__MINGW32__) || defined(__MINGW64__)
678934
soluna_win32_set_ime_font(name, size);
935+
#endif
936+
#if defined(__APPLE__)
937+
soluna_macos_set_ime_font(name, size);
679938
#endif
680939
return 0;
681940
}

0 commit comments

Comments
 (0)