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__ )
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+ 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
102343struct 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