diff options
| -rw-r--r-- | LICENSE | 2 | ||||
| -rw-r--r-- | liberty-xui.c | 876 |
2 files changed, 869 insertions, 9 deletions
@@ -1,4 +1,4 @@ -Copyright (c) 2014 - 2025, Přemysl Eric Janouch <p@janouch.name> +Copyright (c) 2014 - 2026, Přemysl Eric Janouch <p@janouch.name> Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. diff --git a/liberty-xui.c b/liberty-xui.c index 7058b41..ca7fa88 100644 --- a/liberty-xui.c +++ b/liberty-xui.c @@ -1,7 +1,7 @@ /* * liberty-xui.c: the ultimate C unlibrary: hybrid terminal/X11 UI * - * Copyright (c) 2016 - 2024, Přemysl Eric Janouch <p@janouch.name> + * Copyright (c) 2016 - 2026, Přemysl Eric Janouch <p@janouch.name> * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted. @@ -73,6 +73,11 @@ enum { XUI_KEYMOD_DOUBLE_CLICK = 1 << 15 }; #include "liberty-xdg.c" #endif // LIBERTY_XUI_WANT_X11 +#ifdef LIBERTY_XUI_WANT_APPKIT +#import <AppKit/AppKit.h> +#import <CoreFoundation/CoreFoundation.h> +#endif // LIBERTY_XUI_WANT_APPKIT + // The application needs to implement these. static void app_quit (void); static void app_layout (void); @@ -592,7 +597,7 @@ struct widget chtype attrs; ///< Rendition, in Curses terms unsigned extended_attrs; ///< XUI-specific attributes - int id; ///< Post-layouting identification + int widget_id; ///< Post-layouting identification int userdata; ///< Action ID/Tab index/... char text[]; ///< Any text label }; @@ -704,6 +709,23 @@ struct xui XRenderColor *x_fg; ///< Foreground per attribute XRenderColor *x_bg; ///< Background per attribute #endif // LIBERTY_XUI_WANT_X11 + + // AppKit: + +#ifdef LIBERTY_XUI_WANT_APPKIT + NSWindow *appkit_window; ///< Application window + NSView *appkit_view; ///< Application view + id appkit_delegate; ///< Application delegate + + struct poller_timer appkit_event; ///< Poll for AppKit events + + NSFont *appkit_font_regular; ///< Regular proportional font + NSFont *appkit_font_monospace; ///< Regular monospace font + NSColor *appkit_colors[256]; ///< Colour cube + + NSColor **appkit_fg; ///< Foreground per attribute (weak ref) + NSColor **appkit_bg; ///< Background per attribute (weak ref) +#endif // LIBERTY_XUI_WANT_APPKIT } g_xui; @@ -1523,11 +1545,11 @@ on_x11_keypress (XEvent *e) } bool result = true; + key.type = TERMO_TYPE_KEY; + key.modifiers &= ~TERMO_KEYMOD_SHIFT; + if (len) { - key.type = TERMO_TYPE_KEY; - key.modifiers &= ~TERMO_KEYMOD_SHIFT; - int32_t cp = 0; struct utf8_iter iter = { .s = buf, .len = len }; size_t cp_len = 0; @@ -2019,6 +2041,820 @@ x11_init (struct poller *poller, struct attrs *app_attrs, size_t app_attrs_len) #endif // LIBERTY_XUI_WANT_X11 +// --- AppKit ------------------------------------------------------------------ + +#ifdef LIBERTY_XUI_WANT_APPKIT + +static const int appkit_default_fg = 16; +static const int appkit_default_bg = 231; + +static NSString * +appkit_string (const char *text) +{ + NSString *string = [NSString stringWithUTF8String:text]; + if (!string) + string = @"?"; + return string; +} + +static int +appkit_font_hadvance (NSFont *font, const char *text) +{ + NSDictionary *attrs + = [NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName]; + return ceil ([appkit_string (text) sizeWithAttributes:attrs].width); +} + +static void +appkit_font_draw (NSFont *font, NSColor *color, int x, int y, + const char *text, int max_width) +{ + NSMutableParagraphStyle *style = [NSMutableParagraphStyle new]; + [style setLineBreakMode:NSLineBreakByTruncatingTail]; + + NSDictionary *attrs = [NSDictionary dictionaryWithObjectsAndKeys: + font, NSFontAttributeName, + color, NSForegroundColorAttributeName, + style, NSParagraphStyleAttributeName, + nil]; + [style release]; + + NSRect rect = NSMakeRect (x, y, max_width, g_xui.vunit); + [appkit_string (text) drawInRect:rect withAttributes:attrs]; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static NSFont * +appkit_widget_font (struct widget *self) +{ + NSFont *base = (self->extended_attrs & XUI_ATTR_MONOSPACE) + ? g_xui.appkit_font_monospace + : g_xui.appkit_font_regular; + + NSFontTraitMask traits = 0; + if (self->attrs & A_BOLD) + traits |= NSBoldFontMask; +#ifdef A_ITALIC + if (self->attrs & A_ITALIC) + traits |= NSItalicFontMask; +#endif // A_ITALIC + + NSFont *font = [[NSFontManager sharedFontManager] + convertFont:base toHaveTrait:traits]; + return font ? font : base; +} + +static NSColor * +appkit_fg_attrs (chtype attrs) +{ + int pair = PAIR_NUMBER (attrs); + if (!pair--) + return g_xui.appkit_colors[appkit_default_fg]; + return ((attrs & A_REVERSE) ? g_xui.appkit_bg : g_xui.appkit_fg)[pair]; +} + +static NSColor * +appkit_fg (struct widget *self) +{ + return appkit_fg_attrs (self->attrs); +} + +static NSColor * +appkit_bg_attrs (chtype attrs) +{ + int pair = PAIR_NUMBER (attrs); + if (!pair--) + return g_xui.appkit_colors[appkit_default_bg]; + return ((attrs & A_REVERSE) ? g_xui.appkit_fg : g_xui.appkit_bg)[pair]; +} + +static NSColor * +appkit_bg (struct widget *self) +{ + return appkit_bg_attrs (self->attrs); +} + +static void +appkit_render_padding (struct widget *self) +{ + if (PAIR_NUMBER (self->attrs)) + { + [appkit_bg (self) setFill]; + NSRectFill (NSMakeRect (self->x, self->y, self->width, self->height)); + } + if (self->attrs & A_UNDERLINE) + { + [appkit_fg (self) setFill]; + NSRectFill (NSMakeRect + (self->x, self->y + self->height - 1, self->width, 1)); + } +} + +static struct widget * +appkit_make_padding (chtype attrs, float width, float height) +{ + struct widget *w = xcalloc (1, sizeof *w + 2); + w->text[0] = ' '; + w->on_render = appkit_render_padding; + w->attrs = attrs; + w->width = g_xui.vunit * width; + w->height = g_xui.vunit * height; + return w; +} + +static void +appkit_render_label (struct widget *self) +{ + appkit_render_padding (self); + + int space = MIN (self->width, g_xui.width - self->x); + if (space <= 0) + return; + + NSFont *font = appkit_widget_font (self); + int y = self->y + (self->height - g_xui.vunit) / 2; + appkit_font_draw (font, appkit_fg (self), self->x, y, self->text, space); +} + +static struct widget * +appkit_make_label (chtype attrs, unsigned extended, const char *label) +{ + size_t label_len = strlen (label) + 1; + + struct widget *w = xcalloc (1, sizeof *w + label_len); + w->on_render = appkit_render_label; + w->attrs = attrs; + w->extended_attrs = extended; + memcpy (w->text, label, label_len); + + NSFont *font = appkit_widget_font (w); + w->width = appkit_font_hadvance (font, w->text); + w->height = g_xui.vunit; + return w; +} + +static void +appkit_render_widget (struct widget *w, NSRect clip) +{ + if (w->width < 0 || w->height < 0) + return; + + NSRect rect = NSMakeRect (w->x, w->y, w->width, w->height); + NSRect subclip = NSIntersectionRect (rect, clip); + if (NSIsEmptyRect (subclip)) + return; + + [NSGraphicsContext saveGraphicsState]; + NSRectClip (subclip); + if (w->on_render) + w->on_render (w); + [NSGraphicsContext restoreGraphicsState]; + + LIST_FOR_EACH (struct widget, child, w->children) + appkit_render_widget (child, subclip); +} + +static void +appkit_render (void) +{ + @autoreleasepool + { + [g_xui.appkit_view setNeedsDisplay:YES]; + [g_xui.appkit_view displayIfNeeded]; + [NSApp updateWindows]; + } +} + +static void +appkit_flip (void) +{ + // AppKit does its own double-buffering, flipping is done in render(), + // or more specifically in the drawRect: method. +} + +static void +appkit_destroy (void) +{ + [g_xui.appkit_window close]; + g_xui.appkit_event.dispatcher (NULL); + poller_timer_reset (&g_xui.appkit_event); + + [g_xui.appkit_window setDelegate:nil]; + [g_xui.appkit_delegate release]; + g_xui.appkit_delegate = nil; + + [g_xui.appkit_view release]; + g_xui.appkit_view = nil; + + [g_xui.appkit_window release]; + g_xui.appkit_window = nil; + + [g_xui.appkit_font_regular release]; + g_xui.appkit_font_regular = nil; + [g_xui.appkit_font_monospace release]; + g_xui.appkit_font_monospace = nil; + + for (size_t i = 0; i < N_ELEMENTS (g_xui.appkit_colors); i++) + [g_xui.appkit_colors[i] release]; + memset (g_xui.appkit_colors, 0, sizeof g_xui.appkit_colors); + + free (g_xui.appkit_fg); + free (g_xui.appkit_bg); + g_xui.appkit_fg = NULL; + g_xui.appkit_bg = NULL; +} + +static struct ui appkit_ui = +{ + .padding = appkit_make_padding, + .label = appkit_make_label, + + .render = appkit_render, + .flip = appkit_flip, + .destroy = appkit_destroy, +}; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static termo_sym_t +appkit_convert_special (unichar c) +{ + // Leaving out TERMO_TYPE_FUNCTION, TERMO_SYM_DEL (N/A), + // and TERMO_SYM_SPACE (governed by TERMO_FLAG_SPACESYMBOL, not in use). + switch (c) + { + case NSBackspaceCharacter: return TERMO_SYM_BACKSPACE; + case NSDeleteCharacter: return TERMO_SYM_BACKSPACE; + case NSTabCharacter: return TERMO_SYM_TAB; + case NSBackTabCharacter: return TERMO_SYM_TAB; + case NSCarriageReturnCharacter: return TERMO_SYM_ENTER; + case NSEnterCharacter: return TERMO_SYM_ENTER; + case 0x1b: return TERMO_SYM_ESCAPE; + + case NSUpArrowFunctionKey: return TERMO_SYM_UP; + case NSDownArrowFunctionKey: return TERMO_SYM_DOWN; + case NSLeftArrowFunctionKey: return TERMO_SYM_LEFT; + case NSRightArrowFunctionKey: return TERMO_SYM_RIGHT; + case NSBeginFunctionKey: return TERMO_SYM_BEGIN; + case NSFindFunctionKey: return TERMO_SYM_FIND; + case NSInsertFunctionKey: return TERMO_SYM_INSERT; + case NSDeleteFunctionKey: return TERMO_SYM_DELETE; + case NSSelectFunctionKey: return TERMO_SYM_SELECT; + case NSPageUpFunctionKey: return TERMO_SYM_PAGEUP; + case NSPageDownFunctionKey: return TERMO_SYM_PAGEDOWN; + case NSHomeFunctionKey: return TERMO_SYM_HOME; + case NSEndFunctionKey: return TERMO_SYM_END; + + // TERMO_SYM_CANCEL + // TERMO_SYM_CLEAR + // TERMO_SYM_CLOSE + // TERMO_SYM_COMMAND + // TERMO_SYM_COPY + // TERMO_SYM_EXIT + case NSHelpFunctionKey: return TERMO_SYM_HELP; + // TERMO_SYM_MARK + // TERMO_SYM_MESSAGE + // TERMO_SYM_MOVE + // TERMO_SYM_OPEN + // TERMO_SYM_OPTIONS + case NSPrintFunctionKey: return TERMO_SYM_PRINT; + case NSRedoFunctionKey: return TERMO_SYM_REDO; + // TERMO_SYM_REFERENCE + // TERMO_SYM_REFRESH + // TERMO_SYM_REPLACE + // TERMO_SYM_RESTART + // TERMO_SYM_RESUME + // TERMO_SYM_SAVE + // TERMO_SYM_SUSPEND + case NSUndoFunctionKey: return TERMO_SYM_UNDO; + + // TERMO_SYM_KP* + } + return TERMO_SYM_UNKNOWN; +} + +static int +appkit_modifiers (NSEventModifierFlags flags) +{ + int modifiers = 0; + if (flags & NSEventModifierFlagShift) modifiers |= TERMO_KEYMOD_SHIFT; + if (flags & NSEventModifierFlagControl) modifiers |= TERMO_KEYMOD_CTRL; + if (flags & NSEventModifierFlagOption) modifiers |= TERMO_KEYMOD_ALT; + return modifiers; +} + +static bool +on_appkit_key_down (NSEvent *event) +{ + if ([event modifierFlags] & NSEventModifierFlagCommand) + return true; + + termo_key_t key = {}; + key.modifiers = appkit_modifiers ([event modifierFlags]); + + NSString *ignoring_modifiers = [event charactersIgnoringModifiers]; + if ([ignoring_modifiers length] == 1) + { + unichar c = [ignoring_modifiers characterAtIndex:0]; + if (c >= NSF1FunctionKey && c <= NSF35FunctionKey) + { + key.type = TERMO_TYPE_FUNCTION; + key.code.number = 1 + c - NSF1FunctionKey; + return xui_process_termo_event (&key); + } + if ((key.code.sym = appkit_convert_special (c)) != TERMO_SYM_UNKNOWN) + { + key.type = TERMO_TYPE_KEYSYM; + return xui_process_termo_event (&key); + } + } + + // Let's make the right Option key act as AltGr, if possible. + CGEventRef e = [event CGEvent]; + if (e && (CGEventGetFlags (e) & NX_DEVICERALTKEYMASK)) + key.modifiers &= ~TERMO_KEYMOD_ALT; + + bool result = true; + key.type = TERMO_TYPE_KEY; + key.modifiers &= ~TERMO_KEYMOD_SHIFT; + + NSData *utf8 = [[event characters] dataUsingEncoding:NSUTF8StringEncoding]; + if (utf8 && [utf8 length] > 0) + { + int32_t cp = 0; + struct utf8_iter iter = { .s = [utf8 bytes], .len = [utf8 length] }; + const char *p = iter.s; + + size_t cp_len = 0; + while ((cp = utf8_iter_next (&iter, &cp_len)) >= 0) + { + termo_key_t k = key; + memcpy (k.multibyte, p, MIN (cp_len, sizeof k.multibyte - 1)); + p += cp_len; + + // This is all unfortunate, but probably in the right place. + if (!cp) + { + k.code.codepoint = ' '; + if ([event modifierFlags] & NSEventModifierFlagShift) + k.modifiers |= TERMO_KEYMOD_SHIFT; + } + else if (cp >= 32) + k.code.codepoint = cp; + else if ([event modifierFlags] & NSEventModifierFlagShift) + k.code.codepoint = cp + 64; + else + k.code.codepoint = cp + 96; + if (!xui_process_termo_event (&k)) + result = false; + } + } + return result; +} + +static char * +appkit_find_text (struct widget *list, int x, int y) +{ + struct widget *target = NULL; + LIST_FOR_EACH (struct widget, w, list) + if (x >= w->x && x < w->x + w->width + && y >= w->y && y < w->y + w->height) + target = w; + if (!target) + return NULL; + + char *result = appkit_find_text (target->children, x, y); + if (result) + return result; + return xstrdup (target->text); +} + +static void +appkit_update_dimensions (void) +{ + NSRect bounds = [g_xui.appkit_view bounds]; + g_xui.width = bounds.size.width; + g_xui.height = bounds.size.height; +} + +static bool +appkit_event_position (NSEvent *event, int *x, int *y) +{ + NSPoint p = [g_xui.appkit_view + convertPoint:[event locationInWindow] fromView:nil]; + *x = p.x; + *y = p.y; + return true; +} + +static int +appkit_mouse_button (NSEvent *event) +{ + NSUInteger button = [event buttonNumber]; + switch (button) + { + case 0: + return 1; + case 1: + return 3; + case 2: + return 2; + default: + // Skip all scroll wheel directions, so that we match X11. + return button + 1 + 4; + } +} + +static bool +appkit_process_press (int x, int y, int button, int modifiers) +{ + if (button != 3) + goto out; + + char *text = appkit_find_text (g_xui.widgets, x, y); + if (!text || !*(cstr_strip_in_place (text, " \t"))) + { + free (text); + goto out; + } + + NSPasteboard *pasteboard = [NSPasteboard generalPasteboard]; + [pasteboard clearContents]; + [pasteboard setString:appkit_string (text) forType:NSPasteboardTypeString]; + app_on_clipboard_copy (text); + free (text); + return true; + +out: + return app_process_mouse (TERMO_MOUSE_PRESS, x, y, button, modifiers); +} + +static bool +on_appkit_mouse_event (NSEvent *event, termo_mouse_event_t type, int button) +{ + int x = 0, y = 0; + if (!appkit_event_position (event, &x, &y)) + return false; + + int modifiers = appkit_modifiers ([event modifierFlags]); + if (type == TERMO_MOUSE_PRESS && [event clickCount] > 1) + modifiers |= XUI_KEYMOD_DOUBLE_CLICK; + + if (type == TERMO_MOUSE_PRESS) + return appkit_process_press (x, y, button, modifiers); + + return app_process_mouse (type, x, y, button, modifiers); +} + +static bool +on_appkit_scroll_event (NSEvent *event) +{ + int x = 0, y = 0; + if (!appkit_event_position (event, &x, &y)) + return false; + + double delta_y = [event scrollingDeltaY]; + if (!delta_y) + return true; + + int modifiers = appkit_modifiers ([event modifierFlags]); + int button = delta_y > 0 ? 4 : 5; + int repeats = MAX (1, (int) round (fabs (delta_y))); + bool result = true; + while (result && repeats-- > 0) + { + result &= app_process_mouse (TERMO_MOUSE_PRESS, + x, y, button, modifiers); + + // XXX: Terminals do not send it, but X11 does. We should either + // suppress the releases, or synthesize them for terminals as well. + result &= app_process_mouse (TERMO_MOUSE_RELEASE, + x, y, button, modifiers); + } + return result; +} + +static void +appkit_send_focus_event (bool focused) +{ + termo_key_t key = { .type = TERMO_TYPE_FOCUS }; + key.code.focused = focused; + xui_process_termo_event (&key); +} + +@interface XUIAppKitView : NSView +@end + +@implementation XUIAppKitView + +- (BOOL) isFlipped { return YES; } +- (BOOL) acceptsFirstResponder { return YES; } + +- (void) drawRect:(NSRect)dirty_rect +{ + [g_xui.appkit_colors[appkit_default_bg] setFill]; + NSRectFill (dirty_rect); + + LIST_FOR_EACH (struct widget, w, g_xui.widgets) + appkit_render_widget (w, dirty_rect); +} + +- (void) keyDown:(NSEvent *)event +{ + if (!on_appkit_key_down (event)) + NSBeep (); +} + +- (void) mouseDown:(NSEvent *)event +{ + if (!on_appkit_mouse_event (event, TERMO_MOUSE_PRESS, 1)) + NSBeep (); +} + +- (void) mouseUp:(NSEvent *)event +{ + if (!on_appkit_mouse_event (event, TERMO_MOUSE_RELEASE, 1)) + NSBeep (); +} + +- (void) rightMouseDown:(NSEvent *)event +{ + if (!on_appkit_mouse_event (event, TERMO_MOUSE_PRESS, 3)) + NSBeep (); +} + +- (void) rightMouseUp:(NSEvent *)event +{ + if (!on_appkit_mouse_event (event, TERMO_MOUSE_RELEASE, 3)) + NSBeep (); +} + +- (void) otherMouseDown:(NSEvent *)event +{ + if (!on_appkit_mouse_event (event, + TERMO_MOUSE_PRESS, appkit_mouse_button (event))) + NSBeep (); +} + +- (void) otherMouseUp:(NSEvent *)event +{ + if (!on_appkit_mouse_event (event, + TERMO_MOUSE_RELEASE, appkit_mouse_button (event))) + NSBeep (); +} + +- (void) mouseDragged:(NSEvent *)event +{ + if (!on_appkit_mouse_event (event, TERMO_MOUSE_DRAG, 1)) + NSBeep (); +} + +- (void) rightMouseDragged:(NSEvent *)event +{ + if (!on_appkit_mouse_event (event, TERMO_MOUSE_DRAG, 3)) + NSBeep (); +} + +- (void) otherMouseDragged:(NSEvent *)event +{ + if (!on_appkit_mouse_event (event, + TERMO_MOUSE_DRAG, appkit_mouse_button (event))) + NSBeep (); +} + +- (void) scrollWheel:(NSEvent *)event +{ + if (!on_appkit_scroll_event (event)) + NSBeep (); +} + +@end + +@interface XUIAppKitDelegate : NSObject <NSWindowDelegate> +@end + +@implementation XUIAppKitDelegate + +- (void) windowWillClose:(NSNotification *)notification +{ + (void) notification; + app_quit (); +} + +- (void) windowDidResize:(NSNotification *)notification +{ + (void) notification; + appkit_update_dimensions (); + + // AppKit seems to run an event subloop, so relayout immediately. + g_xui.refresh_event.dispatcher (NULL); + poller_idle_reset (&g_xui.refresh_event); +} + +- (void) windowDidBecomeKey:(NSNotification *)notification +{ + (void) notification; + appkit_send_focus_event (true); +} + +- (void) windowDidResignKey:(NSNotification *)notification +{ + (void) notification; + appkit_send_focus_event (false); +} + +@end + +static NSEvent * +appkit_next_event (NSDate *limit) +{ + // Probably not needing NSEventTrackingRunLoopMode + // or NSModalPanelRunLoopMode. + return [NSApp nextEventMatchingMask:NSEventMaskAny + untilDate:limit inMode:NSDefaultRunLoopMode dequeue:true]; +} + +static void +on_appkit_ready (void *user_data) +{ + (void) user_data; + + // AppKit likes to own the event loop; we give up and poll. + poller_timer_set (&g_xui.appkit_event, 1000 / 60 /* FPS */); + + @autoreleasepool + { + NSDate *limit = [NSDate distantPast]; + NSEvent *event = nil; + while ((event = appkit_next_event (limit))) + { + [NSApp sendEvent:event]; + [NSApp updateWindows]; + } + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static NSColor * +appkit_convert_color (int color) +{ + hard_assert (color >= 0 && color <= 255); + + static const uint16_t base[16] = + { + 0x000, 0x800, 0x080, 0x880, 0x008, 0x808, 0x088, 0xccc, + 0x888, 0xf00, 0x0f0, 0xff0, 0x00f, 0xf0f, 0x0ff, 0xfff, + }; + + CGFloat red = 1, green = 1, blue = 1, alpha = 1; + if (color < 16) + { + red = (base[color] >> 8) / 15.0; + green = (0xf & (base[color] >> 4)) / 15.0; + blue = (0xf & (base[color])) / 15.0; + } + else if (color >= 232) + red = green = blue = (8 + (color - 232) * 10) / 255.0; + else + { + color -= 16; + + int r = color / 36; + int g = (color / 6) % 6; + int b = (color % 6); + red = !!r * (55 + 40 * r) / 255.0; + green = !!g * (55 + 40 * g) / 255.0; + blue = !!b * (55 + 40 * b) / 255.0; + } + return [NSColor colorWithDeviceRed:red green:green blue:blue alpha:alpha]; +} + +static void +appkit_init_attributes (struct attrs *attrs, size_t attrs_len) +{ + for (size_t i = 0; i < N_ELEMENTS (g_xui.appkit_colors); i++) + g_xui.appkit_colors[i] = [appkit_convert_color (i) retain]; + + g_xui.appkit_fg = xcalloc (attrs_len, sizeof *g_xui.appkit_fg); + g_xui.appkit_bg = xcalloc (attrs_len, sizeof *g_xui.appkit_bg); + for (size_t a = 0; a < attrs_len; a++) + { + g_xui.appkit_fg[a] = g_xui.appkit_colors[appkit_default_fg]; + g_xui.appkit_bg[a] = g_xui.appkit_colors[appkit_default_bg]; + if (attrs[a].fg >= 256 || attrs[a].fg < -1 + || attrs[a].bg >= 256 || attrs[a].bg < -1) + continue; + + if (attrs[a].fg != -1) + g_xui.appkit_fg[a] = g_xui.appkit_colors[attrs[a].fg]; + if (attrs[a].bg != -1) + g_xui.appkit_bg[a] = g_xui.appkit_colors[attrs[a].bg]; + + attrs[a].attrs |= COLOR_PAIR (a + 1); + } +} + +static void +appkit_init_menu (void) +{ + NSMenu *app_menu = [[NSMenu new] autorelease]; + + NSMenuItem *services_item = [app_menu + addItemWithTitle:@"Services" + action:nil + keyEquivalent:@""]; + NSMenu *services_menu = [[NSMenu new] autorelease]; + [services_item setSubmenu:services_menu]; + [NSApp setServicesMenu:services_menu]; + + [app_menu addItem:[NSMenuItem separatorItem]]; + [app_menu addItemWithTitle:@"Hide " PROGRAM_NAME + action:@selector (hide:) + keyEquivalent:@"h"]; + [app_menu addItem:[NSMenuItem separatorItem]]; + [app_menu addItemWithTitle:@"Quit " PROGRAM_NAME + action:@selector (terminate:) + keyEquivalent:@"q"]; + + NSMenuItem *app_menu_item = [[NSMenuItem new] autorelease]; + [app_menu_item setSubmenu:app_menu]; + NSMenu *menubar = [[NSMenu new] autorelease]; + [menubar addItem:app_menu_item]; + [NSApp setMainMenu:menubar]; +} + +static void +appkit_init (struct poller *poller, struct attrs *attrs, size_t attrs_len) +{ + (void) [NSApplication sharedApplication]; + // Apparently this won't succeed when run from within a bundle. + (void) [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + appkit_init_menu (); + [NSApp finishLaunching]; + + NSFont *system = + [NSFont systemFontOfSize:[NSFont systemFontSize]]; + NSFont *mono = + [NSFont userFixedPitchFontOfSize:[NSFont systemFontSize]]; + if (!mono) + mono = system; + g_xui.appkit_font_regular = [system retain]; + g_xui.appkit_font_monospace = [mono retain]; + if (!g_xui.appkit_font_regular || !g_xui.appkit_font_monospace) + [NSException raise:@"failed" format:@"failed to load fonts"]; + + g_xui.vunit = ceil ([system ascender] - [system descender] + + [system leading]); + g_xui.vunit = MAX (1, g_xui.vunit); + g_xui.hunit = MAX (1, g_xui.vunit / 2); + g_xui.height = 24 * g_xui.vunit; + g_xui.width = g_xui.height * 4 / 3; + + NSRect frame = NSMakeRect (0, 0, g_xui.width, g_xui.height); + NSWindowStyleMask style = NSWindowStyleMaskTitled + | NSWindowStyleMaskClosable + | NSWindowStyleMaskMiniaturizable + | NSWindowStyleMaskResizable; + g_xui.appkit_window = [[NSWindow alloc] + initWithContentRect:frame + styleMask:style + backing:NSBackingStoreBuffered + defer:NO]; + if (!g_xui.appkit_window) + [NSException raise:@"failed" format:@"failed to create window"]; + g_xui.appkit_window.releasedWhenClosed = false; + [g_xui.appkit_window center]; + + g_xui.appkit_view = [[XUIAppKitView alloc] initWithFrame:frame]; + if (!g_xui.appkit_view) + [NSException raise:@"failed" format:@"failed to create view"]; + [g_xui.appkit_window setContentView:g_xui.appkit_view]; + + g_xui.appkit_delegate = [XUIAppKitDelegate new]; + if (!g_xui.appkit_delegate) + [NSException raise:@"failed" format:@"failed to create delegate"]; + [g_xui.appkit_window setDelegate:g_xui.appkit_delegate]; + + [g_xui.appkit_window setAcceptsMouseMovedEvents:YES]; + [g_xui.appkit_window setTitle:appkit_string (PROGRAM_NAME)]; + [g_xui.appkit_window makeFirstResponder:g_xui.appkit_view]; + [g_xui.appkit_window makeKeyAndOrderFront:nil]; + [NSApp activateIgnoringOtherApps:YES]; + + appkit_update_dimensions (); + appkit_init_attributes (attrs, attrs_len); + + g_xui.appkit_event = poller_timer_make (poller); + g_xui.appkit_event.dispatcher = on_appkit_ready; + poller_timer_set (&g_xui.appkit_event, 0); + + g_xui.ui = &appkit_ui; +} + +#endif // LIBERTY_XUI_WANT_APPKIT + // --- Containers -------------------------------------------------------------- static void @@ -2207,7 +3043,9 @@ static void xui_start (struct poller *poller, bool force_x11, struct attrs *attrs, size_t attrs_len) { +#if !defined LIBERTY_XUI_WANT_X11 && !defined LIBERTY_XUI_WANT_APPKIT (void) force_x11; +#endif // !LIBERTY_XUI_WANT_X11 && !LIBERTY_XUI_WANT_APPKIT g_xui.refresh_event = poller_idle_make (poller); g_xui.refresh_event.dispatcher = xui_on_refresh; @@ -2221,11 +3059,33 @@ xui_start (struct poller *poller, g_xui.tk_timer.dispatcher = tui_on_key_timer; #ifdef LIBERTY_XUI_WANT_X11 - if (force_x11 || (!isatty (STDIN_FILENO) && getenv ("DISPLAY"))) + if (getenv ("DISPLAY") + && (force_x11 || !isatty (STDIN_FILENO))) + { x11_init (poller, attrs, attrs_len); - else + return; + } #endif // LIBERTY_XUI_WANT_X11 - tui_init (poller, attrs, attrs_len); + +#ifdef LIBERTY_XUI_WANT_APPKIT + if (force_x11 || !isatty (STDIN_FILENO)) + { + @autoreleasepool + { + @try + { + appkit_init (poller, attrs, attrs_len); + } + @catch (NSException *e) + { + exit_fatal ("AppKit setup failed: %s", [[e reason] UTF8String]); + } + } + return; + } +#endif // LIBERTY_XUI_WANT_APPKIT + + tui_init (poller, attrs, attrs_len); } static void |
