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__ )
99101static 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
102353struct 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