From e8f91b24174d4f4c5afa6379d37c865be4c9e1f4 Mon Sep 17 00:00:00 2001 From: araq Date: Sun, 12 Apr 2026 00:02:49 +0200 Subject: [PATCH 01/14] GTk4 backend --- app/gtk4_driver.nim | 778 ++++++++++++++++++++++++++++++++++++++++++++ app/nimedit.nim | 8 +- app/styles.nim | 4 +- 3 files changed, 786 insertions(+), 4 deletions(-) create mode 100644 app/gtk4_driver.nim diff --git a/app/gtk4_driver.nim b/app/gtk4_driver.nim new file mode 100644 index 0000000..33797f1 --- /dev/null +++ b/app/gtk4_driver.nim @@ -0,0 +1,778 @@ +# GTK 4 backend: thin C bindings. Sets hooks from core/input and core/screen. +# +# Build (from repo root, with nim.cfg): +# nim c -d:gtk4 app/nimedit.nim +# Requires dev packages: gtk4, pangocairo, pangoft2, fontconfig (pkg-config names). +# Uses gtk_event_controller_key_set_im_context (GTK 4.2+). GtkDrawingArea "resize" needs GTK 4.6+. +# If pkg-config is missing, set compile-time flags manually, e.g.: +# nim c -d:gtk4 --passC:"$(pkg-config --cflags gtk4)" --passL:"$(pkg-config --libs gtk4 pangocairo pangoft2 fontconfig)" app/nimedit.nim +# evMouseMove does not set `buttons` (held buttons) yet; SDL drivers do. + +{.emit: """ +#include +#include +#include +#include +#include +#include +#include + +static inline FcPattern *nimedit_fc_font_match(FcPattern *p, void *result_out) { + return FcFontMatch(NULL, p, (FcResult *)result_out); +} +""".} + +import std/unicode +import basetypes, input, screen + +const + gtkCflags {.strdefine.} = staticExec("pkg-config --cflags gtk4 pangocairo pangoft2 fontconfig glib-2.0 2>/dev/null").strip + gtkLibs {.strdefine.} = staticExec("pkg-config --libs gtk4 pangocairo pangoft2 fontconfig glib-2.0 2>/dev/null").strip + +const gtkFallbackCflags = + "-I/usr/include/gtk-4.0 -I/usr/include/glib-2.0 " & + "-I/usr/lib/x86_64-linux-gnu/glib-2.0/include -I/usr/lib/aarch64-linux-gnu/glib-2.0/include " & + "-I/usr/include/pango-1.0 -I/usr/include/harfbuzz -I/usr/include/freetype2 " & + "-I/usr/include/libpng16 -I/usr/include/libmount -I/usr/include/blkid " & + "-I/usr/include/fribidi -I/usr/include/cairo -I/usr/include/pixman-1 " & + "-I/usr/include/gdk-pixbuf-2.0 -I/usr/include/webp -I/usr/include/graphene-1.0 " & + "-I/usr/include/fontconfig -pthread" + +const gtkFallbackLibs = + "-lgtk-4 -lgdk-4 -lpangocairo-1.0 -lpangoft2-1.0 -lpango-1.0 -lgobject-2.0 " & + "-lglib-2.0 -lgio-2.0 -lgmodule-2.0 -lfontconfig -lfreetype -lcairo -lharfbuzz " & + "-lgraphene-1.0 -lfribidi -lgdk_pixbuf-2.0 -lXi -lX11 -ldl -lm" + +when gtkCflags.len > 0: + {.passC: gtkCflags.} +else: + {.warning: "gtk4_driver: pkg-config --cflags failed; using fallback -I paths".} + {.passC: gtkFallbackCflags.} + +when gtkLibs.len > 0: + {.passL: gtkLibs.} +else: + {.warning: "gtk4_driver: pkg-config --libs failed; using fallback -l flags".} + {.passL: gtkFallbackLibs.} + +type + gboolean = cint + guint = cuint + gint = cint + gulong = culong + gdouble = cdouble + GCallback = pointer + GConnectFlags = cint + +type + GtkDrawingArea {.importc, header: "", incompleteStruct.} = object + cairo_t {.importc, header: "", incompleteStruct.} = object + GError {.importc, header: "", incompleteStruct.} = object + GObject {.importc, header: "", incompleteStruct.} = object + GAsyncResult {.importc, header: "", incompleteStruct.} = object + +const + G_FALSE = gboolean(0) + G_TRUE = gboolean(1) + GDK_BUTTON_MIDDLE = 2'u32 + GDK_BUTTON_SECONDARY = 3'u32 + PANGO_SCALE = 1024 + GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES = 3 + FcMatchPattern = 0 + CAIRO_FORMAT_ARGB32 = 0 ## cairo_format_t; pixel buffer must be flushed before another cairo_t reads it + +const + FC_FILE = "file" + +# --- Minimal GObject / GLib / GTK / Gdk / Cairo / Pango / Fontconfig --- + +proc g_signal_connect_data( + inst: pointer; signal: cstring; handler: GCallback; + data, destroyData: pointer; flags: GConnectFlags +): gulong {.importc, nodecl, cdecl.} + +proc g_object_unref(o: pointer) {.importc, nodecl, cdecl.} +proc g_error_free(err: ptr GError) {.importc, nodecl, cdecl.} +proc g_free(p: pointer) {.importc, nodecl, cdecl.} +proc g_get_monotonic_time(): int64 {.importc, nodecl, cdecl.} +proc g_usleep(micros: culong) {.importc, nodecl, cdecl.} + +proc g_main_context_iteration(ctx: pointer; mayBlock: gboolean): gboolean {.importc, nodecl, cdecl.} + +proc gtk_init_check(): gboolean {.importc, nodecl, cdecl.} +proc gtk_window_new(): pointer {.importc, nodecl, cdecl.} +proc gtk_window_set_title(win: pointer; title: cstring) {.importc, nodecl, cdecl.} +proc gtk_window_set_default_size(win: pointer; w, h: gint) {.importc, nodecl, cdecl.} +proc gtk_window_set_child(win: pointer; child: pointer) {.importc, nodecl, cdecl.} +proc gtk_window_destroy(win: pointer) {.importc, nodecl, cdecl.} +proc gtk_window_present(win: pointer) {.importc, nodecl, cdecl.} +proc gtk_widget_queue_draw(w: pointer) {.importc, nodecl, cdecl.} +proc gtk_widget_grab_focus(w: pointer) {.importc, nodecl, cdecl.} +proc gtk_widget_set_cursor(w, cursor: pointer) {.importc, nodecl, cdecl.} +proc gtk_widget_get_width(w: pointer): gint {.importc, nodecl, cdecl.} +proc gtk_widget_get_height(w: pointer): gint {.importc, nodecl, cdecl.} +proc gtk_widget_set_focusable(w: pointer; focusable: gboolean) {.importc, nodecl, cdecl.} +proc gtk_widget_set_hexpand(w: pointer; expand: gboolean) {.importc, nodecl, cdecl.} +proc gtk_widget_set_vexpand(w: pointer; expand: gboolean) {.importc, nodecl, cdecl.} +proc gtk_widget_add_controller(w, ctrl: pointer) {.importc, nodecl, cdecl.} + +proc gtk_drawing_area_new(): pointer {.importc, nodecl, cdecl.} +proc gtk_drawing_area_set_content_width(area: ptr GtkDrawingArea; width: gint) {.importc, nodecl, cdecl.} +proc gtk_drawing_area_set_content_height(area: ptr GtkDrawingArea; height: gint) {.importc, nodecl, cdecl.} +proc gtk_drawing_area_get_content_width(area: ptr GtkDrawingArea): gint {.importc, nodecl, cdecl.} +proc gtk_drawing_area_get_content_height(area: ptr GtkDrawingArea): gint {.importc, nodecl, cdecl.} +proc gtk_drawing_area_set_draw_func( + area: pointer; + drawFunc: proc (area: ptr GtkDrawingArea; cr: ptr cairo_t; w, h: gint; + data: pointer) {.cdecl.}; + data: pointer; destroyNotify: pointer +) {.importc, nodecl, cdecl.} + +proc gtk_event_controller_key_new(): pointer {.importc, nodecl, cdecl.} +proc gtk_event_controller_key_set_im_context(keyCtrl, im: pointer) {.importc, nodecl, cdecl.} +proc gtk_event_controller_get_current_event(ctrl: pointer): pointer {.importc, nodecl, cdecl.} +proc gtk_event_controller_motion_new(): pointer {.importc, nodecl, cdecl.} +proc gtk_event_controller_scroll_new(flags: guint): pointer {.importc, nodecl, cdecl.} +proc gtk_event_controller_focus_new(): pointer {.importc, nodecl, cdecl.} +proc gtk_gesture_click_new(): pointer {.importc, nodecl, cdecl.} +proc gtk_gesture_single_set_button(gesture: pointer; button: gint) {.importc, nodecl, cdecl.} +proc gtk_gesture_single_get_current_button(gesture: pointer): guint {.importc, nodecl, cdecl.} + +proc gtk_im_multicontext_new(): pointer {.importc, nodecl, cdecl.} +proc gtk_im_context_set_client_widget(im, widget: pointer) {.importc, nodecl, cdecl.} +proc gtk_im_context_focus_in(im: pointer) {.importc, nodecl, cdecl.} + +proc gdk_event_get_modifier_state(ev: pointer): guint {.importc, nodecl, cdecl.} +proc gdk_keyval_to_lower(k: guint): guint {.importc, nodecl, cdecl.} +proc gdk_cursor_new_from_name(name: cstring; fallback: pointer): pointer {.importc, nodecl, cdecl.} +proc gdk_display_get_default(): pointer {.importc, nodecl, cdecl.} +proc gdk_display_get_clipboard(disp: pointer): pointer {.importc, nodecl, cdecl.} +proc gdk_clipboard_set_text(clip: pointer; text: cstring) {.importc, nodecl, cdecl.} + +proc gdk_clipboard_read_text_async( + clip: pointer; cancellable: pointer; + callback: proc (sourceObj: ptr GObject; res: ptr GAsyncResult; data: pointer) {.cdecl.}; + data: pointer +) {.importc, nodecl, cdecl.} + +proc gdk_clipboard_read_text_finish(clip: pointer; res: pointer; err: ptr ptr GError): cstring {.importc, nodecl, cdecl.} + +proc cairo_create(surface: pointer): pointer {.importc, nodecl, cdecl.} +proc cairo_destroy(cr: pointer) {.importc, nodecl, cdecl.} +proc cairo_surface_destroy(surf: pointer) {.importc, nodecl, cdecl.} +proc cairo_surface_flush(surf: pointer) {.importc, nodecl, cdecl.} +proc cairo_image_surface_create(fmt, w, h: gint): pointer {.importc, nodecl, cdecl.} +proc cairo_save(cr: pointer) {.importc, nodecl, cdecl.} +proc cairo_restore(cr: pointer) {.importc, nodecl, cdecl.} +proc cairo_reset_clip(cr: pointer) {.importc, nodecl, cdecl.} +proc cairo_rectangle(cr: pointer; x, y, w, h: gdouble) {.importc, nodecl, cdecl.} +proc cairo_clip(cr: pointer) {.importc, nodecl, cdecl.} +proc cairo_set_source_rgba(cr: pointer; r, g, b, a: gdouble) {.importc, nodecl, cdecl.} +proc cairo_set_source_surface(cr, surf: pointer; x, y: gdouble) {.importc, nodecl, cdecl.} +proc cairo_paint(cr: pointer) {.importc, nodecl, cdecl.} +proc cairo_fill(cr: pointer) {.importc, nodecl, cdecl.} +proc cairo_stroke(cr: pointer) {.importc, nodecl, cdecl.} +proc cairo_move_to(cr: pointer; x, y: gdouble) {.importc, nodecl, cdecl.} +proc cairo_line_to(cr: pointer; x, y: gdouble) {.importc, nodecl, cdecl.} +proc cairo_set_line_width(cr: pointer; w: gdouble) {.importc, nodecl, cdecl.} +proc cairo_scale(cr: pointer; sx, sy: gdouble) {.importc, nodecl, cdecl.} + +proc pango_cairo_create_layout(cr: pointer): pointer {.importc, nodecl, cdecl.} +proc pango_cairo_update_layout(cr, layout: pointer) {.importc, nodecl, cdecl.} +proc pango_cairo_show_layout(cr, layout: pointer) {.importc, nodecl, cdecl.} +proc g_object_unref_layout(o: pointer) {.importc: "g_object_unref", nodecl, cdecl.} + +proc pango_layout_set_text(layout: pointer; text: cstring; len: gint) {.importc, nodecl, cdecl.} +proc pango_layout_set_font_description(layout, desc: pointer) {.importc, nodecl, cdecl.} +proc pango_layout_get_pixel_size(layout: pointer; w, h: ptr gint) {.importc, nodecl, cdecl.} + +proc pango_font_description_free(desc: pointer) {.importc, nodecl, cdecl.} +proc pango_font_description_set_absolute_size(desc: pointer; size: gint) {.importc, nodecl, cdecl.} + +proc pango_fc_font_description_from_pattern(pat: pointer; includeSize: gboolean): pointer {.importc, nodecl, cdecl.} + +proc pango_font_metrics_unref(m: pointer) {.importc, nodecl, cdecl.} +proc pango_layout_get_context(layout: pointer): pointer {.importc, nodecl, cdecl.} +proc pango_context_get_font_map(ctx: pointer): pointer {.importc, nodecl, cdecl.} +proc pango_font_map_load_font(map, ctx, desc: pointer): pointer {.importc, nodecl, cdecl.} +proc pango_font_get_metrics(font, lang: pointer): pointer {.importc, nodecl, cdecl.} +proc pango_font_metrics_get_ascent(m: pointer): gint {.importc, nodecl, cdecl.} +proc pango_font_metrics_get_descent(m: pointer): gint {.importc, nodecl, cdecl.} +proc pango_font_metrics_get_height(m: pointer): gint {.importc, nodecl, cdecl.} + +proc FcInit(): gboolean {.importc, nodecl, cdecl.} +proc FcPatternCreate(): pointer {.importc, nodecl, cdecl.} +proc FcPatternDestroy(p: pointer) {.importc, nodecl, cdecl.} +proc FcPatternAddString(p: pointer; obj: cstring; s: cstring): gboolean {.importc, nodecl, cdecl.} +proc FcConfigSubstitute(cfg, p: pointer; kind: gint): gboolean {.importc, nodecl, cdecl.} +proc FcDefaultSubstitute(p: pointer) {.importc, nodecl, cdecl.} +proc nimedit_fc_font_match(p, resultOut: pointer): pointer {.importc: "nimedit_fc_font_match", nodecl, cdecl.} + +# --- Font slots --- + +type + FontSlot = object + desc: pointer ## PangoFontDescription* + metrics: FontMetrics + +var fonts: seq[FontSlot] + +proc getDesc(f: Font): pointer = + let idx = f.int - 1 + if idx >= 0 and idx < fonts.len: fonts[idx].desc + else: nil + +# --- GTK state --- + +var + win: pointer + drawingArea: pointer + backingSurf: pointer + backingCr: pointer + backingW, backingH: int + imContext: pointer + eventQueue: seq[Event] + modState: set[Modifier] + clipboardBuf: string + clipboardLoop: pointer ## GMainLoop* — set during sync read + +proc pushEvent(e: Event) = + eventQueue.add e + +proc gdkModsToSet(st: guint): set[Modifier] = + if (st and (1'u32 shl 0)) != 0: result.incl modShift + if (st and (1'u32 shl 2)) != 0: result.incl modCtrl + if (st and (1'u32 shl 3)) != 0: result.incl modAlt + if (st and (1'u32 shl 26)) != 0: result.incl modGui + if (st and (1'u32 shl 28)) != 0: result.incl modGui + +proc syncModsFromController(ctrl: pointer) = + let ev = gtk_event_controller_get_current_event(ctrl) + if ev != nil: + modState = gdkModsToSet(gdk_event_get_modifier_state(ev)) + +proc translateKeyval(kv: guint): KeyCode = + let k = gdk_keyval_to_lower(kv) + template ck(c: char, key: KeyCode): untyped = + if k == cast[guint](ord(c)): return key + ck('a', keyA); ck('b', keyB); ck('c', keyC); ck('d', keyD); ck('e', keyE) + ck('f', keyF); ck('g', keyG); ck('h', keyH); ck('i', keyI); ck('j', keyJ) + ck('k', keyK); ck('l', keyL); ck('m', keyM); ck('n', keyN); ck('o', keyO) + ck('p', keyP); ck('q', keyQ); ck('r', keyR); ck('s', keyS); ck('t', keyT) + ck('u', keyU); ck('v', keyV); ck('w', keyW); ck('x', keyX); ck('y', keyY) + ck('z', keyZ) + ck('0', key0); ck('1', key1); ck('2', key2); ck('3', key3); ck('4', key4) + ck('5', key5); ck('6', key6); ck('7', key7); ck('8', key8); ck('9', key9) + case k + of 0xff1b: keyEsc + of 0xff09: keyTab + of 0xff0d: keyEnter + of 0x020: keySpace + of 0xff08: keyBackspace + of 0xffff: keyDelete + of 0xff63: keyInsert + of 0xff51: keyLeft + of 0xff53: keyRight + of 0xff52: keyUp + of 0xff54: keyDown + of 0xff55: keyPageUp + of 0xff56: keyPageDown + of 0xff50: keyHome + of 0xff57: keyEnd + of 0xffe5: keyCapslock + of 0x02c: keyComma + of 0x02e: keyPeriod + of 0xffbe: keyF1 + of 0xffbf: keyF2 + of 0xffc0: keyF3 + of 0xffc1: keyF4 + of 0xffc2: keyF5 + of 0xffc3: keyF6 + of 0xffc4: keyF7 + of 0xffc5: keyF8 + of 0xffc6: keyF9 + of 0xffc7: keyF10 + of 0xffc8: keyF11 + of 0xffc9: keyF12 + else: keyNone + +proc enqueueTextFromUtf8(s: string) = + for ch in s.toRunes: + var ev = Event(kind: evTextInput) + let u = toUtf8(ch) + for i in 0 ..< min(4, u.len): + ev.text[i] = u[i] + for i in u.len .. 3: + ev.text[i] = '\0' + pushEvent ev + +proc recreateBacking(w, h: int) = + ## GTK can emit resize with a transient 0 width or height; do not destroy a good buffer then bail. + if w <= 0 or h <= 0: + return + if backingCr != nil: + cairo_destroy(backingCr) + backingCr = nil + if backingSurf != nil: + cairo_surface_destroy(backingSurf) + backingSurf = nil + backingW = w + backingH = h + backingSurf = cairo_image_surface_create(gint(CAIRO_FORMAT_ARGB32), gint(w), gint(h)) + backingCr = cairo_create(backingSurf) + cairo_set_source_rgba(backingCr, 1, 1, 1, 1) + cairo_rectangle(backingCr, 0, 0, gdouble(w), gdouble(h)) + cairo_fill(backingCr) + cairo_surface_flush(backingSurf) + +proc ensureBackingCr() = + if backingCr == nil and win != nil and drawingArea != nil: + let w = gtk_widget_get_width(drawingArea).int + let h = gtk_widget_get_height(drawingArea).int + if w > 0 and h > 0: + recreateBacking(w, h) + +# --- Signal callbacks (cdecl) --- + +proc onCloseRequest(self: pointer; data: pointer): gboolean {.cdecl.} = + pushEvent(Event(kind: evWindowClose)) + G_TRUE + +proc onResize(area: pointer; width, height: gint; data: pointer) {.cdecl.} = + recreateBacking(width.int, height.int) + pushEvent(Event(kind: evWindowResize, x: width.int, y: height.int)) + +proc onDraw(area: ptr GtkDrawingArea; cr: ptr cairo_t; width, height: gint; + data: pointer) {.cdecl.} = + ## Blit only. Never call recreateBacking here: it clears the image surface and + ## drops NimEdit's pixels when the main loop skips redraws (events.len==0 && + ## doRedraw==false). Resize/`createWindow`/GtkDrawingArea::resize already size + ## the backing; if GTK reports a transient mismatch, scale the blit. + let w = width.int + let h = height.int + if w <= 0 or h <= 0 or backingSurf == nil: + return + cairo_surface_flush(backingSurf) + let crp = cast[pointer](cr) + cairo_save(crp) + if backingW != w or backingH != h: + cairo_scale(crp, gdouble(w) / gdouble(max(1, backingW)), + gdouble(h) / gdouble(max(1, backingH))) + cairo_set_source_surface(crp, backingSurf, 0, 0) + cairo_paint(crp) + cairo_restore(crp) + +proc onKeyPressed(ctrl: pointer; keyval, keycode: guint; state: guint; + data: pointer): gboolean {.cdecl.} = + modState = gdkModsToSet(state) + var ev = Event(kind: evKeyDown, key: translateKeyval(keyval), mods: modState) + pushEvent ev + G_FALSE + +proc onKeyReleased(ctrl: pointer; keyval, keycode: guint; state: guint; + data: pointer): gboolean {.cdecl.} = + modState = gdkModsToSet(state) + var ev = Event(kind: evKeyUp, key: translateKeyval(keyval), mods: modState) + pushEvent ev + G_FALSE + +proc onImCommit(ctx: pointer; str: cstring; data: pointer) {.cdecl.} = + if str != nil: + enqueueTextFromUtf8($str) + +proc onMotion(ctrl: pointer; x, y: gdouble; data: pointer) {.cdecl.} = + syncModsFromController(ctrl) + var ev = Event(kind: evMouseMove, x: int(x), y: int(y), mods: modState) + pushEvent ev + +proc onScroll(ctrl: pointer; dx, dy: gdouble; data: pointer): gboolean {.cdecl.} = + syncModsFromController(ctrl) + pushEvent(Event(kind: evMouseWheel, x: int(-dx), y: int(-dy), mods: modState)) + G_TRUE + +proc onFocusEnter(ctrl: pointer; data: pointer) {.cdecl.} = + pushEvent(Event(kind: evWindowFocusGained)) + +proc onFocusLeave(ctrl: pointer; data: pointer) {.cdecl.} = + pushEvent(Event(kind: evWindowFocusLost)) + +proc onClickPressed(gesture: pointer; nPress: gint; x, y: gdouble; data: pointer) {.cdecl.} = + let btn = gtk_gesture_single_get_current_button(gesture) + var b = mbLeft + if btn == GDK_BUTTON_SECONDARY: b = mbRight + elif btn == GDK_BUTTON_MIDDLE: b = mbMiddle + var ev = Event(kind: evMouseDown, x: int(x), y: int(y), button: b, + clicks: nPress.int) + pushEvent ev + +proc onClickReleased(gesture: pointer; nPress: gint; x, y: gdouble; data: pointer) {.cdecl.} = + let btn = gtk_gesture_single_get_current_button(gesture) + var b = mbLeft + if btn == GDK_BUTTON_SECONDARY: b = mbRight + elif btn == GDK_BUTTON_MIDDLE: b = mbMiddle + pushEvent(Event(kind: evMouseUp, x: int(x), y: int(y), button: b)) + +proc g_main_loop_new(ctx: pointer, isRunning: gboolean): pointer {.importc, nodecl, cdecl.} +proc g_main_loop_run(loop: pointer) {.importc, nodecl, cdecl.} +proc g_main_loop_quit(loop: pointer) {.importc, nodecl, cdecl.} +proc g_main_loop_unref(loop: pointer) {.importc, nodecl, cdecl.} + +proc clipboardReadCb(sourceObj: ptr GObject; res: ptr GAsyncResult; user: pointer) {.cdecl.} = + var err: ptr GError + let clip = cast[pointer](sourceObj) + let tres = cast[pointer](res) + let t = gdk_clipboard_read_text_finish(clip, tres, addr err) + clipboardBuf = if t != nil: $t else: "" + if t != nil: + g_free(cast[pointer](t)) + if err != nil: + g_error_free(err) + if clipboardLoop != nil: + g_main_loop_quit(clipboardLoop) + +proc readClipboardSync(): string = + let disp = gdk_display_get_default() + if disp == nil: return "" + let clip = gdk_display_get_clipboard(disp) + if clip == nil: return "" + clipboardBuf = "" + let loop = g_main_loop_new(nil, G_FALSE) + clipboardLoop = loop + gdk_clipboard_read_text_async(clip, nil, clipboardReadCb, nil) + g_main_loop_run(loop) + g_main_loop_unref(loop) + clipboardLoop = nil + result = clipboardBuf + +# --- Screen hooks --- + +proc gtkCreateWindow(layout: var ScreenLayout) = + if win != nil: + let da = cast[ptr GtkDrawingArea](drawingArea) + layout.width = max(1, gtk_drawing_area_get_content_width(da).int) + layout.height = max(1, gtk_drawing_area_get_content_height(da).int) + return + if gtk_init_check() == G_FALSE: + quit("GTK4 init failed") + win = gtk_window_new() + gtk_window_set_title(win, "NimEdit") + gtk_window_set_default_size(win, gint(layout.width), gint(layout.height)) + drawingArea = gtk_drawing_area_new() + gtk_window_set_child(win, drawingArea) + let da = cast[ptr GtkDrawingArea](drawingArea) + ## GtkDrawingArea content size defaults to 0; draw/blit is a no-op until set. + gtk_drawing_area_set_content_width(da, gint(layout.width)) + gtk_drawing_area_set_content_height(da, gint(layout.height)) + gtk_widget_set_hexpand(drawingArea, G_TRUE) + gtk_widget_set_vexpand(drawingArea, G_TRUE) + gtk_drawing_area_set_draw_func(drawingArea, onDraw, nil, nil) + discard g_signal_connect_data(drawingArea, "resize", + cast[GCallback](onResize), nil, nil, 0) + discard g_signal_connect_data(win, "close-request", + cast[GCallback](onCloseRequest), nil, nil, 0) + let keyc = gtk_event_controller_key_new() + gtk_widget_add_controller(drawingArea, keyc) + discard g_signal_connect_data(keyc, "key-pressed", + cast[GCallback](onKeyPressed), nil, nil, 0) + discard g_signal_connect_data(keyc, "key-released", + cast[GCallback](onKeyReleased), nil, nil, 0) + imContext = gtk_im_multicontext_new() + gtk_im_context_set_client_widget(imContext, drawingArea) + gtk_event_controller_key_set_im_context(keyc, imContext) + discard g_signal_connect_data(imContext, "commit", + cast[GCallback](onImCommit), nil, nil, 0) + let motion = gtk_event_controller_motion_new() + gtk_widget_add_controller(drawingArea, motion) + discard g_signal_connect_data(motion, "motion", + cast[GCallback](onMotion), nil, nil, 0) + let scroll = gtk_event_controller_scroll_new(GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES) + gtk_widget_add_controller(drawingArea, scroll) + discard g_signal_connect_data(scroll, "scroll", + cast[GCallback](onScroll), nil, nil, 0) + let focusc = gtk_event_controller_focus_new() + gtk_widget_add_controller(drawingArea, focusc) + discard g_signal_connect_data(focusc, "enter", + cast[GCallback](onFocusEnter), nil, nil, 0) + discard g_signal_connect_data(focusc, "leave", + cast[GCallback](onFocusLeave), nil, nil, 0) + let click = gtk_gesture_click_new() + gtk_gesture_single_set_button(click, 0) + gtk_widget_add_controller(drawingArea, click) + discard g_signal_connect_data(click, "pressed", + cast[GCallback](onClickPressed), nil, nil, 0) + discard g_signal_connect_data(click, "released", + cast[GCallback](onClickReleased), nil, nil, 0) + gtk_window_present(win) + var guard = 0 + while gtk_drawing_area_get_content_width(da) <= 0 and guard < 5000: + discard g_main_context_iteration(nil, G_FALSE) + inc guard + layout.width = max(1, gtk_drawing_area_get_content_width(da).int) + layout.height = max(1, gtk_drawing_area_get_content_height(da).int) + layout.scaleX = 1 + layout.scaleY = 1 + recreateBacking(layout.width, layout.height) + gtk_widget_set_focusable(drawingArea, G_TRUE) + gtk_widget_grab_focus(drawingArea) + gtk_im_context_focus_in(imContext) + +proc gtkRefresh() = + if backingSurf != nil: + cairo_surface_flush(backingSurf) + if drawingArea != nil: + gtk_widget_queue_draw(drawingArea) + +proc gtkSaveState() = discard +proc gtkRestoreState() = discard + +proc gtkSetClipRect(r: basetypes.Rect) = + ensureBackingCr() + if backingCr == nil: return + cairo_reset_clip(backingCr) + cairo_rectangle(backingCr, gdouble(r.x), gdouble(r.y), gdouble(r.w), gdouble(r.h)) + cairo_clip(backingCr) + +proc gtkOpenFont(path: string; size: int; metrics: var FontMetrics): Font = + discard FcInit() + var pat = FcPatternCreate() + if pat == nil: + return Font(0) + if FcPatternAddString(pat, FC_FILE, cstring(path)) == G_FALSE: + FcPatternDestroy(pat) + return Font(0) + discard FcConfigSubstitute(nil, pat, FcMatchPattern) + FcDefaultSubstitute(pat) + var fcRes: cint + let matched = nimedit_fc_font_match(pat, addr fcRes) + FcPatternDestroy(pat) + if matched == nil: + return Font(0) + let desc = pango_fc_font_description_from_pattern(matched, G_TRUE) + FcPatternDestroy(matched) + if desc == nil: + return Font(0) + pango_font_description_set_absolute_size(desc, gint(size * PANGO_SCALE)) + let surf = cairo_image_surface_create(0, 8, 8) + let cr = cairo_create(surf) + let layout = pango_cairo_create_layout(cr) + pango_layout_set_font_description(layout, desc) + pango_layout_set_text(layout, "Mg", -1) + let pctx = pango_layout_get_context(layout) + let fmap = pango_context_get_font_map(pctx) + let font = pango_font_map_load_font(fmap, pctx, desc) + if font != nil: + let m = pango_font_get_metrics(font, nil) + if m != nil: + metrics.ascent = pango_font_metrics_get_ascent(m) div PANGO_SCALE + metrics.descent = pango_font_metrics_get_descent(m) div PANGO_SCALE + metrics.lineHeight = pango_font_metrics_get_height(m) div PANGO_SCALE + pango_font_metrics_unref(m) + g_object_unref(font) + if metrics.lineHeight <= 0: + var tw, th: gint + pango_layout_get_pixel_size(layout, addr tw, addr th) + metrics.lineHeight = th.int + metrics.ascent = int(th.float64 * 0.75) + metrics.descent = th.int - metrics.ascent + g_object_unref_layout(layout) + cairo_destroy(cr) + cairo_surface_destroy(surf) + fonts.add FontSlot(desc: desc, metrics: metrics) + result = Font(fonts.len) + +proc gtkCloseFont(f: Font) = + let idx = f.int - 1 + if idx >= 0 and idx < fonts.len and fonts[idx].desc != nil: + pango_font_description_free(fonts[idx].desc) + fonts[idx].desc = nil + +proc gtkMeasureText(f: Font; text: string): TextExtent = + ensureBackingCr() + let desc = getDesc(f) + if desc == nil or text.len == 0: + return TextExtent() + let cr = backingCr + if cr == nil: + return TextExtent() + let layout = pango_cairo_create_layout(cr) + pango_layout_set_font_description(layout, desc) + pango_layout_set_text(layout, cstring(text), -1) + var w, h: gint + pango_layout_get_pixel_size(layout, addr w, addr h) + g_object_unref_layout(layout) + result = TextExtent(w: w.int, h: h.int) + +proc gtkDrawText(f: Font; x, y: int; text: string; fg, bg: screen.Color): TextExtent = + ensureBackingCr() + let desc = getDesc(f) + if desc == nil or text.len == 0 or backingCr == nil: + return + let ext = gtkMeasureText(f, text) + cairo_save(backingCr) + cairo_set_source_rgba(backingCr, + gdouble(bg.r) / 255.0, gdouble(bg.g) / 255.0, gdouble(bg.b) / 255.0, + gdouble(bg.a) / 255.0) + cairo_rectangle(backingCr, gdouble(x), gdouble(y), gdouble(ext.w), gdouble(ext.h)) + cairo_fill(backingCr) + let layout = pango_cairo_create_layout(backingCr) + pango_layout_set_font_description(layout, desc) + pango_layout_set_text(layout, cstring(text), -1) + cairo_set_source_rgba(backingCr, + gdouble(fg.r) / 255.0, gdouble(fg.g) / 255.0, gdouble(fg.b) / 255.0, + gdouble(fg.a) / 255.0) + pango_cairo_update_layout(backingCr, layout) + cairo_move_to(backingCr, gdouble(x), gdouble(y)) + pango_cairo_show_layout(backingCr, layout) + g_object_unref_layout(layout) + cairo_restore(backingCr) + result = ext + +proc gtkGetFontMetrics(f: Font): FontMetrics = + let idx = f.int - 1 + if idx >= 0 and idx < fonts.len: fonts[idx].metrics + else: FontMetrics() + +proc gtkFillRect(r: basetypes.Rect; color: screen.Color) = + ensureBackingCr() + if backingCr == nil: return + cairo_save(backingCr) + cairo_set_source_rgba(backingCr, + gdouble(color.r) / 255.0, gdouble(color.g) / 255.0, gdouble(color.b) / 255.0, + gdouble(color.a) / 255.0) + cairo_rectangle(backingCr, gdouble(r.x), gdouble(r.y), gdouble(r.w), gdouble(r.h)) + cairo_fill(backingCr) + cairo_restore(backingCr) + +proc gtkDrawLine(x1, y1, x2, y2: int; color: screen.Color) = + ensureBackingCr() + if backingCr == nil: return + cairo_save(backingCr) + cairo_set_source_rgba(backingCr, + gdouble(color.r) / 255.0, gdouble(color.g) / 255.0, gdouble(color.b) / 255.0, + gdouble(color.a) / 255.0) + cairo_set_line_width(backingCr, 1) + cairo_move_to(backingCr, gdouble(x1), gdouble(y1)) + cairo_line_to(backingCr, gdouble(x2), gdouble(y2)) + cairo_stroke(backingCr) + cairo_restore(backingCr) + +proc gtkDrawPoint(x, y: int; color: screen.Color) = + gtkFillRect(rect(x, y, 1, 1), color) + +proc gtkSetCursor(c: CursorKind) = + if drawingArea == nil: return + let name = case c + of curDefault, curArrow: "default" + of curIbeam: "text" + of curWait: "wait" + of curCrosshair: "crosshair" + of curHand: "pointer" + of curSizeNS: "ns-resize" + of curSizeWE: "ew-resize" + let cur = gdk_cursor_new_from_name(cstring(name), nil) + if cur != nil: + gtk_widget_set_cursor(drawingArea, cur) + g_object_unref(cur) + +proc gtkSetWindowTitle(title: string) = + if win != nil: + gtk_window_set_title(win, cstring(title)) + +# --- Input hooks --- + +proc pumpGtk() = + while g_main_context_iteration(nil, G_FALSE) != G_FALSE: + discard + +proc gtkPollEvent(e: var Event): bool = + pumpGtk() + if eventQueue.len > 0: + e = eventQueue[0] + eventQueue.delete(0) + return true + false + +proc gtkWaitEvent(e: var Event; timeoutMs: int): bool = + if gtkPollEvent(e): + return true + if timeoutMs < 0: + while true: + discard g_main_context_iteration(nil, G_TRUE) + if gtkPollEvent(e): + return true + elif timeoutMs == 0: + return false + else: + let t0 = g_get_monotonic_time() div 1000 + while g_get_monotonic_time() div 1000 - t0 < timeoutMs: + discard g_main_context_iteration(nil, G_FALSE) + if gtkPollEvent(e): + return true + g_usleep(5000) + return gtkPollEvent(e) + +proc gtkGetClipboardText(): string = + readClipboardSync() + +proc gtkPutClipboardText(text: string) = + let disp = gdk_display_get_default() + if disp == nil: return + let clip = gdk_display_get_clipboard(disp) + if clip != nil: + gdk_clipboard_set_text(clip, cstring(text)) + +proc gtkGetModState(): set[Modifier] = + modState + +proc gtkGetTicks(): uint32 = + uint32(g_get_monotonic_time() div 1000) + +proc gtkDelay(ms: uint32) = + g_usleep(culong(ms) * 1000) + +proc gtkStartTextInput() = + if drawingArea != nil: + gtk_widget_grab_focus(drawingArea) + if imContext != nil: + gtk_im_context_focus_in(imContext) + +proc gtkQuitRequest() = + if win != nil: + gtk_window_destroy(win) + win = nil + drawingArea = nil + if backingCr != nil: + cairo_destroy(backingCr) + backingCr = nil + if backingSurf != nil: + cairo_surface_destroy(backingSurf) + backingSurf = nil + if imContext != nil: + g_object_unref(imContext) + imContext = nil + +proc initGtk4Driver*() = + createWindowHook = gtkCreateWindow + refreshHook = gtkRefresh + saveStateHook = gtkSaveState + restoreStateHook = gtkRestoreState + setClipRectHook = gtkSetClipRect + openFontHook = gtkOpenFont + closeFontHook = gtkCloseFont + measureTextHook = gtkMeasureText + drawTextHook = gtkDrawText + getFontMetricsHook = gtkGetFontMetrics + fillRectHook = gtkFillRect + drawLineHook = gtkDrawLine + drawPointHook = gtkDrawPoint + setCursorHook = gtkSetCursor + setWindowTitleHook = gtkSetWindowTitle + pollEventHook = gtkPollEvent + waitEventHook = gtkWaitEvent + getClipboardTextHook = gtkGetClipboardText + putClipboardTextHook = gtkPutClipboardText + getModStateHook = gtkGetModState + getTicksHook = gtkGetTicks + delayHook = gtkDelay + startTextInputHook = gtkStartTextInput + quitRequestHook = gtkQuitRequest diff --git a/app/nimedit.nim b/app/nimedit.nim index 163c60d..c6c8acd 100644 --- a/app/nimedit.nim +++ b/app/nimedit.nim @@ -9,7 +9,9 @@ import exitprocs] from parseutils import parseInt import basetypes, screen, input -when defined(sdl2): +when defined(gtk4): + import gtk4_driver +elif defined(sdl2): import sdl2_driver else: import sdl3_driver @@ -1160,7 +1162,9 @@ proc mainProc(ed: Editor) = -when defined(sdl2): +when defined(gtk4): + initGtk4Driver() +elif defined(sdl2): initSdl2Driver() else: initSdl3Driver() diff --git a/app/styles.nim b/app/styles.nim index 66e6dbe..4181910 100644 --- a/app/styles.nim +++ b/app/styles.nim @@ -79,11 +79,11 @@ proc fatal*(msg: string) {.noReturn.} = proc parseColor*(hex: string): Color = let x = parseHexInt(hex) - color(uint8(x shr 16 and 0xff), uint8(x shr 8 and 0xff), uint8(x and 0xff), 0) + color(uint8(x shr 16 and 0xff), uint8(x shr 8 and 0xff), uint8(x and 0xff), 255) proc colorFromInt*(x: BiggestInt): Color = let x = x.int - color(uint8(x shr 16 and 0xff), uint8(x shr 8 and 0xff), uint8(x and 0xff), 0) + color(uint8(x shr 16 and 0xff), uint8(x shr 8 and 0xff), uint8(x and 0xff), 255) proc openFontFromPath(p: Path; s: byte): Font {.inline.} = var metrics: FontMetrics From 405b86e1c22e2b979387d2ead1435d8c807c7f86 Mon Sep 17 00:00:00 2001 From: Araq Date: Sun, 12 Apr 2026 08:01:41 +0200 Subject: [PATCH 02/14] Added cocoa driver --- app/cocoa_backend.m | 909 +++++++++++++++++++++++++++++++++++++++++++ app/cocoa_driver.nim | 300 ++++++++++++++ app/console.nim | 1 + app/nimedit.nim | 8 +- 4 files changed, 1216 insertions(+), 2 deletions(-) create mode 100644 app/cocoa_backend.m create mode 100644 app/cocoa_driver.nim diff --git a/app/cocoa_backend.m b/app/cocoa_backend.m new file mode 100644 index 0000000..47e7d11 --- /dev/null +++ b/app/cocoa_backend.m @@ -0,0 +1,909 @@ +/* cocoa_backend.m – Cocoa/AppKit backend for NimEdit. + * + * Exposes a flat C API that cocoa_driver.nim imports. + * Uses Core Graphics for drawing, Core Text for fonts, + * NSView subclass for events. + * + * Compile: included automatically via {.compile.} in cocoa_driver.nim + * Link: -framework Cocoa -framework CoreText -framework CoreGraphics -framework QuartzCore + */ + +#import +#import +#import +#include + +/* ---- Event queue (ring buffer) ---------------------------------------- */ + +enum { + NE_NONE = 0, + NE_KEY_DOWN, NE_KEY_UP, NE_TEXT_INPUT, + NE_MOUSE_DOWN, NE_MOUSE_UP, NE_MOUSE_MOVE, NE_MOUSE_WHEEL, + NE_WINDOW_RESIZE, NE_WINDOW_CLOSE, + NE_WINDOW_FOCUS_GAINED, NE_WINDOW_FOCUS_LOST, + NE_QUIT +}; + +enum { + NE_MOD_SHIFT = 1, + NE_MOD_CTRL = 2, + NE_MOD_ALT = 4, + NE_MOD_GUI = 8 +}; + +enum { + NE_MB_LEFT = 0, + NE_MB_RIGHT = 1, + NE_MB_MIDDLE = 2 +}; + +typedef struct { + int kind; + int key; /* NimEdit KeyCode ordinal */ + int mods; /* bitmask of NE_MOD_* */ + char text[4]; /* UTF-8 codepoint for NE_TEXT_INPUT */ + int x, y; + int xrel, yrel; + int button; /* NE_MB_* */ + int buttons; /* bitmask: 1=left, 2=right, 4=middle */ + int clicks; +} NEEvent; + +#define EVENT_QUEUE_SIZE 256 +static NEEvent eventQueue[EVENT_QUEUE_SIZE]; +static int eqHead = 0, eqTail = 0; + +static void pushEvent(NEEvent ev) { + int next = (eqHead + 1) % EVENT_QUEUE_SIZE; + if (next == eqTail) return; /* queue full, drop */ + eventQueue[eqHead] = ev; + eqHead = next; +} + +int cocoa_pollEvent(NEEvent *out) { + /* Pump the run loop briefly so Cocoa delivers events */ + @autoreleasepool { + NSEvent *ev; + while ((ev = [NSApp nextEventMatchingMask:NSEventMaskAny + untilDate:nil + inMode:NSDefaultRunLoopMode + dequeue:YES]) != nil) { + [NSApp sendEvent:ev]; + [NSApp updateWindows]; + } + } + if (eqTail == eqHead) { + out->kind = NE_NONE; + return 0; + } + *out = eventQueue[eqTail]; + eqTail = (eqTail + 1) % EVENT_QUEUE_SIZE; + return 1; +} + +int cocoa_waitEvent(NEEvent *out, int timeoutMs) { + @autoreleasepool { + NSDate *deadline = (timeoutMs < 0) + ? [NSDate distantFuture] + : [NSDate dateWithTimeIntervalSinceNow:timeoutMs / 1000.0]; + NSEvent *ev = [NSApp nextEventMatchingMask:NSEventMaskAny + untilDate:deadline + inMode:NSDefaultRunLoopMode + dequeue:YES]; + if (ev) { + [NSApp sendEvent:ev]; + [NSApp updateWindows]; + /* pump remaining */ + while ((ev = [NSApp nextEventMatchingMask:NSEventMaskAny + untilDate:nil + inMode:NSDefaultRunLoopMode + dequeue:YES]) != nil) { + [NSApp sendEvent:ev]; + [NSApp updateWindows]; + } + } + } + if (eqTail == eqHead) { + out->kind = NE_NONE; + return 0; + } + *out = eventQueue[eqTail]; + eqTail = (eqTail + 1) % EVENT_QUEUE_SIZE; + return 1; +} + +/* ---- Modifier helpers ------------------------------------------------- */ + +static int translateModifiers(NSEventModifierFlags flags) { + int m = 0; + if (flags & NSEventModifierFlagShift) m |= NE_MOD_SHIFT; + if (flags & NSEventModifierFlagControl) m |= NE_MOD_CTRL; + if (flags & NSEventModifierFlagOption) m |= NE_MOD_ALT; + if (flags & NSEventModifierFlagCommand) m |= NE_MOD_GUI; + return m; +} + +int cocoa_getModState(void) { + NSEventModifierFlags flags = [NSEvent modifierFlags]; + return translateModifiers(flags); +} + +/* ---- Key translation -------------------------------------------------- */ + +/* Returns NimEdit KeyCode ordinal. Must match input.nim KeyCode enum. */ +static int translateKeyCode(unsigned short kc) { + switch (kc) { + case 0x00: return 1; /* keyA */ + case 0x0B: return 2; /* keyB */ + case 0x08: return 3; /* keyC */ + case 0x02: return 4; /* keyD */ + case 0x0E: return 5; /* keyE */ + case 0x03: return 6; /* keyF */ + case 0x05: return 7; /* keyG */ + case 0x04: return 8; /* keyH */ + case 0x22: return 9; /* keyI */ + case 0x26: return 10; /* keyJ */ + case 0x28: return 11; /* keyK */ + case 0x25: return 12; /* keyL */ + case 0x2E: return 13; /* keyM */ + case 0x2D: return 14; /* keyN */ + case 0x1F: return 15; /* keyO */ + case 0x23: return 16; /* keyP */ + case 0x0C: return 17; /* keyQ */ + case 0x0F: return 18; /* keyR */ + case 0x01: return 19; /* keyS */ + case 0x11: return 20; /* keyT */ + case 0x20: return 21; /* keyU */ + case 0x09: return 22; /* keyV */ + case 0x0D: return 23; /* keyW */ + case 0x07: return 24; /* keyX */ + case 0x10: return 25; /* keyY */ + case 0x06: return 26; /* keyZ */ + case 0x12: return 28; /* key1 -- note: key0 = 27, key1 = 28 .. key9 = 36 */ + case 0x13: return 29; /* key2 */ + case 0x14: return 30; /* key3 */ + case 0x15: return 31; /* key4 */ + case 0x17: return 32; /* key5 */ + case 0x16: return 33; /* key6 */ + case 0x1A: return 34; /* key7 */ + case 0x1C: return 35; /* key8 */ + case 0x19: return 36; /* key9 */ + case 0x1D: return 27; /* key0 */ + case 0x7A: return 37; /* keyF1 */ + case 0x78: return 38; /* keyF2 */ + case 0x63: return 39; /* keyF3 */ + case 0x76: return 40; /* keyF4 */ + case 0x60: return 41; /* keyF5 */ + case 0x61: return 42; /* keyF6 */ + case 0x62: return 43; /* keyF7 */ + case 0x64: return 44; /* keyF8 */ + case 0x65: return 45; /* keyF9 */ + case 0x6D: return 46; /* keyF10 */ + case 0x67: return 47; /* keyF11 */ + case 0x6F: return 48; /* keyF12 */ + case 0x24: return 49; /* keyEnter */ + case 0x31: return 50; /* keySpace */ + case 0x35: return 51; /* keyEsc */ + case 0x30: return 52; /* keyTab */ + case 0x33: return 53; /* keyBackspace */ + case 0x75: return 54; /* keyDelete */ + case 0x72: return 55; /* keyInsert (Help key on Mac) */ + case 0x7B: return 56; /* keyLeft */ + case 0x7C: return 57; /* keyRight */ + case 0x7E: return 58; /* keyUp */ + case 0x7D: return 59; /* keyDown */ + case 0x74: return 60; /* keyPageUp */ + case 0x79: return 61; /* keyPageDown */ + case 0x73: return 62; /* keyHome */ + case 0x77: return 63; /* keyEnd */ + case 0x39: return 64; /* keyCapslock */ + case 0x2B: return 65; /* keyComma */ + case 0x2F: return 66; /* keyPeriod */ + default: return 0; /* keyNone */ + } +} + +/* ---- Font management -------------------------------------------------- */ + +#define MAX_FONTS 64 + +typedef struct { + CTFontRef font; + int ascent, descent, lineHeight; +} FontSlot; + +static FontSlot fonts[MAX_FONTS]; +static int fontCount = 0; + +int cocoa_openFont(const char *path, int size, + int *outAscent, int *outDescent, int *outLineHeight) { + if (fontCount >= MAX_FONTS) return 0; + + /* Create font from file path */ + CFStringRef cfPath = CFStringCreateWithCString(NULL, path, kCFStringEncodingUTF8); + CFURLRef url = CFURLCreateWithFileSystemPath(NULL, cfPath, kCFURLPOSIXPathStyle, false); + CGDataProviderRef provider = CGDataProviderCreateWithURL(url); + CTFontRef ctFont = NULL; + + if (provider) { + CGFontRef cgFont = CGFontCreateWithDataProvider(provider); + if (cgFont) { + ctFont = CTFontCreateWithGraphicsFont(cgFont, (CGFloat)size, NULL, NULL); + CGFontRelease(cgFont); + } + CGDataProviderRelease(provider); + } + CFRelease(url); + CFRelease(cfPath); + + if (!ctFont) return 0; + + int idx = fontCount++; + fonts[idx].font = ctFont; + fonts[idx].ascent = (int)ceil(CTFontGetAscent(ctFont)); + fonts[idx].descent = (int)ceil(CTFontGetDescent(ctFont)); + fonts[idx].lineHeight = fonts[idx].ascent + fonts[idx].descent + + (int)ceil(CTFontGetLeading(ctFont)); + /* Ensure lineHeight is at least ascent + descent */ + if (fonts[idx].lineHeight < fonts[idx].ascent + fonts[idx].descent) + fonts[idx].lineHeight = fonts[idx].ascent + fonts[idx].descent; + + *outAscent = fonts[idx].ascent; + *outDescent = fonts[idx].descent; + *outLineHeight = fonts[idx].lineHeight; + return idx + 1; /* 1-based handle */ +} + +void cocoa_closeFont(int handle) { + int idx = handle - 1; + if (idx >= 0 && idx < fontCount && fonts[idx].font) { + CFRelease(fonts[idx].font); + fonts[idx].font = NULL; + } +} + +void cocoa_getFontMetrics(int handle, int *asc, int *desc, int *lh) { + int idx = handle - 1; + if (idx >= 0 && idx < fontCount && fonts[idx].font) { + *asc = fonts[idx].ascent; + *desc = fonts[idx].descent; + *lh = fonts[idx].lineHeight; + } else { + *asc = *desc = *lh = 0; + } +} + +/* Measure text width/height using Core Text */ +void cocoa_measureText(int handle, const char *text, int *outW, int *outH) { + int idx = handle - 1; + *outW = 0; *outH = 0; + if (idx < 0 || idx >= fontCount || !fonts[idx].font || !text || !text[0]) return; + + CFStringRef str = CFStringCreateWithCString(NULL, text, kCFStringEncodingUTF8); + if (!str) return; + + CFStringRef keys[] = { kCTFontAttributeName }; + CFTypeRef vals[] = { fonts[idx].font }; + CFDictionaryRef attrs = CFDictionaryCreate(NULL, + (const void **)keys, (const void **)vals, 1, + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + + CFAttributedStringRef attrStr = CFAttributedStringCreate(NULL, str, attrs); + CTLineRef line = CTLineCreateWithAttributedString(attrStr); + + CGRect bounds = CTLineGetImageBounds(line, NULL); + double width = CTLineGetTypographicBounds(line, NULL, NULL, NULL); + *outW = (int)ceil(width); + *outH = fonts[idx].lineHeight; + + CFRelease(line); + CFRelease(attrStr); + CFRelease(attrs); + CFRelease(str); +} + +/* ---- Backing bitmap context ------------------------------------------- */ + +static CGContextRef backingCtx = NULL; +static int backingW = 0, backingH = 0; +static CGFloat backingScale = 1.0; + +/* Clip rect stack (simple, max 32 deep) */ +#define MAX_CLIP_STACK 32 +static CGRect clipStack[MAX_CLIP_STACK]; +static int clipTop = 0; + +static void ensureBacking(int w, int h, CGFloat scale) { + if (backingCtx && backingW == w && backingH == h) return; + if (backingCtx) CGContextRelease(backingCtx); + + backingW = w; + backingH = h; + backingScale = scale; + + int pw = (int)(w * scale); + int ph = (int)(h * scale); + + CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); + backingCtx = CGBitmapContextCreate(NULL, pw, ph, 8, pw * 4, + cs, kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host); + CGColorSpaceRelease(cs); + + if (backingCtx) { + /* Scale so we can draw in point coordinates */ + CGContextScaleCTM(backingCtx, scale, scale); + /* Flip coordinate system: Core Graphics is bottom-up, we want top-down */ + CGContextTranslateCTM(backingCtx, 0, h); + CGContextScaleCTM(backingCtx, 1.0, -1.0); + CGContextSetShouldAntialias(backingCtx, true); + CGContextSetShouldSmoothFonts(backingCtx, true); + } +} + +/* ---- Drawing primitives ----------------------------------------------- */ + +void cocoa_fillRect(int x, int y, int w, int h, int r, int g, int b, int a) { + if (!backingCtx) return; + CGContextSetRGBFillColor(backingCtx, r/255.0, g/255.0, b/255.0, a/255.0); + CGContextFillRect(backingCtx, CGRectMake(x, y, w, h)); +} + +void cocoa_drawLine(int x1, int y1, int x2, int y2, int r, int g, int b, int a) { + if (!backingCtx) return; + CGContextSetRGBStrokeColor(backingCtx, r/255.0, g/255.0, b/255.0, a/255.0); + CGContextSetLineWidth(backingCtx, 1.0); + CGContextBeginPath(backingCtx); + CGContextMoveToPoint(backingCtx, x1 + 0.5, y1 + 0.5); + CGContextAddLineToPoint(backingCtx, x2 + 0.5, y2 + 0.5); + CGContextStrokePath(backingCtx); +} + +void cocoa_drawPoint(int x, int y, int r, int g, int b, int a) { + if (!backingCtx) return; + CGContextSetRGBFillColor(backingCtx, r/255.0, g/255.0, b/255.0, a/255.0); + CGContextFillRect(backingCtx, CGRectMake(x, y, 1, 1)); +} + +void cocoa_drawText(int fontHandle, int x, int y, const char *text, + int fgR, int fgG, int fgB, int fgA, + int bgR, int bgG, int bgB, int bgA, + int *outW, int *outH) { + *outW = 0; *outH = 0; + int idx = fontHandle - 1; + if (idx < 0 || idx >= fontCount || !fonts[idx].font || !backingCtx) return; + if (!text || !text[0]) return; + + CTFontRef ctFont = fonts[idx].font; + int lh = fonts[idx].lineHeight; + int asc = fonts[idx].ascent; + + /* Measure first for background fill */ + CFStringRef str = CFStringCreateWithCString(NULL, text, kCFStringEncodingUTF8); + if (!str) return; + + CGColorRef fgColor = CGColorCreateSRGB(fgR/255.0, fgG/255.0, fgB/255.0, fgA/255.0); + + CFStringRef keys[] = { kCTFontAttributeName, kCTForegroundColorAttributeName }; + CFTypeRef vals[] = { ctFont, fgColor }; + CFDictionaryRef attrs = CFDictionaryCreate(NULL, + (const void **)keys, (const void **)vals, 2, + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + + CFAttributedStringRef attrStr = CFAttributedStringCreate(NULL, str, attrs); + CTLineRef line = CTLineCreateWithAttributedString(attrStr); + + double width = CTLineGetTypographicBounds(line, NULL, NULL, NULL); + int tw = (int)ceil(width); + + /* Fill background */ + CGContextSetRGBFillColor(backingCtx, bgR/255.0, bgG/255.0, bgB/255.0, bgA/255.0); + CGContextFillRect(backingCtx, CGRectMake(x, y, tw, lh)); + + /* Draw text. + * Core Text draws bottom-up. Our context is flipped to top-down. + * We need to locally un-flip for Core Text rendering. */ + CGContextSaveGState(backingCtx); + /* Move to the text baseline position: + * In our flipped coords, y is top of the line, baseline is at y + ascent. + * We un-flip around the line center. */ + CGContextTranslateCTM(backingCtx, 0, y + lh); + CGContextScaleCTM(backingCtx, 1.0, -1.0); + /* Now in un-flipped local coords, baseline is at (x, descent) from bottom */ + CGFloat baseline = fonts[idx].descent + (lh - asc - fonts[idx].descent) * 0.5; + if (baseline < fonts[idx].descent) baseline = fonts[idx].descent; + CGContextSetTextPosition(backingCtx, x, baseline); + CTLineDraw(line, backingCtx); + CGContextRestoreGState(backingCtx); + + *outW = tw; + *outH = lh; + + CFRelease(line); + CFRelease(attrStr); + CFRelease(attrs); + CGColorRelease(fgColor); + CFRelease(str); +} + +void cocoa_setClipRect(int x, int y, int w, int h) { + if (!backingCtx) return; + CGContextRestoreGState(backingCtx); + CGContextSaveGState(backingCtx); + CGContextClipToRect(backingCtx, CGRectMake(x, y, w, h)); +} + +void cocoa_saveState(void) { + if (!backingCtx) return; + CGContextSaveGState(backingCtx); +} + +void cocoa_restoreState(void) { + if (!backingCtx) return; + CGContextRestoreGState(backingCtx); +} + +/* ---- Clipboard -------------------------------------------------------- */ + +const char *cocoa_getClipboardText(void) { + @autoreleasepool { + NSPasteboard *pb = [NSPasteboard generalPasteboard]; + NSString *s = [pb stringForType:NSPasteboardTypeString]; + if (!s) return ""; + /* Return a C string that persists until next call */ + static char *buf = NULL; + free(buf); + const char *utf8 = [s UTF8String]; + buf = strdup(utf8); + return buf; + } +} + +void cocoa_putClipboardText(const char *text) { + @autoreleasepool { + NSPasteboard *pb = [NSPasteboard generalPasteboard]; + [pb clearContents]; + [pb setString:[NSString stringWithUTF8String:text] + forType:NSPasteboardTypeString]; + } +} + +/* ---- Timing ----------------------------------------------------------- */ + +static uint64_t startTime = 0; +static mach_timebase_info_data_t timebaseInfo; + +uint32_t cocoa_getTicks(void) { + uint64_t elapsed = mach_absolute_time() - startTime; + uint64_t nanos = elapsed * timebaseInfo.numer / timebaseInfo.denom; + return (uint32_t)(nanos / 1000000); +} + +void cocoa_delay(uint32_t ms) { + [NSThread sleepForTimeInterval:ms / 1000.0]; +} + +/* ---- NSView subclass -------------------------------------------------- */ + +@interface NimEditView : NSView +@property (nonatomic) BOOL hasMarkedText; +@end + +@implementation NimEditView { + NSTrackingArea *_trackingArea; +} + +- (BOOL)acceptsFirstResponder { return YES; } +- (BOOL)canBecomeKeyView { return YES; } +- (BOOL)isFlipped { return YES; } + +- (void)updateTrackingAreas { + [super updateTrackingAreas]; + if (_trackingArea) [self removeTrackingArea:_trackingArea]; + _trackingArea = [[NSTrackingArea alloc] + initWithRect:self.bounds + options:NSTrackingMouseMoved | NSTrackingActiveInKeyWindow | + NSTrackingInVisibleRect + owner:self + userInfo:nil]; + [self addTrackingArea:_trackingArea]; +} + +- (void)drawRect:(NSRect)dirtyRect { + if (!backingCtx) return; + CGContextRef viewCtx = [[NSGraphicsContext currentContext] CGContext]; + CGImageRef img = CGBitmapContextCreateImage(backingCtx); + if (img) { + NSRect bounds = self.bounds; + /* Flip for drawing since view is flipped but CGImage is bottom-up */ + CGContextSaveGState(viewCtx); + CGContextTranslateCTM(viewCtx, 0, bounds.size.height); + CGContextScaleCTM(viewCtx, 1.0, -1.0); + CGContextDrawImage(viewCtx, CGRectMake(0, 0, bounds.size.width, bounds.size.height), img); + CGContextRestoreGState(viewCtx); + CGImageRelease(img); + } +} + +/* ---- Keyboard events ---- */ + +- (void)keyDown:(NSEvent *)event { + NEEvent e = {0}; + e.kind = NE_KEY_DOWN; + e.key = translateKeyCode(event.keyCode); + e.mods = translateModifiers(event.modifierFlags); + pushEvent(e); + + /* Only feed to interpretKeyEvents when the key can produce text input. + Skip for Ctrl/Cmd combos and special keys (arrows, backspace, etc.) + — the app handles those via evKeyDown already. */ + int m = e.mods; + if ((m & (NE_MOD_CTRL | NE_MOD_GUI)) == 0 && e.key == 0) { + [self interpretKeyEvents:@[event]]; + } else if ((m & (NE_MOD_CTRL | NE_MOD_GUI)) == 0) { + /* Known key but no modifier — still try for text (e.g. space, comma). + But skip keys that are purely control keys. */ + unsigned short kc = event.keyCode; + switch (kc) { + case 0x33: /* backspace */ + case 0x75: /* delete */ + case 0x35: /* escape */ + case 0x30: /* tab */ + case 0x24: /* return */ + case 0x7B: case 0x7C: case 0x7E: case 0x7D: /* arrows */ + case 0x74: case 0x79: case 0x73: case 0x77: /* pgup/pgdn/home/end */ + case 0x72: /* insert/help */ + case 0x7A: case 0x78: case 0x63: case 0x76: /* F1-F4 */ + case 0x60: case 0x61: case 0x62: case 0x64: /* F5-F8 */ + case 0x65: case 0x6D: case 0x67: case 0x6F: /* F9-F12 */ + break; + default: + [self interpretKeyEvents:@[event]]; + break; + } + } +} + +/* Suppress the system beep for unhandled key combos */ +- (void)doCommandBySelector:(SEL)selector { + /* intentionally empty — swallows the NSBeep that interpretKeyEvents + would otherwise trigger for keys without a binding */ +} + +- (void)keyUp:(NSEvent *)event { + NEEvent e = {0}; + e.kind = NE_KEY_UP; + e.key = translateKeyCode(event.keyCode); + e.mods = translateModifiers(event.modifierFlags); + pushEvent(e); +} + +- (void)flagsChanged:(NSEvent *)event { + /* Ignore standalone modifier key events */ +} + +/* ---- Mouse events ---- */ + +- (void)mouseDown:(NSEvent *)event { + NSPoint p = [self convertPoint:event.locationInWindow fromView:nil]; + NEEvent e = {0}; + e.kind = NE_MOUSE_DOWN; + e.button = NE_MB_LEFT; + e.x = (int)p.x; + e.y = (int)p.y; + e.clicks = (int)event.clickCount; + e.mods = translateModifiers(event.modifierFlags); + pushEvent(e); +} + +- (void)mouseUp:(NSEvent *)event { + NSPoint p = [self convertPoint:event.locationInWindow fromView:nil]; + NEEvent e = {0}; + e.kind = NE_MOUSE_UP; + e.button = NE_MB_LEFT; + e.x = (int)p.x; + e.y = (int)p.y; + e.mods = translateModifiers(event.modifierFlags); + pushEvent(e); +} + +- (void)rightMouseDown:(NSEvent *)event { + NSPoint p = [self convertPoint:event.locationInWindow fromView:nil]; + NEEvent e = {0}; + e.kind = NE_MOUSE_DOWN; + e.button = NE_MB_RIGHT; + e.x = (int)p.x; + e.y = (int)p.y; + e.clicks = (int)event.clickCount; + e.mods = translateModifiers(event.modifierFlags); + pushEvent(e); +} + +- (void)rightMouseUp:(NSEvent *)event { + NSPoint p = [self convertPoint:event.locationInWindow fromView:nil]; + NEEvent e = {0}; + e.kind = NE_MOUSE_UP; + e.button = NE_MB_RIGHT; + e.x = (int)p.x; + e.y = (int)p.y; + e.mods = translateModifiers(event.modifierFlags); + pushEvent(e); +} + +- (void)otherMouseDown:(NSEvent *)event { + NSPoint p = [self convertPoint:event.locationInWindow fromView:nil]; + NEEvent e = {0}; + e.kind = NE_MOUSE_DOWN; + e.button = NE_MB_MIDDLE; + e.x = (int)p.x; + e.y = (int)p.y; + e.clicks = (int)event.clickCount; + e.mods = translateModifiers(event.modifierFlags); + pushEvent(e); +} + +- (void)otherMouseUp:(NSEvent *)event { + NSPoint p = [self convertPoint:event.locationInWindow fromView:nil]; + NEEvent e = {0}; + e.kind = NE_MOUSE_UP; + e.button = NE_MB_MIDDLE; + e.x = (int)p.x; + e.y = (int)p.y; + e.mods = translateModifiers(event.modifierFlags); + pushEvent(e); +} + +static int currentButtons(NSEvent *event) { + NSUInteger pressed = [NSEvent pressedMouseButtons]; + int b = 0; + if (pressed & (1 << 0)) b |= 1; /* left */ + if (pressed & (1 << 1)) b |= 2; /* right */ + if (pressed & (1 << 2)) b |= 4; /* middle */ + return b; +} + +- (void)mouseMoved:(NSEvent *)event { + NSPoint p = [self convertPoint:event.locationInWindow fromView:nil]; + NEEvent e = {0}; + e.kind = NE_MOUSE_MOVE; + e.x = (int)p.x; + e.y = (int)p.y; + e.xrel = (int)event.deltaX; + e.yrel = (int)event.deltaY; + e.buttons = currentButtons(event); + e.mods = translateModifiers(event.modifierFlags); + pushEvent(e); +} + +- (void)mouseDragged:(NSEvent *)event { + [self mouseMoved:event]; +} + +- (void)rightMouseDragged:(NSEvent *)event { + [self mouseMoved:event]; +} + +- (void)otherMouseDragged:(NSEvent *)event { + [self mouseMoved:event]; +} + +- (void)scrollWheel:(NSEvent *)event { + NEEvent e = {0}; + e.kind = NE_MOUSE_WHEEL; + if (event.hasPreciseScrollingDeltas) { + /* Trackpad: pixel-level deltas, divide down to line-level. + Accumulate fractional remainder so slow swipes still register. */ + static double accumX = 0, accumY = 0; + accumX += event.scrollingDeltaX; + accumY += event.scrollingDeltaY; + e.x = (int)(accumX / 16.0); + e.y = (int)(accumY / 16.0); + accumX -= e.x * 16.0; + accumY -= e.y * 16.0; + if (e.x == 0 && e.y == 0) return; /* sub-line movement, wait */ + } else { + /* Discrete mouse wheel: already line-based */ + e.x = (int)event.scrollingDeltaX; + e.y = (int)event.scrollingDeltaY; + } + e.mods = translateModifiers(event.modifierFlags); + pushEvent(e); +} + +/* ---- NSTextInputClient (for text input / IME) ---- */ + +- (void)insertText:(id)string replacementRange:(NSRange)replacementRange { + NSString *s = ([string isKindOfClass:[NSAttributedString class]]) + ? [string string] : string; + const char *utf8 = [s UTF8String]; + if (!utf8) return; + + /* Send each character as a separate text input event */ + NSUInteger len = strlen(utf8); + NSUInteger i = 0; + while (i < len) { + NEEvent e = {0}; + e.kind = NE_TEXT_INPUT; + /* Copy one UTF-8 codepoint (1-4 bytes) */ + unsigned char c = (unsigned char)utf8[i]; + int cpLen = 1; + if (c >= 0xC0) cpLen = 2; + if (c >= 0xE0) cpLen = 3; + if (c >= 0xF0) cpLen = 4; + for (int j = 0; j < cpLen && j < 4 && (i + j) < len; j++) + e.text[j] = utf8[i + j]; + pushEvent(e); + i += cpLen; + } +} + +- (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange + replacementRange:(NSRange)replacementRange { + self.hasMarkedText = ([(NSString *)string length] > 0); +} + +- (void)unmarkText { self.hasMarkedText = NO; } +- (BOOL)hasMarkedText { return _hasMarkedText; } +- (NSRange)markedRange { return NSMakeRange(NSNotFound, 0); } +- (NSRange)selectedRange { return NSMakeRange(0, 0); } +- (NSRect)firstRectForCharacterRange:(NSRange)range + actualRange:(NSRangePointer)actual { + return NSMakeRect(0, 0, 0, 0); +} +- (NSUInteger)characterIndexForPoint:(NSPoint)point { return NSNotFound; } +- (NSArray *)validAttributesForMarkedText { return @[]; } +- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range + actualRange:(NSRangePointer)actual { + return nil; +} + +@end + +/* ---- Window delegate -------------------------------------------------- */ + +@interface NimEditWindowDelegate : NSObject +@end + +@implementation NimEditWindowDelegate + +- (BOOL)windowShouldClose:(NSWindow *)sender { + NEEvent e = {0}; + e.kind = NE_WINDOW_CLOSE; + pushEvent(e); + return NO; /* let the app decide */ +} + +- (void)windowDidResize:(NSNotification *)n { + NSWindow *w = n.object; + NSRect frame = [[w contentView] frame]; + CGFloat scale = [w backingScaleFactor]; + ensureBacking((int)frame.size.width, (int)frame.size.height, scale); + + NEEvent e = {0}; + e.kind = NE_WINDOW_RESIZE; + e.x = (int)frame.size.width; + e.y = (int)frame.size.height; + pushEvent(e); +} + +- (void)windowDidBecomeKey:(NSNotification *)n { + NEEvent e = {0}; + e.kind = NE_WINDOW_FOCUS_GAINED; + pushEvent(e); +} + +- (void)windowDidResignKey:(NSNotification *)n { + NEEvent e = {0}; + e.kind = NE_WINDOW_FOCUS_LOST; + pushEvent(e); +} + +@end + +/* ---- Window management ------------------------------------------------ */ + +static NSWindow *mainWindow = nil; +static NimEditView *mainView = nil; +static NimEditWindowDelegate *winDelegate = nil; + +void cocoa_createWindow(int w, int h, int *outW, int *outH, + int *outScaleX, int *outScaleY) { + @autoreleasepool { + /* Initialize timing */ + mach_timebase_info(&timebaseInfo); + startTime = mach_absolute_time(); + + /* Set up NSApplication */ + [NSApplication sharedApplication]; + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + + /* Create menu bar (minimal: just app menu with Quit) */ + NSMenu *menubar = [[NSMenu alloc] init]; + NSMenuItem *appMenuItem = [[NSMenuItem alloc] init]; + [menubar addItem:appMenuItem]; + [NSApp setMainMenu:menubar]; + + NSMenu *appMenu = [[NSMenu alloc] init]; + NSMenuItem *quitItem = [[NSMenuItem alloc] + initWithTitle:@"Quit NimEdit" + action:@selector(terminate:) + keyEquivalent:@"q"]; + [appMenu addItem:quitItem]; + [appMenuItem setSubmenu:appMenu]; + + /* Create window */ + NSRect rect = NSMakeRect(100, 100, w, h); + NSUInteger style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; + mainWindow = [[NSWindow alloc] initWithContentRect:rect + styleMask:style + backing:NSBackingStoreBuffered + defer:NO]; + [mainWindow setTitle:@"NimEdit"]; + [mainWindow setAcceptsMouseMovedEvents:YES]; + + /* Create view */ + mainView = [[NimEditView alloc] initWithFrame:rect]; + [mainWindow setContentView:mainView]; + [mainWindow makeFirstResponder:mainView]; + + /* Window delegate */ + winDelegate = [[NimEditWindowDelegate alloc] init]; + [mainWindow setDelegate:winDelegate]; + + /* Show */ + [mainWindow makeKeyAndOrderFront:nil]; + [NSApp activateIgnoringOtherApps:YES]; + + /* Finish launch so events flow */ + [NSApp finishLaunching]; + + /* Set up backing bitmap */ + CGFloat scale = [mainWindow backingScaleFactor]; + NSRect contentFrame = [mainView frame]; + ensureBacking((int)contentFrame.size.width, (int)contentFrame.size.height, scale); + + *outW = (int)contentFrame.size.width; + *outH = (int)contentFrame.size.height; + *outScaleX = (int)scale; + *outScaleY = (int)scale; + } +} + +void cocoa_refresh(void) { + @autoreleasepool { + [mainView setNeedsDisplay:YES]; + [mainView displayIfNeeded]; + } +} + +void cocoa_setWindowTitle(const char *title) { + @autoreleasepool { + [mainWindow setTitle:[NSString stringWithUTF8String:title]]; + } +} + +void cocoa_setCursor(int kind) { + @autoreleasepool { + NSCursor *cur; + switch (kind) { + case 2: cur = [NSCursor IBeamCursor]; break; /* curIbeam */ + case 3: cur = [NSCursor arrowCursor]; break; /* curWait (no wait cursor, use arrow) */ + case 4: cur = [NSCursor crosshairCursor]; break; /* curCrosshair */ + case 5: cur = [NSCursor pointingHandCursor]; break; /* curHand */ + case 6: cur = [NSCursor resizeUpDownCursor]; break; /* curSizeNS */ + case 7: cur = [NSCursor resizeLeftRightCursor]; break; /* curSizeWE */ + default: cur = [NSCursor arrowCursor]; break; + } + [cur set]; + } +} + +void cocoa_startTextInput(void) { + /* NSTextInputClient is always active on our view */ +} + +void cocoa_quitRequest(void) { + NEEvent e = {0}; + e.kind = NE_QUIT; + pushEvent(e); +} diff --git a/app/cocoa_driver.nim b/app/cocoa_driver.nim new file mode 100644 index 0000000..1498ac9 --- /dev/null +++ b/app/cocoa_driver.nim @@ -0,0 +1,300 @@ +# Cocoa/AppKit backend driver for macOS. +# Sets all hooks from core/input and core/screen. +# +# Build: nim c -d:cocoa app/nimedit.nim +# Requires macOS 10.15+ (Catalina). No external dependencies. + +{.compile("cocoa_backend.m", "-fobjc-arc").} +{.passL: "-framework Cocoa -framework CoreText -framework CoreGraphics -framework QuartzCore".} + +{.emit: """ +typedef struct { + int kind; + int key; + int mods; + char text[4]; + int x, y; + int xrel, yrel; + int button; + int buttons; + int clicks; +} NEEvent; +""".} + +import basetypes, input, screen + +# --- C bindings to cocoa_backend.m --- +# All procs use {.importc.} with explicit C names to avoid Nim's +# identifier normalisation colliding with the Nim wrapper procs. + +type + NEEvent {.importc, nodecl.} = object + kind: cint + key: cint + mods: cint + text: array[4, char] + x, y: cint + xrel, yrel: cint + button: cint + buttons: cint + clicks: cint + +# Event kinds (must match cocoa_backend.m) +const + neNone = 0.cint + neKeyDown = 1.cint + neKeyUp = 2.cint + neTextInput = 3.cint + neMouseDown = 4.cint + neMouseUp = 5.cint + neMouseMove = 6.cint + neMouseWheel = 7.cint + neWindowResize = 8.cint + neWindowClose = 9.cint + neWindowFocusGained = 10.cint + neWindowFocusLost = 11.cint + neQuit = 12.cint + + neModShift = 1.cint + neModCtrl = 2.cint + neModAlt = 4.cint + neModGui = 8.cint + +proc cCreateWindow(w, h: cint; outW, outH, outScaleX, outScaleY: ptr cint) + {.importc: "cocoa_createWindow", cdecl.} +proc cRefresh() {.importc: "cocoa_refresh", cdecl.} +proc cPollEvent(ev: ptr NEEvent): cint {.importc: "cocoa_pollEvent", cdecl.} +proc cWaitEvent(ev: ptr NEEvent; timeoutMs: cint): cint {.importc: "cocoa_waitEvent", cdecl.} + +proc cOpenFont(path: cstring; size: cint; + outAsc, outDesc, outLH: ptr cint): cint {.importc: "cocoa_openFont", cdecl.} +proc cCloseFont(handle: cint) {.importc: "cocoa_closeFont", cdecl.} +proc cGetFontMetrics(handle: cint; asc, desc, lh: ptr cint) {.importc: "cocoa_getFontMetrics", cdecl.} +proc cMeasureText(handle: cint; text: cstring; + outW, outH: ptr cint) {.importc: "cocoa_measureText", cdecl.} +proc cDrawText(handle: cint; x, y: cint; text: cstring; + fgR, fgG, fgB, fgA: cint; + bgR, bgG, bgB, bgA: cint; + outW, outH: ptr cint) {.importc: "cocoa_drawText", cdecl.} + +proc cFillRect(x, y, w, h: cint; r, g, b, a: cint) {.importc: "cocoa_fillRect", cdecl.} +proc cDrawLine(x1, y1, x2, y2: cint; r, g, b, a: cint) {.importc: "cocoa_drawLine", cdecl.} +proc cDrawPoint(x, y: cint; r, g, b, a: cint) {.importc: "cocoa_drawPoint", cdecl.} + +proc cSetClipRect(x, y, w, h: cint) {.importc: "cocoa_setClipRect", cdecl.} +proc cSaveState() {.importc: "cocoa_saveState", cdecl.} +proc cRestoreState() {.importc: "cocoa_restoreState", cdecl.} + +proc cGetClipboardText(): cstring {.importc: "cocoa_getClipboardText", cdecl.} +proc cPutClipboardText(text: cstring) {.importc: "cocoa_putClipboardText", cdecl.} + +proc cGetModState(): cint {.importc: "cocoa_getModState", cdecl.} +proc cGetTicks(): uint32 {.importc: "cocoa_getTicks", cdecl.} +proc cDelay(ms: uint32) {.importc: "cocoa_delay", cdecl.} + +proc cSetCursor(kind: cint) {.importc: "cocoa_setCursor", cdecl.} +proc cSetWindowTitle(title: cstring) {.importc: "cocoa_setWindowTitle", cdecl.} +proc cStartTextInput() {.importc: "cocoa_startTextInput", cdecl.} +proc cQuitRequest() {.importc: "cocoa_quitRequest", cdecl.} + +# --- Hook implementations --- + +proc cocoaCreateWindow(layout: var ScreenLayout) = + var w, h, sx, sy: cint + cCreateWindow(layout.width.cint, layout.height.cint, + addr w, addr h, addr sx, addr sy) + layout.width = w + layout.height = h + layout.scaleX = sx + layout.scaleY = sy + +proc cocoaRefresh() = cRefresh() +proc cocoaSaveState() = cSaveState() +proc cocoaRestoreState() = cRestoreState() + +proc cocoaSetClipRect(r: Rect) = + cSetClipRect(r.x.cint, r.y.cint, r.w.cint, r.h.cint) + +proc cocoaOpenFont(path: string; size: int; + metrics: var FontMetrics): Font = + var asc, desc, lh: cint + let handle = cOpenFont(cstring(path), size.cint, + addr asc, addr desc, addr lh) + if handle == 0: return Font(0) + metrics.ascent = asc + metrics.descent = desc + metrics.lineHeight = lh + result = Font(handle) + +proc cocoaCloseFont(f: Font) = + cCloseFont(f.int.cint) + +proc cocoaMeasureText(f: Font; text: string): TextExtent = + if text == "": return TextExtent() + var w, h: cint + cMeasureText(f.int.cint, cstring(text), addr w, addr h) + result = TextExtent(w: w, h: h) + +proc cocoaDrawText(f: Font; x, y: int; text: string; + fg, bg: Color): TextExtent = + if text == "": return TextExtent() + var w, h: cint + cDrawText(f.int.cint, x.cint, y.cint, cstring(text), + fg.r.cint, fg.g.cint, fg.b.cint, fg.a.cint, + bg.r.cint, bg.g.cint, bg.b.cint, bg.a.cint, + addr w, addr h) + result = TextExtent(w: w, h: h) + +proc cocoaGetFontMetrics(f: Font): FontMetrics = + var asc, desc, lh: cint + cGetFontMetrics(f.int.cint, addr asc, addr desc, addr lh) + result = FontMetrics(ascent: asc, descent: desc, lineHeight: lh) + +proc cocoaFillRect(r: Rect; color: Color) = + cFillRect(r.x.cint, r.y.cint, r.w.cint, r.h.cint, + color.r.cint, color.g.cint, color.b.cint, color.a.cint) + +proc cocoaDrawLine(x1, y1, x2, y2: int; color: Color) = + cDrawLine(x1.cint, y1.cint, x2.cint, y2.cint, + color.r.cint, color.g.cint, color.b.cint, color.a.cint) + +proc cocoaDrawPoint(x, y: int; color: Color) = + cDrawPoint(x.cint, y.cint, + color.r.cint, color.g.cint, color.b.cint, color.a.cint) + +proc cocoaSetCursor(c: CursorKind) = + cSetCursor(ord(c).cint) + +proc cocoaSetWindowTitle(title: string) = + cSetWindowTitle(cstring(title)) + +# --- Event translation --- + +proc translateNEEvent(ne: NEEvent; e: var input.Event) = + e = input.Event(kind: evNone) + # Translate modifiers + if (ne.mods and neModShift) != 0: e.mods.incl modShift + if (ne.mods and neModCtrl) != 0: e.mods.incl modCtrl + if (ne.mods and neModAlt) != 0: e.mods.incl modAlt + if (ne.mods and neModGui) != 0: e.mods.incl modGui + + case ne.kind + of neQuit: + e.kind = evQuit + of neWindowResize: + e.kind = evWindowResize + e.x = ne.x + e.y = ne.y + of neWindowClose: + e.kind = evWindowClose + of neWindowFocusGained: + e.kind = evWindowFocusGained + of neWindowFocusLost: + e.kind = evWindowFocusLost + of neKeyDown: + e.kind = evKeyDown + e.key = KeyCode(ne.key) + of neKeyUp: + e.kind = evKeyUp + e.key = KeyCode(ne.key) + of neTextInput: + e.kind = evTextInput + for i in 0..3: + e.text[i] = ne.text[i] + of neMouseDown: + e.kind = evMouseDown + e.x = ne.x + e.y = ne.y + e.clicks = ne.clicks + case ne.button + of 0: e.button = mbLeft + of 1: e.button = mbRight + of 2: e.button = mbMiddle + else: e.button = mbLeft + of neMouseUp: + e.kind = evMouseUp + e.x = ne.x + e.y = ne.y + case ne.button + of 0: e.button = mbLeft + of 1: e.button = mbRight + of 2: e.button = mbMiddle + else: e.button = mbLeft + of neMouseMove: + e.kind = evMouseMove + e.x = ne.x + e.y = ne.y + e.xrel = ne.xrel + e.yrel = ne.yrel + if (ne.buttons and 1) != 0: e.buttons.incl mbLeft + if (ne.buttons and 2) != 0: e.buttons.incl mbRight + if (ne.buttons and 4) != 0: e.buttons.incl mbMiddle + of neMouseWheel: + e.kind = evMouseWheel + e.x = ne.x + e.y = ne.y + else: discard + +proc cocoaPollEvent(e: var input.Event): bool = + var ne: NEEvent + if cPollEvent(addr ne) == 0: + return false + translateNEEvent(ne, e) + result = true + +proc cocoaWaitEvent(e: var input.Event; timeoutMs: int): bool = + var ne: NEEvent + if cWaitEvent(addr ne, timeoutMs.cint) == 0: + return false + translateNEEvent(ne, e) + result = true + +proc cocoaGetClipboardText(): string = + let t = cGetClipboardText() + if t != nil: result = $t + else: result = "" + +proc cocoaPutClipboardText(text: string) = + cPutClipboardText(cstring(text)) + +proc cocoaGetModState(): set[Modifier] = + let m = cGetModState() + if (m and neModShift) != 0: result.incl modShift + if (m and neModCtrl) != 0: result.incl modCtrl + if (m and neModAlt) != 0: result.incl modAlt + if (m and neModGui) != 0: result.incl modGui + +proc cocoaGetTicks(): uint32 = cGetTicks() +proc cocoaDelay(ms: uint32) = cDelay(ms) +proc cocoaStartTextInput() = cStartTextInput() +proc cocoaQuitRequest() = cQuitRequest() + +# --- Init --- + +proc initCocoaDriver*() = + # Screen hooks + createWindowHook = cocoaCreateWindow + refreshHook = cocoaRefresh + saveStateHook = cocoaSaveState + restoreStateHook = cocoaRestoreState + setClipRectHook = cocoaSetClipRect + openFontHook = cocoaOpenFont + closeFontHook = cocoaCloseFont + measureTextHook = cocoaMeasureText + drawTextHook = cocoaDrawText + getFontMetricsHook = cocoaGetFontMetrics + fillRectHook = cocoaFillRect + drawLineHook = cocoaDrawLine + drawPointHook = cocoaDrawPoint + setCursorHook = cocoaSetCursor + setWindowTitleHook = cocoaSetWindowTitle + # Input hooks + pollEventHook = cocoaPollEvent + waitEventHook = cocoaWaitEvent + getClipboardTextHook = cocoaGetClipboardText + putClipboardTextHook = cocoaPutClipboardText + getModStateHook = cocoaGetModState + getTicksHook = cocoaGetTicks + delayHook = cocoaDelay + startTextInputHook = cocoaStartTextInput + quitRequestHook = cocoaQuitRequest diff --git a/app/console.nim b/app/console.nim index cccee6d..43983db 100644 --- a/app/console.nim +++ b/app/console.nim @@ -203,6 +203,7 @@ proc startsWithIgnoreCase(s, prefix: string): bool = var i = 0 while true: if i >= prefix.len: return true + if i >= s.len: return false if s[i].toLowerAscii != prefix[i].toLowerAscii: return false inc(i) diff --git a/app/nimedit.nim b/app/nimedit.nim index c6c8acd..e99ec4e 100644 --- a/app/nimedit.nim +++ b/app/nimedit.nim @@ -9,7 +9,9 @@ import exitprocs] from parseutils import parseInt import basetypes, screen, input -when defined(gtk4): +when defined(cocoa): + import cocoa_driver +elif defined(gtk4): import gtk4_driver elif defined(sdl2): import sdl2_driver @@ -1162,7 +1164,9 @@ proc mainProc(ed: Editor) = -when defined(gtk4): +when defined(cocoa): + initCocoaDriver() +elif defined(gtk4): initGtk4Driver() elif defined(sdl2): initSdl2Driver() From 3d056cbf6fa5bd8867fa480c69ec3dc09bb03316 Mon Sep 17 00:00:00 2001 From: araq Date: Sun, 12 Apr 2026 10:31:47 +0200 Subject: [PATCH 03/14] added WinAPI driver --- app/nimedit.nim | 6 +- app/winapi_driver.nim | 939 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 944 insertions(+), 1 deletion(-) create mode 100644 app/winapi_driver.nim diff --git a/app/nimedit.nim b/app/nimedit.nim index e99ec4e..5eebf75 100644 --- a/app/nimedit.nim +++ b/app/nimedit.nim @@ -13,6 +13,8 @@ when defined(cocoa): import cocoa_driver elif defined(gtk4): import gtk4_driver +elif defined(winapi): + import winapi_driver elif defined(sdl2): import sdl2_driver else: @@ -1089,8 +1091,8 @@ proc mainProc(ed: Editor) = scriptContext.setupApi(sh) compileActions(sh.cfgActions) - loadTheme(sh) createSdlWindow(ed, 1u32) + loadTheme(sh) input.startTextInput() include nimscript/keybindings #XXX TODO: nimscript instead of include @@ -1168,6 +1170,8 @@ when defined(cocoa): initCocoaDriver() elif defined(gtk4): initGtk4Driver() +elif defined(winapi): + initWinapiDriver() elif defined(sdl2): initSdl2Driver() else: diff --git a/app/winapi_driver.nim b/app/winapi_driver.nim new file mode 100644 index 0000000..f2aa834 --- /dev/null +++ b/app/winapi_driver.nim @@ -0,0 +1,939 @@ +# WinAPI (GDI) backend driver. Sets all hooks from core/input and core/screen. +# Uses a double-buffered GDI approach: draw to an off-screen bitmap, +# BitBlt to window on refresh. + +import basetypes, input, screen +import std/[widestrs, strutils, os] + +{.passL: "-lgdi32 -luser32 -lkernel32".} + +# ---- Win32 type definitions ---- + +type + UINT = uint32 + DWORD = uint32 + LONG = int32 + BOOL = int32 + BYTE = uint8 + WORD = uint16 + WPARAM = uint + LPARAM = int + LRESULT = int + ATOM = WORD + HANDLE = pointer + HWND = HANDLE + HDC = HANDLE + HINSTANCE = HANDLE + HBRUSH = HANDLE + HBITMAP = HANDLE + HFONT = HANDLE + HCURSOR = HANDLE + HICON = HANDLE + HGDIOBJ = HANDLE + HMENU = HANDLE + HRGN = HANDLE + HGLOBAL = HANDLE + COLORREF = DWORD + + WNDCLASSEXW {.pure.} = object + cbSize: UINT + style: UINT + lpfnWndProc: proc (hwnd: HWND; msg: UINT; wp: WPARAM; lp: LPARAM): LRESULT {.stdcall.} + cbClsExtra: int32 + cbWndExtra: int32 + hInstance: HINSTANCE + hIcon: HICON + hCursor: HCURSOR + hbrBackground: HBRUSH + lpszMenuName: ptr uint16 + lpszClassName: ptr uint16 + hIconSm: HICON + + MSG {.pure.} = object + hwnd: HWND + message: UINT + wParam: WPARAM + lParam: LPARAM + time: DWORD + x: LONG + y: LONG + + WINAPIPOINT {.pure.} = object + x, y: LONG + + WINAPIRECT {.pure.} = object + left, top, right, bottom: LONG + + PAINTSTRUCT {.pure.} = object + hdc: HDC + fErase: BOOL + rcPaint: WINAPIRECT + fRestore: BOOL + fIncUpdate: BOOL + rgbReserved: array[32, BYTE] + + TEXTMETRICW {.pure.} = object + tmHeight: LONG + tmAscent: LONG + tmDescent: LONG + tmInternalLeading: LONG + tmExternalLeading: LONG + tmAveCharWidth: LONG + tmMaxCharWidth: LONG + tmWeight: LONG + tmOverhang: LONG + tmDigitizedAspectX: LONG + tmDigitizedAspectY: LONG + tmFirstChar: uint16 + tmLastChar: uint16 + tmDefaultChar: uint16 + tmBreakChar: uint16 + tmItalic: BYTE + tmUnderlined: BYTE + tmStruckOut: BYTE + tmPitchAndFamily: BYTE + tmCharSet: BYTE + + SIZE {.pure.} = object + cx, cy: LONG + +# ---- Win32 constants ---- + +const + CS_HREDRAW = 0x0002'u32 + CS_VREDRAW = 0x0001'u32 + WS_OVERLAPPEDWINDOW = 0x00CF0000'u32 + WS_VISIBLE = 0x10000000'u32 + + WM_DESTROY = 0x0002'u32 + WM_SIZE = 0x0005'u32 + WM_PAINT = 0x000F'u32 + WM_CLOSE = 0x0010'u32 + WM_QUIT = 0x0012'u32 + WM_ERASEBKGND = 0x0014'u32 + WM_KEYDOWN = 0x0100'u32 + WM_KEYUP = 0x0101'u32 + WM_CHAR = 0x0102'u32 + WM_MOUSEMOVE = 0x0200'u32 + WM_LBUTTONDOWN = 0x0201'u32 + WM_LBUTTONUP = 0x0202'u32 + WM_RBUTTONDOWN = 0x0204'u32 + WM_RBUTTONUP = 0x0205'u32 + WM_MBUTTONDOWN = 0x0207'u32 + WM_MBUTTONUP = 0x0208'u32 + WM_MOUSEWHEEL = 0x020A'u32 + WM_SETFOCUS = 0x0007'u32 + WM_KILLFOCUS = 0x0008'u32 + + PM_REMOVE = 0x0001'u32 + INFINITE = 0xFFFFFFFF'u32 + + MK_LBUTTON = 0x0001'u32 + MK_RBUTTON = 0x0002'u32 + MK_MBUTTON = 0x0010'u32 + + SW_SHOW = 5'i32 + TRANSPARENT = 1 + OPAQUE = 2 + + SRCCOPY = 0x00CC0020'u32 + DIB_RGB_COLORS = 0'u32 + + IDC_ARROW = cast[ptr uint16](32512) + IDC_IBEAM = cast[ptr uint16](32513) + IDC_WAIT = cast[ptr uint16](32514) + IDC_CROSS = cast[ptr uint16](32515) + IDC_HAND = cast[ptr uint16](32649) + IDC_SIZENS = cast[ptr uint16](32645) + IDC_SIZEWE = cast[ptr uint16](32644) + + VK_BACK = 0x08'u32 + VK_TAB = 0x09'u32 + VK_RETURN = 0x0D'u32 + VK_ESCAPE = 0x1B'u32 + VK_SPACE = 0x20'u32 + VK_DELETE = 0x2E'u32 + VK_INSERT = 0x2D'u32 + VK_LEFT = 0x25'u32 + VK_UP = 0x26'u32 + VK_RIGHT = 0x27'u32 + VK_DOWN = 0x28'u32 + VK_PRIOR = 0x21'u32 # Page Up + VK_NEXT = 0x22'u32 # Page Down + VK_HOME = 0x24'u32 + VK_END = 0x23'u32 + VK_CAPITAL = 0x14'u32 + VK_F1 = 0x70'u32 + VK_F12 = 0x7B'u32 + VK_OEM_COMMA = 0xBC'u32 + VK_OEM_PERIOD = 0xBE'u32 + VK_SHIFT = 0x10'u32 + VK_CONTROL = 0x11'u32 + VK_MENU = 0x12'u32 # Alt + + FW_NORMAL = 400'i32 + DEFAULT_CHARSET = 1'u8 + OUT_TT_PRECIS = 4'u32 + CLIP_DEFAULT_PRECIS = 0'u32 + CLEARTYPE_QUALITY = 5'u32 + FF_DONTCARE = 0'u32 + DEFAULT_PITCH = 0'u32 + + CF_UNICODETEXT = 13'u32 + GMEM_MOVEABLE = 0x0002'u32 + + WAIT_TIMEOUT = 258'u32 + QS_ALLINPUT = 0x04FF'u32 + +# ---- Win32 API imports ---- + +proc GetModuleHandleW(lpModuleName: ptr uint16): HINSTANCE + {.stdcall, dynlib: "kernel32", importc.} +proc GetLastError(): DWORD + {.stdcall, dynlib: "kernel32", importc.} +proc GetTickCount(): DWORD + {.stdcall, dynlib: "kernel32", importc.} +proc Sleep(dwMilliseconds: DWORD) + {.stdcall, dynlib: "kernel32", importc.} +proc GlobalAlloc(uFlags: UINT; dwBytes: uint): HGLOBAL + {.stdcall, dynlib: "kernel32", importc.} +proc GlobalLock(hMem: HGLOBAL): pointer + {.stdcall, dynlib: "kernel32", importc.} +proc GlobalUnlock(hMem: HGLOBAL): BOOL + {.stdcall, dynlib: "kernel32", importc.} +proc GlobalSize(hMem: HGLOBAL): uint + {.stdcall, dynlib: "kernel32", importc.} + +proc RegisterClassExW(lpwcx: ptr WNDCLASSEXW): ATOM + {.stdcall, dynlib: "user32", importc.} +proc CreateWindowExW(dwExStyle: DWORD; lpClassName, lpWindowName: ptr uint16; + dwStyle: DWORD; x, y, nWidth, nHeight: int32; + hWndParent: HWND; hMenu: HMENU; hInstance: HINSTANCE; + lpParam: pointer): HWND + {.stdcall, dynlib: "user32", importc.} +proc ShowWindow(hWnd: HWND; nCmdShow: int32): BOOL + {.stdcall, dynlib: "user32", importc.} +proc UpdateWindow(hWnd: HWND): BOOL + {.stdcall, dynlib: "user32", importc.} +proc DestroyWindow(hWnd: HWND): BOOL + {.stdcall, dynlib: "user32", importc.} +proc PostQuitMessage(nExitCode: int32) + {.stdcall, dynlib: "user32", importc.} +proc DefWindowProcW(hWnd: HWND; msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT + {.stdcall, dynlib: "user32", importc.} +proc PeekMessageW(lpMsg: ptr MSG; hWnd: HWND; wMsgFilterMin, wMsgFilterMax: UINT; + wRemoveMsg: UINT): BOOL + {.stdcall, dynlib: "user32", importc.} +proc TranslateMessage(lpMsg: ptr MSG): BOOL + {.stdcall, dynlib: "user32", importc.} +proc DispatchMessageW(lpMsg: ptr MSG): LRESULT + {.stdcall, dynlib: "user32", importc.} +proc GetClientRect(hWnd: HWND; lpRect: ptr WINAPIRECT): BOOL + {.stdcall, dynlib: "user32", importc.} +proc InvalidateRect(hWnd: HWND; lpRect: ptr WINAPIRECT; bErase: BOOL): BOOL + {.stdcall, dynlib: "user32", importc.} +proc BeginPaint(hWnd: HWND; lpPaint: ptr PAINTSTRUCT): HDC + {.stdcall, dynlib: "user32", importc.} +proc EndPaint(hWnd: HWND; lpPaint: ptr PAINTSTRUCT): BOOL + {.stdcall, dynlib: "user32", importc.} +proc GetDC(hWnd: HWND): HDC + {.stdcall, dynlib: "user32", importc.} +proc ReleaseDC(hWnd: HWND; hDC: HDC): int32 + {.stdcall, dynlib: "user32", importc.} +proc SetWindowTextW(hWnd: HWND; lpString: ptr uint16): BOOL + {.stdcall, dynlib: "user32", importc.} +proc LoadCursorW(hInstance: HINSTANCE; lpCursorName: ptr uint16): HCURSOR + {.stdcall, dynlib: "user32", importc.} +proc SetCursorWin(hCursor: HCURSOR): HCURSOR + {.stdcall, dynlib: "user32", importc: "SetCursor".} +proc GetKeyState(nVirtKey: int32): int16 + {.stdcall, dynlib: "user32", importc.} +proc OpenClipboard(hWndNewOwner: HWND): BOOL + {.stdcall, dynlib: "user32", importc.} +proc CloseClipboard(): BOOL + {.stdcall, dynlib: "user32", importc.} +proc EmptyClipboard(): BOOL + {.stdcall, dynlib: "user32", importc.} +proc GetClipboardData(uFormat: UINT): HANDLE + {.stdcall, dynlib: "user32", importc.} +proc SetClipboardData(uFormat: UINT; hMem: HANDLE): HANDLE + {.stdcall, dynlib: "user32", importc.} +proc MsgWaitForMultipleObjects(nCount: DWORD; pHandles: pointer; + fWaitAll: BOOL; dwMilliseconds: DWORD; dwWakeMask: DWORD): DWORD + {.stdcall, dynlib: "user32", importc.} + +proc CreateCompatibleDC(hdc: HDC): HDC + {.stdcall, dynlib: "gdi32", importc.} +proc CreateCompatibleBitmap(hdc: HDC; cx, cy: int32): HBITMAP + {.stdcall, dynlib: "gdi32", importc.} +proc SelectObject(hdc: HDC; h: HGDIOBJ): HGDIOBJ + {.stdcall, dynlib: "gdi32", importc.} +proc DeleteObject(ho: HGDIOBJ): BOOL + {.stdcall, dynlib: "gdi32", importc.} +proc DeleteDC(hdc: HDC): BOOL + {.stdcall, dynlib: "gdi32", importc.} +proc BitBlt(hdc: HDC; x, y, cx, cy: int32; hdcSrc: HDC; + x1, y1: int32; rop: DWORD): BOOL + {.stdcall, dynlib: "gdi32", importc.} +proc SetBkMode(hdc: HDC; mode: int32): int32 + {.stdcall, dynlib: "gdi32", importc.} +proc SetBkColor(hdc: HDC; color: COLORREF): COLORREF + {.stdcall, dynlib: "gdi32", importc.} +proc SetTextColor(hdc: HDC; color: COLORREF): COLORREF + {.stdcall, dynlib: "gdi32", importc.} +proc TextOutW(hdc: HDC; x, y: int32; lpString: ptr uint16; c: int32): BOOL + {.stdcall, dynlib: "gdi32", importc.} +proc GetTextExtentPoint32W(hdc: HDC; lpString: ptr uint16; c: int32; + lpSize: ptr SIZE): BOOL + {.stdcall, dynlib: "gdi32", importc.} +proc GetTextMetricsW(hdc: HDC; lptm: ptr TEXTMETRICW): BOOL + {.stdcall, dynlib: "gdi32", importc.} +proc CreateFontW(cHeight, cWidth, cEscapement, cOrientation, cWeight: int32; + bItalic, bUnderline, bStrikeOut: DWORD; + iCharSet: DWORD; iOutPrecision, iClipPrecision, iQuality: DWORD; + iPitchAndFamily: DWORD; pszFaceName: ptr uint16): HFONT + {.stdcall, dynlib: "gdi32", importc.} +proc CreateSolidBrush(color: COLORREF): HBRUSH + {.stdcall, dynlib: "gdi32", importc.} +proc FillRectGdi(hDC: HDC; lprc: ptr WINAPIRECT; hbr: HBRUSH): int32 + {.stdcall, dynlib: "user32", importc: "FillRect".} +proc MoveToEx(hdc: HDC; x, y: int32; lppt: ptr WINAPIPOINT): BOOL + {.stdcall, dynlib: "gdi32", importc.} +proc LineTo(hdc: HDC; x, y: int32): BOOL + {.stdcall, dynlib: "gdi32", importc.} +proc CreatePen(iStyle, cWidth: int32; color: COLORREF): HGDIOBJ + {.stdcall, dynlib: "gdi32", importc.} +proc SetPixel(hdc: HDC; x, y: int32; color: COLORREF): COLORREF + {.stdcall, dynlib: "gdi32", importc.} +proc IntersectClipRect(hdc: HDC; left, top, right, bottom: int32): int32 + {.stdcall, dynlib: "gdi32", importc.} +proc SelectClipRgn(hdc: HDC; hrgn: HRGN): int32 + {.stdcall, dynlib: "gdi32", importc.} +proc CreateRectRgn(x1, y1, x2, y2: int32): HRGN + {.stdcall, dynlib: "gdi32", importc.} +proc AddFontResourceExW(name: ptr uint16; fl: DWORD; res: pointer): int32 + {.stdcall, dynlib: "gdi32", importc.} + +# ---- Helpers ---- + +proc rgb(c: screen.Color): COLORREF {.inline.} = + COLORREF(c.r.uint32 or (c.g.uint32 shl 8) or (c.b.uint32 shl 16)) + +proc loWord(lp: LPARAM): int {.inline.} = int(lp and 0xFFFF) +proc hiWord(lp: LPARAM): int {.inline.} = int((lp shr 16) and 0xFFFF) +proc signedHiWord(wp: WPARAM): int {.inline.} = + ## For WM_MOUSEWHEEL: wParam high word is signed + cast[int16](uint16((wp shr 16) and 0xFFFF)).int + +# ---- Font handle management ---- + +type + FontSlot = object + hFont: HFONT + metrics: FontMetrics + faceName: string + size: int + +var fonts: seq[FontSlot] + +proc getFontHandle(f: screen.Font): HFONT {.inline.} = + let idx = f.int - 1 + if idx >= 0 and idx < fonts.len: fonts[idx].hFont + else: nil + +# ---- Driver state ---- + +var + gHwnd: HWND + gHinstance: HINSTANCE + gBackDC: HDC # off-screen DC for double buffering + gBackBmp: HBITMAP # off-screen bitmap + gOldBmp: HGDIOBJ # original bitmap selected into gBackDC + gWidth, gHeight: int32 + gQuitFlag: bool + gSavedClipRgn: HRGN + +# Event queue: WndProc pushes events, pollEvent/waitEvent consumes them +var eventQueue: seq[input.Event] + +proc pushEvent(e: input.Event) = + eventQueue.add e + +# ---- Back-buffer management ---- + +proc recreateBackBuffer() = + let screenDC = GetDC(gHwnd) + let newBmp = CreateCompatibleBitmap(screenDC, gWidth, gHeight) + if gBackDC == nil: + gBackDC = CreateCompatibleDC(screenDC) + if gBackBmp != nil: + discard SelectObject(gBackDC, cast[HGDIOBJ](newBmp)) + discard DeleteObject(cast[HGDIOBJ](gBackBmp)) + else: + gOldBmp = SelectObject(gBackDC, cast[HGDIOBJ](newBmp)) + gBackBmp = newBmp + # Clear to opaque black -- CreateCompatibleBitmap inits to all zeros which + # DWM interprets as fully transparent on 32-bit displays. + var rc = WINAPIRECT(left: 0, top: 0, right: gWidth, bottom: gHeight) + let blackBrush = CreateSolidBrush(0x00000000'u32) + discard FillRectGdi(gBackDC, addr rc, blackBrush) + discard DeleteObject(cast[HGDIOBJ](blackBrush)) + discard ReleaseDC(gHwnd, screenDC) + +# ---- WndProc ---- + +proc translateVK(vk: WPARAM): input.KeyCode = + let vk = vk.uint32 + if vk >= ord('A').uint32 and vk <= ord('Z').uint32: + return input.KeyCode(ord(keyA) + (vk.int - ord('A'))) + if vk >= ord('0').uint32 and vk <= ord('9').uint32: + return input.KeyCode(ord(key0) + (vk.int - ord('0'))) + if vk >= VK_F1 and vk <= VK_F12: + return input.KeyCode(ord(keyF1) + (vk.int - VK_F1.int)) + case vk + of VK_RETURN: keyEnter + of VK_SPACE: keySpace + of VK_ESCAPE: keyEsc + of VK_TAB: keyTab + of VK_BACK: keyBackspace + of VK_DELETE: keyDelete + of VK_INSERT: keyInsert + of VK_LEFT: keyLeft + of VK_RIGHT: keyRight + of VK_UP: keyUp + of VK_DOWN: keyDown + of VK_PRIOR: keyPageUp + of VK_NEXT: keyPageDown + of VK_HOME: keyHome + of VK_END: keyEnd + of VK_CAPITAL: keyCapslock + of VK_OEM_COMMA: keyComma + of VK_OEM_PERIOD: keyPeriod + else: keyNone + +proc getModifiers(): set[Modifier] = + if GetKeyState(VK_SHIFT.int32) < 0: result.incl modShift + if GetKeyState(VK_CONTROL.int32) < 0: result.incl modCtrl + if GetKeyState(VK_MENU.int32) < 0: result.incl modAlt + +proc getMouseButtons(wp: WPARAM): set[MouseButton] = + let flags = wp.uint32 + if (flags and MK_LBUTTON) != 0: result.incl mbLeft + if (flags and MK_RBUTTON) != 0: result.incl mbRight + if (flags and MK_MBUTTON) != 0: result.incl mbMiddle + +var lastClickTime: DWORD +var lastClickX, lastClickY: int +var clickCount: int + +proc pumpMessages() = + ## Drain Win32 message queue. Must be called frequently to prevent + ## the "Not Responding" ghost window (Windows triggers it after ~5s + ## of not processing sent messages). + var msg: MSG + while PeekMessageW(addr msg, nil, 0, 0, PM_REMOVE) != 0: + discard TranslateMessage(addr msg) + discard DispatchMessageW(addr msg) + if msg.message == WM_QUIT: + pushEvent(input.Event(kind: evQuit)) + +proc wndProc(hwnd: HWND; msg: UINT; wp: WPARAM; lp: LPARAM): LRESULT {.stdcall.} = + # Capture gHwnd from the first message -- WM_SIZE etc. arrive + # during CreateWindowExW, *before* it returns and assigns gHwnd. + if gHwnd == nil and hwnd != nil: + gHwnd = hwnd + case msg + of WM_DESTROY: + PostQuitMessage(0) + pushEvent(input.Event(kind: evQuit)) + return 0 + + of WM_CLOSE: + pushEvent(input.Event(kind: evWindowClose)) + return 0 # don't call DestroyWindow yet; let the app decide + + of WM_ERASEBKGND: + return 1 # we handle erasing via double buffer + + of WM_SIZE: + let newW = loWord(lp).int32 + let newH = hiWord(lp).int32 + if newW > 0 and newH > 0 and (newW != gWidth or newH != gHeight): + gWidth = newW + gHeight = newH + recreateBackBuffer() + var e = input.Event(kind: evWindowResize) + e.x = gWidth + e.y = gHeight + pushEvent(e) + return 0 + + of WM_PAINT: + var ps: PAINTSTRUCT + let hdc = BeginPaint(hwnd, addr ps) + if gBackDC != nil: + discard BitBlt(hdc, 0, 0, gWidth, gHeight, gBackDC, 0, 0, SRCCOPY) + discard EndPaint(hwnd, addr ps) + return 0 + + of WM_KEYDOWN: + var e = input.Event(kind: evKeyDown) + e.key = translateVK(wp) + e.mods = getModifiers() + pushEvent(e) + return 0 + + of WM_KEYUP: + var e = input.Event(kind: evKeyUp) + e.key = translateVK(wp) + e.mods = getModifiers() + pushEvent(e) + return 0 + + of WM_CHAR: + # wp is a UTF-16 code unit. For BMP characters, emit evTextInput. + let ch = wp.uint16 + if ch >= 32 and ch != 127: + var e = input.Event(kind: evTextInput) + # Convert UTF-16 to UTF-8 into e.text[0..3] + let codepoint = ch.uint32 + if codepoint < 0x80: + e.text[0] = chr(codepoint) + elif codepoint < 0x800: + e.text[0] = chr(0xC0 or (codepoint shr 6)) + e.text[1] = chr(0x80 or (codepoint and 0x3F)) + else: + e.text[0] = chr(0xE0 or (codepoint shr 12)) + e.text[1] = chr(0x80 or ((codepoint shr 6) and 0x3F)) + e.text[2] = chr(0x80 or (codepoint and 0x3F)) + pushEvent(e) + return 0 + + of WM_SETFOCUS: + pushEvent(input.Event(kind: evWindowFocusGained)) + return 0 + + of WM_KILLFOCUS: + pushEvent(input.Event(kind: evWindowFocusLost)) + return 0 + + of WM_LBUTTONDOWN, WM_RBUTTONDOWN, WM_MBUTTONDOWN: + var e = input.Event(kind: evMouseDown) + e.x = loWord(lp) + e.y = hiWord(lp) + e.mods = getModifiers() + case msg + of WM_LBUTTONDOWN: e.button = mbLeft + of WM_RBUTTONDOWN: e.button = mbRight + else: e.button = mbMiddle + # Track click count for double/triple click + let now = GetTickCount() + if now - lastClickTime < 500 and + abs(e.x - lastClickX) < 4 and abs(e.y - lastClickY) < 4: + inc clickCount + else: + clickCount = 1 + lastClickTime = now + lastClickX = e.x + lastClickY = e.y + e.clicks = clickCount + pushEvent(e) + return 0 + + of WM_LBUTTONUP, WM_RBUTTONUP, WM_MBUTTONUP: + var e = input.Event(kind: evMouseUp) + e.x = loWord(lp) + e.y = hiWord(lp) + case msg + of WM_LBUTTONUP: e.button = mbLeft + of WM_RBUTTONUP: e.button = mbRight + else: e.button = mbMiddle + pushEvent(e) + return 0 + + of WM_MOUSEMOVE: + var e = input.Event(kind: evMouseMove) + e.x = loWord(lp) + e.y = hiWord(lp) + e.buttons = getMouseButtons(wp) + pushEvent(e) + return 0 + + of WM_MOUSEWHEEL: + var e = input.Event(kind: evMouseWheel) + let delta = signedHiWord(wp) + e.y = delta div 120 # standard wheel delta + var pt = WINAPIPOINT(x: loWord(lp).LONG, y: hiWord(lp).LONG) + # wheel coords are screen-relative; could ScreenToClient but + # Nimedit only uses e.y for scroll direction + pushEvent(e) + return 0 + + else: + discard + + return DefWindowProcW(hwnd, msg, wp, lp) + +# ---- Screen hook implementations ---- + +proc winCreateWindow(layout: var ScreenLayout) = + gHinstance = GetModuleHandleW(nil) + let className = newWideCString("NimEditWinAPI") + + var wc: WNDCLASSEXW + wc.cbSize = UINT(sizeof(WNDCLASSEXW)) + wc.style = CS_HREDRAW or CS_VREDRAW + wc.lpfnWndProc = wndProc + wc.hInstance = gHinstance + wc.hCursor = LoadCursorW(nil, IDC_ARROW) + wc.lpszClassName = cast[ptr uint16](className[0].addr) + + discard RegisterClassExW(addr wc) + + let title = newWideCString("NimEdit") + gHwnd = CreateWindowExW( + 0, + cast[ptr uint16](className[0].addr), + cast[ptr uint16](title[0].addr), + WS_OVERLAPPEDWINDOW or WS_VISIBLE, + 0x80000000'i32, 0x80000000'i32, # CW_USEDEFAULT + layout.width.int32, layout.height.int32, + nil, nil, gHinstance, nil) + + if gHwnd == nil: + quit("CreateWindowExW failed") + + discard ShowWindow(gHwnd, SW_SHOW) + discard UpdateWindow(gHwnd) + + var rc: WINAPIRECT + discard GetClientRect(gHwnd, addr rc) + gWidth = rc.right - rc.left + gHeight = rc.bottom - rc.top + layout.width = gWidth + layout.height = gHeight + layout.scaleX = 1 + layout.scaleY = 1 + + recreateBackBuffer() + +proc winRefresh() = + discard InvalidateRect(gHwnd, nil, 0) + discard UpdateWindow(gHwnd) + +proc winSaveState() = + # Save current clip region + gSavedClipRgn = CreateRectRgn(0, 0, 0, 0) + # GetClipRgn not easily available; we just reset to full on restore + discard + +proc winRestoreState() = + # Restore by removing clip region + if gBackDC != nil: + discard SelectClipRgn(gBackDC, nil) + if gSavedClipRgn != nil: + discard DeleteObject(cast[HGDIOBJ](gSavedClipRgn)) + gSavedClipRgn = nil + +proc winSetClipRect(r: basetypes.Rect) = + if gBackDC != nil: + # Reset clip region first, then set new one + discard SelectClipRgn(gBackDC, nil) + discard IntersectClipRect(gBackDC, + r.x.int32, r.y.int32, (r.x + r.w).int32, (r.y + r.h).int32) + +proc winOpenFont(path: string; size: int; + metrics: var FontMetrics): screen.Font = + # Ensure the font file is available as a private resource (needed for + # fonts outside C:\Windows\Fonts, harmless for system-installed ones). + let wpath = newWideCString(path) + let FR_PRIVATE = 0x10'u32 + discard AddFontResourceExW(cast[ptr uint16](wpath[0].addr), FR_PRIVATE, nil) + + # Detect bold/italic from filename + let lpath = path.toLowerAscii() + let isBold = "bold" in lpath + let isItalic = "italic" in lpath or "oblique" in lpath + let weight = if isBold: 700'i32 else: FW_NORMAL + let italic = if isItalic: 1'u32 else: 0'u32 + let FIXED_PITCH = 1'u32 + + # Map known font filenames to their GDI face names. + # Consolas is the safe default -- ships since Vista with excellent + # ClearType hinting. + var faceName = "Consolas" + let baseName = path.extractFilename.toLowerAscii + if "dejavu" in baseName and "mono" in baseName: + faceName = "DejaVu Sans Mono" + elif "dejavu" in baseName: + faceName = "DejaVu Sans" + elif "consola" in baseName: + faceName = "Consolas" + elif "courier" in baseName: + faceName = "Courier New" + elif "arial" in baseName: + faceName = "Arial" + elif "segoe" in baseName: + faceName = "Segoe UI" + elif "cascadia" in baseName: + if "mono" in baseName: faceName = "Cascadia Mono" + else: faceName = "Cascadia Code" + elif "hack" in baseName: + faceName = "Hack" + elif "fira" in baseName and "code" in baseName: + faceName = "Fira Code" + elif "roboto" in baseName and "mono" in baseName: + faceName = "Roboto Mono" + elif "source" in baseName and "code" in baseName: + faceName = "Source Code Pro" + elif "jetbrains" in baseName: + faceName = "JetBrains Mono" + else: + # Unknown font -- use the filename stem as face name + var stem = path.extractFilename + let dot = stem.rfind('.') + if dot >= 0: stem = stem[0 ..< dot] + for suffix in ["-BoldOblique", "-BoldItalic", "-Bold", "-Oblique", + "-Italic", "-Regular"]: + if stem.endsWith(suffix): + stem = stem[0 ..< stem.len - suffix.len] + break + faceName = stem + + let wface = newWideCString(faceName) + let hf = CreateFontW( + -size.int32, # negative = character height in pixels + 0, 0, 0, # width, escapement, orientation + weight, + italic, 0, 0, # italic, underline, strikeout + DEFAULT_CHARSET.DWORD, + OUT_TT_PRECIS, + CLIP_DEFAULT_PRECIS, + CLEARTYPE_QUALITY, + FIXED_PITCH or FF_DONTCARE, + cast[ptr uint16](wface[0].addr) + ) + + if hf == nil: + return screen.Font(0) + + # Get font metrics + let oldFont = SelectObject(gBackDC, cast[HGDIOBJ](hf)) + var tm: TEXTMETRICW + discard GetTextMetricsW(gBackDC, addr tm) + discard SelectObject(gBackDC, oldFont) + + metrics.ascent = tm.tmAscent.int + metrics.descent = tm.tmDescent.int + metrics.lineHeight = tm.tmHeight.int + tm.tmExternalLeading.int + + fonts.add FontSlot(hFont: hf, metrics: metrics, faceName: path, size: size) + result = screen.Font(fonts.len) + +proc winCloseFont(f: screen.Font) = + let idx = f.int - 1 + if idx >= 0 and idx < fonts.len and fonts[idx].hFont != nil: + discard DeleteObject(cast[HGDIOBJ](fonts[idx].hFont)) + fonts[idx].hFont = nil + +proc winMeasureText(f: screen.Font; text: string): TextExtent = + let hf = getFontHandle(f) + if hf == nil or text.len == 0: return + let oldFont = SelectObject(gBackDC, cast[HGDIOBJ](hf)) + let wtext = newWideCString(text) + var sz: SIZE + discard GetTextExtentPoint32W(gBackDC, cast[ptr uint16](wtext[0].addr), + wtext.len.int32, addr sz) + discard SelectObject(gBackDC, oldFont) + result = TextExtent(w: sz.cx.int, h: sz.cy.int) + +proc winDrawText(f: screen.Font; x, y: int; text: string; + fg, bg: screen.Color): TextExtent = + let hf = getFontHandle(f) + if hf == nil or text.len == 0: return + let oldFont = SelectObject(gBackDC, cast[HGDIOBJ](hf)) + discard SetBkMode(gBackDC, OPAQUE) + discard SetBkColor(gBackDC, rgb(bg)) + discard SetTextColor(gBackDC, rgb(fg)) + let wtext = newWideCString(text) + let wlen = wtext.len.int32 + discard TextOutW(gBackDC, x.int32, y.int32, + cast[ptr uint16](wtext[0].addr), wlen) + var sz: SIZE + discard GetTextExtentPoint32W(gBackDC, cast[ptr uint16](wtext[0].addr), + wlen, addr sz) + discard SelectObject(gBackDC, oldFont) + result = TextExtent(w: sz.cx.int, h: sz.cy.int) + +proc winGetFontMetrics(f: screen.Font): FontMetrics = + let idx = f.int - 1 + if idx >= 0 and idx < fonts.len: fonts[idx].metrics + else: screen.FontMetrics() + +proc winFillRect(r: basetypes.Rect; color: screen.Color) = + let brush = CreateSolidBrush(rgb(color)) + var wr = WINAPIRECT( + left: r.x.int32, top: r.y.int32, + right: (r.x + r.w).int32, bottom: (r.y + r.h).int32) + discard FillRectGdi(gBackDC, addr wr, brush) + discard DeleteObject(cast[HGDIOBJ](brush)) + +proc winDrawLine(x1, y1, x2, y2: int; color: screen.Color) = + let pen = CreatePen(0, 1, rgb(color)) # PS_SOLID = 0 + let oldPen = SelectObject(gBackDC, pen) + discard MoveToEx(gBackDC, x1.int32, y1.int32, nil) + discard LineTo(gBackDC, x2.int32, y2.int32) + discard SelectObject(gBackDC, oldPen) + discard DeleteObject(pen) + +proc winDrawPoint(x, y: int; color: screen.Color) = + discard SetPixel(gBackDC, x.int32, y.int32, rgb(color)) + +proc winSetCursor(c: CursorKind) = + let id = case c + of curDefault, curArrow: IDC_ARROW + of curIbeam: IDC_IBEAM + of curWait: IDC_WAIT + of curCrosshair: IDC_CROSS + of curHand: IDC_HAND + of curSizeNS: IDC_SIZENS + of curSizeWE: IDC_SIZEWE + let cur = LoadCursorW(nil, id) + discard SetCursorWin(cur) + +proc winSetWindowTitle(title: string) = + if gHwnd != nil: + let wtitle = newWideCString(title) + discard SetWindowTextW(gHwnd, cast[ptr uint16](wtitle[0].addr)) + +# ---- Input hook implementations ---- + +proc winPollEvent(e: var input.Event): bool = + pumpMessages() + if eventQueue.len > 0: + e = eventQueue[0] + eventQueue.delete(0) + return true + return false + +proc winWaitEvent(e: var input.Event; timeoutMs: int): bool = + # Check already-queued events first + if eventQueue.len > 0: + e = eventQueue[0] + eventQueue.delete(0) + return true + # Drain any pending Win32 messages before blocking + if winPollEvent(e): + return true + # Pump messages in a loop with short sleeps, like SDL does internally. + # A single MWFMO(INFINITE) would block the thread entirely, causing + # Windows to show the "Not Responding" ghost window after ~5 seconds + # which then intercepts all user input -- deadlock. + let deadline = if timeoutMs < 0: uint32.high + else: GetTickCount() + timeoutMs.uint32 + while true: + let now = GetTickCount() + if now >= deadline: return false + let remaining = if timeoutMs < 0: 100'u32 + else: min(deadline - now, 100'u32) + let res = MsgWaitForMultipleObjects(0, nil, 0, remaining, QS_ALLINPUT) + if res != WAIT_TIMEOUT: + if winPollEvent(e): return true + elif timeoutMs >= 0: + # Finite timeout: check if expired + if GetTickCount() >= deadline: return false + +proc winGetClipboardText(): string = + if OpenClipboard(gHwnd) == 0: return "" + let hData = GetClipboardData(CF_UNICODETEXT) + if hData != nil: + let p = cast[ptr UncheckedArray[uint16]](GlobalLock(cast[HGLOBAL](hData))) + if p != nil: + var wlen = 0 + while p[wlen] != 0: inc wlen + var ws = newWideCString("", wlen) + copyMem(addr ws[0], p, wlen * 2) + result = $ws + discard GlobalUnlock(cast[HGLOBAL](hData)) + discard CloseClipboard() + +proc winPutClipboardText(text: string) = + if OpenClipboard(gHwnd) == 0: return + discard EmptyClipboard() + let ws = newWideCString(text) + let bytes = (ws.len + 1) * 2 + let hMem = GlobalAlloc(GMEM_MOVEABLE, bytes.uint) + if hMem != nil: + let p = GlobalLock(hMem) + if p != nil: + copyMem(p, addr ws[0], bytes) + discard GlobalUnlock(hMem) + discard SetClipboardData(CF_UNICODETEXT, cast[HANDLE](hMem)) + discard CloseClipboard() + +proc winGetModState(): set[Modifier] = + getModifiers() + +proc winGetTicks(): uint32 = GetTickCount() +proc winDelay(ms: uint32) = + let deadline = GetTickCount() + ms + while true: + let now = GetTickCount() + if now >= deadline: break + let remaining = min(deadline - now, ms) + discard MsgWaitForMultipleObjects(0, nil, 0, remaining, QS_ALLINPUT) + pumpMessages() +proc winStartTextInput() = discard # Win32 always delivers WM_CHAR +proc winQuitRequest() = + gQuitFlag = true + if gHwnd != nil: + discard DestroyWindow(gHwnd) + +# ---- Init ---- + +proc setDpiAware() = + # Try the modern API first (Windows 10 1703+), fall back to older ones. + # Without this, Windows bitmap-scales the window on high-DPI displays, + # causing blurry/pixelated rendering. + type DPI_AWARENESS_CONTEXT = HANDLE + try: + proc SetProcessDpiAwarenessContext(value: DPI_AWARENESS_CONTEXT): BOOL + {.stdcall, dynlib: "user32", importc.} + let DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = cast[DPI_AWARENESS_CONTEXT](-4) + if SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) != 0: + return + except: discard + try: + proc SetProcessDPIAware(): BOOL + {.stdcall, dynlib: "user32", importc.} + discard SetProcessDPIAware() + except: discard + +proc initWinapiDriver*() = + setDpiAware() + # Screen hooks + createWindowHook = winCreateWindow + refreshHook = winRefresh + saveStateHook = winSaveState + restoreStateHook = winRestoreState + setClipRectHook = winSetClipRect + openFontHook = winOpenFont + closeFontHook = winCloseFont + measureTextHook = winMeasureText + drawTextHook = winDrawText + getFontMetricsHook = winGetFontMetrics + fillRectHook = winFillRect + drawLineHook = winDrawLine + drawPointHook = winDrawPoint + setCursorHook = winSetCursor + setWindowTitleHook = winSetWindowTitle + # Input hooks + pollEventHook = winPollEvent + waitEventHook = winWaitEvent + getClipboardTextHook = winGetClipboardText + putClipboardTextHook = winPutClipboardText + getModStateHook = winGetModState + getTicksHook = winGetTicks + delayHook = winDelay + startTextInputHook = winStartTextInput + quitRequestHook = winQuitRequest From 893dcdc53ea396c69e05193c6f70f48702e5f9f7 Mon Sep 17 00:00:00 2001 From: araq Date: Sun, 12 Apr 2026 11:16:57 +0200 Subject: [PATCH 04/14] added X11 driver --- app/nimedit.nim | 4 + app/x11_driver.nim | 958 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 962 insertions(+) create mode 100644 app/x11_driver.nim diff --git a/app/nimedit.nim b/app/nimedit.nim index 5eebf75..3a4ffbb 100644 --- a/app/nimedit.nim +++ b/app/nimedit.nim @@ -13,6 +13,8 @@ when defined(cocoa): import cocoa_driver elif defined(gtk4): import gtk4_driver +elif defined(x11): + import x11_driver elif defined(winapi): import winapi_driver elif defined(sdl2): @@ -1170,6 +1172,8 @@ when defined(cocoa): initCocoaDriver() elif defined(gtk4): initGtk4Driver() +elif defined(x11): + initX11Driver() elif defined(winapi): initWinapiDriver() elif defined(sdl2): diff --git a/app/x11_driver.nim b/app/x11_driver.nim new file mode 100644 index 0000000..ee55615 --- /dev/null +++ b/app/x11_driver.nim @@ -0,0 +1,958 @@ +# X11 + Xft backend driver. Sets all hooks from core/input and core/screen. +# Uses Xlib for windowing/events, Xft for antialiased font rendering. +# Double-buffered via X Pixmap. + +import basetypes, input, screen +import std/[strutils, os] + +{.passL: "-lX11 -lXft".} + +# ---- X11 type definitions ---- + +const + libX11 = "libX11.so(|.6)" + libXft = "libXft.so(|.2)" + +type + XID = culong + Atom = culong + XTime = culong + XKeySym = culong + XBool = cint + XStatus = cint + + XRectangle {.pure.} = object + x, y: cshort + width, height: cushort + + XColor {.pure.} = object + pixel: culong + red, green, blue: cushort + flags: uint8 + pad: uint8 + + # ---- Event types ---- + + XAnyEvent {.pure.} = object + theType: cint + serial: culong + send_event: XBool + display: pointer + window: XID + + XKeyEvent {.pure.} = object + theType: cint + serial: culong + send_event: XBool + display: pointer + window: XID + root: XID + subwindow: XID + time: XTime + x, y: cint + x_root, y_root: cint + state: cuint + keycode: cuint + same_screen: XBool + + XButtonEvent {.pure.} = object + theType: cint + serial: culong + send_event: XBool + display: pointer + window: XID + root: XID + subwindow: XID + time: XTime + x, y: cint + x_root, y_root: cint + state: cuint + button: cuint + same_screen: XBool + + XMotionEvent {.pure.} = object + theType: cint + serial: culong + send_event: XBool + display: pointer + window: XID + root: XID + subwindow: XID + time: XTime + x, y: cint + x_root, y_root: cint + state: cuint + is_hint: uint8 + same_screen: XBool + + XConfigureEvent {.pure.} = object + theType: cint + serial: culong + send_event: XBool + display: pointer + event: XID + window: XID + x, y: cint + width, height: cint + border_width: cint + above: XID + override_redirect: XBool + + XExposeEvent {.pure.} = object + theType: cint + serial: culong + send_event: XBool + display: pointer + window: XID + x, y: cint + width, height: cint + count: cint + + XClientMessageEvent {.pure.} = object + theType: cint + serial: culong + send_event: XBool + display: pointer + window: XID + message_type: Atom + format: cint + data: array[5, clong] + + XFocusChangeEvent {.pure.} = object + theType: cint + serial: culong + send_event: XBool + display: pointer + window: XID + mode: cint + detail: cint + + XSelectionRequestEvent {.pure.} = object + theType: cint + serial: culong + send_event: XBool + display: pointer + owner: XID + requestor: XID + selection: Atom + target: Atom + property: Atom + time: XTime + + XSelectionEvent {.pure.} = object + theType: cint + serial: culong + send_event: XBool + display: pointer + requestor: XID + selection: Atom + target: Atom + property: Atom + time: XTime + + XEvent {.union.} = object + theType: cint + xany: XAnyEvent + xkey: XKeyEvent + xbutton: XButtonEvent + xmotion: XMotionEvent + xconfigure: XConfigureEvent + xexpose: XExposeEvent + xclient: XClientMessageEvent + xfocus: XFocusChangeEvent + xselection: XSelectionEvent + xselectionrequest: XSelectionRequestEvent + pad: array[24, clong] # XEvent is 192 bytes on 64-bit + + # ---- Xft types ---- + + XRenderColor {.pure.} = object + red, green, blue, alpha: cushort + + XftColor {.pure.} = object + pixel: culong + color: XRenderColor + + XftFont {.pure.} = object + ascent: cint + descent: cint + height: cint + max_advance_width: cint + charset: pointer + pattern: pointer + + XGlyphInfo {.pure.} = object + width, height: cushort + x, y: cshort + xOff, yOff: cshort + +# ---- X11 constants ---- + +const + None = 0.XID + CurrentTime = 0.XTime + XA_ATOM = 4.Atom + XA_STRING = 31.Atom + PropModeReplace = 0.cint + + # Event types + KeyPress = 2.cint + KeyRelease = 3.cint + ButtonPress = 4.cint + ButtonRelease = 5.cint + MotionNotify = 6.cint + FocusIn = 9.cint + FocusOut = 10.cint + Expose = 12.cint + ConfigureNotify = 22.cint + SelectionNotify = 31.cint + SelectionRequest = 30.cint + ClientMessage = 33.cint + + # Event masks + ExposureMask = 1 shl 15 + KeyPressMask = 1 shl 0 + KeyReleaseMask = 1 shl 1 + ButtonPressMask = 1 shl 2 + ButtonReleaseMask = 1 shl 3 + PointerMotionMask = 1 shl 6 + StructureNotifyMask = 1 shl 17 + FocusChangeMask = 1 shl 21 + + # Modifier masks + ShiftMask = 1'u32 + ControlMask = 4'u32 + Mod1Mask = 8'u32 # Alt + Mod4Mask = 64'u32 # Super/GUI + + # Mouse buttons + Button1 = 1'u32 + Button2 = 2'u32 + Button3 = 3'u32 + Button4 = 4'u32 # scroll up + Button5 = 5'u32 # scroll down + Button1Mask = 1'u32 shl 8 + Button2Mask = 1'u32 shl 9 + Button3Mask = 1'u32 shl 10 + + # Cursor shapes + XC_left_ptr = 68'u32 + XC_xterm = 152'u32 + XC_watch = 150'u32 + XC_crosshair = 34'u32 + XC_hand2 = 60'u32 + XC_sb_v_double_arrow = 116'u32 + XC_sb_h_double_arrow = 108'u32 + + # KeySyms + XK_a = 0x61'u + XK_z = 0x7a'u + XK_0 = 0x30'u + XK_9 = 0x39'u + XK_F1 = 0xffbe'u + XK_F12 = 0xffc9'u + XK_Return = 0xff0d'u + XK_space = 0x20'u + XK_Escape = 0xff1b'u + XK_Tab = 0xff09'u + XK_BackSpace = 0xff08'u + XK_Delete = 0xffff'u + XK_Insert = 0xff63'u + XK_Left = 0xff51'u + XK_Up = 0xff52'u + XK_Right = 0xff53'u + XK_Down = 0xff54'u + XK_Page_Up = 0xff55'u + XK_Page_Down = 0xff56'u + XK_Home = 0xff50'u + XK_End = 0xff57'u + XK_Caps_Lock = 0xffe5'u + XK_comma = 0x2c'u + XK_period = 0x2e'u + +# ---- X11 function imports ---- + +proc XOpenDisplay(name: cstring): pointer + {.cdecl, dynlib: libX11, importc.} +proc XDefaultScreen(dpy: pointer): cint + {.cdecl, dynlib: libX11, importc.} +proc XRootWindow(dpy: pointer; screen: cint): XID + {.cdecl, dynlib: libX11, importc.} +proc XDefaultVisual(dpy: pointer; screen: cint): pointer + {.cdecl, dynlib: libX11, importc.} +proc XDefaultColormap(dpy: pointer; screen: cint): XID + {.cdecl, dynlib: libX11, importc.} +proc XDefaultDepth(dpy: pointer; screen: cint): cint + {.cdecl, dynlib: libX11, importc.} +proc XBlackPixel(dpy: pointer; screen: cint): culong + {.cdecl, dynlib: libX11, importc.} +proc XWhitePixel(dpy: pointer; screen: cint): culong + {.cdecl, dynlib: libX11, importc.} +proc XCreateSimpleWindow(dpy: pointer; parent: XID; + x, y: cint; w, h, border: cuint; borderColor, bgColor: culong): XID + {.cdecl, dynlib: libX11, importc.} +proc XMapWindow(dpy: pointer; w: XID): cint + {.cdecl, dynlib: libX11, importc.} +proc XDestroyWindow(dpy: pointer; w: XID): cint + {.cdecl, dynlib: libX11, importc.} +proc XSelectInput(dpy: pointer; w: XID; mask: clong): cint + {.cdecl, dynlib: libX11, importc.} +proc XCreatePixmap(dpy: pointer; d: XID; w, h, depth: cuint): XID + {.cdecl, dynlib: libX11, importc.} +proc XFreePixmap(dpy: pointer; p: XID): cint + {.cdecl, dynlib: libX11, importc.} +proc XCreateGC(dpy: pointer; d: XID; mask: culong; values: pointer): pointer + {.cdecl, dynlib: libX11, importc.} +proc XFreeGC(dpy: pointer; gc: pointer): cint + {.cdecl, dynlib: libX11, importc.} +proc XSetForeground(dpy: pointer; gc: pointer; pixel: culong): cint + {.cdecl, dynlib: libX11, importc.} +proc XFillRectangle(dpy: pointer; d: XID; gc: pointer; + x, y: cint; w, h: cuint): cint + {.cdecl, dynlib: libX11, importc.} +proc XDrawLine(dpy: pointer; d: XID; gc: pointer; + x1, y1, x2, y2: cint): cint + {.cdecl, dynlib: libX11, importc.} +proc XDrawPoint(dpy: pointer; d: XID; gc: pointer; x, y: cint): cint + {.cdecl, dynlib: libX11, importc.} +proc XCopyArea(dpy: pointer; src, dst: XID; gc: pointer; + srcX, srcY: cint; w, h: cuint; dstX, dstY: cint): cint + {.cdecl, dynlib: libX11, importc.} +proc XSetClipRectangles(dpy: pointer; gc: pointer; + x, y: cint; rects: ptr XRectangle; n, ordering: cint): cint + {.cdecl, dynlib: libX11, importc.} +proc XSetClipMask(dpy: pointer; gc: pointer; pixmap: XID): cint + {.cdecl, dynlib: libX11, importc.} +proc XNextEvent(dpy: pointer; ev: ptr XEvent): cint + {.cdecl, dynlib: libX11, importc.} +proc XPending(dpy: pointer): cint + {.cdecl, dynlib: libX11, importc.} +proc XFlush(dpy: pointer): cint + {.cdecl, dynlib: libX11, importc.} +proc XStoreName(dpy: pointer; w: XID; name: cstring): cint + {.cdecl, dynlib: libX11, importc.} +proc XInternAtom(dpy: pointer; name: cstring; onlyIfExists: XBool): Atom + {.cdecl, dynlib: libX11, importc.} +proc XSetWMProtocols(dpy: pointer; w: XID; protocols: ptr Atom; count: cint): XStatus + {.cdecl, dynlib: libX11, importc.} +proc XCreateFontCursor(dpy: pointer; shape: cuint): XID + {.cdecl, dynlib: libX11, importc.} +proc XDefineCursor(dpy: pointer; w: XID; cursor: XID): cint + {.cdecl, dynlib: libX11, importc.} +proc XLookupString(ev: ptr XKeyEvent; buf: cstring; bufSize: cint; + keysym: ptr XKeySym; compose: pointer): cint + {.cdecl, dynlib: libX11, importc.} +proc XSetSelectionOwner(dpy: pointer; selection: Atom; owner: XID; time: XTime): cint + {.cdecl, dynlib: libX11, importc.} +proc XConvertSelection(dpy: pointer; selection, target, property: Atom; + requestor: XID; time: XTime): cint + {.cdecl, dynlib: libX11, importc.} +proc XChangeProperty(dpy: pointer; w: XID; property, propType: Atom; + format, mode: cint; data: pointer; nelements: cint): cint + {.cdecl, dynlib: libX11, importc.} +proc XGetWindowProperty(dpy: pointer; w: XID; property: Atom; + offset, length: clong; delete: XBool; reqType: Atom; + actualType: ptr Atom; actualFormat: ptr cint; + nitems, bytesAfter: ptr culong; prop: ptr pointer): cint + {.cdecl, dynlib: libX11, importc.} +proc XSendEvent(dpy: pointer; w: XID; propagate: XBool; + mask: clong; ev: ptr XEvent): XStatus + {.cdecl, dynlib: libX11, importc.} +proc XFree(data: pointer): cint + {.cdecl, dynlib: libX11, importc.} +proc XCloseDisplay(dpy: pointer): cint + {.cdecl, dynlib: libX11, importc.} + +# ---- Xft function imports ---- + +proc XftFontOpenName(dpy: pointer; screen: cint; name: cstring): ptr XftFont + {.cdecl, dynlib: libXft, importc.} +proc XftFontClose(dpy: pointer; font: ptr XftFont): void + {.cdecl, dynlib: libXft, importc.} +proc XftDrawCreate(dpy: pointer; d: XID; visual: pointer; cmap: XID): pointer + {.cdecl, dynlib: libXft, importc.} +proc XftDrawDestroy(draw: pointer): void + {.cdecl, dynlib: libXft, importc.} +proc XftDrawChange(draw: pointer; d: XID): void + {.cdecl, dynlib: libXft, importc.} +proc XftDrawStringUtf8(draw: pointer; color: ptr XftColor; font: ptr XftFont; + x, y: cint; text: cstring; len: cint): void + {.cdecl, dynlib: libXft, importc.} +proc XftTextExtentsUtf8(dpy: pointer; font: ptr XftFont; + text: cstring; len: cint; extents: ptr XGlyphInfo): void + {.cdecl, dynlib: libXft, importc.} +proc XftDrawRect(draw: pointer; color: ptr XftColor; + x, y: cint; w, h: cuint): void + {.cdecl, dynlib: libXft, importc.} +proc XftDrawSetClipRectangles(draw: pointer; x, y: cint; + rects: ptr XRectangle; n: cint): XBool + {.cdecl, dynlib: libXft, importc.} +proc XftDrawSetClip(draw: pointer; region: pointer): XBool + {.cdecl, dynlib: libXft, importc.} + +# ---- Helpers ---- + +proc toXftColor(c: screen.Color): XftColor = + result.pixel = (c.r.culong shl 16) or (c.g.culong shl 8) or c.b.culong + result.color = XRenderColor( + red: c.r.cushort * 257, + green: c.g.cushort * 257, + blue: c.b.cushort * 257, + alpha: c.a.cushort * 257) + +proc toPixel(c: screen.Color): culong {.inline.} = + (c.r.culong shl 16) or (c.g.culong shl 8) or c.b.culong + +# ---- Font handle management ---- + +type + FontSlot = object + xftFont: ptr XftFont + metrics: FontMetrics + +var fonts: seq[FontSlot] + +proc getFontPtr(f: screen.Font): ptr XftFont {.inline.} = + let idx = f.int - 1 + if idx >= 0 and idx < fonts.len: fonts[idx].xftFont + else: nil + +# ---- Driver state ---- + +var + gDisplay: pointer + gScreen: cint + gVisual: pointer + gColormap: XID + gDepth: cint + gWindow: XID + gGC: pointer # Xlib GC for primitives + gBackPixmap: XID # double buffer + gXftDraw: pointer # Xft draw context on back pixmap + gWidth, gHeight: cint + gWmDeleteWindow: Atom + gClipboard: Atom + gUtf8String: Atom + gTargets: Atom + gClipboardText: string + gClipProperty: Atom + +var eventQueue: seq[input.Event] + +proc pushEvent(e: input.Event) = + eventQueue.add e + +# ---- Back-buffer management ---- + +proc recreateBackBuffer() = + if gBackPixmap != None: + XftDrawDestroy(gXftDraw) + discard XFreePixmap(gDisplay, gBackPixmap) + gBackPixmap = XCreatePixmap(gDisplay, gWindow, + gWidth.cuint, gHeight.cuint, gDepth.cuint) + gXftDraw = XftDrawCreate(gDisplay, gBackPixmap, gVisual, gColormap) + # Clear to black + discard XSetForeground(gDisplay, gGC, 0) + discard XFillRectangle(gDisplay, gBackPixmap, gGC, 0, 0, + gWidth.cuint, gHeight.cuint) + +# ---- Key translation ---- + +proc translateKeySym(ks: XKeySym): input.KeyCode = + if ks >= XK_a and ks <= XK_z: + return input.KeyCode(ord(keyA) + (ks.int - XK_a.int)) + if ks >= XK_0 and ks <= XK_9: + return input.KeyCode(ord(key0) + (ks.int - XK_0.int)) + if ks >= XK_F1 and ks <= XK_F12: + return input.KeyCode(ord(keyF1) + (ks.int - XK_F1.int)) + case ks.uint + of XK_Return: keyEnter + of XK_space: keySpace + of XK_Escape: keyEsc + of XK_Tab: keyTab + of XK_BackSpace: keyBackspace + of XK_Delete: keyDelete + of XK_Insert: keyInsert + of XK_Left: keyLeft + of XK_Right: keyRight + of XK_Up: keyUp + of XK_Down: keyDown + of XK_Page_Up: keyPageUp + of XK_Page_Down: keyPageDown + of XK_Home: keyHome + of XK_End: keyEnd + of XK_Caps_Lock: keyCapslock + of XK_comma: keyComma + of XK_period: keyPeriod + else: keyNone + +proc translateMods(state: cuint): set[Modifier] = + if (state and ShiftMask) != 0: result.incl modShift + if (state and ControlMask) != 0: result.incl modCtrl + if (state and Mod1Mask) != 0: result.incl modAlt + if (state and Mod4Mask) != 0: result.incl modGui + +proc translateButton(button: cuint): MouseButton = + case button + of Button1: mbLeft + of Button3: mbRight + of Button2: mbMiddle + else: mbLeft + +proc heldButtons(state: cuint): set[MouseButton] = + if (state and Button1Mask) != 0: result.incl mbLeft + if (state and Button2Mask) != 0: result.incl mbMiddle + if (state and Button3Mask) != 0: result.incl mbRight + +# ---- Clipboard handling ---- + +proc handleSelectionRequest(req: XSelectionRequestEvent) = + var ev: XEvent + zeroMem(addr ev, sizeof(XEvent)) + ev.xselection.theType = SelectionNotify + ev.xselection.requestor = req.requestor + ev.xselection.selection = req.selection + ev.xselection.target = req.target + ev.xselection.time = req.time + + if req.target == gUtf8String or req.target == XA_STRING: + discard XChangeProperty(gDisplay, req.requestor, req.property, + gUtf8String, 8, PropModeReplace, + cstring(gClipboardText), gClipboardText.len.cint) + ev.xselection.property = req.property + elif req.target == gTargets: + var targets = [gUtf8String, XA_STRING, gTargets] + discard XChangeProperty(gDisplay, req.requestor, req.property, + XA_ATOM, 32, PropModeReplace, + addr targets[0], 3) + ev.xselection.property = req.property + else: + ev.xselection.property = None + + discard XSendEvent(gDisplay, req.requestor, 0, 0, addr ev) + +# ---- Event processing ---- + +var lastClickTime: XTime +var lastClickX, lastClickY: int +var clickCount: int + +proc processXEvent(xev: XEvent) = + case xev.theType + of Expose: + if xev.xexpose.count == 0 and gBackPixmap != None: + discard XCopyArea(gDisplay, gBackPixmap, gWindow, gGC, + 0, 0, gWidth.cuint, gHeight.cuint, 0, 0) + + of ConfigureNotify: + let newW = xev.xconfigure.width + let newH = xev.xconfigure.height + if newW > 0 and newH > 0 and (newW != gWidth or newH != gHeight): + gWidth = newW + gHeight = newH + recreateBackBuffer() + var e = input.Event(kind: evWindowResize) + e.x = gWidth + e.y = gHeight + pushEvent(e) + + of ClientMessage: + if xev.xclient.data[0] == gWmDeleteWindow.clong: + pushEvent(input.Event(kind: evWindowClose)) + + of FocusIn: + pushEvent(input.Event(kind: evWindowFocusGained)) + + of FocusOut: + pushEvent(input.Event(kind: evWindowFocusLost)) + + of KeyPress: + var buf: array[8, char] + var ks: XKeySym + let textLen = XLookupString(unsafeAddr xev.xkey, cast[cstring](addr buf[0]), + 8, addr ks, nil) + # Key event + var e = input.Event(kind: evKeyDown) + e.key = translateKeySym(ks) + e.mods = translateMods(xev.xkey.state) + pushEvent(e) + # Text input (if printable) + if textLen > 0 and buf[0].uint8 >= 32 and buf[0].uint8 != 127: + var te = input.Event(kind: evTextInput) + for i in 0 ..< min(textLen, 4): + te.text[i] = buf[i] + pushEvent(te) + + of KeyRelease: + var ks: XKeySym + discard XLookupString(unsafeAddr xev.xkey, nil, 0, addr ks, nil) + var e = input.Event(kind: evKeyUp) + e.key = translateKeySym(ks) + e.mods = translateMods(xev.xkey.state) + pushEvent(e) + + of ButtonPress: + let btn = xev.xbutton.button + if btn == Button4 or btn == Button5: + # Scroll wheel + var e = input.Event(kind: evMouseWheel) + e.y = if btn == Button4: 1 else: -1 + pushEvent(e) + else: + var e = input.Event(kind: evMouseDown) + e.x = xev.xbutton.x + e.y = xev.xbutton.y + e.button = translateButton(btn) + e.mods = translateMods(xev.xbutton.state) + # Click counting for double/triple click + let now = xev.xbutton.time + if now - lastClickTime < 500 and + abs(e.x - lastClickX) < 4 and abs(e.y - lastClickY) < 4: + inc clickCount + else: + clickCount = 1 + lastClickTime = now + lastClickX = e.x + lastClickY = e.y + e.clicks = clickCount + pushEvent(e) + + of ButtonRelease: + let btn = xev.xbutton.button + if btn != Button4 and btn != Button5: + var e = input.Event(kind: evMouseUp) + e.x = xev.xbutton.x + e.y = xev.xbutton.y + e.button = translateButton(btn) + pushEvent(e) + + of MotionNotify: + var e = input.Event(kind: evMouseMove) + e.x = xev.xmotion.x + e.y = xev.xmotion.y + e.buttons = heldButtons(xev.xmotion.state) + pushEvent(e) + + of SelectionRequest: + handleSelectionRequest(xev.xselectionrequest) + + else: + discard + +proc drainXEvents() = + while XPending(gDisplay) > 0: + var xev: XEvent + discard XNextEvent(gDisplay, addr xev) + processXEvent(xev) + +# ---- Screen hook implementations ---- + +proc x11CreateWindow(layout: var ScreenLayout) = + gDisplay = XOpenDisplay(nil) + if gDisplay == nil: + quit("Cannot open X11 display") + gScreen = XDefaultScreen(gDisplay) + gVisual = XDefaultVisual(gDisplay, gScreen) + gColormap = XDefaultColormap(gDisplay, gScreen) + gDepth = XDefaultDepth(gDisplay, gScreen) + + gWindow = XCreateSimpleWindow(gDisplay, XRootWindow(gDisplay, gScreen), + 0, 0, layout.width.cuint, layout.height.cuint, 0, + XBlackPixel(gDisplay, gScreen), XBlackPixel(gDisplay, gScreen)) + + discard XSelectInput(gDisplay, gWindow, + (ExposureMask or KeyPressMask or KeyReleaseMask or + ButtonPressMask or ButtonReleaseMask or PointerMotionMask or + StructureNotifyMask or FocusChangeMask).clong) + + # Register WM_DELETE_WINDOW + gWmDeleteWindow = XInternAtom(gDisplay, "WM_DELETE_WINDOW", 0) + discard XSetWMProtocols(gDisplay, gWindow, addr gWmDeleteWindow, 1) + + # Clipboard atoms + gClipboard = XInternAtom(gDisplay, "CLIPBOARD", 0) + gUtf8String = XInternAtom(gDisplay, "UTF8_STRING", 0) + gTargets = XInternAtom(gDisplay, "TARGETS", 0) + gClipProperty = XInternAtom(gDisplay, "NIMEDIT_CLIP", 0) + + discard XStoreName(gDisplay, gWindow, "NimEdit") + discard XMapWindow(gDisplay, gWindow) + + gGC = XCreateGC(gDisplay, gWindow, 0, nil) + + # Wait for the first Expose/ConfigureNotify to get actual size + gWidth = layout.width.cint + gHeight = layout.height.cint + recreateBackBuffer() + + layout.scaleX = 1 + layout.scaleY = 1 + +proc x11Refresh() = + if gBackPixmap != None: + discard XCopyArea(gDisplay, gBackPixmap, gWindow, gGC, + 0, 0, gWidth.cuint, gHeight.cuint, 0, 0) + discard XFlush(gDisplay) + +proc x11SaveState() = discard +proc x11RestoreState() = + # Reset clip on both GC and XftDraw + discard XSetClipMask(gDisplay, gGC, None) + discard XftDrawSetClip(gXftDraw, nil) + +proc x11SetClipRect(r: basetypes.Rect) = + var xr = XRectangle( + x: r.x.cshort, y: r.y.cshort, + width: r.w.cushort, height: r.h.cushort) + discard XSetClipRectangles(gDisplay, gGC, 0, 0, addr xr, 1, 0) + discard XftDrawSetClipRectangles(gXftDraw, 0, 0, addr xr, 1) + +proc x11OpenFont(path: string; size: int; + metrics: var FontMetrics): screen.Font = + # Detect bold/italic from filename + let lpath = path.toLowerAscii() + let isBold = "bold" in lpath + let isItalic = "italic" in lpath or "oblique" in lpath + + # Map known font filenames to fontconfig names + var faceName = "monospace" # safe default + let baseName = path.extractFilename.toLowerAscii + if "dejavu" in baseName and "mono" in baseName: + faceName = "DejaVu Sans Mono" + elif "dejavu" in baseName: + faceName = "DejaVu Sans" + elif "consola" in baseName: + faceName = "Consolas" + elif "courier" in baseName: + faceName = "Courier New" + elif "arial" in baseName: + faceName = "Arial" + elif "cascadia" in baseName: + if "mono" in baseName: faceName = "Cascadia Mono" + else: faceName = "Cascadia Code" + elif "hack" in baseName: + faceName = "Hack" + elif "fira" in baseName and "code" in baseName: + faceName = "Fira Code" + elif "roboto" in baseName and "mono" in baseName: + faceName = "Roboto Mono" + elif "source" in baseName and "code" in baseName: + faceName = "Source Code Pro" + elif "jetbrains" in baseName: + faceName = "JetBrains Mono" + + # Build Xft/fontconfig pattern + var pattern = faceName & ":pixelsize=" & $size + if isBold: pattern &= ":weight=bold" + if isItalic: pattern &= ":slant=italic" + + let f = XftFontOpenName(gDisplay, gScreen, cstring(pattern)) + if f == nil: return screen.Font(0) + + metrics.ascent = f.ascent + metrics.descent = f.descent + metrics.lineHeight = f.height + fonts.add FontSlot(xftFont: f, metrics: metrics) + result = screen.Font(fonts.len) + +proc x11CloseFont(f: screen.Font) = + let idx = f.int - 1 + if idx >= 0 and idx < fonts.len and fonts[idx].xftFont != nil: + XftFontClose(gDisplay, fonts[idx].xftFont) + fonts[idx].xftFont = nil + +proc x11MeasureText(f: screen.Font; text: string): TextExtent = + let fp = getFontPtr(f) + if fp != nil and text.len > 0: + var extents: XGlyphInfo + XftTextExtentsUtf8(gDisplay, fp, cstring(text), text.len.cint, addr extents) + result = TextExtent(w: extents.xOff.int, h: fp.height.int) + +proc x11DrawText(f: screen.Font; x, y: int; text: string; + fg, bg: screen.Color): TextExtent = + let fp = getFontPtr(f) + if fp == nil or text.len == 0: return + # Measure first for background fill + var extents: XGlyphInfo + XftTextExtentsUtf8(gDisplay, fp, cstring(text), text.len.cint, addr extents) + result = TextExtent(w: extents.xOff.int, h: fp.height.int) + # Fill background + var bgColor = toXftColor(bg) + XftDrawRect(gXftDraw, addr bgColor, x.cint, y.cint, + extents.xOff.cuint, fp.height.cuint) + # Draw text (y is baseline, not top) + var fgColor = toXftColor(fg) + XftDrawStringUtf8(gXftDraw, addr fgColor, fp, + x.cint, (y + fp.ascent).cint, cstring(text), text.len.cint) + +proc x11GetFontMetrics(f: screen.Font): FontMetrics = + let idx = f.int - 1 + if idx >= 0 and idx < fonts.len: fonts[idx].metrics + else: screen.FontMetrics() + +proc x11FillRect(r: basetypes.Rect; color: screen.Color) = + var c = toXftColor(color) + XftDrawRect(gXftDraw, addr c, r.x.cint, r.y.cint, r.w.cuint, r.h.cuint) + +proc x11DrawLine(x1, y1, x2, y2: int; color: screen.Color) = + discard XSetForeground(gDisplay, gGC, toPixel(color)) + discard XDrawLine(gDisplay, gBackPixmap, gGC, + x1.cint, y1.cint, x2.cint, y2.cint) + +proc x11DrawPoint(x, y: int; color: screen.Color) = + discard XSetForeground(gDisplay, gGC, toPixel(color)) + discard XDrawPoint(gDisplay, gBackPixmap, gGC, x.cint, y.cint) + +proc x11SetCursor(c: CursorKind) = + let shape = case c + of curDefault, curArrow: XC_left_ptr + of curIbeam: XC_xterm + of curWait: XC_watch + of curCrosshair: XC_crosshair + of curHand: XC_hand2 + of curSizeNS: XC_sb_v_double_arrow + of curSizeWE: XC_sb_h_double_arrow + let cur = XCreateFontCursor(gDisplay, shape) + discard XDefineCursor(gDisplay, gWindow, cur) + +proc x11SetWindowTitle(title: string) = + discard XStoreName(gDisplay, gWindow, cstring(title)) + +# ---- Input hook implementations ---- + +proc x11PollEvent(e: var input.Event): bool = + drainXEvents() + if eventQueue.len > 0: + e = eventQueue[0] + eventQueue.delete(0) + return true + return false + +proc x11WaitEvent(e: var input.Event; timeoutMs: int): bool = + if eventQueue.len > 0: + e = eventQueue[0] + eventQueue.delete(0) + return true + if x11PollEvent(e): return true + + if timeoutMs < 0: + # Block efficiently until an X11 event arrives + var xev: XEvent + discard XNextEvent(gDisplay, addr xev) + processXEvent(xev) + # Drain any remaining + drainXEvents() + if eventQueue.len > 0: + e = eventQueue[0] + eventQueue.delete(0) + return true + return false + else: + # Poll with short sleeps + let deadline = getTicks() + timeoutMs.uint32 + while true: + let now = getTicks() + if now >= deadline: return false + os.sleep(10) + if x11PollEvent(e): return true + +proc x11GetClipboardText(): string = + discard XConvertSelection(gDisplay, gClipboard, gUtf8String, + gClipProperty, gWindow, CurrentTime) + discard XFlush(gDisplay) + # Wait for SelectionNotify (with timeout) + let deadline = getTicks() + 500 # 500ms timeout + while getTicks() < deadline: + if XPending(gDisplay) > 0: + var xev: XEvent + discard XNextEvent(gDisplay, addr xev) + if xev.theType == SelectionNotify: + if xev.xselection.property != None: + var actualType: Atom + var actualFormat: cint + var nitems, bytesAfter: culong + var data: pointer + discard XGetWindowProperty(gDisplay, gWindow, gClipProperty, + 0, 1024*1024, 1, 0, # delete=True, AnyPropertyType + addr actualType, addr actualFormat, + addr nitems, addr bytesAfter, addr data) + if data != nil: + result = $cast[cstring](data) + discard XFree(data) + return + else: + processXEvent(xev) + else: + os.sleep(5) + +proc x11PutClipboardText(text: string) = + gClipboardText = text + discard XSetSelectionOwner(gDisplay, gClipboard, gWindow, CurrentTime) + +proc x11GetModState(): set[Modifier] = + # X11 doesn't have a direct "get modifier state" API outside of events. + # Return empty; the event-level mods are more reliable. + result = {} + +# ---- POSIX imports for getTicks ---- + +type + ClockId {.importc: "clockid_t", header: "".} = distinct cint + Timespec {.importc: "struct timespec", header: "".} = object + tv_sec: clong + tv_nsec: clong + +proc clock_gettime(clk: ClockId; tp: var Timespec): cint + {.importc, header: "".} + +proc x11GetTicks(): uint32 = + # Use POSIX clock + var ts: Timespec + discard clock_gettime(0.ClockId, ts) # CLOCK_REALTIME = 0 + result = uint32(ts.tv_sec.int64 * 1000 + ts.tv_nsec.int64 div 1_000_000) + +proc x11Delay(ms: uint32) = + # Drain events during delay to stay responsive + let deadline = x11GetTicks() + ms + while true: + let now = x11GetTicks() + if now >= deadline: break + drainXEvents() + os.sleep(min(int(deadline - now), 10)) + +proc x11StartTextInput() = discard +proc x11QuitRequest() = + if gDisplay != nil: + discard XDestroyWindow(gDisplay, gWindow) + discard XCloseDisplay(gDisplay) + + +# ---- Init ---- + +proc initX11Driver*() = + # Screen hooks + createWindowHook = x11CreateWindow + refreshHook = x11Refresh + saveStateHook = x11SaveState + restoreStateHook = x11RestoreState + setClipRectHook = x11SetClipRect + openFontHook = x11OpenFont + closeFontHook = x11CloseFont + measureTextHook = x11MeasureText + drawTextHook = x11DrawText + getFontMetricsHook = x11GetFontMetrics + fillRectHook = x11FillRect + drawLineHook = x11DrawLine + drawPointHook = x11DrawPoint + setCursorHook = x11SetCursor + setWindowTitleHook = x11SetWindowTitle + # Input hooks + pollEventHook = x11PollEvent + waitEventHook = x11WaitEvent + getClipboardTextHook = x11GetClipboardText + putClipboardTextHook = x11PutClipboardText + getModStateHook = x11GetModState + getTicksHook = x11GetTicks + delayHook = x11Delay + startTextInputHook = x11StartTextInput + quitRequestHook = x11QuitRequest From d74c3cbb33ccd4644b431e63a9c28f70580d2948 Mon Sep 17 00:00:00 2001 From: araq Date: Sun, 12 Apr 2026 11:25:53 +0200 Subject: [PATCH 05/14] renamefest: Hooks are Relays --- app/cocoa_driver.nim | 50 ++++++++++++++-------------- app/gtk4_driver.nim | 48 +++++++++++++-------------- app/sdl2_driver.nim | 48 +++++++++++++-------------- app/sdl3_driver.nim | 48 +++++++++++++-------------- app/winapi_driver.nim | 48 +++++++++++++-------------- app/x11_driver.nim | 48 +++++++++++++-------------- core/input.nim | 38 ++++++++++----------- core/screen.nim | 77 +++++++++++++++++++++---------------------- 8 files changed, 202 insertions(+), 203 deletions(-) diff --git a/app/cocoa_driver.nim b/app/cocoa_driver.nim index 1498ac9..6e9e590 100644 --- a/app/cocoa_driver.nim +++ b/app/cocoa_driver.nim @@ -97,7 +97,7 @@ proc cSetWindowTitle(title: cstring) {.importc: "cocoa_setWindowTitle", cdecl.} proc cStartTextInput() {.importc: "cocoa_startTextInput", cdecl.} proc cQuitRequest() {.importc: "cocoa_quitRequest", cdecl.} -# --- Hook implementations --- +# --- Relay implementations --- proc cocoaCreateWindow(layout: var ScreenLayout) = var w, h, sx, sy: cint @@ -273,28 +273,28 @@ proc cocoaQuitRequest() = cQuitRequest() proc initCocoaDriver*() = # Screen hooks - createWindowHook = cocoaCreateWindow - refreshHook = cocoaRefresh - saveStateHook = cocoaSaveState - restoreStateHook = cocoaRestoreState - setClipRectHook = cocoaSetClipRect - openFontHook = cocoaOpenFont - closeFontHook = cocoaCloseFont - measureTextHook = cocoaMeasureText - drawTextHook = cocoaDrawText - getFontMetricsHook = cocoaGetFontMetrics - fillRectHook = cocoaFillRect - drawLineHook = cocoaDrawLine - drawPointHook = cocoaDrawPoint - setCursorHook = cocoaSetCursor - setWindowTitleHook = cocoaSetWindowTitle + createWindowRelay = cocoaCreateWindow + refreshRelay = cocoaRefresh + saveStateRelay = cocoaSaveState + restoreStateRelay = cocoaRestoreState + setClipRectRelay = cocoaSetClipRect + openFontRelay = cocoaOpenFont + closeFontRelay = cocoaCloseFont + measureTextRelay = cocoaMeasureText + drawTextRelay = cocoaDrawText + getFontMetricsRelay = cocoaGetFontMetrics + fillRectRelay = cocoaFillRect + drawLineRelay = cocoaDrawLine + drawPointRelay = cocoaDrawPoint + setCursorRelay = cocoaSetCursor + setWindowTitleRelay = cocoaSetWindowTitle # Input hooks - pollEventHook = cocoaPollEvent - waitEventHook = cocoaWaitEvent - getClipboardTextHook = cocoaGetClipboardText - putClipboardTextHook = cocoaPutClipboardText - getModStateHook = cocoaGetModState - getTicksHook = cocoaGetTicks - delayHook = cocoaDelay - startTextInputHook = cocoaStartTextInput - quitRequestHook = cocoaQuitRequest + pollEventRelay = cocoaPollEvent + waitEventRelay = cocoaWaitEvent + getClipboardTextRelay = cocoaGetClipboardText + putClipboardTextRelay = cocoaPutClipboardText + getModStateRelay = cocoaGetModState + getTicksRelay = cocoaGetTicks + delayRelay = cocoaDelay + startTextInputRelay = cocoaStartTextInput + quitRequestRelay = cocoaQuitRequest diff --git a/app/gtk4_driver.nim b/app/gtk4_driver.nim index 33797f1..e8fd497 100644 --- a/app/gtk4_driver.nim +++ b/app/gtk4_driver.nim @@ -752,27 +752,27 @@ proc gtkQuitRequest() = imContext = nil proc initGtk4Driver*() = - createWindowHook = gtkCreateWindow - refreshHook = gtkRefresh - saveStateHook = gtkSaveState - restoreStateHook = gtkRestoreState - setClipRectHook = gtkSetClipRect - openFontHook = gtkOpenFont - closeFontHook = gtkCloseFont - measureTextHook = gtkMeasureText - drawTextHook = gtkDrawText - getFontMetricsHook = gtkGetFontMetrics - fillRectHook = gtkFillRect - drawLineHook = gtkDrawLine - drawPointHook = gtkDrawPoint - setCursorHook = gtkSetCursor - setWindowTitleHook = gtkSetWindowTitle - pollEventHook = gtkPollEvent - waitEventHook = gtkWaitEvent - getClipboardTextHook = gtkGetClipboardText - putClipboardTextHook = gtkPutClipboardText - getModStateHook = gtkGetModState - getTicksHook = gtkGetTicks - delayHook = gtkDelay - startTextInputHook = gtkStartTextInput - quitRequestHook = gtkQuitRequest + createWindowRelay = gtkCreateWindow + refreshRelay = gtkRefresh + saveStateRelay = gtkSaveState + restoreStateRelay = gtkRestoreState + setClipRectRelay = gtkSetClipRect + openFontRelay = gtkOpenFont + closeFontRelay = gtkCloseFont + measureTextRelay = gtkMeasureText + drawTextRelay = gtkDrawText + getFontMetricsRelay = gtkGetFontMetrics + fillRectRelay = gtkFillRect + drawLineRelay = gtkDrawLine + drawPointRelay = gtkDrawPoint + setCursorRelay = gtkSetCursor + setWindowTitleRelay = gtkSetWindowTitle + pollEventRelay = gtkPollEvent + waitEventRelay = gtkWaitEvent + getClipboardTextRelay = gtkGetClipboardText + putClipboardTextRelay = gtkPutClipboardText + getModStateRelay = gtkGetModState + getTicksRelay = gtkGetTicks + delayRelay = gtkDelay + startTextInputRelay = gtkStartTextInput + quitRequestRelay = gtkQuitRequest diff --git a/app/sdl2_driver.nim b/app/sdl2_driver.nim index 7588533..aa0c80b 100644 --- a/app/sdl2_driver.nim +++ b/app/sdl2_driver.nim @@ -330,28 +330,28 @@ proc initSdl2Driver*() = if ttfInit() != SdlSuccess: quit("TTF init failed") # Screen hooks - createWindowHook = sdlCreateWindow - refreshHook = sdlRefresh - saveStateHook = sdlSaveState - restoreStateHook = sdlRestoreState - setClipRectHook = sdlSetClipRect - openFontHook = sdlOpenFont - closeFontHook = sdlCloseFont - measureTextHook = sdlMeasureText - drawTextShadedHook = sdlDrawTextShaded - getFontMetricsHook = sdlGetFontMetrics - fillRectHook = sdlFillRect - drawLineHook = sdlDrawLine - drawPointHook = sdlDrawPoint - setCursorHook = sdlSetCursor - setWindowTitleHook = sdlSetWindowTitle + createWindowRelay = sdlCreateWindow + refreshRelay = sdlRefresh + saveStateRelay = sdlSaveState + restoreStateRelay = sdlRestoreState + setClipRectRelay = sdlSetClipRect + openFontRelay = sdlOpenFont + closeFontRelay = sdlCloseFont + measureTextRelay = sdlMeasureText + drawTextRelay = sdlDrawTextShaded + getFontMetricsRelay = sdlGetFontMetrics + fillRectRelay = sdlFillRect + drawLineRelay = sdlDrawLine + drawPointRelay = sdlDrawPoint + setCursorRelay = sdlSetCursor + setWindowTitleRelay = sdlSetWindowTitle # Input hooks - pollEventHook = sdlPollEvent - waitEventHook = sdlWaitEvent - getClipboardTextHook = sdlGetClipboardText - putClipboardTextHook = sdlPutClipboardText - getModStateHook = sdlGetModState - getTicksHook = sdlGetTicks - delayHook = sdlDelay - startTextInputHook = sdlStartTextInput - quitRequestHook = sdlQuitRequest + pollEventRelay = sdlPollEvent + waitEventRelay = sdlWaitEvent + getClipboardTextRelay = sdlGetClipboardText + putClipboardTextRelay = sdlPutClipboardText + getModStateRelay = sdlGetModState + getTicksRelay = sdlGetTicks + delayRelay = sdlDelay + startTextInputRelay = sdlStartTextInput + quitRequestRelay = sdlQuitRequest diff --git a/app/sdl3_driver.nim b/app/sdl3_driver.nim index 7c2ab42..24a2381 100644 --- a/app/sdl3_driver.nim +++ b/app/sdl3_driver.nim @@ -317,28 +317,28 @@ proc initSdl3Driver*() = if not sdl3_ttf.init(): quit("TTF3 init failed") # Screen hooks - createWindowHook = sdlCreateWindow - refreshHook = sdlRefresh - saveStateHook = sdlSaveState - restoreStateHook = sdlRestoreState - setClipRectHook = sdlSetClipRect - openFontHook = sdlOpenFont - closeFontHook = sdlCloseFont - measureTextHook = sdlMeasureText - drawTextHook = sdlDrawText - getFontMetricsHook = sdlGetFontMetrics - fillRectHook = sdlFillRect - drawLineHook = sdlDrawLine - drawPointHook = sdlDrawPoint - setCursorHook = sdlSetCursor - setWindowTitleHook = sdlSetWindowTitle + createWindowRelay = sdlCreateWindow + refreshRelay = sdlRefresh + saveStateRelay = sdlSaveState + restoreStateRelay = sdlRestoreState + setClipRectRelay = sdlSetClipRect + openFontRelay = sdlOpenFont + closeFontRelay = sdlCloseFont + measureTextRelay = sdlMeasureText + drawTextRelay = sdlDrawText + getFontMetricsRelay = sdlGetFontMetrics + fillRectRelay = sdlFillRect + drawLineRelay = sdlDrawLine + drawPointRelay = sdlDrawPoint + setCursorRelay = sdlSetCursor + setWindowTitleRelay = sdlSetWindowTitle # Input hooks - pollEventHook = sdlPollEvent - waitEventHook = sdlWaitEvent - getClipboardTextHook = sdlGetClipboardText - putClipboardTextHook = sdlPutClipboardText - getModStateHook = sdlGetModState - getTicksHook = sdlGetTicks - delayHook = sdlDelay - startTextInputHook = sdlStartTextInput - quitRequestHook = sdlQuitRequest + pollEventRelay = sdlPollEvent + waitEventRelay = sdlWaitEvent + getClipboardTextRelay = sdlGetClipboardText + putClipboardTextRelay = sdlPutClipboardText + getModStateRelay = sdlGetModState + getTicksRelay = sdlGetTicks + delayRelay = sdlDelay + startTextInputRelay = sdlStartTextInput + quitRequestRelay = sdlQuitRequest diff --git a/app/winapi_driver.nim b/app/winapi_driver.nim index f2aa834..14eaac4 100644 --- a/app/winapi_driver.nim +++ b/app/winapi_driver.nim @@ -912,28 +912,28 @@ proc setDpiAware() = proc initWinapiDriver*() = setDpiAware() # Screen hooks - createWindowHook = winCreateWindow - refreshHook = winRefresh - saveStateHook = winSaveState - restoreStateHook = winRestoreState - setClipRectHook = winSetClipRect - openFontHook = winOpenFont - closeFontHook = winCloseFont - measureTextHook = winMeasureText - drawTextHook = winDrawText - getFontMetricsHook = winGetFontMetrics - fillRectHook = winFillRect - drawLineHook = winDrawLine - drawPointHook = winDrawPoint - setCursorHook = winSetCursor - setWindowTitleHook = winSetWindowTitle + createWindowRelay = winCreateWindow + refreshRelay = winRefresh + saveStateRelay = winSaveState + restoreStateRelay = winRestoreState + setClipRectRelay = winSetClipRect + openFontRelay = winOpenFont + closeFontRelay = winCloseFont + measureTextRelay = winMeasureText + drawTextRelay = winDrawText + getFontMetricsRelay = winGetFontMetrics + fillRectRelay = winFillRect + drawLineRelay = winDrawLine + drawPointRelay = winDrawPoint + setCursorRelay = winSetCursor + setWindowTitleRelay = winSetWindowTitle # Input hooks - pollEventHook = winPollEvent - waitEventHook = winWaitEvent - getClipboardTextHook = winGetClipboardText - putClipboardTextHook = winPutClipboardText - getModStateHook = winGetModState - getTicksHook = winGetTicks - delayHook = winDelay - startTextInputHook = winStartTextInput - quitRequestHook = winQuitRequest + pollEventRelay = winPollEvent + waitEventRelay = winWaitEvent + getClipboardTextRelay = winGetClipboardText + putClipboardTextRelay = winPutClipboardText + getModStateRelay = winGetModState + getTicksRelay = winGetTicks + delayRelay = winDelay + startTextInputRelay = winStartTextInput + quitRequestRelay = winQuitRequest diff --git a/app/x11_driver.nim b/app/x11_driver.nim index ee55615..53258d6 100644 --- a/app/x11_driver.nim +++ b/app/x11_driver.nim @@ -931,28 +931,28 @@ proc x11QuitRequest() = proc initX11Driver*() = # Screen hooks - createWindowHook = x11CreateWindow - refreshHook = x11Refresh - saveStateHook = x11SaveState - restoreStateHook = x11RestoreState - setClipRectHook = x11SetClipRect - openFontHook = x11OpenFont - closeFontHook = x11CloseFont - measureTextHook = x11MeasureText - drawTextHook = x11DrawText - getFontMetricsHook = x11GetFontMetrics - fillRectHook = x11FillRect - drawLineHook = x11DrawLine - drawPointHook = x11DrawPoint - setCursorHook = x11SetCursor - setWindowTitleHook = x11SetWindowTitle + createWindowRelay = x11CreateWindow + refreshRelay = x11Refresh + saveStateRelay = x11SaveState + restoreStateRelay = x11RestoreState + setClipRectRelay = x11SetClipRect + openFontRelay = x11OpenFont + closeFontRelay = x11CloseFont + measureTextRelay = x11MeasureText + drawTextRelay = x11DrawText + getFontMetricsRelay = x11GetFontMetrics + fillRectRelay = x11FillRect + drawLineRelay = x11DrawLine + drawPointRelay = x11DrawPoint + setCursorRelay = x11SetCursor + setWindowTitleRelay = x11SetWindowTitle # Input hooks - pollEventHook = x11PollEvent - waitEventHook = x11WaitEvent - getClipboardTextHook = x11GetClipboardText - putClipboardTextHook = x11PutClipboardText - getModStateHook = x11GetModState - getTicksHook = x11GetTicks - delayHook = x11Delay - startTextInputHook = x11StartTextInput - quitRequestHook = x11QuitRequest + pollEventRelay = x11PollEvent + waitEventRelay = x11WaitEvent + getClipboardTextRelay = x11GetClipboardText + putClipboardTextRelay = x11PutClipboardText + getModStateRelay = x11GetModState + getTicksRelay = x11GetTicks + delayRelay = x11Delay + startTextInputRelay = x11StartTextInput + quitRequestRelay = x11QuitRequest diff --git a/core/input.nim b/core/input.nim index f6e1493..80aeb0f 100644 --- a/core/input.nim +++ b/core/input.nim @@ -1,4 +1,4 @@ -# Platform-independent input events and hooks. +# Platform-independent input events and relays. # Part of the core stdlib abstraction (plan.md). @@ -42,32 +42,32 @@ type buttons*: set[MouseButton] ## evMouseMove: which buttons are held clicks*: int ## number of consecutive clicks (double-click = 2) -var pollEventHook*: proc (e: var Event): bool {.nimcall.} = +var pollEventRelay*: proc (e: var Event): bool {.nimcall.} = proc (e: var Event): bool = false -var getClipboardTextHook*: proc (): string {.nimcall.} = +var getClipboardTextRelay*: proc (): string {.nimcall.} = proc (): string = "" -var putClipboardTextHook*: proc (text: string) {.nimcall.} = +var putClipboardTextRelay*: proc (text: string) {.nimcall.} = proc (text: string) = discard -var waitEventHook*: proc (e: var Event; timeoutMs: int): bool {.nimcall.} = +var waitEventRelay*: proc (e: var Event; timeoutMs: int): bool {.nimcall.} = proc (e: var Event; timeoutMs: int): bool = false -var getModStateHook*: proc (): set[Modifier] {.nimcall.} = +var getModStateRelay*: proc (): set[Modifier] {.nimcall.} = proc (): set[Modifier] = {} -var getTicksHook*: proc (): uint32 {.nimcall.} = +var getTicksRelay*: proc (): uint32 {.nimcall.} = proc (): uint32 = 0 -var delayHook*: proc (ms: uint32) {.nimcall.} = +var delayRelay*: proc (ms: uint32) {.nimcall.} = proc (ms: uint32) = discard -var startTextInputHook*: proc () {.nimcall.} = +var startTextInputRelay*: proc () {.nimcall.} = proc () = discard -var quitRequestHook*: proc () {.nimcall.} = +var quitRequestRelay*: proc () {.nimcall.} = proc () = discard -proc pollEvent*(e: var Event): bool = pollEventHook(e) -proc waitEvent*(e: var Event; timeoutMs: int = -1): bool = waitEventHook(e, timeoutMs) -proc getClipboardText*(): string = getClipboardTextHook() -proc putClipboardText*(text: string) = putClipboardTextHook(text) -proc getModState*(): set[Modifier] = getModStateHook() -proc getTicks*(): uint32 = getTicksHook() -proc delay*(ms: uint32) = delayHook(ms) -proc startTextInput*() = startTextInputHook() -proc quitRequest*() = quitRequestHook() +proc pollEvent*(e: var Event): bool = pollEventRelay(e) +proc waitEvent*(e: var Event; timeoutMs: int = -1): bool = waitEventRelay(e, timeoutMs) +proc getClipboardText*(): string = getClipboardTextRelay() +proc putClipboardText*(text: string) = putClipboardTextRelay(text) +proc getModState*(): set[Modifier] = getModStateRelay() +proc getTicks*(): uint32 = getTicksRelay() +proc delay*(ms: uint32) = delayRelay(ms) +proc startTextInput*() = startTextInputRelay() +proc quitRequest*() = quitRequestRelay() diff --git a/core/screen.nim b/core/screen.nim index e0bd3df..5ba75e4 100644 --- a/core/screen.nim +++ b/core/screen.nim @@ -1,4 +1,4 @@ -# Platform-independent screen/drawing hooks. +# Platform-independent screen/drawing relays. # Part of the core stdlib abstraction (plan.md). import basetypes @@ -30,86 +30,85 @@ proc `==`*(a, b: Font): bool {.borrow.} proc `==`*(a, b: Image): bool {.borrow.} # Window lifecycle -var createWindowHook*: proc (layout: var ScreenLayout) {.nimcall.} = +var createWindowRelay*: proc (layout: var ScreenLayout) {.nimcall.} = proc (layout: var ScreenLayout) = discard -var refreshHook*: proc () {.nimcall.} = +var refreshRelay*: proc () {.nimcall.} = proc () = discard # Graphics state -var saveStateHook*: proc () {.nimcall.} = +var saveStateRelay*: proc () {.nimcall.} = proc () = discard -var restoreStateHook*: proc () {.nimcall.} = +var restoreStateRelay*: proc () {.nimcall.} = proc () = discard -var setClipRectHook*: proc (r: Rect) {.nimcall.} = +var setClipRectRelay*: proc (r: Rect) {.nimcall.} = proc (r: Rect) = discard # Font management -var openFontHook*: proc (path: string; size: int; +var openFontRelay*: proc (path: string; size: int; metrics: var FontMetrics): Font {.nimcall.} = proc (path: string; size: int; metrics: var FontMetrics): Font = Font(0) -var closeFontHook*: proc (f: Font) {.nimcall.} = +var closeFontRelay*: proc (f: Font) {.nimcall.} = proc (f: Font) = discard # Text -var measureTextHook*: proc (f: Font; text: string): TextExtent {.nimcall.} = +var measureTextRelay*: proc (f: Font; text: string): TextExtent {.nimcall.} = proc (f: Font; text: string): TextExtent = TextExtent() -var drawTextHook*: proc (f: Font; x, y: int; text: string; +var drawTextRelay*: proc (f: Font; x, y: int; text: string; fg, bg: Color): TextExtent {.nimcall.} = proc (f: Font; x, y: int; text: string; fg, bg: Color): TextExtent = TextExtent() -var getFontMetricsHook*: proc (f: Font): FontMetrics {.nimcall.} = +var getFontMetricsRelay*: proc (f: Font): FontMetrics {.nimcall.} = proc (f: Font): FontMetrics = FontMetrics() # Drawing primitives -var fillRectHook*: proc (r: Rect; color: Color) {.nimcall.} = +var fillRectRelay*: proc (r: Rect; color: Color) {.nimcall.} = proc (r: Rect; color: Color) = discard -var drawLineHook*: proc (x1, y1, x2, y2: int; color: Color) {.nimcall.} = +var drawLineRelay*: proc (x1, y1, x2, y2: int; color: Color) {.nimcall.} = proc (x1, y1, x2, y2: int; color: Color) = discard -var drawPointHook*: proc (x, y: int; color: Color) {.nimcall.} = +var drawPointRelay*: proc (x, y: int; color: Color) {.nimcall.} = proc (x, y: int; color: Color) = discard # Images -var loadImageHook*: proc (path: string): Image {.nimcall.} = +var loadImageRelay*: proc (path: string): Image {.nimcall.} = proc (path: string): Image = Image(0) -var freeImageHook*: proc (img: Image) {.nimcall.} = +var freeImageRelay*: proc (img: Image) {.nimcall.} = proc (img: Image) = discard -var drawImageHook*: proc (img: Image; src, dst: Rect) {.nimcall.} = +var drawImageRelay*: proc (img: Image; src, dst: Rect) {.nimcall.} = proc (img: Image; src, dst: Rect) = discard # Cursor and window -var setCursorHook*: proc (c: CursorKind) {.nimcall.} = +var setCursorRelay*: proc (c: CursorKind) {.nimcall.} = proc (c: CursorKind) = discard -var setWindowTitleHook*: proc (title: string) {.nimcall.} = +var setWindowTitleRelay*: proc (title: string) {.nimcall.} = proc (title: string) = discard # Convenience wrappers proc createWindow*(requestedW, requestedH: int): ScreenLayout = result = ScreenLayout(width: requestedW, height: requestedH) - createWindowHook(result) + createWindowRelay(result) -proc refresh*() = refreshHook() -proc saveState*() = saveStateHook() -proc restoreState*() = restoreStateHook() -proc setClipRect*(r: Rect) = setClipRectHook(r) +proc refresh*() = refreshRelay() +proc saveState*() = saveStateRelay() +proc restoreState*() = restoreStateRelay() +proc setClipRect*(r: Rect) = setClipRectRelay(r) proc openFont*(path: string; size: int; metrics: var FontMetrics): Font = - openFontHook(path, size, metrics) -proc closeFont*(f: Font) = closeFontHook(f) -proc measureText*(f: Font; text: string): TextExtent = measureTextHook(f, text) + openFontRelay(path, size, metrics) +proc closeFont*(f: Font) = closeFontRelay(f) +proc measureText*(f: Font; text: string): TextExtent = measureTextRelay(f, text) proc drawText*(f: Font; x, y: int; text: string; fg, bg: Color): TextExtent = - drawTextHook(f, x, y, text, fg, bg) -proc getFontMetrics*(f: Font): FontMetrics = getFontMetricsHook(f) -proc fontLineSkip*(f: Font): int = getFontMetricsHook(f).lineHeight -proc fillRect*(r: Rect; color: Color) = fillRectHook(r, color) + drawTextRelay(f, x, y, text, fg, bg) +proc getFontMetrics*(f: Font): FontMetrics = getFontMetricsRelay(f) +proc fontLineSkip*(f: Font): int = getFontMetricsRelay(f).lineHeight +proc fillRect*(r: Rect; color: Color) = fillRectRelay(r, color) proc drawLine*(x1, y1, x2, y2: int; color: Color) = - drawLineHook(x1, y1, x2, y2, color) -proc drawPoint*(x, y: int; color: Color) = drawPointHook(x, y, color) -proc loadImage*(path: string): Image = loadImageHook(path) -proc freeImage*(img: Image) = freeImageHook(img) -proc drawImage*(img: Image; src, dst: Rect) = drawImageHook(img, src, dst) -proc setCursor*(c: CursorKind) = setCursorHook(c) -proc setWindowTitle*(title: string) = setWindowTitleHook(title) + drawLineRelay(x1, y1, x2, y2, color) +proc drawPoint*(x, y: int; color: Color) = drawPointRelay(x, y, color) +proc loadImage*(path: string): Image = loadImageRelay(path) +proc freeImage*(img: Image) = freeImageRelay(img) +proc drawImage*(img: Image; src, dst: Rect) = drawImageRelay(img, src, dst) +proc setCursor*(c: CursorKind) = setCursorRelay(c) +proc setWindowTitle*(title: string) = setWindowTitleRelay(title) # Color constructors proc color*(r, g, b: uint8; a: uint8 = 255): Color = Color(r: r, g: g, b: b, a: a) - From cf765cd4e6a7df0b0d054dec285aad7b4e54df11 Mon Sep 17 00:00:00 2001 From: araq Date: Sun, 12 Apr 2026 11:54:50 +0200 Subject: [PATCH 06/14] big refactoring --- app/cocoa_driver.nim | 55 ++++++---------- app/gtk4_driver.nim | 50 ++++++--------- app/nimedit.nim | 19 +++--- app/sdl2_driver.nim | 50 ++++++--------- app/sdl3_driver.nim | 50 ++++++--------- app/winapi_driver.nim | 55 +++++++--------- app/x11_driver.nim | 58 +++++++---------- core/input.nim | 58 +++++++++-------- core/screen.nim | 143 +++++++++++++++++++++--------------------- 9 files changed, 233 insertions(+), 305 deletions(-) diff --git a/app/cocoa_driver.nim b/app/cocoa_driver.nim index 6e9e590..842e311 100644 --- a/app/cocoa_driver.nim +++ b/app/cocoa_driver.nim @@ -257,44 +257,29 @@ proc cocoaGetClipboardText(): string = proc cocoaPutClipboardText(text: string) = cPutClipboardText(cstring(text)) -proc cocoaGetModState(): set[Modifier] = - let m = cGetModState() - if (m and neModShift) != 0: result.incl modShift - if (m and neModCtrl) != 0: result.incl modCtrl - if (m and neModAlt) != 0: result.incl modAlt - if (m and neModGui) != 0: result.incl modGui - -proc cocoaGetTicks(): uint32 = cGetTicks() -proc cocoaDelay(ms: uint32) = cDelay(ms) +proc cocoaGetTicks(): int = cGetTicks().int +proc cocoaDelay(ms: int) = cDelay(ms.uint32) proc cocoaStartTextInput() = cStartTextInput() proc cocoaQuitRequest() = cQuitRequest() # --- Init --- proc initCocoaDriver*() = - # Screen hooks - createWindowRelay = cocoaCreateWindow - refreshRelay = cocoaRefresh - saveStateRelay = cocoaSaveState - restoreStateRelay = cocoaRestoreState - setClipRectRelay = cocoaSetClipRect - openFontRelay = cocoaOpenFont - closeFontRelay = cocoaCloseFont - measureTextRelay = cocoaMeasureText - drawTextRelay = cocoaDrawText - getFontMetricsRelay = cocoaGetFontMetrics - fillRectRelay = cocoaFillRect - drawLineRelay = cocoaDrawLine - drawPointRelay = cocoaDrawPoint - setCursorRelay = cocoaSetCursor - setWindowTitleRelay = cocoaSetWindowTitle - # Input hooks - pollEventRelay = cocoaPollEvent - waitEventRelay = cocoaWaitEvent - getClipboardTextRelay = cocoaGetClipboardText - putClipboardTextRelay = cocoaPutClipboardText - getModStateRelay = cocoaGetModState - getTicksRelay = cocoaGetTicks - delayRelay = cocoaDelay - startTextInputRelay = cocoaStartTextInput - quitRequestRelay = cocoaQuitRequest + windowRelays = WindowRelays( + createWindow: cocoaCreateWindow, refresh: cocoaRefresh, + saveState: cocoaSaveState, restoreState: cocoaRestoreState, + setClipRect: cocoaSetClipRect, setCursor: cocoaSetCursor, + setWindowTitle: cocoaSetWindowTitle) + fontRelays = FontRelays( + openFont: cocoaOpenFont, closeFont: cocoaCloseFont, + getFontMetrics: cocoaGetFontMetrics, measureText: cocoaMeasureText, + drawText: cocoaDrawText) + drawRelays = DrawRelays( + fillRect: cocoaFillRect, drawLine: cocoaDrawLine, + drawPoint: cocoaDrawPoint) + inputRelays = InputRelays( + pollEvent: cocoaPollEvent, waitEvent: cocoaWaitEvent, + getTicks: cocoaGetTicks, delay: cocoaDelay, + startTextInput: cocoaStartTextInput, quitRequest: cocoaQuitRequest) + clipboardRelays = ClipboardRelays( + getText: cocoaGetClipboardText, putText: cocoaPutClipboardText) diff --git a/app/gtk4_driver.nim b/app/gtk4_driver.nim index e8fd497..aa57069 100644 --- a/app/gtk4_driver.nim +++ b/app/gtk4_driver.nim @@ -721,13 +721,10 @@ proc gtkPutClipboardText(text: string) = if clip != nil: gdk_clipboard_set_text(clip, cstring(text)) -proc gtkGetModState(): set[Modifier] = - modState +proc gtkGetTicks(): int = + int(g_get_monotonic_time() div 1000) -proc gtkGetTicks(): uint32 = - uint32(g_get_monotonic_time() div 1000) - -proc gtkDelay(ms: uint32) = +proc gtkDelay(ms: int) = g_usleep(culong(ms) * 1000) proc gtkStartTextInput() = @@ -752,27 +749,20 @@ proc gtkQuitRequest() = imContext = nil proc initGtk4Driver*() = - createWindowRelay = gtkCreateWindow - refreshRelay = gtkRefresh - saveStateRelay = gtkSaveState - restoreStateRelay = gtkRestoreState - setClipRectRelay = gtkSetClipRect - openFontRelay = gtkOpenFont - closeFontRelay = gtkCloseFont - measureTextRelay = gtkMeasureText - drawTextRelay = gtkDrawText - getFontMetricsRelay = gtkGetFontMetrics - fillRectRelay = gtkFillRect - drawLineRelay = gtkDrawLine - drawPointRelay = gtkDrawPoint - setCursorRelay = gtkSetCursor - setWindowTitleRelay = gtkSetWindowTitle - pollEventRelay = gtkPollEvent - waitEventRelay = gtkWaitEvent - getClipboardTextRelay = gtkGetClipboardText - putClipboardTextRelay = gtkPutClipboardText - getModStateRelay = gtkGetModState - getTicksRelay = gtkGetTicks - delayRelay = gtkDelay - startTextInputRelay = gtkStartTextInput - quitRequestRelay = gtkQuitRequest + windowRelays = WindowRelays( + createWindow: gtkCreateWindow, refresh: gtkRefresh, + saveState: gtkSaveState, restoreState: gtkRestoreState, + setClipRect: gtkSetClipRect, setCursor: gtkSetCursor, + setWindowTitle: gtkSetWindowTitle) + fontRelays = FontRelays( + openFont: gtkOpenFont, closeFont: gtkCloseFont, + getFontMetrics: gtkGetFontMetrics, measureText: gtkMeasureText, + drawText: gtkDrawText) + drawRelays = DrawRelays( + fillRect: gtkFillRect, drawLine: gtkDrawLine, drawPoint: gtkDrawPoint) + inputRelays = InputRelays( + pollEvent: gtkPollEvent, waitEvent: gtkWaitEvent, + getTicks: gtkGetTicks, delay: gtkDelay, + startTextInput: gtkStartTextInput, quitRequest: gtkQuitRequest) + clipboardRelays = ClipboardRelays( + getText: gtkGetClipboardText, putText: gtkPutClipboardText) diff --git a/app/nimedit.nim b/app/nimedit.nim index 3a4ffbb..b947382 100644 --- a/app/nimedit.nim +++ b/app/nimedit.nim @@ -549,11 +549,8 @@ proc pollEvents*(someConsoleRunning, windowHasFocus: bool): seq[input.Event] = while input.pollEvent(e): result.add e -proc ctrlKeyPressed*(): bool = - modCtrl in input.getModState() - -proc shiftKeyPressed*(): bool = - modShift in input.getModState() +proc ctrlKeyPressed*(e: Event): bool = modCtrl in e.mods +proc shiftKeyPressed*(e: Event): bool = modShift in e.mods proc loadTheme(ed: SharedState) = loadTheme(ed.cfgColors, ed.theme, ed.mgr, ed.fontM) @@ -682,7 +679,7 @@ proc closeWindow(ed: Editor) = left.next = left.next.next ed.sh.activeWindow = left -proc runAction(ed: Editor; action: Action; arg: string): bool = +proc runAction(ed: Editor; action: Action; arg: string; shiftKeyPressed: bool): bool = template console: untyped = ed.console case action @@ -756,7 +753,7 @@ proc runAction(ed: Editor; action: Action; arg: string): bool = main.insertEnter() trackSpot(ed.sh.hotspots, main) elif focus==prompt: - if ed.runCmd(prompt.fullText, shiftKeyPressed()): + if ed.runCmd(prompt.fullText, shiftKeyPressed): saveOpenTabs(ed) result = true elif focus==console: @@ -931,7 +928,7 @@ proc processEvents(events: out seq[Event]; ed: Editor): bool = of evMouseDown: var clicks = e.clicks if clicks == 0 or clicks > 5: clicks = 1 - if ctrlKeyPressed(): inc(clicks) + if ctrlKeyPressed(e): inc(clicks) let p = point(e.x, e.y) if ed.mainRect.contains(p) and ed.main.scrollingEnabled: var rawMainRect = ed.mainRect @@ -957,7 +954,7 @@ proc processEvents(events: out seq[Event]; ed: Editor): bool = of evTextInput: var surpress = false if e.text[0] == ' ' and e.text[1] == '\0': - if ctrlKeyPressed(): + if ctrlKeyPressed(e): surpress = true if not surpress: var textStr = "" @@ -978,7 +975,7 @@ proc processEvents(events: out seq[Event]; ed: Editor): bool = of evKeyDown, evKeyUp: let ks = eventToKeySet(e) let cmd = sh.keymapping.getOrDefault(ks) - if ed.runAction(cmd.action, cmd.arg): + if ed.runAction(cmd.action, cmd.arg, shiftKeyPressed(e)): result = true break else: discard @@ -1148,7 +1145,7 @@ proc mainProc(ed: Editor) = # reduce CPU usage: delay(20) let newTicks = getTicks() - if newTicks - oldTicks > timeout.uint32: + if newTicks - oldTicks > timeout: oldTicks = newTicks inc sh.idle diff --git a/app/sdl2_driver.nim b/app/sdl2_driver.nim index aa0c80b..064af15 100644 --- a/app/sdl2_driver.nim +++ b/app/sdl2_driver.nim @@ -311,12 +311,9 @@ proc sdlWaitEvent(e: var input.Event; timeoutMs: int): bool = sdl2.delay(10) return false -proc sdlGetModState(): set[Modifier] = - translateMods(sdl2.getModState().int16) +proc sdlGetTicks(): int = sdl2.getTicks().int -proc sdlGetTicks(): uint32 = sdl2.getTicks() - -proc sdlDelay(ms: uint32) = sdl2.delay(ms) +proc sdlDelay(ms: int) = sdl2.delay(ms.uint32) proc sdlStartTextInput() = sdl2.startTextInput() @@ -329,29 +326,20 @@ proc initSdl2Driver*() = quit("SDL init failed") if ttfInit() != SdlSuccess: quit("TTF init failed") - # Screen hooks - createWindowRelay = sdlCreateWindow - refreshRelay = sdlRefresh - saveStateRelay = sdlSaveState - restoreStateRelay = sdlRestoreState - setClipRectRelay = sdlSetClipRect - openFontRelay = sdlOpenFont - closeFontRelay = sdlCloseFont - measureTextRelay = sdlMeasureText - drawTextRelay = sdlDrawTextShaded - getFontMetricsRelay = sdlGetFontMetrics - fillRectRelay = sdlFillRect - drawLineRelay = sdlDrawLine - drawPointRelay = sdlDrawPoint - setCursorRelay = sdlSetCursor - setWindowTitleRelay = sdlSetWindowTitle - # Input hooks - pollEventRelay = sdlPollEvent - waitEventRelay = sdlWaitEvent - getClipboardTextRelay = sdlGetClipboardText - putClipboardTextRelay = sdlPutClipboardText - getModStateRelay = sdlGetModState - getTicksRelay = sdlGetTicks - delayRelay = sdlDelay - startTextInputRelay = sdlStartTextInput - quitRequestRelay = sdlQuitRequest + windowRelays = WindowRelays( + createWindow: sdlCreateWindow, refresh: sdlRefresh, + saveState: sdlSaveState, restoreState: sdlRestoreState, + setClipRect: sdlSetClipRect, setCursor: sdlSetCursor, + setWindowTitle: sdlSetWindowTitle) + fontRelays = FontRelays( + openFont: sdlOpenFont, closeFont: sdlCloseFont, + getFontMetrics: sdlGetFontMetrics, measureText: sdlMeasureText, + drawText: sdlDrawTextShaded) + drawRelays = DrawRelays( + fillRect: sdlFillRect, drawLine: sdlDrawLine, drawPoint: sdlDrawPoint) + inputRelays = InputRelays( + pollEvent: sdlPollEvent, waitEvent: sdlWaitEvent, + getTicks: sdlGetTicks, delay: sdlDelay, + startTextInput: sdlStartTextInput, quitRequest: sdlQuitRequest) + clipboardRelays = ClipboardRelays( + getText: sdlGetClipboardText, putText: sdlPutClipboardText) diff --git a/app/sdl3_driver.nim b/app/sdl3_driver.nim index 24a2381..a790d20 100644 --- a/app/sdl3_driver.nim +++ b/app/sdl3_driver.nim @@ -301,11 +301,8 @@ proc sdlWaitEvent(e: var input.Event; timeoutMs: int): bool = translateEvent(sdlEvent, e) result = true -proc sdlGetModState(): set[Modifier] = - translateMods(sdl3.getModState()) - -proc sdlGetTicks(): uint32 = uint32(sdl3.getTicks()) -proc sdlDelay(ms: uint32) = sdl3.delay(ms) +proc sdlGetTicks(): int = sdl3.getTicks().int +proc sdlDelay(ms: int) = sdl3.delay(ms.uint32) proc sdlStartTextInput() = discard startTextInput(win) proc sdlQuitRequest() = sdl3.quit() @@ -316,29 +313,20 @@ proc initSdl3Driver*() = quit("SDL3 init failed") if not sdl3_ttf.init(): quit("TTF3 init failed") - # Screen hooks - createWindowRelay = sdlCreateWindow - refreshRelay = sdlRefresh - saveStateRelay = sdlSaveState - restoreStateRelay = sdlRestoreState - setClipRectRelay = sdlSetClipRect - openFontRelay = sdlOpenFont - closeFontRelay = sdlCloseFont - measureTextRelay = sdlMeasureText - drawTextRelay = sdlDrawText - getFontMetricsRelay = sdlGetFontMetrics - fillRectRelay = sdlFillRect - drawLineRelay = sdlDrawLine - drawPointRelay = sdlDrawPoint - setCursorRelay = sdlSetCursor - setWindowTitleRelay = sdlSetWindowTitle - # Input hooks - pollEventRelay = sdlPollEvent - waitEventRelay = sdlWaitEvent - getClipboardTextRelay = sdlGetClipboardText - putClipboardTextRelay = sdlPutClipboardText - getModStateRelay = sdlGetModState - getTicksRelay = sdlGetTicks - delayRelay = sdlDelay - startTextInputRelay = sdlStartTextInput - quitRequestRelay = sdlQuitRequest + windowRelays = WindowRelays( + createWindow: sdlCreateWindow, refresh: sdlRefresh, + saveState: sdlSaveState, restoreState: sdlRestoreState, + setClipRect: sdlSetClipRect, setCursor: sdlSetCursor, + setWindowTitle: sdlSetWindowTitle) + fontRelays = FontRelays( + openFont: sdlOpenFont, closeFont: sdlCloseFont, + getFontMetrics: sdlGetFontMetrics, measureText: sdlMeasureText, + drawText: sdlDrawText) + drawRelays = DrawRelays( + fillRect: sdlFillRect, drawLine: sdlDrawLine, drawPoint: sdlDrawPoint) + inputRelays = InputRelays( + pollEvent: sdlPollEvent, waitEvent: sdlWaitEvent, + getTicks: sdlGetTicks, delay: sdlDelay, + startTextInput: sdlStartTextInput, quitRequest: sdlQuitRequest) + clipboardRelays = ClipboardRelays( + getText: sdlGetClipboardText, putText: sdlPutClipboardText) diff --git a/app/winapi_driver.nim b/app/winapi_driver.nim index 14eaac4..bfffa93 100644 --- a/app/winapi_driver.nim +++ b/app/winapi_driver.nim @@ -871,16 +871,14 @@ proc winPutClipboardText(text: string) = discard SetClipboardData(CF_UNICODETEXT, cast[HANDLE](hMem)) discard CloseClipboard() -proc winGetModState(): set[Modifier] = - getModifiers() - -proc winGetTicks(): uint32 = GetTickCount() -proc winDelay(ms: uint32) = - let deadline = GetTickCount() + ms +proc winGetTicks(): int = GetTickCount().int +proc winDelay(ms: int) = + let msU = ms.DWORD + let deadline = GetTickCount() + msU while true: let now = GetTickCount() if now >= deadline: break - let remaining = min(deadline - now, ms) + let remaining = min(deadline - now, msU) discard MsgWaitForMultipleObjects(0, nil, 0, remaining, QS_ALLINPUT) pumpMessages() proc winStartTextInput() = discard # Win32 always delivers WM_CHAR @@ -911,29 +909,20 @@ proc setDpiAware() = proc initWinapiDriver*() = setDpiAware() - # Screen hooks - createWindowRelay = winCreateWindow - refreshRelay = winRefresh - saveStateRelay = winSaveState - restoreStateRelay = winRestoreState - setClipRectRelay = winSetClipRect - openFontRelay = winOpenFont - closeFontRelay = winCloseFont - measureTextRelay = winMeasureText - drawTextRelay = winDrawText - getFontMetricsRelay = winGetFontMetrics - fillRectRelay = winFillRect - drawLineRelay = winDrawLine - drawPointRelay = winDrawPoint - setCursorRelay = winSetCursor - setWindowTitleRelay = winSetWindowTitle - # Input hooks - pollEventRelay = winPollEvent - waitEventRelay = winWaitEvent - getClipboardTextRelay = winGetClipboardText - putClipboardTextRelay = winPutClipboardText - getModStateRelay = winGetModState - getTicksRelay = winGetTicks - delayRelay = winDelay - startTextInputRelay = winStartTextInput - quitRequestRelay = winQuitRequest + windowRelays = WindowRelays( + createWindow: winCreateWindow, refresh: winRefresh, + saveState: winSaveState, restoreState: winRestoreState, + setClipRect: winSetClipRect, setCursor: winSetCursor, + setWindowTitle: winSetWindowTitle) + fontRelays = FontRelays( + openFont: winOpenFont, closeFont: winCloseFont, + getFontMetrics: winGetFontMetrics, measureText: winMeasureText, + drawText: winDrawText) + drawRelays = DrawRelays( + fillRect: winFillRect, drawLine: winDrawLine, drawPoint: winDrawPoint) + inputRelays = InputRelays( + pollEvent: winPollEvent, waitEvent: winWaitEvent, + getTicks: winGetTicks, delay: winDelay, + startTextInput: winStartTextInput, quitRequest: winQuitRequest) + clipboardRelays = ClipboardRelays( + getText: winGetClipboardText, putText: winPutClipboardText) diff --git a/app/x11_driver.nim b/app/x11_driver.nim index 53258d6..190ce65 100644 --- a/app/x11_driver.nim +++ b/app/x11_driver.nim @@ -849,7 +849,7 @@ proc x11WaitEvent(e: var input.Event; timeoutMs: int): bool = return false else: # Poll with short sleeps - let deadline = getTicks() + timeoutMs.uint32 + let deadline = getTicks() + timeoutMs while true: let now = getTicks() if now >= deadline: return false @@ -889,11 +889,6 @@ proc x11PutClipboardText(text: string) = gClipboardText = text discard XSetSelectionOwner(gDisplay, gClipboard, gWindow, CurrentTime) -proc x11GetModState(): set[Modifier] = - # X11 doesn't have a direct "get modifier state" API outside of events. - # Return empty; the event-level mods are more reliable. - result = {} - # ---- POSIX imports for getTicks ---- type @@ -905,20 +900,20 @@ type proc clock_gettime(clk: ClockId; tp: var Timespec): cint {.importc, header: "".} -proc x11GetTicks(): uint32 = +proc x11GetTicks(): int = # Use POSIX clock var ts: Timespec discard clock_gettime(0.ClockId, ts) # CLOCK_REALTIME = 0 - result = uint32(ts.tv_sec.int64 * 1000 + ts.tv_nsec.int64 div 1_000_000) + result = int(ts.tv_sec.int64 * 1000 + ts.tv_nsec.int64 div 1_000_000) -proc x11Delay(ms: uint32) = +proc x11Delay(ms: int) = # Drain events during delay to stay responsive let deadline = x11GetTicks() + ms while true: let now = x11GetTicks() if now >= deadline: break drainXEvents() - os.sleep(min(int(deadline - now), 10)) + os.sleep(min(deadline - now, 10)) proc x11StartTextInput() = discard proc x11QuitRequest() = @@ -930,29 +925,20 @@ proc x11QuitRequest() = # ---- Init ---- proc initX11Driver*() = - # Screen hooks - createWindowRelay = x11CreateWindow - refreshRelay = x11Refresh - saveStateRelay = x11SaveState - restoreStateRelay = x11RestoreState - setClipRectRelay = x11SetClipRect - openFontRelay = x11OpenFont - closeFontRelay = x11CloseFont - measureTextRelay = x11MeasureText - drawTextRelay = x11DrawText - getFontMetricsRelay = x11GetFontMetrics - fillRectRelay = x11FillRect - drawLineRelay = x11DrawLine - drawPointRelay = x11DrawPoint - setCursorRelay = x11SetCursor - setWindowTitleRelay = x11SetWindowTitle - # Input hooks - pollEventRelay = x11PollEvent - waitEventRelay = x11WaitEvent - getClipboardTextRelay = x11GetClipboardText - putClipboardTextRelay = x11PutClipboardText - getModStateRelay = x11GetModState - getTicksRelay = x11GetTicks - delayRelay = x11Delay - startTextInputRelay = x11StartTextInput - quitRequestRelay = x11QuitRequest + windowRelays = WindowRelays( + createWindow: x11CreateWindow, refresh: x11Refresh, + saveState: x11SaveState, restoreState: x11RestoreState, + setClipRect: x11SetClipRect, setCursor: x11SetCursor, + setWindowTitle: x11SetWindowTitle) + fontRelays = FontRelays( + openFont: x11OpenFont, closeFont: x11CloseFont, + getFontMetrics: x11GetFontMetrics, measureText: x11MeasureText, + drawText: x11DrawText) + drawRelays = DrawRelays( + fillRect: x11FillRect, drawLine: x11DrawLine, drawPoint: x11DrawPoint) + inputRelays = InputRelays( + pollEvent: x11PollEvent, waitEvent: x11WaitEvent, + getTicks: x11GetTicks, delay: x11Delay, + startTextInput: x11StartTextInput, quitRequest: x11QuitRequest) + clipboardRelays = ClipboardRelays( + getText: x11GetClipboardText, putText: x11PutClipboardText) diff --git a/core/input.nim b/core/input.nim index 80aeb0f..eae80b8 100644 --- a/core/input.nim +++ b/core/input.nim @@ -42,32 +42,36 @@ type buttons*: set[MouseButton] ## evMouseMove: which buttons are held clicks*: int ## number of consecutive clicks (double-click = 2) -var pollEventRelay*: proc (e: var Event): bool {.nimcall.} = - proc (e: var Event): bool = false -var getClipboardTextRelay*: proc (): string {.nimcall.} = - proc (): string = "" -var putClipboardTextRelay*: proc (text: string) {.nimcall.} = - proc (text: string) = discard + ClipboardRelays* = object + getText*: proc (): string {.nimcall.} + putText*: proc (text: string) {.nimcall.} -var waitEventRelay*: proc (e: var Event; timeoutMs: int): bool {.nimcall.} = - proc (e: var Event; timeoutMs: int): bool = false -var getModStateRelay*: proc (): set[Modifier] {.nimcall.} = - proc (): set[Modifier] = {} -var getTicksRelay*: proc (): uint32 {.nimcall.} = - proc (): uint32 = 0 -var delayRelay*: proc (ms: uint32) {.nimcall.} = - proc (ms: uint32) = discard -var startTextInputRelay*: proc () {.nimcall.} = - proc () = discard -var quitRequestRelay*: proc () {.nimcall.} = - proc () = discard + InputRelays* = object + pollEvent*: proc (e: var Event): bool {.nimcall.} + waitEvent*: proc (e: var Event; timeoutMs: int): bool {.nimcall.} + getTicks*: proc (): int {.nimcall.} + delay*: proc (ms: int) {.nimcall.} + startTextInput*: proc () {.nimcall.} + quitRequest*: proc () {.nimcall.} -proc pollEvent*(e: var Event): bool = pollEventRelay(e) -proc waitEvent*(e: var Event; timeoutMs: int = -1): bool = waitEventRelay(e, timeoutMs) -proc getClipboardText*(): string = getClipboardTextRelay() -proc putClipboardText*(text: string) = putClipboardTextRelay(text) -proc getModState*(): set[Modifier] = getModStateRelay() -proc getTicks*(): uint32 = getTicksRelay() -proc delay*(ms: uint32) = delayRelay(ms) -proc startTextInput*() = startTextInputRelay() -proc quitRequest*() = quitRequestRelay() +var clipboardRelays* = ClipboardRelays( + getText: proc (): string = "", + putText: proc (text: string) = discard) + +var inputRelays* = InputRelays( + pollEvent: proc (e: var Event): bool = false, + waitEvent: proc (e: var Event; timeoutMs: int): bool = false, + getTicks: proc (): int = 0, + delay: proc (ms: int) = discard, + startTextInput: proc () = discard, + quitRequest: proc () = discard) + +proc pollEvent*(e: var Event): bool = inputRelays.pollEvent(e) +proc waitEvent*(e: var Event; timeoutMs: int = -1): bool = + inputRelays.waitEvent(e, timeoutMs) +proc getClipboardText*(): string = clipboardRelays.getText() +proc putClipboardText*(text: string) = clipboardRelays.putText(text) +proc getTicks*(): int = inputRelays.getTicks() +proc delay*(ms: int) = inputRelays.delay(ms) +proc startTextInput*() = inputRelays.startTextInput() +proc quitRequest*() = inputRelays.quitRequest() diff --git a/core/screen.nim b/core/screen.nim index 5ba75e4..cbdc631 100644 --- a/core/screen.nim +++ b/core/screen.nim @@ -26,88 +26,89 @@ type curDefault, curArrow, curIbeam, curWait, curCrosshair, curHand, curSizeNS, curSizeWE + WindowRelays* = object + createWindow*: proc (layout: var ScreenLayout) {.nimcall.} + refresh*: proc () {.nimcall.} + saveState*: proc () {.nimcall.} + restoreState*: proc () {.nimcall.} + setClipRect*: proc (r: Rect) {.nimcall.} + setCursor*: proc (c: CursorKind) {.nimcall.} + setWindowTitle*: proc (title: string) {.nimcall.} + + FontRelays* = object + openFont*: proc (path: string; size: int; + metrics: var FontMetrics): Font {.nimcall.} + closeFont*: proc (f: Font) {.nimcall.} + getFontMetrics*: proc (f: Font): FontMetrics {.nimcall.} + measureText*: proc (f: Font; text: string): TextExtent {.nimcall.} + drawText*: proc (f: Font; x, y: int; text: string; + fg, bg: Color): TextExtent {.nimcall.} + + DrawRelays* = object + fillRect*: proc (r: Rect; color: Color) {.nimcall.} + drawLine*: proc (x1, y1, x2, y2: int; color: Color) {.nimcall.} + drawPoint*: proc (x, y: int; color: Color) {.nimcall.} + loadImage*: proc (path: string): Image {.nimcall.} + freeImage*: proc (img: Image) {.nimcall.} + drawImage*: proc (img: Image; src, dst: Rect) {.nimcall.} + proc `==`*(a, b: Font): bool {.borrow.} proc `==`*(a, b: Image): bool {.borrow.} -# Window lifecycle -var createWindowRelay*: proc (layout: var ScreenLayout) {.nimcall.} = - proc (layout: var ScreenLayout) = discard -var refreshRelay*: proc () {.nimcall.} = - proc () = discard - -# Graphics state -var saveStateRelay*: proc () {.nimcall.} = - proc () = discard -var restoreStateRelay*: proc () {.nimcall.} = - proc () = discard -var setClipRectRelay*: proc (r: Rect) {.nimcall.} = - proc (r: Rect) = discard - -# Font management -var openFontRelay*: proc (path: string; size: int; - metrics: var FontMetrics): Font {.nimcall.} = - proc (path: string; size: int; metrics: var FontMetrics): Font = Font(0) -var closeFontRelay*: proc (f: Font) {.nimcall.} = - proc (f: Font) = discard - -# Text -var measureTextRelay*: proc (f: Font; text: string): TextExtent {.nimcall.} = - proc (f: Font; text: string): TextExtent = TextExtent() -var drawTextRelay*: proc (f: Font; x, y: int; text: string; - fg, bg: Color): TextExtent {.nimcall.} = - proc (f: Font; x, y: int; text: string; fg, bg: Color): TextExtent = - TextExtent() -var getFontMetricsRelay*: proc (f: Font): FontMetrics {.nimcall.} = - proc (f: Font): FontMetrics = FontMetrics() - -# Drawing primitives -var fillRectRelay*: proc (r: Rect; color: Color) {.nimcall.} = - proc (r: Rect; color: Color) = discard -var drawLineRelay*: proc (x1, y1, x2, y2: int; color: Color) {.nimcall.} = - proc (x1, y1, x2, y2: int; color: Color) = discard -var drawPointRelay*: proc (x, y: int; color: Color) {.nimcall.} = - proc (x, y: int; color: Color) = discard - -# Images -var loadImageRelay*: proc (path: string): Image {.nimcall.} = - proc (path: string): Image = Image(0) -var freeImageRelay*: proc (img: Image) {.nimcall.} = - proc (img: Image) = discard -var drawImageRelay*: proc (img: Image; src, dst: Rect) {.nimcall.} = - proc (img: Image; src, dst: Rect) = discard - -# Cursor and window -var setCursorRelay*: proc (c: CursorKind) {.nimcall.} = - proc (c: CursorKind) = discard -var setWindowTitleRelay*: proc (title: string) {.nimcall.} = - proc (title: string) = discard +var windowRelays* = WindowRelays( + createWindow: proc (layout: var ScreenLayout) = discard, + refresh: proc () = discard, + saveState: proc () = discard, + restoreState: proc () = discard, + setClipRect: proc (r: Rect) = discard, + setCursor: proc (c: CursorKind) = discard, + setWindowTitle: proc (title: string) = discard) + +var fontRelays* = FontRelays( + openFont: proc (path: string; size: int; metrics: var FontMetrics): Font = Font(0), + closeFont: proc (f: Font) = discard, + getFontMetrics: proc (f: Font): FontMetrics = FontMetrics(), + measureText: proc (f: Font; text: string): TextExtent = TextExtent(), + drawText: proc (f: Font; x, y: int; text: string; + fg, bg: Color): TextExtent = TextExtent()) + +var drawRelays* = DrawRelays( + fillRect: proc (r: Rect; color: Color) = discard, + drawLine: proc (x1, y1, x2, y2: int; color: Color) = discard, + drawPoint: proc (x, y: int; color: Color) = discard, + loadImage: proc (path: string): Image = Image(0), + freeImage: proc (img: Image) = discard, + drawImage: proc (img: Image; src, dst: Rect) = discard) # Convenience wrappers proc createWindow*(requestedW, requestedH: int): ScreenLayout = result = ScreenLayout(width: requestedW, height: requestedH) - createWindowRelay(result) + windowRelays.createWindow(result) + +proc refresh*() = windowRelays.refresh() +proc saveState*() = windowRelays.saveState() +proc restoreState*() = windowRelays.restoreState() +proc setClipRect*(r: Rect) = windowRelays.setClipRect(r) +proc setCursor*(c: CursorKind) = windowRelays.setCursor(c) +proc setWindowTitle*(title: string) = windowRelays.setWindowTitle(title) -proc refresh*() = refreshRelay() -proc saveState*() = saveStateRelay() -proc restoreState*() = restoreStateRelay() -proc setClipRect*(r: Rect) = setClipRectRelay(r) proc openFont*(path: string; size: int; metrics: var FontMetrics): Font = - openFontRelay(path, size, metrics) -proc closeFont*(f: Font) = closeFontRelay(f) -proc measureText*(f: Font; text: string): TextExtent = measureTextRelay(f, text) + fontRelays.openFont(path, size, metrics) +proc closeFont*(f: Font) = fontRelays.closeFont(f) +proc getFontMetrics*(f: Font): FontMetrics = fontRelays.getFontMetrics(f) +proc fontLineSkip*(f: Font): int = fontRelays.getFontMetrics(f).lineHeight +proc measureText*(f: Font; text: string): TextExtent = + fontRelays.measureText(f, text) proc drawText*(f: Font; x, y: int; text: string; fg, bg: Color): TextExtent = - drawTextRelay(f, x, y, text, fg, bg) -proc getFontMetrics*(f: Font): FontMetrics = getFontMetricsRelay(f) -proc fontLineSkip*(f: Font): int = getFontMetricsRelay(f).lineHeight -proc fillRect*(r: Rect; color: Color) = fillRectRelay(r, color) + fontRelays.drawText(f, x, y, text, fg, bg) + +proc fillRect*(r: Rect; color: Color) = drawRelays.fillRect(r, color) proc drawLine*(x1, y1, x2, y2: int; color: Color) = - drawLineRelay(x1, y1, x2, y2, color) -proc drawPoint*(x, y: int; color: Color) = drawPointRelay(x, y, color) -proc loadImage*(path: string): Image = loadImageRelay(path) -proc freeImage*(img: Image) = freeImageRelay(img) -proc drawImage*(img: Image; src, dst: Rect) = drawImageRelay(img, src, dst) -proc setCursor*(c: CursorKind) = setCursorRelay(c) -proc setWindowTitle*(title: string) = setWindowTitleRelay(title) + drawRelays.drawLine(x1, y1, x2, y2, color) +proc drawPoint*(x, y: int; color: Color) = drawRelays.drawPoint(x, y, color) +proc loadImage*(path: string): Image = drawRelays.loadImage(path) +proc freeImage*(img: Image) = drawRelays.freeImage(img) +proc drawImage*(img: Image; src, dst: Rect) = drawRelays.drawImage(img, src, dst) # Color constructors proc color*(r, g, b: uint8; a: uint8 = 255): Color = From 558cd79f28e895c85804dc5a83588d7ef63bbca5 Mon Sep 17 00:00:00 2001 From: araq Date: Sun, 12 Apr 2026 12:28:07 +0200 Subject: [PATCH 07/14] another big rename --- app/cocoa_driver.nim | 64 ++++++------- app/gtk4_driver.nim | 147 ++++++++++++++--------------- app/nimedit.nim | 82 ++++++++--------- app/scrollbar.nim | 6 +- app/sdl2_driver.nim | 208 +++++++++++++++++++++--------------------- app/sdl3_driver.nim | 197 +++++++++++++++++++-------------------- app/tabbar.nim | 8 +- app/winapi_driver.nim | 108 +++++++++++----------- app/x11_driver.nim | 100 ++++++++++---------- core/input.nim | 73 ++++++++------- testprims.nim | 1 - 11 files changed, 496 insertions(+), 498 deletions(-) diff --git a/app/cocoa_driver.nim b/app/cocoa_driver.nim index 842e311..b49cc2e 100644 --- a/app/cocoa_driver.nim +++ b/app/cocoa_driver.nim @@ -171,78 +171,79 @@ proc cocoaSetWindowTitle(title: string) = # --- Event translation --- proc translateNEEvent(ne: NEEvent; e: var input.Event) = - e = input.Event(kind: evNone) + e = input.Event(kind: NoEvent) # Translate modifiers - if (ne.mods and neModShift) != 0: e.mods.incl modShift - if (ne.mods and neModCtrl) != 0: e.mods.incl modCtrl - if (ne.mods and neModAlt) != 0: e.mods.incl modAlt - if (ne.mods and neModGui) != 0: e.mods.incl modGui + if (ne.mods and neModShift) != 0: e.mods.incl ShiftPressed + if (ne.mods and neModCtrl) != 0: e.mods.incl CtrlPressed + if (ne.mods and neModAlt) != 0: e.mods.incl AltPressed + if (ne.mods and neModGui) != 0: e.mods.incl GuiPressed case ne.kind of neQuit: - e.kind = evQuit + e.kind = QuitEvent of neWindowResize: - e.kind = evWindowResize + e.kind = WindowResizeEvent e.x = ne.x e.y = ne.y of neWindowClose: - e.kind = evWindowClose + e.kind = WindowCloseEvent of neWindowFocusGained: - e.kind = evWindowFocusGained + e.kind = WindowFocusGainedEvent of neWindowFocusLost: - e.kind = evWindowFocusLost + e.kind = WindowFocusLostEvent of neKeyDown: - e.kind = evKeyDown + e.kind = KeyDownEvent e.key = KeyCode(ne.key) of neKeyUp: - e.kind = evKeyUp + e.kind = KeyUpEvent e.key = KeyCode(ne.key) of neTextInput: - e.kind = evTextInput + e.kind = TextInputEvent for i in 0..3: e.text[i] = ne.text[i] of neMouseDown: - e.kind = evMouseDown + e.kind = MouseDownEvent e.x = ne.x e.y = ne.y e.clicks = ne.clicks case ne.button - of 0: e.button = mbLeft - of 1: e.button = mbRight - of 2: e.button = mbMiddle - else: e.button = mbLeft + of 0: e.button = LeftButton + of 1: e.button = RightButton + of 2: e.button = MiddleButton + else: e.button = LeftButton of neMouseUp: - e.kind = evMouseUp + e.kind = MouseUpEvent e.x = ne.x e.y = ne.y case ne.button - of 0: e.button = mbLeft - of 1: e.button = mbRight - of 2: e.button = mbMiddle - else: e.button = mbLeft + of 0: e.button = LeftButton + of 1: e.button = RightButton + of 2: e.button = MiddleButton + else: e.button = LeftButton of neMouseMove: - e.kind = evMouseMove + e.kind = MouseMoveEvent e.x = ne.x e.y = ne.y e.xrel = ne.xrel e.yrel = ne.yrel - if (ne.buttons and 1) != 0: e.buttons.incl mbLeft - if (ne.buttons and 2) != 0: e.buttons.incl mbRight - if (ne.buttons and 4) != 0: e.buttons.incl mbMiddle + if (ne.buttons and 1) != 0: e.buttons.incl LeftButton + if (ne.buttons and 2) != 0: e.buttons.incl RightButton + if (ne.buttons and 4) != 0: e.buttons.incl MiddleButton of neMouseWheel: - e.kind = evMouseWheel + e.kind = MouseWheelEvent e.x = ne.x e.y = ne.y else: discard -proc cocoaPollEvent(e: var input.Event): bool = +proc cocoaPollEvent(e: var input.Event; flags: set[InputFlag]): bool = var ne: NEEvent if cPollEvent(addr ne) == 0: return false translateNEEvent(ne, e) result = true -proc cocoaWaitEvent(e: var input.Event; timeoutMs: int): bool = +proc cocoaWaitEvent(e: var input.Event; timeoutMs: int; + flags: set[InputFlag]): bool = var ne: NEEvent if cWaitEvent(addr ne, timeoutMs.cint) == 0: return false @@ -259,7 +260,6 @@ proc cocoaPutClipboardText(text: string) = proc cocoaGetTicks(): int = cGetTicks().int proc cocoaDelay(ms: int) = cDelay(ms.uint32) -proc cocoaStartTextInput() = cStartTextInput() proc cocoaQuitRequest() = cQuitRequest() # --- Init --- @@ -280,6 +280,6 @@ proc initCocoaDriver*() = inputRelays = InputRelays( pollEvent: cocoaPollEvent, waitEvent: cocoaWaitEvent, getTicks: cocoaGetTicks, delay: cocoaDelay, - startTextInput: cocoaStartTextInput, quitRequest: cocoaQuitRequest) + quitRequest: cocoaQuitRequest) clipboardRelays = ClipboardRelays( getText: cocoaGetClipboardText, putText: cocoaPutClipboardText) diff --git a/app/gtk4_driver.nim b/app/gtk4_driver.nim index aa57069..531d8f0 100644 --- a/app/gtk4_driver.nim +++ b/app/gtk4_driver.nim @@ -6,7 +6,7 @@ # Uses gtk_event_controller_key_set_im_context (GTK 4.2+). GtkDrawingArea "resize" needs GTK 4.6+. # If pkg-config is missing, set compile-time flags manually, e.g.: # nim c -d:gtk4 --passC:"$(pkg-config --cflags gtk4)" --passL:"$(pkg-config --libs gtk4 pangocairo pangoft2 fontconfig)" app/nimedit.nim -# evMouseMove does not set `buttons` (held buttons) yet; SDL drivers do. +# MouseMoveEvent does not set `buttons` (held buttons) yet; SDL drivers do. {.emit: """ #include @@ -129,7 +129,7 @@ proc gtk_drawing_area_set_draw_func( ) {.importc, nodecl, cdecl.} proc gtk_event_controller_key_new(): pointer {.importc, nodecl, cdecl.} -proc gtk_event_controller_key_set_im_context(keyCtrl, im: pointer) {.importc, nodecl, cdecl.} +proc gtk_event_controller_key_set_im_context(KeyCtrl, im: pointer) {.importc, nodecl, cdecl.} proc gtk_event_controller_get_current_event(ctrl: pointer): pointer {.importc, nodecl, cdecl.} proc gtk_event_controller_motion_new(): pointer {.importc, nodecl, cdecl.} proc gtk_event_controller_scroll_new(flags: guint): pointer {.importc, nodecl, cdecl.} @@ -240,11 +240,11 @@ proc pushEvent(e: Event) = eventQueue.add e proc gdkModsToSet(st: guint): set[Modifier] = - if (st and (1'u32 shl 0)) != 0: result.incl modShift - if (st and (1'u32 shl 2)) != 0: result.incl modCtrl - if (st and (1'u32 shl 3)) != 0: result.incl modAlt - if (st and (1'u32 shl 26)) != 0: result.incl modGui - if (st and (1'u32 shl 28)) != 0: result.incl modGui + if (st and (1'u32 shl 0)) != 0: result.incl ShiftPressed + if (st and (1'u32 shl 2)) != 0: result.incl CtrlPressed + if (st and (1'u32 shl 3)) != 0: result.incl AltPressed + if (st and (1'u32 shl 26)) != 0: result.incl GuiPressed + if (st and (1'u32 shl 28)) != 0: result.incl GuiPressed proc syncModsFromController(ctrl: pointer) = let ev = gtk_event_controller_get_current_event(ctrl) @@ -255,50 +255,50 @@ proc translateKeyval(kv: guint): KeyCode = let k = gdk_keyval_to_lower(kv) template ck(c: char, key: KeyCode): untyped = if k == cast[guint](ord(c)): return key - ck('a', keyA); ck('b', keyB); ck('c', keyC); ck('d', keyD); ck('e', keyE) - ck('f', keyF); ck('g', keyG); ck('h', keyH); ck('i', keyI); ck('j', keyJ) - ck('k', keyK); ck('l', keyL); ck('m', keyM); ck('n', keyN); ck('o', keyO) - ck('p', keyP); ck('q', keyQ); ck('r', keyR); ck('s', keyS); ck('t', keyT) - ck('u', keyU); ck('v', keyV); ck('w', keyW); ck('x', keyX); ck('y', keyY) - ck('z', keyZ) - ck('0', key0); ck('1', key1); ck('2', key2); ck('3', key3); ck('4', key4) - ck('5', key5); ck('6', key6); ck('7', key7); ck('8', key8); ck('9', key9) + ck('a', KeyA); ck('b', KeyB); ck('c', KeyC); ck('d', KeyD); ck('e', KeyE) + ck('f', KeyF); ck('g', KeyG); ck('h', KeyH); ck('i', KeyI); ck('j', KeyJ) + ck('k', KeyK); ck('l', KeyL); ck('m', KeyM); ck('n', KeyN); ck('o', KeyO) + ck('p', KeyP); ck('q', KeyQ); ck('r', KeyR); ck('s', KeyS); ck('t', KeyT) + ck('u', KeyU); ck('v', KeyV); ck('w', KeyW); ck('x', KeyX); ck('y', KeyY) + ck('z', KeyZ) + ck('0', Key0); ck('1', Key1); ck('2', Key2); ck('3', Key3); ck('4', Key4) + ck('5', Key5); ck('6', Key6); ck('7', Key7); ck('8', Key8); ck('9', Key9) case k - of 0xff1b: keyEsc - of 0xff09: keyTab - of 0xff0d: keyEnter - of 0x020: keySpace - of 0xff08: keyBackspace - of 0xffff: keyDelete - of 0xff63: keyInsert - of 0xff51: keyLeft - of 0xff53: keyRight - of 0xff52: keyUp - of 0xff54: keyDown - of 0xff55: keyPageUp - of 0xff56: keyPageDown - of 0xff50: keyHome - of 0xff57: keyEnd - of 0xffe5: keyCapslock - of 0x02c: keyComma - of 0x02e: keyPeriod - of 0xffbe: keyF1 - of 0xffbf: keyF2 - of 0xffc0: keyF3 - of 0xffc1: keyF4 - of 0xffc2: keyF5 - of 0xffc3: keyF6 - of 0xffc4: keyF7 - of 0xffc5: keyF8 - of 0xffc6: keyF9 - of 0xffc7: keyF10 - of 0xffc8: keyF11 - of 0xffc9: keyF12 - else: keyNone + of 0xff1b: KeyEsc + of 0xff09: KeyTab + of 0xff0d: KeyEnter + of 0x020: KeySpace + of 0xff08: KeyBackspace + of 0xffff: KeyDelete + of 0xff63: KeyInsert + of 0xff51: KeyLeft + of 0xff53: KeyRight + of 0xff52: KeyUp + of 0xff54: KeyDown + of 0xff55: KeyPageUp + of 0xff56: KeyPageDown + of 0xff50: KeyHome + of 0xff57: KeyEnd + of 0xffe5: KeyCapslock + of 0x02c: KeyComma + of 0x02e: KeyPeriod + of 0xffbe: KeyF1 + of 0xffbf: KeyF2 + of 0xffc0: KeyF3 + of 0xffc1: KeyF4 + of 0xffc2: KeyF5 + of 0xffc3: KeyF6 + of 0xffc4: KeyF7 + of 0xffc5: KeyF8 + of 0xffc6: KeyF9 + of 0xffc7: KeyF10 + of 0xffc8: KeyF11 + of 0xffc9: KeyF12 + else: KeyNone proc enqueueTextFromUtf8(s: string) = for ch in s.toRunes: - var ev = Event(kind: evTextInput) + var ev = Event(kind: TextInputEvent) let u = toUtf8(ch) for i in 0 ..< min(4, u.len): ev.text[i] = u[i] @@ -335,12 +335,12 @@ proc ensureBackingCr() = # --- Signal callbacks (cdecl) --- proc onCloseRequest(self: pointer; data: pointer): gboolean {.cdecl.} = - pushEvent(Event(kind: evWindowClose)) + pushEvent(Event(kind: WindowCloseEvent)) G_TRUE proc onResize(area: pointer; width, height: gint; data: pointer) {.cdecl.} = recreateBacking(width.int, height.int) - pushEvent(Event(kind: evWindowResize, x: width.int, y: height.int)) + pushEvent(Event(kind: WindowResizeEvent, x: width.int, y: height.int)) proc onDraw(area: ptr GtkDrawingArea; cr: ptr cairo_t; width, height: gint; data: pointer) {.cdecl.} = @@ -365,14 +365,14 @@ proc onDraw(area: ptr GtkDrawingArea; cr: ptr cairo_t; width, height: gint; proc onKeyPressed(ctrl: pointer; keyval, keycode: guint; state: guint; data: pointer): gboolean {.cdecl.} = modState = gdkModsToSet(state) - var ev = Event(kind: evKeyDown, key: translateKeyval(keyval), mods: modState) + var ev = Event(kind: KeyDownEvent, key: translateKeyval(keyval), mods: modState) pushEvent ev G_FALSE proc onKeyReleased(ctrl: pointer; keyval, keycode: guint; state: guint; data: pointer): gboolean {.cdecl.} = modState = gdkModsToSet(state) - var ev = Event(kind: evKeyUp, key: translateKeyval(keyval), mods: modState) + var ev = Event(kind: KeyUpEvent, key: translateKeyval(keyval), mods: modState) pushEvent ev G_FALSE @@ -382,35 +382,35 @@ proc onImCommit(ctx: pointer; str: cstring; data: pointer) {.cdecl.} = proc onMotion(ctrl: pointer; x, y: gdouble; data: pointer) {.cdecl.} = syncModsFromController(ctrl) - var ev = Event(kind: evMouseMove, x: int(x), y: int(y), mods: modState) + var ev = Event(kind: MouseMoveEvent, x: int(x), y: int(y), mods: modState) pushEvent ev proc onScroll(ctrl: pointer; dx, dy: gdouble; data: pointer): gboolean {.cdecl.} = syncModsFromController(ctrl) - pushEvent(Event(kind: evMouseWheel, x: int(-dx), y: int(-dy), mods: modState)) + pushEvent(Event(kind: MouseWheelEvent, x: int(-dx), y: int(-dy), mods: modState)) G_TRUE proc onFocusEnter(ctrl: pointer; data: pointer) {.cdecl.} = - pushEvent(Event(kind: evWindowFocusGained)) + pushEvent(Event(kind: WindowFocusGainedEvent)) proc onFocusLeave(ctrl: pointer; data: pointer) {.cdecl.} = - pushEvent(Event(kind: evWindowFocusLost)) + pushEvent(Event(kind: WindowFocusLostEvent)) proc onClickPressed(gesture: pointer; nPress: gint; x, y: gdouble; data: pointer) {.cdecl.} = let btn = gtk_gesture_single_get_current_button(gesture) - var b = mbLeft - if btn == GDK_BUTTON_SECONDARY: b = mbRight - elif btn == GDK_BUTTON_MIDDLE: b = mbMiddle - var ev = Event(kind: evMouseDown, x: int(x), y: int(y), button: b, + var b = LeftButton + if btn == GDK_BUTTON_SECONDARY: b = RightButton + elif btn == GDK_BUTTON_MIDDLE: b = MiddleButton + var ev = Event(kind: MouseDownEvent, x: int(x), y: int(y), button: b, clicks: nPress.int) pushEvent ev proc onClickReleased(gesture: pointer; nPress: gint; x, y: gdouble; data: pointer) {.cdecl.} = let btn = gtk_gesture_single_get_current_button(gesture) - var b = mbLeft - if btn == GDK_BUTTON_SECONDARY: b = mbRight - elif btn == GDK_BUTTON_MIDDLE: b = mbMiddle - pushEvent(Event(kind: evMouseUp, x: int(x), y: int(y), button: b)) + var b = LeftButton + if btn == GDK_BUTTON_SECONDARY: b = RightButton + elif btn == GDK_BUTTON_MIDDLE: b = MiddleButton + pushEvent(Event(kind: MouseUpEvent, x: int(x), y: int(y), button: b)) proc g_main_loop_new(ctx: pointer, isRunning: gboolean): pointer {.importc, nodecl, cdecl.} proc g_main_loop_run(loop: pointer) {.importc, nodecl, cdecl.} @@ -684,7 +684,7 @@ proc pumpGtk() = while g_main_context_iteration(nil, G_FALSE) != G_FALSE: discard -proc gtkPollEvent(e: var Event): bool = +proc gtkPollEvent(e: var Event; flags: set[InputFlag]): bool = pumpGtk() if eventQueue.len > 0: e = eventQueue[0] @@ -692,13 +692,14 @@ proc gtkPollEvent(e: var Event): bool = return true false -proc gtkWaitEvent(e: var Event; timeoutMs: int): bool = - if gtkPollEvent(e): +proc gtkWaitEvent(e: var Event; timeoutMs: int; + flags: set[InputFlag]): bool = + if gtkPollEvent(e, flags): return true if timeoutMs < 0: while true: discard g_main_context_iteration(nil, G_TRUE) - if gtkPollEvent(e): + if gtkPollEvent(e, flags): return true elif timeoutMs == 0: return false @@ -706,10 +707,10 @@ proc gtkWaitEvent(e: var Event; timeoutMs: int): bool = let t0 = g_get_monotonic_time() div 1000 while g_get_monotonic_time() div 1000 - t0 < timeoutMs: discard g_main_context_iteration(nil, G_FALSE) - if gtkPollEvent(e): + if gtkPollEvent(e, flags): return true g_usleep(5000) - return gtkPollEvent(e) + return gtkPollEvent(e, flags) proc gtkGetClipboardText(): string = readClipboardSync() @@ -727,12 +728,6 @@ proc gtkGetTicks(): int = proc gtkDelay(ms: int) = g_usleep(culong(ms) * 1000) -proc gtkStartTextInput() = - if drawingArea != nil: - gtk_widget_grab_focus(drawingArea) - if imContext != nil: - gtk_im_context_focus_in(imContext) - proc gtkQuitRequest() = if win != nil: gtk_window_destroy(win) @@ -763,6 +758,6 @@ proc initGtk4Driver*() = inputRelays = InputRelays( pollEvent: gtkPollEvent, waitEvent: gtkWaitEvent, getTicks: gtkGetTicks, delay: gtkDelay, - startTextInput: gtkStartTextInput, quitRequest: gtkQuitRequest) + quitRequest: gtkQuitRequest) clipboardRelays = ClipboardRelays( getText: gtkGetClipboardText, putText: gtkPutClipboardText) diff --git a/app/nimedit.nim b/app/nimedit.nim index b947382..ab4f42b 100644 --- a/app/nimedit.nim +++ b/app/nimedit.nim @@ -549,8 +549,8 @@ proc pollEvents*(someConsoleRunning, windowHasFocus: bool): seq[input.Event] = while input.pollEvent(e): result.add e -proc ctrlKeyPressed*(e: Event): bool = modCtrl in e.mods -proc shiftKeyPressed*(e: Event): bool = modShift in e.mods +proc ctrlKeyPressed*(e: Event): bool = CtrlPressed in e.mods +proc shiftKeyPressed*(e: Event): bool = ShiftPressed in e.mods proc loadTheme(ed: SharedState) = loadTheme(ed.cfgColors, ed.theme, ed.mgr, ed.fontM) @@ -561,39 +561,39 @@ proc loadTheme(ed: SharedState) = proc eventToKeySet(e: input.Event): set[Key] = result = {} - if e.kind == evKeyDown: discard - elif e.kind == evKeyUp: result.incl(Key.KeyReleased) + if e.kind == KeyDownEvent: discard + elif e.kind == KeyUpEvent: result.incl(Key.KeyReleased) else: return # Map KeyCode to Key enum for the keybinding system case e.key - of keyA..keyZ: - result.incl(Key(ord(Key.A) + ord(e.key) - ord(keyA))) - of key0..key9: - result.incl(Key(ord(Key.N0) + ord(e.key) - ord(key0))) - of keyF1..keyF12: - result.incl(Key(ord(Key.F1) + ord(e.key) - ord(keyF1))) - of keyEnter: result.incl Key.Enter - of keySpace: result.incl Key.Space - of keyEsc: result.incl Key.Esc - of keyDelete: result.incl Key.Del - of keyBackspace: result.incl Key.Backspace - of keyInsert: result.incl Key.Ins - of keyPageUp: result.incl Key.PageUp - of keyPageDown: result.incl Key.PageDown - of keyCapslock: result.incl Key.Capslock - of keyTab: result.incl Key.Tab - of keyLeft: result.incl Key.Left - of keyRight: result.incl Key.Right - of keyUp: result.incl Key.Up - of keyDown: result.incl Key.Down - of keyComma: result.incl Key.Comma - of keyPeriod: result.incl Key.Period + of KeyA..KeyZ: + result.incl(Key(ord(Key.A) + ord(e.key) - ord(KeyA))) + of Key0..Key9: + result.incl(Key(ord(Key.N0) + ord(e.key) - ord(Key0))) + of KeyF1..KeyF12: + result.incl(Key(ord(Key.F1) + ord(e.key) - ord(KeyF1))) + of KeyEnter: result.incl Key.Enter + of KeySpace: result.incl Key.Space + of KeyEsc: result.incl Key.Esc + of KeyDelete: result.incl Key.Del + of KeyBackspace: result.incl Key.Backspace + of KeyInsert: result.incl Key.Ins + of KeyPageUp: result.incl Key.PageUp + of KeyPageDown: result.incl Key.PageDown + of KeyCapslock: result.incl Key.Capslock + of KeyTab: result.incl Key.Tab + of KeyLeft: result.incl Key.Left + of KeyRight: result.incl Key.Right + of KeyUp: result.incl Key.Up + of KeyDown: result.incl Key.Down + of KeyComma: result.incl Key.Comma + of KeyPeriod: result.incl Key.Period else: discard when defined(macosx): - if modGui in e.mods: result.incl Key.Apple - if modCtrl in e.mods: result.incl Key.Ctrl - if modShift in e.mods: result.incl Key.Shift - if modAlt in e.mods: result.incl Key.Alt + if GuiPressed in e.mods: result.incl Key.Apple + if CtrlPressed in e.mods: result.incl Key.Ctrl + if ShiftPressed in e.mods: result.incl Key.Shift + if AltPressed in e.mods: result.incl Key.Alt proc produceHelp(ed: Editor): string = proc getArg(a: Command): string = @@ -906,26 +906,26 @@ proc processEvents(events: out seq[Event]; ed: Editor): bool = for e in events: case e.kind - of evQuit: + of QuitEvent: if handleQuitEvent(ed): result = true break - of evWindowResize: + of WindowResizeEvent: ed.screenW = e.x ed.screenH = e.y layout(ed) - of evWindowFocusLost: + of WindowFocusLostEvent: sh.windowHasFocus = false - of evWindowFocusGained: + of WindowFocusGainedEvent: sh.windowHasFocus = true - of evWindowClose: + of WindowCloseEvent: if sh.firstWindow.next.isNil: if handleQuitEvent(ed): result = true break else: closeWindow(sh.activeWindow) - of evMouseDown: + of MouseDownEvent: var clicks = e.clicks if clicks == 0 or clicks > 5: clicks = 1 if ctrlKeyPressed(e): inc(clicks) @@ -948,10 +948,10 @@ proc processEvents(events: out seq[Event]; ed: Editor): bool = ed.sh.clickOnFilename = clicks >= 2 else: focus = console - of evMouseWheel: + of MouseWheelEvent: # use last known mouse position for target determination focus.scrollLines(-e.y*3) - of evTextInput: + of TextInputEvent: var surpress = false if e.text[0] == ' ' and e.text[1] == '\0': if ctrlKeyPressed(e): @@ -972,7 +972,7 @@ proc processEvents(events: out seq[Event]; ed: Editor): bool = else: focus.insertSingleKey(textStr) if focus==main: trackSpot(ed.sh.hotspots, main) - of evKeyDown, evKeyUp: + of KeyDownEvent, KeyUpEvent: let ks = eventToKeySet(e) let cmd = sh.keymapping.getOrDefault(ks) if ed.runAction(cmd.action, cmd.arg, shiftKeyPressed(e)): @@ -991,7 +991,7 @@ proc draw(events: sink seq[input.Event]; ed: Editor) = if activeTab != nil: var rightClick = false for e in events: - if e.kind == evMouseDown and e.button == mbRight: + if e.kind == MouseDownEvent and e.button == RightButton: rightClick = true break if rightClick: @@ -1092,7 +1092,7 @@ proc mainProc(ed: Editor) = createSdlWindow(ed, 1u32) loadTheme(sh) - input.startTextInput() + include nimscript/keybindings #XXX TODO: nimscript instead of include diff --git a/app/scrollbar.nim b/app/scrollbar.nim index 021f815..d925247 100644 --- a/app/scrollbar.nim +++ b/app/scrollbar.nim @@ -48,15 +48,15 @@ proc drawScrollBar*(b: Buffer; t: InternalTheme; events: seq[Event]; for e in events: case state.usingScrollbar of false: - if e.kind == evMouseDown: + if e.kind == MouseDownEvent: let p = point(e.x, e.y) if grip.contains(p): state = ScrollBarState(usingScrollbar: true, initiallyGrippedAt: e.y.int - grip.y) of true: - if e.kind == evMouseUp: + if e.kind == MouseUpEvent: state = ScrollBarState(usingScrollbar: false) - elif e.kind == evMouseMove: + elif e.kind == MouseMoveEvent: let mousePosRelativeToScrollbar = e.y - grip.y yMovement = mousePosRelativeToScrollbar - state.initiallyGrippedAt diff --git a/app/sdl2_driver.nim b/app/sdl2_driver.nim index 064af15..394619a 100644 --- a/app/sdl2_driver.nim +++ b/app/sdl2_driver.nim @@ -48,6 +48,7 @@ proc sdlCreateWindow(layout: var ScreenLayout) = layout.height = h layout.scaleX = 1 layout.scaleY = 1 + sdl2.startTextInput() proc sdlRefresh() = renderer.present() @@ -148,166 +149,167 @@ proc sdlPutClipboardText(text: string) = proc translateScancode(sc: Scancode): KeyCode = case sc - of SDL_SCANCODE_A: keyA - of SDL_SCANCODE_B: keyB - of SDL_SCANCODE_C: keyC - of SDL_SCANCODE_D: keyD - of SDL_SCANCODE_E: keyE - of SDL_SCANCODE_F: keyF - of SDL_SCANCODE_G: keyG - of SDL_SCANCODE_H: keyH - of SDL_SCANCODE_I: keyI - of SDL_SCANCODE_J: keyJ - of SDL_SCANCODE_K: keyK - of SDL_SCANCODE_L: keyL - of SDL_SCANCODE_M: keyM - of SDL_SCANCODE_N: keyN - of SDL_SCANCODE_O: keyO - of SDL_SCANCODE_P: keyP - of SDL_SCANCODE_Q: keyQ - of SDL_SCANCODE_R: keyR - of SDL_SCANCODE_S: keyS - of SDL_SCANCODE_T: keyT - of SDL_SCANCODE_U: keyU - of SDL_SCANCODE_V: keyV - of SDL_SCANCODE_W: keyW - of SDL_SCANCODE_X: keyX - of SDL_SCANCODE_Y: keyY - of SDL_SCANCODE_Z: keyZ - of SDL_SCANCODE_1: key1 - of SDL_SCANCODE_2: key2 - of SDL_SCANCODE_3: key3 - of SDL_SCANCODE_4: key4 - of SDL_SCANCODE_5: key5 - of SDL_SCANCODE_6: key6 - of SDL_SCANCODE_7: key7 - of SDL_SCANCODE_8: key8 - of SDL_SCANCODE_9: key9 - of SDL_SCANCODE_0: key0 - of SDL_SCANCODE_F1: keyF1 - of SDL_SCANCODE_F2: keyF2 - of SDL_SCANCODE_F3: keyF3 - of SDL_SCANCODE_F4: keyF4 - of SDL_SCANCODE_F5: keyF5 - of SDL_SCANCODE_F6: keyF6 - of SDL_SCANCODE_F7: keyF7 - of SDL_SCANCODE_F8: keyF8 - of SDL_SCANCODE_F9: keyF9 - of SDL_SCANCODE_F10: keyF10 - of SDL_SCANCODE_F11: keyF11 - of SDL_SCANCODE_F12: keyF12 - of SDL_SCANCODE_RETURN: keyEnter - of SDL_SCANCODE_SPACE: keySpace - of SDL_SCANCODE_ESCAPE: keyEsc - of SDL_SCANCODE_TAB: keyTab - of SDL_SCANCODE_BACKSPACE: keyBackspace - of SDL_SCANCODE_DELETE: keyDelete - of SDL_SCANCODE_INSERT: keyInsert - of SDL_SCANCODE_LEFT: keyLeft - of SDL_SCANCODE_RIGHT: keyRight - of SDL_SCANCODE_UP: keyUp - of SDL_SCANCODE_DOWN: keyDown - of SDL_SCANCODE_PAGEUP: keyPageUp - of SDL_SCANCODE_PAGEDOWN: keyPageDown - of SDL_SCANCODE_HOME: keyHome - of SDL_SCANCODE_END: keyEnd - of SDL_SCANCODE_CAPSLOCK: keyCapslock - of SDL_SCANCODE_COMMA: keyComma - of SDL_SCANCODE_PERIOD: keyPeriod - else: keyNone + of SDL_SCANCODE_A: KeyA + of SDL_SCANCODE_B: KeyB + of SDL_SCANCODE_C: KeyC + of SDL_SCANCODE_D: KeyD + of SDL_SCANCODE_E: KeyE + of SDL_SCANCODE_F: KeyF + of SDL_SCANCODE_G: KeyG + of SDL_SCANCODE_H: KeyH + of SDL_SCANCODE_I: KeyI + of SDL_SCANCODE_J: KeyJ + of SDL_SCANCODE_K: KeyK + of SDL_SCANCODE_L: KeyL + of SDL_SCANCODE_M: KeyM + of SDL_SCANCODE_N: KeyN + of SDL_SCANCODE_O: KeyO + of SDL_SCANCODE_P: KeyP + of SDL_SCANCODE_Q: KeyQ + of SDL_SCANCODE_R: KeyR + of SDL_SCANCODE_S: KeyS + of SDL_SCANCODE_T: KeyT + of SDL_SCANCODE_U: KeyU + of SDL_SCANCODE_V: KeyV + of SDL_SCANCODE_W: KeyW + of SDL_SCANCODE_X: KeyX + of SDL_SCANCODE_Y: KeyY + of SDL_SCANCODE_Z: KeyZ + of SDL_SCANCODE_1: Key1 + of SDL_SCANCODE_2: Key2 + of SDL_SCANCODE_3: Key3 + of SDL_SCANCODE_4: Key4 + of SDL_SCANCODE_5: Key5 + of SDL_SCANCODE_6: Key6 + of SDL_SCANCODE_7: Key7 + of SDL_SCANCODE_8: Key8 + of SDL_SCANCODE_9: Key9 + of SDL_SCANCODE_0: Key0 + of SDL_SCANCODE_F1: KeyF1 + of SDL_SCANCODE_F2: KeyF2 + of SDL_SCANCODE_F3: KeyF3 + of SDL_SCANCODE_F4: KeyF4 + of SDL_SCANCODE_F5: KeyF5 + of SDL_SCANCODE_F6: KeyF6 + of SDL_SCANCODE_F7: KeyF7 + of SDL_SCANCODE_F8: KeyF8 + of SDL_SCANCODE_F9: KeyF9 + of SDL_SCANCODE_F10: KeyF10 + of SDL_SCANCODE_F11: KeyF11 + of SDL_SCANCODE_F12: KeyF12 + of SDL_SCANCODE_RETURN: KeyEnter + of SDL_SCANCODE_SPACE: KeySpace + of SDL_SCANCODE_ESCAPE: KeyEsc + of SDL_SCANCODE_TAB: KeyTab + of SDL_SCANCODE_BACKSPACE: KeyBackspace + of SDL_SCANCODE_DELETE: KeyDelete + of SDL_SCANCODE_INSERT: KeyInsert + of SDL_SCANCODE_LEFT: KeyLeft + of SDL_SCANCODE_RIGHT: KeyRight + of SDL_SCANCODE_UP: KeyUp + of SDL_SCANCODE_DOWN: KeyDown + of SDL_SCANCODE_PAGEUP: KeyPageUp + of SDL_SCANCODE_PAGEDOWN: KeyPageDown + of SDL_SCANCODE_HOME: KeyHome + of SDL_SCANCODE_END: KeyEnd + of SDL_SCANCODE_CAPSLOCK: KeyCapslock + of SDL_SCANCODE_COMMA: KeyComma + of SDL_SCANCODE_PERIOD: KeyPeriod + else: KeyNone proc translateMods(m: int16): set[Modifier] = let m = m.int32 - if (m and KMOD_SHIFT) != 0: result.incl modShift - if (m and KMOD_CTRL) != 0: result.incl modCtrl - if (m and KMOD_ALT) != 0: result.incl modAlt - if (m and KMOD_GUI) != 0: result.incl modGui + if (m and KMOD_SHIFT) != 0: result.incl ShiftPressed + if (m and KMOD_CTRL) != 0: result.incl CtrlPressed + if (m and KMOD_ALT) != 0: result.incl AltPressed + if (m and KMOD_GUI) != 0: result.incl GuiPressed -proc sdlPollEvent(e: var input.Event): bool = +proc sdlPollEvent(e: var input.Event; flags: set[InputFlag]): bool = var sdlEvent: sdl2.Event if not sdl2.pollEvent(sdlEvent): return false result = true - e = input.Event(kind: evNone) + e = input.Event(kind: NoEvent) case sdlEvent.kind of QuitEvent: - e.kind = evQuit + e.kind = input.QuitEvent of WindowEvent: let wev = sdlEvent.window case wev.event of WindowEvent_Resized, WindowEvent_SizeChanged: - e.kind = evWindowResize + e.kind = WindowResizeEvent e.x = wev.data1 e.y = wev.data2 of WindowEvent_Close: - e.kind = evWindowClose + e.kind = WindowCloseEvent of WindowEvent_FocusGained: - e.kind = evWindowFocusGained + e.kind = WindowFocusGainedEvent of WindowEvent_FocusLost: - e.kind = evWindowFocusLost + e.kind = WindowFocusLostEvent else: - e.kind = evNone + e.kind = NoEvent of KeyDown: - e.kind = evKeyDown + e.kind = KeyDownEvent e.key = translateScancode(sdlEvent.key.keysym.scancode) e.mods = translateMods(sdlEvent.key.keysym.modstate) of KeyUp: - e.kind = evKeyUp + e.kind = KeyUpEvent e.key = translateScancode(sdlEvent.key.keysym.scancode) e.mods = translateMods(sdlEvent.key.keysym.modstate) of TextInput: - e.kind = evTextInput + e.kind = TextInputEvent for i in 0..3: e.text[i] = sdlEvent.text.text[i] of MouseButtonDown: - e.kind = evMouseDown + e.kind = MouseDownEvent e.x = sdlEvent.button.x e.y = sdlEvent.button.y e.clicks = sdlEvent.button.clicks.int case sdlEvent.button.button - of BUTTON_LEFT: e.button = mbLeft - of BUTTON_RIGHT: e.button = mbRight - of BUTTON_MIDDLE: e.button = mbMiddle - else: e.button = mbLeft + of BUTTON_LEFT: e.button = LeftButton + of BUTTON_RIGHT: e.button = RightButton + of BUTTON_MIDDLE: e.button = MiddleButton + else: e.button = LeftButton of MouseButtonUp: - e.kind = evMouseUp + e.kind = MouseUpEvent e.x = sdlEvent.button.x e.y = sdlEvent.button.y case sdlEvent.button.button - of BUTTON_LEFT: e.button = mbLeft - of BUTTON_RIGHT: e.button = mbRight - of BUTTON_MIDDLE: e.button = mbMiddle - else: e.button = mbLeft + of BUTTON_LEFT: e.button = LeftButton + of BUTTON_RIGHT: e.button = RightButton + of BUTTON_MIDDLE: e.button = MiddleButton + else: e.button = LeftButton of MouseMotion: - e.kind = evMouseMove + e.kind = MouseMoveEvent e.x = sdlEvent.motion.x e.y = sdlEvent.motion.y e.xrel = sdlEvent.motion.xrel e.yrel = sdlEvent.motion.yrel if (sdlEvent.motion.state and BUTTON_LMASK) != 0: - e.buttons.incl mbLeft + e.buttons.incl LeftButton if (sdlEvent.motion.state and BUTTON_RMASK) != 0: - e.buttons.incl mbRight + e.buttons.incl RightButton of MouseWheel: - e.kind = evMouseWheel + e.kind = MouseWheelEvent e.x = sdlEvent.wheel.x e.y = sdlEvent.wheel.y else: - e.kind = evNone + e.kind = NoEvent -proc sdlWaitEvent(e: var input.Event; timeoutMs: int): bool = +proc sdlWaitEvent(e: var input.Event; timeoutMs: int; + flags: set[InputFlag]): bool = # Use pollEvent with delay for timeout behavior if timeoutMs < 0: # Block until event while true: - if sdlPollEvent(e): return true + if sdlPollEvent(e, flags): return true sdl2.delay(10) elif timeoutMs == 0: - return sdlPollEvent(e) + return sdlPollEvent(e, flags) else: let start = sdl2.getTicks() while sdl2.getTicks() - start < timeoutMs.uint32: - if sdlPollEvent(e): return true + if sdlPollEvent(e, flags): return true sdl2.delay(10) return false @@ -315,8 +317,6 @@ proc sdlGetTicks(): int = sdl2.getTicks().int proc sdlDelay(ms: int) = sdl2.delay(ms.uint32) -proc sdlStartTextInput() = sdl2.startTextInput() - proc sdlQuitRequest() = sdl2.quit() # --- Init --- @@ -340,6 +340,6 @@ proc initSdl2Driver*() = inputRelays = InputRelays( pollEvent: sdlPollEvent, waitEvent: sdlWaitEvent, getTicks: sdlGetTicks, delay: sdlDelay, - startTextInput: sdlStartTextInput, quitRequest: sdlQuitRequest) + quitRequest: sdlQuitRequest) clipboardRelays = ClipboardRelays( getText: sdlGetClipboardText, putText: sdlPutClipboardText) diff --git a/app/sdl3_driver.nim b/app/sdl3_driver.nim index a790d20..abbfce0 100644 --- a/app/sdl3_driver.nim +++ b/app/sdl3_driver.nim @@ -32,6 +32,7 @@ var proc sdlCreateWindow(layout: var ScreenLayout) = discard createWindowAndRenderer(cstring"NimEdit", layout.width.cint, layout.height.cint, WINDOW_RESIZABLE, win, ren) + discard startTextInput(win) var w, h: cint discard getWindowSize(win, w, h) layout.width = w @@ -146,106 +147,106 @@ proc sdlPutClipboardText(text: string) = proc translateScancode(sc: Scancode): input.KeyCode = case sc - of SCANCODE_A: keyA - of SCANCODE_B: keyB - of SCANCODE_C: keyC - of SCANCODE_D: keyD - of SCANCODE_E: keyE - of SCANCODE_F: keyF - of SCANCODE_G: keyG - of SCANCODE_H: keyH - of SCANCODE_I: keyI - of SCANCODE_J: keyJ - of SCANCODE_K: keyK - of SCANCODE_L: keyL - of SCANCODE_M: keyM - of SCANCODE_N: keyN - of SCANCODE_O: keyO - of SCANCODE_P: keyP - of SCANCODE_Q: keyQ - of SCANCODE_R: keyR - of SCANCODE_S: keyS - of SCANCODE_T: keyT - of SCANCODE_U: keyU - of SCANCODE_V: keyV - of SCANCODE_W: keyW - of SCANCODE_X: keyX - of SCANCODE_Y: keyY - of SCANCODE_Z: keyZ - of SCANCODE_1: key1 - of SCANCODE_2: key2 - of SCANCODE_3: key3 - of SCANCODE_4: key4 - of SCANCODE_5: key5 - of SCANCODE_6: key6 - of SCANCODE_7: key7 - of SCANCODE_8: key8 - of SCANCODE_9: key9 - of SCANCODE_0: key0 - of SCANCODE_F1: keyF1 - of SCANCODE_F2: keyF2 - of SCANCODE_F3: keyF3 - of SCANCODE_F4: keyF4 - of SCANCODE_F5: keyF5 - of SCANCODE_F6: keyF6 - of SCANCODE_F7: keyF7 - of SCANCODE_F8: keyF8 - of SCANCODE_F9: keyF9 - of SCANCODE_F10: keyF10 - of SCANCODE_F11: keyF11 - of SCANCODE_F12: keyF12 - of SCANCODE_RETURN: keyEnter - of SCANCODE_SPACE: keySpace - of SCANCODE_ESCAPE: keyEsc - of SCANCODE_TAB: keyTab - of SCANCODE_BACKSPACE: keyBackspace - of SCANCODE_DELETE: keyDelete - of SCANCODE_INSERT: keyInsert - of SCANCODE_LEFT: keyLeft - of SCANCODE_RIGHT: keyRight - of SCANCODE_UP: keyUp - of SCANCODE_DOWN: keyDown - of SCANCODE_PAGEUP: keyPageUp - of SCANCODE_PAGEDOWN: keyPageDown - of SCANCODE_HOME: keyHome - of SCANCODE_END: keyEnd - of SCANCODE_CAPSLOCK: keyCapslock - of SCANCODE_COMMA: keyComma - of SCANCODE_PERIOD: keyPeriod - else: keyNone + of SCANCODE_A: KeyA + of SCANCODE_B: KeyB + of SCANCODE_C: KeyC + of SCANCODE_D: KeyD + of SCANCODE_E: KeyE + of SCANCODE_F: KeyF + of SCANCODE_G: KeyG + of SCANCODE_H: KeyH + of SCANCODE_I: KeyI + of SCANCODE_J: KeyJ + of SCANCODE_K: KeyK + of SCANCODE_L: KeyL + of SCANCODE_M: KeyM + of SCANCODE_N: KeyN + of SCANCODE_O: KeyO + of SCANCODE_P: KeyP + of SCANCODE_Q: KeyQ + of SCANCODE_R: KeyR + of SCANCODE_S: KeyS + of SCANCODE_T: KeyT + of SCANCODE_U: KeyU + of SCANCODE_V: KeyV + of SCANCODE_W: KeyW + of SCANCODE_X: KeyX + of SCANCODE_Y: KeyY + of SCANCODE_Z: KeyZ + of SCANCODE_1: Key1 + of SCANCODE_2: Key2 + of SCANCODE_3: Key3 + of SCANCODE_4: Key4 + of SCANCODE_5: Key5 + of SCANCODE_6: Key6 + of SCANCODE_7: Key7 + of SCANCODE_8: Key8 + of SCANCODE_9: Key9 + of SCANCODE_0: Key0 + of SCANCODE_F1: KeyF1 + of SCANCODE_F2: KeyF2 + of SCANCODE_F3: KeyF3 + of SCANCODE_F4: KeyF4 + of SCANCODE_F5: KeyF5 + of SCANCODE_F6: KeyF6 + of SCANCODE_F7: KeyF7 + of SCANCODE_F8: KeyF8 + of SCANCODE_F9: KeyF9 + of SCANCODE_F10: KeyF10 + of SCANCODE_F11: KeyF11 + of SCANCODE_F12: KeyF12 + of SCANCODE_RETURN: KeyEnter + of SCANCODE_SPACE: KeySpace + of SCANCODE_ESCAPE: KeyEsc + of SCANCODE_TAB: KeyTab + of SCANCODE_BACKSPACE: KeyBackspace + of SCANCODE_DELETE: KeyDelete + of SCANCODE_INSERT: KeyInsert + of SCANCODE_LEFT: KeyLeft + of SCANCODE_RIGHT: KeyRight + of SCANCODE_UP: KeyUp + of SCANCODE_DOWN: KeyDown + of SCANCODE_PAGEUP: KeyPageUp + of SCANCODE_PAGEDOWN: KeyPageDown + of SCANCODE_HOME: KeyHome + of SCANCODE_END: KeyEnd + of SCANCODE_CAPSLOCK: KeyCapslock + of SCANCODE_COMMA: KeyComma + of SCANCODE_PERIOD: KeyPeriod + else: KeyNone proc translateMods(m: Keymod): set[Modifier] = let m = m.uint32 - if (m and KMOD_SHIFT) != 0: result.incl modShift - if (m and KMOD_CTRL) != 0: result.incl modCtrl - if (m and KMOD_ALT) != 0: result.incl modAlt - if (m and KMOD_GUI) != 0: result.incl modGui + if (m and KMOD_SHIFT) != 0: result.incl ShiftPressed + if (m and KMOD_CTRL) != 0: result.incl CtrlPressed + if (m and KMOD_ALT) != 0: result.incl AltPressed + if (m and KMOD_GUI) != 0: result.incl GuiPressed proc translateEvent(sdlEvent: sdl3.Event; e: var input.Event) = - e = input.Event(kind: evNone) + e = input.Event(kind: NoEvent) let evType = uint32(sdlEvent.common.`type`) if evType == uint32(EVENT_QUIT): - e.kind = evQuit + e.kind = QuitEvent elif evType == uint32(EVENT_WINDOW_RESIZED): - e.kind = evWindowResize + e.kind = WindowResizeEvent e.x = sdlEvent.window.data1 e.y = sdlEvent.window.data2 elif evType == uint32(EVENT_WINDOW_CLOSE_REQUESTED): - e.kind = evWindowClose + e.kind = WindowCloseEvent elif evType == uint32(EVENT_WINDOW_FOCUS_GAINED): - e.kind = evWindowFocusGained + e.kind = WindowFocusGainedEvent elif evType == uint32(EVENT_WINDOW_FOCUS_LOST): - e.kind = evWindowFocusLost + e.kind = WindowFocusLostEvent elif evType == uint32(EVENT_KEY_DOWN): - e.kind = evKeyDown + e.kind = KeyDownEvent e.key = translateScancode(sdlEvent.key.scancode) e.mods = translateMods(sdlEvent.key.`mod`) elif evType == uint32(EVENT_KEY_UP): - e.kind = evKeyUp + e.kind = KeyUpEvent e.key = translateScancode(sdlEvent.key.scancode) e.mods = translateMods(sdlEvent.key.`mod`) elif evType == uint32(EVENT_TEXT_INPUT): - e.kind = evTextInput + e.kind = TextInputEvent if sdlEvent.text.text != nil: for i in 0..3: if sdlEvent.text.text[i] == '\0': @@ -253,47 +254,48 @@ proc translateEvent(sdlEvent: sdl3.Event; e: var input.Event) = break e.text[i] = sdlEvent.text.text[i] elif evType == uint32(EVENT_MOUSE_BUTTON_DOWN): - e.kind = evMouseDown + e.kind = MouseDownEvent e.x = sdlEvent.button.x.int e.y = sdlEvent.button.y.int e.clicks = sdlEvent.button.clicks.int case sdlEvent.button.button - of BUTTON_LEFT: e.button = mbLeft - of BUTTON_RIGHT: e.button = mbRight - of BUTTON_MIDDLE: e.button = mbMiddle - else: e.button = mbLeft + of BUTTON_LEFT: e.button = LeftButton + of BUTTON_RIGHT: e.button = RightButton + of BUTTON_MIDDLE: e.button = MiddleButton + else: e.button = LeftButton elif evType == uint32(EVENT_MOUSE_BUTTON_UP): - e.kind = evMouseUp + e.kind = MouseUpEvent e.x = sdlEvent.button.x.int e.y = sdlEvent.button.y.int case sdlEvent.button.button - of BUTTON_LEFT: e.button = mbLeft - of BUTTON_RIGHT: e.button = mbRight - of BUTTON_MIDDLE: e.button = mbMiddle - else: e.button = mbLeft + of BUTTON_LEFT: e.button = LeftButton + of BUTTON_RIGHT: e.button = RightButton + of BUTTON_MIDDLE: e.button = MiddleButton + else: e.button = LeftButton elif evType == uint32(EVENT_MOUSE_MOTION): - e.kind = evMouseMove + e.kind = MouseMoveEvent e.x = sdlEvent.motion.x.int e.y = sdlEvent.motion.y.int e.xrel = sdlEvent.motion.xrel.int e.yrel = sdlEvent.motion.yrel.int if (sdlEvent.motion.state and BUTTON_LMASK) != 0: - e.buttons.incl mbLeft + e.buttons.incl LeftButton if (sdlEvent.motion.state and BUTTON_RMASK) != 0: - e.buttons.incl mbRight + e.buttons.incl RightButton elif evType == uint32(EVENT_MOUSE_WHEEL): - e.kind = evMouseWheel + e.kind = MouseWheelEvent e.x = sdlEvent.wheel.x.int e.y = sdlEvent.wheel.y.int -proc sdlPollEvent(e: var input.Event): bool = +proc sdlPollEvent(e: var input.Event; flags: set[InputFlag]): bool = var sdlEvent: sdl3.Event if not pollEvent(sdlEvent): return false translateEvent(sdlEvent, e) result = true -proc sdlWaitEvent(e: var input.Event; timeoutMs: int): bool = +proc sdlWaitEvent(e: var input.Event; timeoutMs: int; + flags: set[InputFlag]): bool = var sdlEvent: sdl3.Event let ok = if timeoutMs < 0: waitEvent(sdlEvent) else: waitEventTimeout(sdlEvent, timeoutMs.int32) @@ -303,7 +305,6 @@ proc sdlWaitEvent(e: var input.Event; timeoutMs: int): bool = proc sdlGetTicks(): int = sdl3.getTicks().int proc sdlDelay(ms: int) = sdl3.delay(ms.uint32) -proc sdlStartTextInput() = discard startTextInput(win) proc sdlQuitRequest() = sdl3.quit() # --- Init --- @@ -327,6 +328,6 @@ proc initSdl3Driver*() = inputRelays = InputRelays( pollEvent: sdlPollEvent, waitEvent: sdlWaitEvent, getTicks: sdlGetTicks, delay: sdlDelay, - startTextInput: sdlStartTextInput, quitRequest: sdlQuitRequest) + quitRequest: sdlQuitRequest) clipboardRelays = ClipboardRelays( getText: sdlGetClipboardText, putText: sdlPutClipboardText) diff --git a/app/tabbar.nim b/app/tabbar.nim index a3e4f34..156f005 100644 --- a/app/tabbar.nim +++ b/app/tabbar.nim @@ -76,7 +76,7 @@ proc drawButtonList*(buttons: openArray[string]; t: Internaltheme; for i in 0..buttons.high: let b = buttons[i] let rect = drawTextWithBorder(t, b, i == active, xx, y, screenW) - if e.kind == evMouseDown: + if e.kind == MouseDownEvent: if e.clicks >= 1: let p = point(e.x, e.y) if rect.contains(p): @@ -103,13 +103,13 @@ proc drawTabBar*(tabs: var TabBar; t: InternalTheme; activeDrawn = activeDrawn or it == active for e in events: - if e.kind == evMouseDown: + if e.kind == MouseDownEvent: if e.clicks >= 1: let p = point(e.x, e.y) if rect.contains(p): result = it - elif e.kind == evMouseMove: - if mbLeft in e.buttons: + elif e.kind == MouseMoveEvent: + if LeftButton in e.buttons: let p = point(e.x, e.y) if rect.contains(p): if e.xrel >= 4: diff --git a/app/winapi_driver.nim b/app/winapi_driver.nim index bfffa93..155da16 100644 --- a/app/winapi_driver.nim +++ b/app/winapi_driver.nim @@ -385,42 +385,42 @@ proc recreateBackBuffer() = proc translateVK(vk: WPARAM): input.KeyCode = let vk = vk.uint32 if vk >= ord('A').uint32 and vk <= ord('Z').uint32: - return input.KeyCode(ord(keyA) + (vk.int - ord('A'))) + return input.KeyCode(ord(KeyA) + (vk.int - ord('A'))) if vk >= ord('0').uint32 and vk <= ord('9').uint32: - return input.KeyCode(ord(key0) + (vk.int - ord('0'))) + return input.KeyCode(ord(Key0) + (vk.int - ord('0'))) if vk >= VK_F1 and vk <= VK_F12: - return input.KeyCode(ord(keyF1) + (vk.int - VK_F1.int)) + return input.KeyCode(ord(KeyF1) + (vk.int - VK_F1.int)) case vk - of VK_RETURN: keyEnter - of VK_SPACE: keySpace - of VK_ESCAPE: keyEsc - of VK_TAB: keyTab - of VK_BACK: keyBackspace - of VK_DELETE: keyDelete - of VK_INSERT: keyInsert - of VK_LEFT: keyLeft - of VK_RIGHT: keyRight - of VK_UP: keyUp - of VK_DOWN: keyDown - of VK_PRIOR: keyPageUp - of VK_NEXT: keyPageDown - of VK_HOME: keyHome - of VK_END: keyEnd - of VK_CAPITAL: keyCapslock - of VK_OEM_COMMA: keyComma - of VK_OEM_PERIOD: keyPeriod - else: keyNone + of VK_RETURN: KeyEnter + of VK_SPACE: KeySpace + of VK_ESCAPE: KeyEsc + of VK_TAB: KeyTab + of VK_BACK: KeyBackspace + of VK_DELETE: KeyDelete + of VK_INSERT: KeyInsert + of VK_LEFT: KeyLeft + of VK_RIGHT: KeyRight + of VK_UP: KeyUp + of VK_DOWN: KeyDown + of VK_PRIOR: KeyPageUp + of VK_NEXT: KeyPageDown + of VK_HOME: KeyHome + of VK_END: KeyEnd + of VK_CAPITAL: KeyCapslock + of VK_OEM_COMMA: KeyComma + of VK_OEM_PERIOD: KeyPeriod + else: KeyNone proc getModifiers(): set[Modifier] = - if GetKeyState(VK_SHIFT.int32) < 0: result.incl modShift - if GetKeyState(VK_CONTROL.int32) < 0: result.incl modCtrl - if GetKeyState(VK_MENU.int32) < 0: result.incl modAlt + if GetKeyState(VK_SHIFT.int32) < 0: result.incl ShiftPressed + if GetKeyState(VK_CONTROL.int32) < 0: result.incl CtrlPressed + if GetKeyState(VK_MENU.int32) < 0: result.incl AltPressed proc getMouseButtons(wp: WPARAM): set[MouseButton] = let flags = wp.uint32 - if (flags and MK_LBUTTON) != 0: result.incl mbLeft - if (flags and MK_RBUTTON) != 0: result.incl mbRight - if (flags and MK_MBUTTON) != 0: result.incl mbMiddle + if (flags and MK_LBUTTON) != 0: result.incl LeftButton + if (flags and MK_RBUTTON) != 0: result.incl RightButton + if (flags and MK_MBUTTON) != 0: result.incl MiddleButton var lastClickTime: DWORD var lastClickX, lastClickY: int @@ -435,7 +435,7 @@ proc pumpMessages() = discard TranslateMessage(addr msg) discard DispatchMessageW(addr msg) if msg.message == WM_QUIT: - pushEvent(input.Event(kind: evQuit)) + pushEvent(input.Event(kind: QuitEvent)) proc wndProc(hwnd: HWND; msg: UINT; wp: WPARAM; lp: LPARAM): LRESULT {.stdcall.} = # Capture gHwnd from the first message -- WM_SIZE etc. arrive @@ -445,11 +445,11 @@ proc wndProc(hwnd: HWND; msg: UINT; wp: WPARAM; lp: LPARAM): LRESULT {.stdcall.} case msg of WM_DESTROY: PostQuitMessage(0) - pushEvent(input.Event(kind: evQuit)) + pushEvent(input.Event(kind: QuitEvent)) return 0 of WM_CLOSE: - pushEvent(input.Event(kind: evWindowClose)) + pushEvent(input.Event(kind: WindowCloseEvent)) return 0 # don't call DestroyWindow yet; let the app decide of WM_ERASEBKGND: @@ -462,7 +462,7 @@ proc wndProc(hwnd: HWND; msg: UINT; wp: WPARAM; lp: LPARAM): LRESULT {.stdcall.} gWidth = newW gHeight = newH recreateBackBuffer() - var e = input.Event(kind: evWindowResize) + var e = input.Event(kind: WindowResizeEvent) e.x = gWidth e.y = gHeight pushEvent(e) @@ -477,24 +477,24 @@ proc wndProc(hwnd: HWND; msg: UINT; wp: WPARAM; lp: LPARAM): LRESULT {.stdcall.} return 0 of WM_KEYDOWN: - var e = input.Event(kind: evKeyDown) + var e = input.Event(kind: KeyDownEvent) e.key = translateVK(wp) e.mods = getModifiers() pushEvent(e) return 0 of WM_KEYUP: - var e = input.Event(kind: evKeyUp) + var e = input.Event(kind: KeyUpEvent) e.key = translateVK(wp) e.mods = getModifiers() pushEvent(e) return 0 of WM_CHAR: - # wp is a UTF-16 code unit. For BMP characters, emit evTextInput. + # wp is a UTF-16 code unit. For BMP characters, emit TextInputEvent. let ch = wp.uint16 if ch >= 32 and ch != 127: - var e = input.Event(kind: evTextInput) + var e = input.Event(kind: TextInputEvent) # Convert UTF-16 to UTF-8 into e.text[0..3] let codepoint = ch.uint32 if codepoint < 0x80: @@ -510,22 +510,22 @@ proc wndProc(hwnd: HWND; msg: UINT; wp: WPARAM; lp: LPARAM): LRESULT {.stdcall.} return 0 of WM_SETFOCUS: - pushEvent(input.Event(kind: evWindowFocusGained)) + pushEvent(input.Event(kind: WindowFocusGainedEvent)) return 0 of WM_KILLFOCUS: - pushEvent(input.Event(kind: evWindowFocusLost)) + pushEvent(input.Event(kind: WindowFocusLostEvent)) return 0 of WM_LBUTTONDOWN, WM_RBUTTONDOWN, WM_MBUTTONDOWN: - var e = input.Event(kind: evMouseDown) + var e = input.Event(kind: MouseDownEvent) e.x = loWord(lp) e.y = hiWord(lp) e.mods = getModifiers() case msg - of WM_LBUTTONDOWN: e.button = mbLeft - of WM_RBUTTONDOWN: e.button = mbRight - else: e.button = mbMiddle + of WM_LBUTTONDOWN: e.button = LeftButton + of WM_RBUTTONDOWN: e.button = RightButton + else: e.button = MiddleButton # Track click count for double/triple click let now = GetTickCount() if now - lastClickTime < 500 and @@ -541,18 +541,18 @@ proc wndProc(hwnd: HWND; msg: UINT; wp: WPARAM; lp: LPARAM): LRESULT {.stdcall.} return 0 of WM_LBUTTONUP, WM_RBUTTONUP, WM_MBUTTONUP: - var e = input.Event(kind: evMouseUp) + var e = input.Event(kind: MouseUpEvent) e.x = loWord(lp) e.y = hiWord(lp) case msg - of WM_LBUTTONUP: e.button = mbLeft - of WM_RBUTTONUP: e.button = mbRight - else: e.button = mbMiddle + of WM_LBUTTONUP: e.button = LeftButton + of WM_RBUTTONUP: e.button = RightButton + else: e.button = MiddleButton pushEvent(e) return 0 of WM_MOUSEMOVE: - var e = input.Event(kind: evMouseMove) + var e = input.Event(kind: MouseMoveEvent) e.x = loWord(lp) e.y = hiWord(lp) e.buttons = getMouseButtons(wp) @@ -560,7 +560,7 @@ proc wndProc(hwnd: HWND; msg: UINT; wp: WPARAM; lp: LPARAM): LRESULT {.stdcall.} return 0 of WM_MOUSEWHEEL: - var e = input.Event(kind: evMouseWheel) + var e = input.Event(kind: MouseWheelEvent) let delta = signedHiWord(wp) e.y = delta div 120 # standard wheel delta var pt = WINAPIPOINT(x: loWord(lp).LONG, y: hiWord(lp).LONG) @@ -808,7 +808,7 @@ proc winSetWindowTitle(title: string) = # ---- Input hook implementations ---- -proc winPollEvent(e: var input.Event): bool = +proc winPollEvent(e: var input.Event; flags: set[InputFlag]): bool = pumpMessages() if eventQueue.len > 0: e = eventQueue[0] @@ -816,14 +816,15 @@ proc winPollEvent(e: var input.Event): bool = return true return false -proc winWaitEvent(e: var input.Event; timeoutMs: int): bool = +proc winWaitEvent(e: var input.Event; timeoutMs: int; + flags: set[InputFlag]): bool = # Check already-queued events first if eventQueue.len > 0: e = eventQueue[0] eventQueue.delete(0) return true # Drain any pending Win32 messages before blocking - if winPollEvent(e): + if winPollEvent(e, flags): return true # Pump messages in a loop with short sleeps, like SDL does internally. # A single MWFMO(INFINITE) would block the thread entirely, causing @@ -838,7 +839,7 @@ proc winWaitEvent(e: var input.Event; timeoutMs: int): bool = else: min(deadline - now, 100'u32) let res = MsgWaitForMultipleObjects(0, nil, 0, remaining, QS_ALLINPUT) if res != WAIT_TIMEOUT: - if winPollEvent(e): return true + if winPollEvent(e, flags): return true elif timeoutMs >= 0: # Finite timeout: check if expired if GetTickCount() >= deadline: return false @@ -881,7 +882,6 @@ proc winDelay(ms: int) = let remaining = min(deadline - now, msU) discard MsgWaitForMultipleObjects(0, nil, 0, remaining, QS_ALLINPUT) pumpMessages() -proc winStartTextInput() = discard # Win32 always delivers WM_CHAR proc winQuitRequest() = gQuitFlag = true if gHwnd != nil: @@ -923,6 +923,6 @@ proc initWinapiDriver*() = inputRelays = InputRelays( pollEvent: winPollEvent, waitEvent: winWaitEvent, getTicks: winGetTicks, delay: winDelay, - startTextInput: winStartTextInput, quitRequest: winQuitRequest) + quitRequest: winQuitRequest) clipboardRelays = ClipboardRelays( getText: winGetClipboardText, putText: winPutClipboardText) diff --git a/app/x11_driver.nim b/app/x11_driver.nim index 190ce65..db90717 100644 --- a/app/x11_driver.nim +++ b/app/x11_driver.nim @@ -460,49 +460,49 @@ proc recreateBackBuffer() = proc translateKeySym(ks: XKeySym): input.KeyCode = if ks >= XK_a and ks <= XK_z: - return input.KeyCode(ord(keyA) + (ks.int - XK_a.int)) + return input.KeyCode(ord(KeyA) + (ks.int - XK_a.int)) if ks >= XK_0 and ks <= XK_9: - return input.KeyCode(ord(key0) + (ks.int - XK_0.int)) + return input.KeyCode(ord(Key0) + (ks.int - XK_0.int)) if ks >= XK_F1 and ks <= XK_F12: - return input.KeyCode(ord(keyF1) + (ks.int - XK_F1.int)) + return input.KeyCode(ord(KeyF1) + (ks.int - XK_F1.int)) case ks.uint - of XK_Return: keyEnter - of XK_space: keySpace - of XK_Escape: keyEsc - of XK_Tab: keyTab - of XK_BackSpace: keyBackspace - of XK_Delete: keyDelete - of XK_Insert: keyInsert - of XK_Left: keyLeft - of XK_Right: keyRight - of XK_Up: keyUp - of XK_Down: keyDown - of XK_Page_Up: keyPageUp - of XK_Page_Down: keyPageDown - of XK_Home: keyHome - of XK_End: keyEnd - of XK_Caps_Lock: keyCapslock - of XK_comma: keyComma - of XK_period: keyPeriod - else: keyNone + of XK_Return: KeyEnter + of XK_space: KeySpace + of XK_Escape: KeyEsc + of XK_Tab: KeyTab + of XK_BackSpace: KeyBackspace + of XK_Delete: KeyDelete + of XK_Insert: KeyInsert + of XK_Left: KeyLeft + of XK_Right: KeyRight + of XK_Up: KeyUp + of XK_Down: KeyDown + of XK_Page_Up: KeyPageUp + of XK_Page_Down: KeyPageDown + of XK_Home: KeyHome + of XK_End: KeyEnd + of XK_Caps_Lock: KeyCapslock + of XK_comma: KeyComma + of XK_period: KeyPeriod + else: KeyNone proc translateMods(state: cuint): set[Modifier] = - if (state and ShiftMask) != 0: result.incl modShift - if (state and ControlMask) != 0: result.incl modCtrl - if (state and Mod1Mask) != 0: result.incl modAlt - if (state and Mod4Mask) != 0: result.incl modGui + if (state and ShiftMask) != 0: result.incl ShiftPressed + if (state and ControlMask) != 0: result.incl CtrlPressed + if (state and Mod1Mask) != 0: result.incl AltPressed + if (state and Mod4Mask) != 0: result.incl GuiPressed proc translateButton(button: cuint): MouseButton = case button - of Button1: mbLeft - of Button3: mbRight - of Button2: mbMiddle - else: mbLeft + of Button1: LeftButton + of Button3: RightButton + of Button2: MiddleButton + else: LeftButton proc heldButtons(state: cuint): set[MouseButton] = - if (state and Button1Mask) != 0: result.incl mbLeft - if (state and Button2Mask) != 0: result.incl mbMiddle - if (state and Button3Mask) != 0: result.incl mbRight + if (state and Button1Mask) != 0: result.incl LeftButton + if (state and Button2Mask) != 0: result.incl MiddleButton + if (state and Button3Mask) != 0: result.incl RightButton # ---- Clipboard handling ---- @@ -551,20 +551,20 @@ proc processXEvent(xev: XEvent) = gWidth = newW gHeight = newH recreateBackBuffer() - var e = input.Event(kind: evWindowResize) + var e = input.Event(kind: WindowResizeEvent) e.x = gWidth e.y = gHeight pushEvent(e) of ClientMessage: if xev.xclient.data[0] == gWmDeleteWindow.clong: - pushEvent(input.Event(kind: evWindowClose)) + pushEvent(input.Event(kind: WindowCloseEvent)) of FocusIn: - pushEvent(input.Event(kind: evWindowFocusGained)) + pushEvent(input.Event(kind: WindowFocusGainedEvent)) of FocusOut: - pushEvent(input.Event(kind: evWindowFocusLost)) + pushEvent(input.Event(kind: WindowFocusLostEvent)) of KeyPress: var buf: array[8, char] @@ -572,13 +572,13 @@ proc processXEvent(xev: XEvent) = let textLen = XLookupString(unsafeAddr xev.xkey, cast[cstring](addr buf[0]), 8, addr ks, nil) # Key event - var e = input.Event(kind: evKeyDown) + var e = input.Event(kind: KeyDownEvent) e.key = translateKeySym(ks) e.mods = translateMods(xev.xkey.state) pushEvent(e) # Text input (if printable) if textLen > 0 and buf[0].uint8 >= 32 and buf[0].uint8 != 127: - var te = input.Event(kind: evTextInput) + var te = input.Event(kind: TextInputEvent) for i in 0 ..< min(textLen, 4): te.text[i] = buf[i] pushEvent(te) @@ -586,7 +586,7 @@ proc processXEvent(xev: XEvent) = of KeyRelease: var ks: XKeySym discard XLookupString(unsafeAddr xev.xkey, nil, 0, addr ks, nil) - var e = input.Event(kind: evKeyUp) + var e = input.Event(kind: KeyUpEvent) e.key = translateKeySym(ks) e.mods = translateMods(xev.xkey.state) pushEvent(e) @@ -595,11 +595,11 @@ proc processXEvent(xev: XEvent) = let btn = xev.xbutton.button if btn == Button4 or btn == Button5: # Scroll wheel - var e = input.Event(kind: evMouseWheel) + var e = input.Event(kind: MouseWheelEvent) e.y = if btn == Button4: 1 else: -1 pushEvent(e) else: - var e = input.Event(kind: evMouseDown) + var e = input.Event(kind: MouseDownEvent) e.x = xev.xbutton.x e.y = xev.xbutton.y e.button = translateButton(btn) @@ -620,14 +620,14 @@ proc processXEvent(xev: XEvent) = of ButtonRelease: let btn = xev.xbutton.button if btn != Button4 and btn != Button5: - var e = input.Event(kind: evMouseUp) + var e = input.Event(kind: MouseUpEvent) e.x = xev.xbutton.x e.y = xev.xbutton.y e.button = translateButton(btn) pushEvent(e) of MotionNotify: - var e = input.Event(kind: evMouseMove) + var e = input.Event(kind: MouseMoveEvent) e.x = xev.xmotion.x e.y = xev.xmotion.y e.buttons = heldButtons(xev.xmotion.state) @@ -820,7 +820,7 @@ proc x11SetWindowTitle(title: string) = # ---- Input hook implementations ---- -proc x11PollEvent(e: var input.Event): bool = +proc x11PollEvent(e: var input.Event; flags: set[InputFlag]): bool = drainXEvents() if eventQueue.len > 0: e = eventQueue[0] @@ -828,12 +828,13 @@ proc x11PollEvent(e: var input.Event): bool = return true return false -proc x11WaitEvent(e: var input.Event; timeoutMs: int): bool = +proc x11WaitEvent(e: var input.Event; timeoutMs: int; + flags: set[InputFlag]): bool = if eventQueue.len > 0: e = eventQueue[0] eventQueue.delete(0) return true - if x11PollEvent(e): return true + if x11PollEvent(e, flags): return true if timeoutMs < 0: # Block efficiently until an X11 event arrives @@ -854,7 +855,7 @@ proc x11WaitEvent(e: var input.Event; timeoutMs: int): bool = let now = getTicks() if now >= deadline: return false os.sleep(10) - if x11PollEvent(e): return true + if x11PollEvent(e, flags): return true proc x11GetClipboardText(): string = discard XConvertSelection(gDisplay, gClipboard, gUtf8String, @@ -915,7 +916,6 @@ proc x11Delay(ms: int) = drainXEvents() os.sleep(min(deadline - now, 10)) -proc x11StartTextInput() = discard proc x11QuitRequest() = if gDisplay != nil: discard XDestroyWindow(gDisplay, gWindow) @@ -939,6 +939,6 @@ proc initX11Driver*() = inputRelays = InputRelays( pollEvent: x11PollEvent, waitEvent: x11WaitEvent, getTicks: x11GetTicks, delay: x11Delay, - startTextInput: x11StartTextInput, quitRequest: x11QuitRequest) + quitRequest: x11QuitRequest) clipboardRelays = ClipboardRelays( getText: x11GetClipboardText, putText: x11PutClipboardText) diff --git a/core/input.nim b/core/input.nim index eae80b8..cd83a71 100644 --- a/core/input.nim +++ b/core/input.nim @@ -1,45 +1,47 @@ # Platform-independent input events and relays. -# Part of the core stdlib abstraction (plan.md). - +# Part of the core stdlib abstraction. type KeyCode* = enum - keyNone, - keyA, keyB, keyC, keyD, keyE, keyF, keyG, keyH, keyI, keyJ, - keyK, keyL, keyM, keyN, keyO, keyP, keyQ, keyR, keyS, keyT, - keyU, keyV, keyW, keyX, keyY, keyZ, - key0, key1, key2, key3, key4, key5, key6, key7, key8, key9, - keyF1, keyF2, keyF3, keyF4, keyF5, keyF6, - keyF7, keyF8, keyF9, keyF10, keyF11, keyF12, - keyEnter, keySpace, keyEsc, keyTab, - keyBackspace, keyDelete, keyInsert, - keyLeft, keyRight, keyUp, keyDown, - keyPageUp, keyPageDown, keyHome, keyEnd, - keyCapslock, keyComma, keyPeriod, + KeyNone, + KeyA, KeyB, KeyC, KeyD, KeyE, KeyF, KeyG, KeyH, KeyI, KeyJ, + KeyK, KeyL, KeyM, KeyN, KeyO, KeyP, KeyQ, KeyR, KeyS, KeyT, + KeyU, KeyV, KeyW, KeyX, KeyY, KeyZ, + Key0, Key1, Key2, Key3, Key4, Key5, Key6, Key7, Key8, Key9, + KeyF1, KeyF2, KeyF3, KeyF4, KeyF5, KeyF6, + KeyF7, KeyF8, KeyF9, KeyF10, KeyF11, KeyF12, + KeyEnter, KeySpace, KeyEsc, KeyTab, + KeyBackspace, KeyDelete, KeyInsert, + KeyLeft, KeyRight, KeyUp, KeyDown, + KeyPageUp, KeyPageDown, KeyHome, KeyEnd, + KeyCapslock, KeyComma, KeyPeriod, EventKind* = enum - evNone, - evKeyDown, evKeyUp, evTextInput, - evMouseDown, evMouseUp, evMouseMove, evMouseWheel, - evWindowResize, evWindowClose, - evWindowFocusGained, evWindowFocusLost, - evQuit + NoEvent, + KeyDownEvent, KeyUpEvent, TextInputEvent, + MouseDownEvent, MouseUpEvent, MouseMoveEvent, MouseWheelEvent, + WindowResizeEvent, WindowCloseEvent, + WindowFocusGainedEvent, WindowFocusLostEvent, + QuitEvent Modifier* = enum - modShift, modCtrl, modAlt, modGui + ShiftPressed, CtrlPressed, AltPressed, GuiPressed MouseButton* = enum - mbLeft, mbRight, mbMiddle + LeftButton, RightButton, MiddleButton + + InputFlag* = enum + WantTextInput ## show on-screen keyboard / enable IME Event* = object kind*: EventKind key*: KeyCode mods*: set[Modifier] - text*: array[4, char] ## evTextInput: one UTF-8 codepoint, no alloc + text*: array[4, char] ## TextInputEvent: one UTF-8 codepoint, no alloc x*, y*: int ## mouse position, scroll delta, or new window size - xrel*, yrel*: int ## evMouseMove: relative motion + xrel*, yrel*: int ## MouseMoveEvent: relative motion button*: MouseButton - buttons*: set[MouseButton] ## evMouseMove: which buttons are held + buttons*: set[MouseButton] ## MouseMoveEvent: which buttons are held clicks*: int ## number of consecutive clicks (double-click = 2) ClipboardRelays* = object @@ -47,11 +49,11 @@ type putText*: proc (text: string) {.nimcall.} InputRelays* = object - pollEvent*: proc (e: var Event): bool {.nimcall.} - waitEvent*: proc (e: var Event; timeoutMs: int): bool {.nimcall.} + pollEvent*: proc (e: var Event; flags: set[InputFlag]): bool {.nimcall.} + waitEvent*: proc (e: var Event; timeoutMs: int; + flags: set[InputFlag]): bool {.nimcall.} getTicks*: proc (): int {.nimcall.} delay*: proc (ms: int) {.nimcall.} - startTextInput*: proc () {.nimcall.} quitRequest*: proc () {.nimcall.} var clipboardRelays* = ClipboardRelays( @@ -59,19 +61,20 @@ var clipboardRelays* = ClipboardRelays( putText: proc (text: string) = discard) var inputRelays* = InputRelays( - pollEvent: proc (e: var Event): bool = false, - waitEvent: proc (e: var Event; timeoutMs: int): bool = false, + pollEvent: proc (e: var Event; flags: set[InputFlag]): bool = false, + waitEvent: proc (e: var Event; timeoutMs: int; + flags: set[InputFlag]): bool = false, getTicks: proc (): int = 0, delay: proc (ms: int) = discard, - startTextInput: proc () = discard, quitRequest: proc () = discard) -proc pollEvent*(e: var Event): bool = inputRelays.pollEvent(e) -proc waitEvent*(e: var Event; timeoutMs: int = -1): bool = - inputRelays.waitEvent(e, timeoutMs) +proc pollEvent*(e: var Event; flags: set[InputFlag] = {}): bool = + inputRelays.pollEvent(e, flags) +proc waitEvent*(e: var Event; timeoutMs: int = -1; + flags: set[InputFlag] = {}): bool = + inputRelays.waitEvent(e, timeoutMs, flags) proc getClipboardText*(): string = clipboardRelays.getText() proc putClipboardText*(text: string) = clipboardRelays.putText(text) proc getTicks*(): int = inputRelays.getTicks() proc delay*(ms: int) = inputRelays.delay(ms) -proc startTextInput*() = inputRelays.startTextInput() proc quitRequest*() = inputRelays.quitRequest() diff --git a/testprims.nim b/testprims.nim index b2901d8..3771136 100644 --- a/testprims.nim +++ b/testprims.nim @@ -51,6 +51,5 @@ if sdl2.init(INIT_VIDEO) != SdlSuccess: elif ttfInit() != SdlSuccess: echo "TTF_Init" else: - startTextInput() mainProc() sdl2.quit() From d9f3a86ae1d3f9c5fc757630cd980e4ee4bfa925 Mon Sep 17 00:00:00 2001 From: araq Date: Sun, 12 Apr 2026 12:44:11 +0200 Subject: [PATCH 08/14] API cleanups --- app/cocoa_driver.nim | 5 ----- app/sdl2_driver.nim | 6 ------ app/sdl3_driver.nim | 6 ------ app/tabbar.nim | 38 ++++++++++++++++++++++++++++---------- app/winapi_driver.nim | 7 ------- app/x11_driver.nim | 6 ------ core/input.nim | 4 +--- 7 files changed, 29 insertions(+), 43 deletions(-) diff --git a/app/cocoa_driver.nim b/app/cocoa_driver.nim index b49cc2e..bf968cf 100644 --- a/app/cocoa_driver.nim +++ b/app/cocoa_driver.nim @@ -224,11 +224,6 @@ proc translateNEEvent(ne: NEEvent; e: var input.Event) = e.kind = MouseMoveEvent e.x = ne.x e.y = ne.y - e.xrel = ne.xrel - e.yrel = ne.yrel - if (ne.buttons and 1) != 0: e.buttons.incl LeftButton - if (ne.buttons and 2) != 0: e.buttons.incl RightButton - if (ne.buttons and 4) != 0: e.buttons.incl MiddleButton of neMouseWheel: e.kind = MouseWheelEvent e.x = ne.x diff --git a/app/sdl2_driver.nim b/app/sdl2_driver.nim index 394619a..74ac576 100644 --- a/app/sdl2_driver.nim +++ b/app/sdl2_driver.nim @@ -283,12 +283,6 @@ proc sdlPollEvent(e: var input.Event; flags: set[InputFlag]): bool = e.kind = MouseMoveEvent e.x = sdlEvent.motion.x e.y = sdlEvent.motion.y - e.xrel = sdlEvent.motion.xrel - e.yrel = sdlEvent.motion.yrel - if (sdlEvent.motion.state and BUTTON_LMASK) != 0: - e.buttons.incl LeftButton - if (sdlEvent.motion.state and BUTTON_RMASK) != 0: - e.buttons.incl RightButton of MouseWheel: e.kind = MouseWheelEvent e.x = sdlEvent.wheel.x diff --git a/app/sdl3_driver.nim b/app/sdl3_driver.nim index abbfce0..f01dc93 100644 --- a/app/sdl3_driver.nim +++ b/app/sdl3_driver.nim @@ -276,12 +276,6 @@ proc translateEvent(sdlEvent: sdl3.Event; e: var input.Event) = e.kind = MouseMoveEvent e.x = sdlEvent.motion.x.int e.y = sdlEvent.motion.y.int - e.xrel = sdlEvent.motion.xrel.int - e.yrel = sdlEvent.motion.yrel.int - if (sdlEvent.motion.state and BUTTON_LMASK) != 0: - e.buttons.incl LeftButton - if (sdlEvent.motion.state and BUTTON_RMASK) != 0: - e.buttons.incl RightButton elif evType == uint32(EVENT_MOUSE_WHEEL): e.kind = MouseWheelEvent e.x = sdlEvent.wheel.x.int diff --git a/app/tabbar.nim b/app/tabbar.nim index 156f005..2dc4b2d 100644 --- a/app/tabbar.nim +++ b/app/tabbar.nim @@ -83,9 +83,27 @@ proc drawButtonList*(buttons: openArray[string]; t: Internaltheme; result = i inc xx, rect.w + t.uiXGap*2 +var tabDragX: int ## last known mouse X during tab drag +var tabDragging: bool ## whether left button is held for tab dragging + proc drawTabBar*(tabs: var TabBar; t: InternalTheme; x, screenW: int; events: seq[Event]; active: Buffer): Buffer = + # Track drag state from events + for e in events: + case e.kind + of MouseDownEvent: + if e.button == LeftButton: + tabDragging = true + tabDragX = e.x + of MouseUpEvent: + if e.button == LeftButton: + tabDragging = false + of MouseMoveEvent: + if tabDragging: + tabDragX = e.x + else: discard + var it = tabs.first var activeDrawn = false var xx = x @@ -108,16 +126,16 @@ proc drawTabBar*(tabs: var TabBar; t: InternalTheme; let p = point(e.x, e.y) if rect.contains(p): result = it - elif e.kind == MouseMoveEvent: - if LeftButton in e.buttons: - let p = point(e.x, e.y) - if rect.contains(p): - if e.xrel >= 4: - if it == tabs.first: tabs.first = it.next - swapBuffers(it, it.next) - elif e.xrel <= -4: - if it == tabs.first: tabs.first = it.prev - swapBuffers(it.prev, it) + elif e.kind == MouseMoveEvent and tabDragging: + let p = point(e.x, e.y) + if rect.contains(p): + let dx = e.x - tabDragX + if dx >= 4: + if it == tabs.first: tabs.first = it.next + swapBuffers(it, it.next) + elif dx <= -4: + if it == tabs.first: tabs.first = it.prev + swapBuffers(it.prev, it) inc xx, rect.w + t.uiXGap*2 if it == tabs.last: break diff --git a/app/winapi_driver.nim b/app/winapi_driver.nim index 155da16..40ffd68 100644 --- a/app/winapi_driver.nim +++ b/app/winapi_driver.nim @@ -416,12 +416,6 @@ proc getModifiers(): set[Modifier] = if GetKeyState(VK_CONTROL.int32) < 0: result.incl CtrlPressed if GetKeyState(VK_MENU.int32) < 0: result.incl AltPressed -proc getMouseButtons(wp: WPARAM): set[MouseButton] = - let flags = wp.uint32 - if (flags and MK_LBUTTON) != 0: result.incl LeftButton - if (flags and MK_RBUTTON) != 0: result.incl RightButton - if (flags and MK_MBUTTON) != 0: result.incl MiddleButton - var lastClickTime: DWORD var lastClickX, lastClickY: int var clickCount: int @@ -555,7 +549,6 @@ proc wndProc(hwnd: HWND; msg: UINT; wp: WPARAM; lp: LPARAM): LRESULT {.stdcall.} var e = input.Event(kind: MouseMoveEvent) e.x = loWord(lp) e.y = hiWord(lp) - e.buttons = getMouseButtons(wp) pushEvent(e) return 0 diff --git a/app/x11_driver.nim b/app/x11_driver.nim index db90717..9a372a8 100644 --- a/app/x11_driver.nim +++ b/app/x11_driver.nim @@ -499,11 +499,6 @@ proc translateButton(button: cuint): MouseButton = of Button2: MiddleButton else: LeftButton -proc heldButtons(state: cuint): set[MouseButton] = - if (state and Button1Mask) != 0: result.incl LeftButton - if (state and Button2Mask) != 0: result.incl MiddleButton - if (state and Button3Mask) != 0: result.incl RightButton - # ---- Clipboard handling ---- proc handleSelectionRequest(req: XSelectionRequestEvent) = @@ -630,7 +625,6 @@ proc processXEvent(xev: XEvent) = var e = input.Event(kind: MouseMoveEvent) e.x = xev.xmotion.x e.y = xev.xmotion.y - e.buttons = heldButtons(xev.xmotion.state) pushEvent(e) of SelectionRequest: diff --git a/core/input.nim b/core/input.nim index cd83a71..368bd1a 100644 --- a/core/input.nim +++ b/core/input.nim @@ -39,9 +39,7 @@ type mods*: set[Modifier] text*: array[4, char] ## TextInputEvent: one UTF-8 codepoint, no alloc x*, y*: int ## mouse position, scroll delta, or new window size - xrel*, yrel*: int ## MouseMoveEvent: relative motion - button*: MouseButton - buttons*: set[MouseButton] ## MouseMoveEvent: which buttons are held + button*: MouseButton ## MouseDownEvent/MouseUpEvent: which button clicks*: int ## number of consecutive clicks (double-click = 2) ClipboardRelays* = object From 588dab5c2511e7ff1b4234ed756cc384b6d2ed06 Mon Sep 17 00:00:00 2001 From: araq Date: Sun, 12 Apr 2026 12:47:55 +0200 Subject: [PATCH 09/14] bugfix --- app/nimedit.nim | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/nimedit.nim b/app/nimedit.nim index ab4f42b..98ad956 100644 --- a/app/nimedit.nim +++ b/app/nimedit.nim @@ -930,9 +930,10 @@ proc processEvents(events: out seq[Event]; ed: Editor): bool = if clicks == 0 or clicks > 5: clicks = 1 if ctrlKeyPressed(e): inc(clicks) let p = point(e.x, e.y) - if ed.mainRect.contains(p) and ed.main.scrollingEnabled: + if ed.mainRect.contains(p): var rawMainRect = ed.mainRect - rawMainRect.w -= scrollBarWidth + if ed.main.scrollingEnabled: + rawMainRect.w -= scrollBarWidth if focus == main and rawMainRect.contains(p): main.setCursorFromMouse(ed.mainRect, p, clicks) else: From 8afce7886ac867d21dca2c6c484b9198230e9fdb Mon Sep 17 00:00:00 2001 From: araq Date: Sun, 12 Apr 2026 16:58:54 +0200 Subject: [PATCH 10/14] use the new uirelays library instead --- NimEdit.nimble | 1 + app/cocoa_backend.m | 909 ---------------------------------------- app/cocoa_driver.nim | 280 ------------- app/gtk4_driver.nim | 763 ---------------------------------- app/nimedit.nim | 27 +- app/prims.nim | 2 +- app/scrollbar.nim | 2 +- app/sdl2_driver.nim | 339 --------------- app/sdl3_driver.nim | 327 --------------- app/styles.nim | 2 +- app/tabbar.nim | 2 +- app/themes.nim | 2 +- app/winapi_driver.nim | 921 ----------------------------------------- app/x11_driver.nim | 938 ------------------------------------------ core/basetypes.nim | 22 - core/input.nim | 78 ---- core/screen.nim | 115 ------ editor/buffer.nim | 2 +- editor/buffertype.nim | 2 +- nim.cfg | 1 - 20 files changed, 10 insertions(+), 4725 deletions(-) delete mode 100644 app/cocoa_backend.m delete mode 100644 app/cocoa_driver.nim delete mode 100644 app/gtk4_driver.nim delete mode 100644 app/sdl2_driver.nim delete mode 100644 app/sdl3_driver.nim delete mode 100644 app/winapi_driver.nim delete mode 100644 app/x11_driver.nim delete mode 100644 core/basetypes.nim delete mode 100644 core/input.nim delete mode 100644 core/screen.nim diff --git a/NimEdit.nimble b/NimEdit.nimble index 1046556..14818b3 100644 --- a/NimEdit.nimble +++ b/NimEdit.nimble @@ -14,3 +14,4 @@ bin = @["app/nimedit"] # [Deps] requires: "nim >= 0.19.0, sdl2#head, dialogs >= 1.0" requires: "https://github.com/nim-lang/sdl3" +requires: "https://github.com/nim-lang/uirelays >= 0.8.0" diff --git a/app/cocoa_backend.m b/app/cocoa_backend.m deleted file mode 100644 index 47e7d11..0000000 --- a/app/cocoa_backend.m +++ /dev/null @@ -1,909 +0,0 @@ -/* cocoa_backend.m – Cocoa/AppKit backend for NimEdit. - * - * Exposes a flat C API that cocoa_driver.nim imports. - * Uses Core Graphics for drawing, Core Text for fonts, - * NSView subclass for events. - * - * Compile: included automatically via {.compile.} in cocoa_driver.nim - * Link: -framework Cocoa -framework CoreText -framework CoreGraphics -framework QuartzCore - */ - -#import -#import -#import -#include - -/* ---- Event queue (ring buffer) ---------------------------------------- */ - -enum { - NE_NONE = 0, - NE_KEY_DOWN, NE_KEY_UP, NE_TEXT_INPUT, - NE_MOUSE_DOWN, NE_MOUSE_UP, NE_MOUSE_MOVE, NE_MOUSE_WHEEL, - NE_WINDOW_RESIZE, NE_WINDOW_CLOSE, - NE_WINDOW_FOCUS_GAINED, NE_WINDOW_FOCUS_LOST, - NE_QUIT -}; - -enum { - NE_MOD_SHIFT = 1, - NE_MOD_CTRL = 2, - NE_MOD_ALT = 4, - NE_MOD_GUI = 8 -}; - -enum { - NE_MB_LEFT = 0, - NE_MB_RIGHT = 1, - NE_MB_MIDDLE = 2 -}; - -typedef struct { - int kind; - int key; /* NimEdit KeyCode ordinal */ - int mods; /* bitmask of NE_MOD_* */ - char text[4]; /* UTF-8 codepoint for NE_TEXT_INPUT */ - int x, y; - int xrel, yrel; - int button; /* NE_MB_* */ - int buttons; /* bitmask: 1=left, 2=right, 4=middle */ - int clicks; -} NEEvent; - -#define EVENT_QUEUE_SIZE 256 -static NEEvent eventQueue[EVENT_QUEUE_SIZE]; -static int eqHead = 0, eqTail = 0; - -static void pushEvent(NEEvent ev) { - int next = (eqHead + 1) % EVENT_QUEUE_SIZE; - if (next == eqTail) return; /* queue full, drop */ - eventQueue[eqHead] = ev; - eqHead = next; -} - -int cocoa_pollEvent(NEEvent *out) { - /* Pump the run loop briefly so Cocoa delivers events */ - @autoreleasepool { - NSEvent *ev; - while ((ev = [NSApp nextEventMatchingMask:NSEventMaskAny - untilDate:nil - inMode:NSDefaultRunLoopMode - dequeue:YES]) != nil) { - [NSApp sendEvent:ev]; - [NSApp updateWindows]; - } - } - if (eqTail == eqHead) { - out->kind = NE_NONE; - return 0; - } - *out = eventQueue[eqTail]; - eqTail = (eqTail + 1) % EVENT_QUEUE_SIZE; - return 1; -} - -int cocoa_waitEvent(NEEvent *out, int timeoutMs) { - @autoreleasepool { - NSDate *deadline = (timeoutMs < 0) - ? [NSDate distantFuture] - : [NSDate dateWithTimeIntervalSinceNow:timeoutMs / 1000.0]; - NSEvent *ev = [NSApp nextEventMatchingMask:NSEventMaskAny - untilDate:deadline - inMode:NSDefaultRunLoopMode - dequeue:YES]; - if (ev) { - [NSApp sendEvent:ev]; - [NSApp updateWindows]; - /* pump remaining */ - while ((ev = [NSApp nextEventMatchingMask:NSEventMaskAny - untilDate:nil - inMode:NSDefaultRunLoopMode - dequeue:YES]) != nil) { - [NSApp sendEvent:ev]; - [NSApp updateWindows]; - } - } - } - if (eqTail == eqHead) { - out->kind = NE_NONE; - return 0; - } - *out = eventQueue[eqTail]; - eqTail = (eqTail + 1) % EVENT_QUEUE_SIZE; - return 1; -} - -/* ---- Modifier helpers ------------------------------------------------- */ - -static int translateModifiers(NSEventModifierFlags flags) { - int m = 0; - if (flags & NSEventModifierFlagShift) m |= NE_MOD_SHIFT; - if (flags & NSEventModifierFlagControl) m |= NE_MOD_CTRL; - if (flags & NSEventModifierFlagOption) m |= NE_MOD_ALT; - if (flags & NSEventModifierFlagCommand) m |= NE_MOD_GUI; - return m; -} - -int cocoa_getModState(void) { - NSEventModifierFlags flags = [NSEvent modifierFlags]; - return translateModifiers(flags); -} - -/* ---- Key translation -------------------------------------------------- */ - -/* Returns NimEdit KeyCode ordinal. Must match input.nim KeyCode enum. */ -static int translateKeyCode(unsigned short kc) { - switch (kc) { - case 0x00: return 1; /* keyA */ - case 0x0B: return 2; /* keyB */ - case 0x08: return 3; /* keyC */ - case 0x02: return 4; /* keyD */ - case 0x0E: return 5; /* keyE */ - case 0x03: return 6; /* keyF */ - case 0x05: return 7; /* keyG */ - case 0x04: return 8; /* keyH */ - case 0x22: return 9; /* keyI */ - case 0x26: return 10; /* keyJ */ - case 0x28: return 11; /* keyK */ - case 0x25: return 12; /* keyL */ - case 0x2E: return 13; /* keyM */ - case 0x2D: return 14; /* keyN */ - case 0x1F: return 15; /* keyO */ - case 0x23: return 16; /* keyP */ - case 0x0C: return 17; /* keyQ */ - case 0x0F: return 18; /* keyR */ - case 0x01: return 19; /* keyS */ - case 0x11: return 20; /* keyT */ - case 0x20: return 21; /* keyU */ - case 0x09: return 22; /* keyV */ - case 0x0D: return 23; /* keyW */ - case 0x07: return 24; /* keyX */ - case 0x10: return 25; /* keyY */ - case 0x06: return 26; /* keyZ */ - case 0x12: return 28; /* key1 -- note: key0 = 27, key1 = 28 .. key9 = 36 */ - case 0x13: return 29; /* key2 */ - case 0x14: return 30; /* key3 */ - case 0x15: return 31; /* key4 */ - case 0x17: return 32; /* key5 */ - case 0x16: return 33; /* key6 */ - case 0x1A: return 34; /* key7 */ - case 0x1C: return 35; /* key8 */ - case 0x19: return 36; /* key9 */ - case 0x1D: return 27; /* key0 */ - case 0x7A: return 37; /* keyF1 */ - case 0x78: return 38; /* keyF2 */ - case 0x63: return 39; /* keyF3 */ - case 0x76: return 40; /* keyF4 */ - case 0x60: return 41; /* keyF5 */ - case 0x61: return 42; /* keyF6 */ - case 0x62: return 43; /* keyF7 */ - case 0x64: return 44; /* keyF8 */ - case 0x65: return 45; /* keyF9 */ - case 0x6D: return 46; /* keyF10 */ - case 0x67: return 47; /* keyF11 */ - case 0x6F: return 48; /* keyF12 */ - case 0x24: return 49; /* keyEnter */ - case 0x31: return 50; /* keySpace */ - case 0x35: return 51; /* keyEsc */ - case 0x30: return 52; /* keyTab */ - case 0x33: return 53; /* keyBackspace */ - case 0x75: return 54; /* keyDelete */ - case 0x72: return 55; /* keyInsert (Help key on Mac) */ - case 0x7B: return 56; /* keyLeft */ - case 0x7C: return 57; /* keyRight */ - case 0x7E: return 58; /* keyUp */ - case 0x7D: return 59; /* keyDown */ - case 0x74: return 60; /* keyPageUp */ - case 0x79: return 61; /* keyPageDown */ - case 0x73: return 62; /* keyHome */ - case 0x77: return 63; /* keyEnd */ - case 0x39: return 64; /* keyCapslock */ - case 0x2B: return 65; /* keyComma */ - case 0x2F: return 66; /* keyPeriod */ - default: return 0; /* keyNone */ - } -} - -/* ---- Font management -------------------------------------------------- */ - -#define MAX_FONTS 64 - -typedef struct { - CTFontRef font; - int ascent, descent, lineHeight; -} FontSlot; - -static FontSlot fonts[MAX_FONTS]; -static int fontCount = 0; - -int cocoa_openFont(const char *path, int size, - int *outAscent, int *outDescent, int *outLineHeight) { - if (fontCount >= MAX_FONTS) return 0; - - /* Create font from file path */ - CFStringRef cfPath = CFStringCreateWithCString(NULL, path, kCFStringEncodingUTF8); - CFURLRef url = CFURLCreateWithFileSystemPath(NULL, cfPath, kCFURLPOSIXPathStyle, false); - CGDataProviderRef provider = CGDataProviderCreateWithURL(url); - CTFontRef ctFont = NULL; - - if (provider) { - CGFontRef cgFont = CGFontCreateWithDataProvider(provider); - if (cgFont) { - ctFont = CTFontCreateWithGraphicsFont(cgFont, (CGFloat)size, NULL, NULL); - CGFontRelease(cgFont); - } - CGDataProviderRelease(provider); - } - CFRelease(url); - CFRelease(cfPath); - - if (!ctFont) return 0; - - int idx = fontCount++; - fonts[idx].font = ctFont; - fonts[idx].ascent = (int)ceil(CTFontGetAscent(ctFont)); - fonts[idx].descent = (int)ceil(CTFontGetDescent(ctFont)); - fonts[idx].lineHeight = fonts[idx].ascent + fonts[idx].descent + - (int)ceil(CTFontGetLeading(ctFont)); - /* Ensure lineHeight is at least ascent + descent */ - if (fonts[idx].lineHeight < fonts[idx].ascent + fonts[idx].descent) - fonts[idx].lineHeight = fonts[idx].ascent + fonts[idx].descent; - - *outAscent = fonts[idx].ascent; - *outDescent = fonts[idx].descent; - *outLineHeight = fonts[idx].lineHeight; - return idx + 1; /* 1-based handle */ -} - -void cocoa_closeFont(int handle) { - int idx = handle - 1; - if (idx >= 0 && idx < fontCount && fonts[idx].font) { - CFRelease(fonts[idx].font); - fonts[idx].font = NULL; - } -} - -void cocoa_getFontMetrics(int handle, int *asc, int *desc, int *lh) { - int idx = handle - 1; - if (idx >= 0 && idx < fontCount && fonts[idx].font) { - *asc = fonts[idx].ascent; - *desc = fonts[idx].descent; - *lh = fonts[idx].lineHeight; - } else { - *asc = *desc = *lh = 0; - } -} - -/* Measure text width/height using Core Text */ -void cocoa_measureText(int handle, const char *text, int *outW, int *outH) { - int idx = handle - 1; - *outW = 0; *outH = 0; - if (idx < 0 || idx >= fontCount || !fonts[idx].font || !text || !text[0]) return; - - CFStringRef str = CFStringCreateWithCString(NULL, text, kCFStringEncodingUTF8); - if (!str) return; - - CFStringRef keys[] = { kCTFontAttributeName }; - CFTypeRef vals[] = { fonts[idx].font }; - CFDictionaryRef attrs = CFDictionaryCreate(NULL, - (const void **)keys, (const void **)vals, 1, - &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); - - CFAttributedStringRef attrStr = CFAttributedStringCreate(NULL, str, attrs); - CTLineRef line = CTLineCreateWithAttributedString(attrStr); - - CGRect bounds = CTLineGetImageBounds(line, NULL); - double width = CTLineGetTypographicBounds(line, NULL, NULL, NULL); - *outW = (int)ceil(width); - *outH = fonts[idx].lineHeight; - - CFRelease(line); - CFRelease(attrStr); - CFRelease(attrs); - CFRelease(str); -} - -/* ---- Backing bitmap context ------------------------------------------- */ - -static CGContextRef backingCtx = NULL; -static int backingW = 0, backingH = 0; -static CGFloat backingScale = 1.0; - -/* Clip rect stack (simple, max 32 deep) */ -#define MAX_CLIP_STACK 32 -static CGRect clipStack[MAX_CLIP_STACK]; -static int clipTop = 0; - -static void ensureBacking(int w, int h, CGFloat scale) { - if (backingCtx && backingW == w && backingH == h) return; - if (backingCtx) CGContextRelease(backingCtx); - - backingW = w; - backingH = h; - backingScale = scale; - - int pw = (int)(w * scale); - int ph = (int)(h * scale); - - CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); - backingCtx = CGBitmapContextCreate(NULL, pw, ph, 8, pw * 4, - cs, kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host); - CGColorSpaceRelease(cs); - - if (backingCtx) { - /* Scale so we can draw in point coordinates */ - CGContextScaleCTM(backingCtx, scale, scale); - /* Flip coordinate system: Core Graphics is bottom-up, we want top-down */ - CGContextTranslateCTM(backingCtx, 0, h); - CGContextScaleCTM(backingCtx, 1.0, -1.0); - CGContextSetShouldAntialias(backingCtx, true); - CGContextSetShouldSmoothFonts(backingCtx, true); - } -} - -/* ---- Drawing primitives ----------------------------------------------- */ - -void cocoa_fillRect(int x, int y, int w, int h, int r, int g, int b, int a) { - if (!backingCtx) return; - CGContextSetRGBFillColor(backingCtx, r/255.0, g/255.0, b/255.0, a/255.0); - CGContextFillRect(backingCtx, CGRectMake(x, y, w, h)); -} - -void cocoa_drawLine(int x1, int y1, int x2, int y2, int r, int g, int b, int a) { - if (!backingCtx) return; - CGContextSetRGBStrokeColor(backingCtx, r/255.0, g/255.0, b/255.0, a/255.0); - CGContextSetLineWidth(backingCtx, 1.0); - CGContextBeginPath(backingCtx); - CGContextMoveToPoint(backingCtx, x1 + 0.5, y1 + 0.5); - CGContextAddLineToPoint(backingCtx, x2 + 0.5, y2 + 0.5); - CGContextStrokePath(backingCtx); -} - -void cocoa_drawPoint(int x, int y, int r, int g, int b, int a) { - if (!backingCtx) return; - CGContextSetRGBFillColor(backingCtx, r/255.0, g/255.0, b/255.0, a/255.0); - CGContextFillRect(backingCtx, CGRectMake(x, y, 1, 1)); -} - -void cocoa_drawText(int fontHandle, int x, int y, const char *text, - int fgR, int fgG, int fgB, int fgA, - int bgR, int bgG, int bgB, int bgA, - int *outW, int *outH) { - *outW = 0; *outH = 0; - int idx = fontHandle - 1; - if (idx < 0 || idx >= fontCount || !fonts[idx].font || !backingCtx) return; - if (!text || !text[0]) return; - - CTFontRef ctFont = fonts[idx].font; - int lh = fonts[idx].lineHeight; - int asc = fonts[idx].ascent; - - /* Measure first for background fill */ - CFStringRef str = CFStringCreateWithCString(NULL, text, kCFStringEncodingUTF8); - if (!str) return; - - CGColorRef fgColor = CGColorCreateSRGB(fgR/255.0, fgG/255.0, fgB/255.0, fgA/255.0); - - CFStringRef keys[] = { kCTFontAttributeName, kCTForegroundColorAttributeName }; - CFTypeRef vals[] = { ctFont, fgColor }; - CFDictionaryRef attrs = CFDictionaryCreate(NULL, - (const void **)keys, (const void **)vals, 2, - &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); - - CFAttributedStringRef attrStr = CFAttributedStringCreate(NULL, str, attrs); - CTLineRef line = CTLineCreateWithAttributedString(attrStr); - - double width = CTLineGetTypographicBounds(line, NULL, NULL, NULL); - int tw = (int)ceil(width); - - /* Fill background */ - CGContextSetRGBFillColor(backingCtx, bgR/255.0, bgG/255.0, bgB/255.0, bgA/255.0); - CGContextFillRect(backingCtx, CGRectMake(x, y, tw, lh)); - - /* Draw text. - * Core Text draws bottom-up. Our context is flipped to top-down. - * We need to locally un-flip for Core Text rendering. */ - CGContextSaveGState(backingCtx); - /* Move to the text baseline position: - * In our flipped coords, y is top of the line, baseline is at y + ascent. - * We un-flip around the line center. */ - CGContextTranslateCTM(backingCtx, 0, y + lh); - CGContextScaleCTM(backingCtx, 1.0, -1.0); - /* Now in un-flipped local coords, baseline is at (x, descent) from bottom */ - CGFloat baseline = fonts[idx].descent + (lh - asc - fonts[idx].descent) * 0.5; - if (baseline < fonts[idx].descent) baseline = fonts[idx].descent; - CGContextSetTextPosition(backingCtx, x, baseline); - CTLineDraw(line, backingCtx); - CGContextRestoreGState(backingCtx); - - *outW = tw; - *outH = lh; - - CFRelease(line); - CFRelease(attrStr); - CFRelease(attrs); - CGColorRelease(fgColor); - CFRelease(str); -} - -void cocoa_setClipRect(int x, int y, int w, int h) { - if (!backingCtx) return; - CGContextRestoreGState(backingCtx); - CGContextSaveGState(backingCtx); - CGContextClipToRect(backingCtx, CGRectMake(x, y, w, h)); -} - -void cocoa_saveState(void) { - if (!backingCtx) return; - CGContextSaveGState(backingCtx); -} - -void cocoa_restoreState(void) { - if (!backingCtx) return; - CGContextRestoreGState(backingCtx); -} - -/* ---- Clipboard -------------------------------------------------------- */ - -const char *cocoa_getClipboardText(void) { - @autoreleasepool { - NSPasteboard *pb = [NSPasteboard generalPasteboard]; - NSString *s = [pb stringForType:NSPasteboardTypeString]; - if (!s) return ""; - /* Return a C string that persists until next call */ - static char *buf = NULL; - free(buf); - const char *utf8 = [s UTF8String]; - buf = strdup(utf8); - return buf; - } -} - -void cocoa_putClipboardText(const char *text) { - @autoreleasepool { - NSPasteboard *pb = [NSPasteboard generalPasteboard]; - [pb clearContents]; - [pb setString:[NSString stringWithUTF8String:text] - forType:NSPasteboardTypeString]; - } -} - -/* ---- Timing ----------------------------------------------------------- */ - -static uint64_t startTime = 0; -static mach_timebase_info_data_t timebaseInfo; - -uint32_t cocoa_getTicks(void) { - uint64_t elapsed = mach_absolute_time() - startTime; - uint64_t nanos = elapsed * timebaseInfo.numer / timebaseInfo.denom; - return (uint32_t)(nanos / 1000000); -} - -void cocoa_delay(uint32_t ms) { - [NSThread sleepForTimeInterval:ms / 1000.0]; -} - -/* ---- NSView subclass -------------------------------------------------- */ - -@interface NimEditView : NSView -@property (nonatomic) BOOL hasMarkedText; -@end - -@implementation NimEditView { - NSTrackingArea *_trackingArea; -} - -- (BOOL)acceptsFirstResponder { return YES; } -- (BOOL)canBecomeKeyView { return YES; } -- (BOOL)isFlipped { return YES; } - -- (void)updateTrackingAreas { - [super updateTrackingAreas]; - if (_trackingArea) [self removeTrackingArea:_trackingArea]; - _trackingArea = [[NSTrackingArea alloc] - initWithRect:self.bounds - options:NSTrackingMouseMoved | NSTrackingActiveInKeyWindow | - NSTrackingInVisibleRect - owner:self - userInfo:nil]; - [self addTrackingArea:_trackingArea]; -} - -- (void)drawRect:(NSRect)dirtyRect { - if (!backingCtx) return; - CGContextRef viewCtx = [[NSGraphicsContext currentContext] CGContext]; - CGImageRef img = CGBitmapContextCreateImage(backingCtx); - if (img) { - NSRect bounds = self.bounds; - /* Flip for drawing since view is flipped but CGImage is bottom-up */ - CGContextSaveGState(viewCtx); - CGContextTranslateCTM(viewCtx, 0, bounds.size.height); - CGContextScaleCTM(viewCtx, 1.0, -1.0); - CGContextDrawImage(viewCtx, CGRectMake(0, 0, bounds.size.width, bounds.size.height), img); - CGContextRestoreGState(viewCtx); - CGImageRelease(img); - } -} - -/* ---- Keyboard events ---- */ - -- (void)keyDown:(NSEvent *)event { - NEEvent e = {0}; - e.kind = NE_KEY_DOWN; - e.key = translateKeyCode(event.keyCode); - e.mods = translateModifiers(event.modifierFlags); - pushEvent(e); - - /* Only feed to interpretKeyEvents when the key can produce text input. - Skip for Ctrl/Cmd combos and special keys (arrows, backspace, etc.) - — the app handles those via evKeyDown already. */ - int m = e.mods; - if ((m & (NE_MOD_CTRL | NE_MOD_GUI)) == 0 && e.key == 0) { - [self interpretKeyEvents:@[event]]; - } else if ((m & (NE_MOD_CTRL | NE_MOD_GUI)) == 0) { - /* Known key but no modifier — still try for text (e.g. space, comma). - But skip keys that are purely control keys. */ - unsigned short kc = event.keyCode; - switch (kc) { - case 0x33: /* backspace */ - case 0x75: /* delete */ - case 0x35: /* escape */ - case 0x30: /* tab */ - case 0x24: /* return */ - case 0x7B: case 0x7C: case 0x7E: case 0x7D: /* arrows */ - case 0x74: case 0x79: case 0x73: case 0x77: /* pgup/pgdn/home/end */ - case 0x72: /* insert/help */ - case 0x7A: case 0x78: case 0x63: case 0x76: /* F1-F4 */ - case 0x60: case 0x61: case 0x62: case 0x64: /* F5-F8 */ - case 0x65: case 0x6D: case 0x67: case 0x6F: /* F9-F12 */ - break; - default: - [self interpretKeyEvents:@[event]]; - break; - } - } -} - -/* Suppress the system beep for unhandled key combos */ -- (void)doCommandBySelector:(SEL)selector { - /* intentionally empty — swallows the NSBeep that interpretKeyEvents - would otherwise trigger for keys without a binding */ -} - -- (void)keyUp:(NSEvent *)event { - NEEvent e = {0}; - e.kind = NE_KEY_UP; - e.key = translateKeyCode(event.keyCode); - e.mods = translateModifiers(event.modifierFlags); - pushEvent(e); -} - -- (void)flagsChanged:(NSEvent *)event { - /* Ignore standalone modifier key events */ -} - -/* ---- Mouse events ---- */ - -- (void)mouseDown:(NSEvent *)event { - NSPoint p = [self convertPoint:event.locationInWindow fromView:nil]; - NEEvent e = {0}; - e.kind = NE_MOUSE_DOWN; - e.button = NE_MB_LEFT; - e.x = (int)p.x; - e.y = (int)p.y; - e.clicks = (int)event.clickCount; - e.mods = translateModifiers(event.modifierFlags); - pushEvent(e); -} - -- (void)mouseUp:(NSEvent *)event { - NSPoint p = [self convertPoint:event.locationInWindow fromView:nil]; - NEEvent e = {0}; - e.kind = NE_MOUSE_UP; - e.button = NE_MB_LEFT; - e.x = (int)p.x; - e.y = (int)p.y; - e.mods = translateModifiers(event.modifierFlags); - pushEvent(e); -} - -- (void)rightMouseDown:(NSEvent *)event { - NSPoint p = [self convertPoint:event.locationInWindow fromView:nil]; - NEEvent e = {0}; - e.kind = NE_MOUSE_DOWN; - e.button = NE_MB_RIGHT; - e.x = (int)p.x; - e.y = (int)p.y; - e.clicks = (int)event.clickCount; - e.mods = translateModifiers(event.modifierFlags); - pushEvent(e); -} - -- (void)rightMouseUp:(NSEvent *)event { - NSPoint p = [self convertPoint:event.locationInWindow fromView:nil]; - NEEvent e = {0}; - e.kind = NE_MOUSE_UP; - e.button = NE_MB_RIGHT; - e.x = (int)p.x; - e.y = (int)p.y; - e.mods = translateModifiers(event.modifierFlags); - pushEvent(e); -} - -- (void)otherMouseDown:(NSEvent *)event { - NSPoint p = [self convertPoint:event.locationInWindow fromView:nil]; - NEEvent e = {0}; - e.kind = NE_MOUSE_DOWN; - e.button = NE_MB_MIDDLE; - e.x = (int)p.x; - e.y = (int)p.y; - e.clicks = (int)event.clickCount; - e.mods = translateModifiers(event.modifierFlags); - pushEvent(e); -} - -- (void)otherMouseUp:(NSEvent *)event { - NSPoint p = [self convertPoint:event.locationInWindow fromView:nil]; - NEEvent e = {0}; - e.kind = NE_MOUSE_UP; - e.button = NE_MB_MIDDLE; - e.x = (int)p.x; - e.y = (int)p.y; - e.mods = translateModifiers(event.modifierFlags); - pushEvent(e); -} - -static int currentButtons(NSEvent *event) { - NSUInteger pressed = [NSEvent pressedMouseButtons]; - int b = 0; - if (pressed & (1 << 0)) b |= 1; /* left */ - if (pressed & (1 << 1)) b |= 2; /* right */ - if (pressed & (1 << 2)) b |= 4; /* middle */ - return b; -} - -- (void)mouseMoved:(NSEvent *)event { - NSPoint p = [self convertPoint:event.locationInWindow fromView:nil]; - NEEvent e = {0}; - e.kind = NE_MOUSE_MOVE; - e.x = (int)p.x; - e.y = (int)p.y; - e.xrel = (int)event.deltaX; - e.yrel = (int)event.deltaY; - e.buttons = currentButtons(event); - e.mods = translateModifiers(event.modifierFlags); - pushEvent(e); -} - -- (void)mouseDragged:(NSEvent *)event { - [self mouseMoved:event]; -} - -- (void)rightMouseDragged:(NSEvent *)event { - [self mouseMoved:event]; -} - -- (void)otherMouseDragged:(NSEvent *)event { - [self mouseMoved:event]; -} - -- (void)scrollWheel:(NSEvent *)event { - NEEvent e = {0}; - e.kind = NE_MOUSE_WHEEL; - if (event.hasPreciseScrollingDeltas) { - /* Trackpad: pixel-level deltas, divide down to line-level. - Accumulate fractional remainder so slow swipes still register. */ - static double accumX = 0, accumY = 0; - accumX += event.scrollingDeltaX; - accumY += event.scrollingDeltaY; - e.x = (int)(accumX / 16.0); - e.y = (int)(accumY / 16.0); - accumX -= e.x * 16.0; - accumY -= e.y * 16.0; - if (e.x == 0 && e.y == 0) return; /* sub-line movement, wait */ - } else { - /* Discrete mouse wheel: already line-based */ - e.x = (int)event.scrollingDeltaX; - e.y = (int)event.scrollingDeltaY; - } - e.mods = translateModifiers(event.modifierFlags); - pushEvent(e); -} - -/* ---- NSTextInputClient (for text input / IME) ---- */ - -- (void)insertText:(id)string replacementRange:(NSRange)replacementRange { - NSString *s = ([string isKindOfClass:[NSAttributedString class]]) - ? [string string] : string; - const char *utf8 = [s UTF8String]; - if (!utf8) return; - - /* Send each character as a separate text input event */ - NSUInteger len = strlen(utf8); - NSUInteger i = 0; - while (i < len) { - NEEvent e = {0}; - e.kind = NE_TEXT_INPUT; - /* Copy one UTF-8 codepoint (1-4 bytes) */ - unsigned char c = (unsigned char)utf8[i]; - int cpLen = 1; - if (c >= 0xC0) cpLen = 2; - if (c >= 0xE0) cpLen = 3; - if (c >= 0xF0) cpLen = 4; - for (int j = 0; j < cpLen && j < 4 && (i + j) < len; j++) - e.text[j] = utf8[i + j]; - pushEvent(e); - i += cpLen; - } -} - -- (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange - replacementRange:(NSRange)replacementRange { - self.hasMarkedText = ([(NSString *)string length] > 0); -} - -- (void)unmarkText { self.hasMarkedText = NO; } -- (BOOL)hasMarkedText { return _hasMarkedText; } -- (NSRange)markedRange { return NSMakeRange(NSNotFound, 0); } -- (NSRange)selectedRange { return NSMakeRange(0, 0); } -- (NSRect)firstRectForCharacterRange:(NSRange)range - actualRange:(NSRangePointer)actual { - return NSMakeRect(0, 0, 0, 0); -} -- (NSUInteger)characterIndexForPoint:(NSPoint)point { return NSNotFound; } -- (NSArray *)validAttributesForMarkedText { return @[]; } -- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range - actualRange:(NSRangePointer)actual { - return nil; -} - -@end - -/* ---- Window delegate -------------------------------------------------- */ - -@interface NimEditWindowDelegate : NSObject -@end - -@implementation NimEditWindowDelegate - -- (BOOL)windowShouldClose:(NSWindow *)sender { - NEEvent e = {0}; - e.kind = NE_WINDOW_CLOSE; - pushEvent(e); - return NO; /* let the app decide */ -} - -- (void)windowDidResize:(NSNotification *)n { - NSWindow *w = n.object; - NSRect frame = [[w contentView] frame]; - CGFloat scale = [w backingScaleFactor]; - ensureBacking((int)frame.size.width, (int)frame.size.height, scale); - - NEEvent e = {0}; - e.kind = NE_WINDOW_RESIZE; - e.x = (int)frame.size.width; - e.y = (int)frame.size.height; - pushEvent(e); -} - -- (void)windowDidBecomeKey:(NSNotification *)n { - NEEvent e = {0}; - e.kind = NE_WINDOW_FOCUS_GAINED; - pushEvent(e); -} - -- (void)windowDidResignKey:(NSNotification *)n { - NEEvent e = {0}; - e.kind = NE_WINDOW_FOCUS_LOST; - pushEvent(e); -} - -@end - -/* ---- Window management ------------------------------------------------ */ - -static NSWindow *mainWindow = nil; -static NimEditView *mainView = nil; -static NimEditWindowDelegate *winDelegate = nil; - -void cocoa_createWindow(int w, int h, int *outW, int *outH, - int *outScaleX, int *outScaleY) { - @autoreleasepool { - /* Initialize timing */ - mach_timebase_info(&timebaseInfo); - startTime = mach_absolute_time(); - - /* Set up NSApplication */ - [NSApplication sharedApplication]; - [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; - - /* Create menu bar (minimal: just app menu with Quit) */ - NSMenu *menubar = [[NSMenu alloc] init]; - NSMenuItem *appMenuItem = [[NSMenuItem alloc] init]; - [menubar addItem:appMenuItem]; - [NSApp setMainMenu:menubar]; - - NSMenu *appMenu = [[NSMenu alloc] init]; - NSMenuItem *quitItem = [[NSMenuItem alloc] - initWithTitle:@"Quit NimEdit" - action:@selector(terminate:) - keyEquivalent:@"q"]; - [appMenu addItem:quitItem]; - [appMenuItem setSubmenu:appMenu]; - - /* Create window */ - NSRect rect = NSMakeRect(100, 100, w, h); - NSUInteger style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | - NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; - mainWindow = [[NSWindow alloc] initWithContentRect:rect - styleMask:style - backing:NSBackingStoreBuffered - defer:NO]; - [mainWindow setTitle:@"NimEdit"]; - [mainWindow setAcceptsMouseMovedEvents:YES]; - - /* Create view */ - mainView = [[NimEditView alloc] initWithFrame:rect]; - [mainWindow setContentView:mainView]; - [mainWindow makeFirstResponder:mainView]; - - /* Window delegate */ - winDelegate = [[NimEditWindowDelegate alloc] init]; - [mainWindow setDelegate:winDelegate]; - - /* Show */ - [mainWindow makeKeyAndOrderFront:nil]; - [NSApp activateIgnoringOtherApps:YES]; - - /* Finish launch so events flow */ - [NSApp finishLaunching]; - - /* Set up backing bitmap */ - CGFloat scale = [mainWindow backingScaleFactor]; - NSRect contentFrame = [mainView frame]; - ensureBacking((int)contentFrame.size.width, (int)contentFrame.size.height, scale); - - *outW = (int)contentFrame.size.width; - *outH = (int)contentFrame.size.height; - *outScaleX = (int)scale; - *outScaleY = (int)scale; - } -} - -void cocoa_refresh(void) { - @autoreleasepool { - [mainView setNeedsDisplay:YES]; - [mainView displayIfNeeded]; - } -} - -void cocoa_setWindowTitle(const char *title) { - @autoreleasepool { - [mainWindow setTitle:[NSString stringWithUTF8String:title]]; - } -} - -void cocoa_setCursor(int kind) { - @autoreleasepool { - NSCursor *cur; - switch (kind) { - case 2: cur = [NSCursor IBeamCursor]; break; /* curIbeam */ - case 3: cur = [NSCursor arrowCursor]; break; /* curWait (no wait cursor, use arrow) */ - case 4: cur = [NSCursor crosshairCursor]; break; /* curCrosshair */ - case 5: cur = [NSCursor pointingHandCursor]; break; /* curHand */ - case 6: cur = [NSCursor resizeUpDownCursor]; break; /* curSizeNS */ - case 7: cur = [NSCursor resizeLeftRightCursor]; break; /* curSizeWE */ - default: cur = [NSCursor arrowCursor]; break; - } - [cur set]; - } -} - -void cocoa_startTextInput(void) { - /* NSTextInputClient is always active on our view */ -} - -void cocoa_quitRequest(void) { - NEEvent e = {0}; - e.kind = NE_QUIT; - pushEvent(e); -} diff --git a/app/cocoa_driver.nim b/app/cocoa_driver.nim deleted file mode 100644 index bf968cf..0000000 --- a/app/cocoa_driver.nim +++ /dev/null @@ -1,280 +0,0 @@ -# Cocoa/AppKit backend driver for macOS. -# Sets all hooks from core/input and core/screen. -# -# Build: nim c -d:cocoa app/nimedit.nim -# Requires macOS 10.15+ (Catalina). No external dependencies. - -{.compile("cocoa_backend.m", "-fobjc-arc").} -{.passL: "-framework Cocoa -framework CoreText -framework CoreGraphics -framework QuartzCore".} - -{.emit: """ -typedef struct { - int kind; - int key; - int mods; - char text[4]; - int x, y; - int xrel, yrel; - int button; - int buttons; - int clicks; -} NEEvent; -""".} - -import basetypes, input, screen - -# --- C bindings to cocoa_backend.m --- -# All procs use {.importc.} with explicit C names to avoid Nim's -# identifier normalisation colliding with the Nim wrapper procs. - -type - NEEvent {.importc, nodecl.} = object - kind: cint - key: cint - mods: cint - text: array[4, char] - x, y: cint - xrel, yrel: cint - button: cint - buttons: cint - clicks: cint - -# Event kinds (must match cocoa_backend.m) -const - neNone = 0.cint - neKeyDown = 1.cint - neKeyUp = 2.cint - neTextInput = 3.cint - neMouseDown = 4.cint - neMouseUp = 5.cint - neMouseMove = 6.cint - neMouseWheel = 7.cint - neWindowResize = 8.cint - neWindowClose = 9.cint - neWindowFocusGained = 10.cint - neWindowFocusLost = 11.cint - neQuit = 12.cint - - neModShift = 1.cint - neModCtrl = 2.cint - neModAlt = 4.cint - neModGui = 8.cint - -proc cCreateWindow(w, h: cint; outW, outH, outScaleX, outScaleY: ptr cint) - {.importc: "cocoa_createWindow", cdecl.} -proc cRefresh() {.importc: "cocoa_refresh", cdecl.} -proc cPollEvent(ev: ptr NEEvent): cint {.importc: "cocoa_pollEvent", cdecl.} -proc cWaitEvent(ev: ptr NEEvent; timeoutMs: cint): cint {.importc: "cocoa_waitEvent", cdecl.} - -proc cOpenFont(path: cstring; size: cint; - outAsc, outDesc, outLH: ptr cint): cint {.importc: "cocoa_openFont", cdecl.} -proc cCloseFont(handle: cint) {.importc: "cocoa_closeFont", cdecl.} -proc cGetFontMetrics(handle: cint; asc, desc, lh: ptr cint) {.importc: "cocoa_getFontMetrics", cdecl.} -proc cMeasureText(handle: cint; text: cstring; - outW, outH: ptr cint) {.importc: "cocoa_measureText", cdecl.} -proc cDrawText(handle: cint; x, y: cint; text: cstring; - fgR, fgG, fgB, fgA: cint; - bgR, bgG, bgB, bgA: cint; - outW, outH: ptr cint) {.importc: "cocoa_drawText", cdecl.} - -proc cFillRect(x, y, w, h: cint; r, g, b, a: cint) {.importc: "cocoa_fillRect", cdecl.} -proc cDrawLine(x1, y1, x2, y2: cint; r, g, b, a: cint) {.importc: "cocoa_drawLine", cdecl.} -proc cDrawPoint(x, y: cint; r, g, b, a: cint) {.importc: "cocoa_drawPoint", cdecl.} - -proc cSetClipRect(x, y, w, h: cint) {.importc: "cocoa_setClipRect", cdecl.} -proc cSaveState() {.importc: "cocoa_saveState", cdecl.} -proc cRestoreState() {.importc: "cocoa_restoreState", cdecl.} - -proc cGetClipboardText(): cstring {.importc: "cocoa_getClipboardText", cdecl.} -proc cPutClipboardText(text: cstring) {.importc: "cocoa_putClipboardText", cdecl.} - -proc cGetModState(): cint {.importc: "cocoa_getModState", cdecl.} -proc cGetTicks(): uint32 {.importc: "cocoa_getTicks", cdecl.} -proc cDelay(ms: uint32) {.importc: "cocoa_delay", cdecl.} - -proc cSetCursor(kind: cint) {.importc: "cocoa_setCursor", cdecl.} -proc cSetWindowTitle(title: cstring) {.importc: "cocoa_setWindowTitle", cdecl.} -proc cStartTextInput() {.importc: "cocoa_startTextInput", cdecl.} -proc cQuitRequest() {.importc: "cocoa_quitRequest", cdecl.} - -# --- Relay implementations --- - -proc cocoaCreateWindow(layout: var ScreenLayout) = - var w, h, sx, sy: cint - cCreateWindow(layout.width.cint, layout.height.cint, - addr w, addr h, addr sx, addr sy) - layout.width = w - layout.height = h - layout.scaleX = sx - layout.scaleY = sy - -proc cocoaRefresh() = cRefresh() -proc cocoaSaveState() = cSaveState() -proc cocoaRestoreState() = cRestoreState() - -proc cocoaSetClipRect(r: Rect) = - cSetClipRect(r.x.cint, r.y.cint, r.w.cint, r.h.cint) - -proc cocoaOpenFont(path: string; size: int; - metrics: var FontMetrics): Font = - var asc, desc, lh: cint - let handle = cOpenFont(cstring(path), size.cint, - addr asc, addr desc, addr lh) - if handle == 0: return Font(0) - metrics.ascent = asc - metrics.descent = desc - metrics.lineHeight = lh - result = Font(handle) - -proc cocoaCloseFont(f: Font) = - cCloseFont(f.int.cint) - -proc cocoaMeasureText(f: Font; text: string): TextExtent = - if text == "": return TextExtent() - var w, h: cint - cMeasureText(f.int.cint, cstring(text), addr w, addr h) - result = TextExtent(w: w, h: h) - -proc cocoaDrawText(f: Font; x, y: int; text: string; - fg, bg: Color): TextExtent = - if text == "": return TextExtent() - var w, h: cint - cDrawText(f.int.cint, x.cint, y.cint, cstring(text), - fg.r.cint, fg.g.cint, fg.b.cint, fg.a.cint, - bg.r.cint, bg.g.cint, bg.b.cint, bg.a.cint, - addr w, addr h) - result = TextExtent(w: w, h: h) - -proc cocoaGetFontMetrics(f: Font): FontMetrics = - var asc, desc, lh: cint - cGetFontMetrics(f.int.cint, addr asc, addr desc, addr lh) - result = FontMetrics(ascent: asc, descent: desc, lineHeight: lh) - -proc cocoaFillRect(r: Rect; color: Color) = - cFillRect(r.x.cint, r.y.cint, r.w.cint, r.h.cint, - color.r.cint, color.g.cint, color.b.cint, color.a.cint) - -proc cocoaDrawLine(x1, y1, x2, y2: int; color: Color) = - cDrawLine(x1.cint, y1.cint, x2.cint, y2.cint, - color.r.cint, color.g.cint, color.b.cint, color.a.cint) - -proc cocoaDrawPoint(x, y: int; color: Color) = - cDrawPoint(x.cint, y.cint, - color.r.cint, color.g.cint, color.b.cint, color.a.cint) - -proc cocoaSetCursor(c: CursorKind) = - cSetCursor(ord(c).cint) - -proc cocoaSetWindowTitle(title: string) = - cSetWindowTitle(cstring(title)) - -# --- Event translation --- - -proc translateNEEvent(ne: NEEvent; e: var input.Event) = - e = input.Event(kind: NoEvent) - # Translate modifiers - if (ne.mods and neModShift) != 0: e.mods.incl ShiftPressed - if (ne.mods and neModCtrl) != 0: e.mods.incl CtrlPressed - if (ne.mods and neModAlt) != 0: e.mods.incl AltPressed - if (ne.mods and neModGui) != 0: e.mods.incl GuiPressed - - case ne.kind - of neQuit: - e.kind = QuitEvent - of neWindowResize: - e.kind = WindowResizeEvent - e.x = ne.x - e.y = ne.y - of neWindowClose: - e.kind = WindowCloseEvent - of neWindowFocusGained: - e.kind = WindowFocusGainedEvent - of neWindowFocusLost: - e.kind = WindowFocusLostEvent - of neKeyDown: - e.kind = KeyDownEvent - e.key = KeyCode(ne.key) - of neKeyUp: - e.kind = KeyUpEvent - e.key = KeyCode(ne.key) - of neTextInput: - e.kind = TextInputEvent - for i in 0..3: - e.text[i] = ne.text[i] - of neMouseDown: - e.kind = MouseDownEvent - e.x = ne.x - e.y = ne.y - e.clicks = ne.clicks - case ne.button - of 0: e.button = LeftButton - of 1: e.button = RightButton - of 2: e.button = MiddleButton - else: e.button = LeftButton - of neMouseUp: - e.kind = MouseUpEvent - e.x = ne.x - e.y = ne.y - case ne.button - of 0: e.button = LeftButton - of 1: e.button = RightButton - of 2: e.button = MiddleButton - else: e.button = LeftButton - of neMouseMove: - e.kind = MouseMoveEvent - e.x = ne.x - e.y = ne.y - of neMouseWheel: - e.kind = MouseWheelEvent - e.x = ne.x - e.y = ne.y - else: discard - -proc cocoaPollEvent(e: var input.Event; flags: set[InputFlag]): bool = - var ne: NEEvent - if cPollEvent(addr ne) == 0: - return false - translateNEEvent(ne, e) - result = true - -proc cocoaWaitEvent(e: var input.Event; timeoutMs: int; - flags: set[InputFlag]): bool = - var ne: NEEvent - if cWaitEvent(addr ne, timeoutMs.cint) == 0: - return false - translateNEEvent(ne, e) - result = true - -proc cocoaGetClipboardText(): string = - let t = cGetClipboardText() - if t != nil: result = $t - else: result = "" - -proc cocoaPutClipboardText(text: string) = - cPutClipboardText(cstring(text)) - -proc cocoaGetTicks(): int = cGetTicks().int -proc cocoaDelay(ms: int) = cDelay(ms.uint32) -proc cocoaQuitRequest() = cQuitRequest() - -# --- Init --- - -proc initCocoaDriver*() = - windowRelays = WindowRelays( - createWindow: cocoaCreateWindow, refresh: cocoaRefresh, - saveState: cocoaSaveState, restoreState: cocoaRestoreState, - setClipRect: cocoaSetClipRect, setCursor: cocoaSetCursor, - setWindowTitle: cocoaSetWindowTitle) - fontRelays = FontRelays( - openFont: cocoaOpenFont, closeFont: cocoaCloseFont, - getFontMetrics: cocoaGetFontMetrics, measureText: cocoaMeasureText, - drawText: cocoaDrawText) - drawRelays = DrawRelays( - fillRect: cocoaFillRect, drawLine: cocoaDrawLine, - drawPoint: cocoaDrawPoint) - inputRelays = InputRelays( - pollEvent: cocoaPollEvent, waitEvent: cocoaWaitEvent, - getTicks: cocoaGetTicks, delay: cocoaDelay, - quitRequest: cocoaQuitRequest) - clipboardRelays = ClipboardRelays( - getText: cocoaGetClipboardText, putText: cocoaPutClipboardText) diff --git a/app/gtk4_driver.nim b/app/gtk4_driver.nim deleted file mode 100644 index 531d8f0..0000000 --- a/app/gtk4_driver.nim +++ /dev/null @@ -1,763 +0,0 @@ -# GTK 4 backend: thin C bindings. Sets hooks from core/input and core/screen. -# -# Build (from repo root, with nim.cfg): -# nim c -d:gtk4 app/nimedit.nim -# Requires dev packages: gtk4, pangocairo, pangoft2, fontconfig (pkg-config names). -# Uses gtk_event_controller_key_set_im_context (GTK 4.2+). GtkDrawingArea "resize" needs GTK 4.6+. -# If pkg-config is missing, set compile-time flags manually, e.g.: -# nim c -d:gtk4 --passC:"$(pkg-config --cflags gtk4)" --passL:"$(pkg-config --libs gtk4 pangocairo pangoft2 fontconfig)" app/nimedit.nim -# MouseMoveEvent does not set `buttons` (held buttons) yet; SDL drivers do. - -{.emit: """ -#include -#include -#include -#include -#include -#include -#include - -static inline FcPattern *nimedit_fc_font_match(FcPattern *p, void *result_out) { - return FcFontMatch(NULL, p, (FcResult *)result_out); -} -""".} - -import std/unicode -import basetypes, input, screen - -const - gtkCflags {.strdefine.} = staticExec("pkg-config --cflags gtk4 pangocairo pangoft2 fontconfig glib-2.0 2>/dev/null").strip - gtkLibs {.strdefine.} = staticExec("pkg-config --libs gtk4 pangocairo pangoft2 fontconfig glib-2.0 2>/dev/null").strip - -const gtkFallbackCflags = - "-I/usr/include/gtk-4.0 -I/usr/include/glib-2.0 " & - "-I/usr/lib/x86_64-linux-gnu/glib-2.0/include -I/usr/lib/aarch64-linux-gnu/glib-2.0/include " & - "-I/usr/include/pango-1.0 -I/usr/include/harfbuzz -I/usr/include/freetype2 " & - "-I/usr/include/libpng16 -I/usr/include/libmount -I/usr/include/blkid " & - "-I/usr/include/fribidi -I/usr/include/cairo -I/usr/include/pixman-1 " & - "-I/usr/include/gdk-pixbuf-2.0 -I/usr/include/webp -I/usr/include/graphene-1.0 " & - "-I/usr/include/fontconfig -pthread" - -const gtkFallbackLibs = - "-lgtk-4 -lgdk-4 -lpangocairo-1.0 -lpangoft2-1.0 -lpango-1.0 -lgobject-2.0 " & - "-lglib-2.0 -lgio-2.0 -lgmodule-2.0 -lfontconfig -lfreetype -lcairo -lharfbuzz " & - "-lgraphene-1.0 -lfribidi -lgdk_pixbuf-2.0 -lXi -lX11 -ldl -lm" - -when gtkCflags.len > 0: - {.passC: gtkCflags.} -else: - {.warning: "gtk4_driver: pkg-config --cflags failed; using fallback -I paths".} - {.passC: gtkFallbackCflags.} - -when gtkLibs.len > 0: - {.passL: gtkLibs.} -else: - {.warning: "gtk4_driver: pkg-config --libs failed; using fallback -l flags".} - {.passL: gtkFallbackLibs.} - -type - gboolean = cint - guint = cuint - gint = cint - gulong = culong - gdouble = cdouble - GCallback = pointer - GConnectFlags = cint - -type - GtkDrawingArea {.importc, header: "", incompleteStruct.} = object - cairo_t {.importc, header: "", incompleteStruct.} = object - GError {.importc, header: "", incompleteStruct.} = object - GObject {.importc, header: "", incompleteStruct.} = object - GAsyncResult {.importc, header: "", incompleteStruct.} = object - -const - G_FALSE = gboolean(0) - G_TRUE = gboolean(1) - GDK_BUTTON_MIDDLE = 2'u32 - GDK_BUTTON_SECONDARY = 3'u32 - PANGO_SCALE = 1024 - GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES = 3 - FcMatchPattern = 0 - CAIRO_FORMAT_ARGB32 = 0 ## cairo_format_t; pixel buffer must be flushed before another cairo_t reads it - -const - FC_FILE = "file" - -# --- Minimal GObject / GLib / GTK / Gdk / Cairo / Pango / Fontconfig --- - -proc g_signal_connect_data( - inst: pointer; signal: cstring; handler: GCallback; - data, destroyData: pointer; flags: GConnectFlags -): gulong {.importc, nodecl, cdecl.} - -proc g_object_unref(o: pointer) {.importc, nodecl, cdecl.} -proc g_error_free(err: ptr GError) {.importc, nodecl, cdecl.} -proc g_free(p: pointer) {.importc, nodecl, cdecl.} -proc g_get_monotonic_time(): int64 {.importc, nodecl, cdecl.} -proc g_usleep(micros: culong) {.importc, nodecl, cdecl.} - -proc g_main_context_iteration(ctx: pointer; mayBlock: gboolean): gboolean {.importc, nodecl, cdecl.} - -proc gtk_init_check(): gboolean {.importc, nodecl, cdecl.} -proc gtk_window_new(): pointer {.importc, nodecl, cdecl.} -proc gtk_window_set_title(win: pointer; title: cstring) {.importc, nodecl, cdecl.} -proc gtk_window_set_default_size(win: pointer; w, h: gint) {.importc, nodecl, cdecl.} -proc gtk_window_set_child(win: pointer; child: pointer) {.importc, nodecl, cdecl.} -proc gtk_window_destroy(win: pointer) {.importc, nodecl, cdecl.} -proc gtk_window_present(win: pointer) {.importc, nodecl, cdecl.} -proc gtk_widget_queue_draw(w: pointer) {.importc, nodecl, cdecl.} -proc gtk_widget_grab_focus(w: pointer) {.importc, nodecl, cdecl.} -proc gtk_widget_set_cursor(w, cursor: pointer) {.importc, nodecl, cdecl.} -proc gtk_widget_get_width(w: pointer): gint {.importc, nodecl, cdecl.} -proc gtk_widget_get_height(w: pointer): gint {.importc, nodecl, cdecl.} -proc gtk_widget_set_focusable(w: pointer; focusable: gboolean) {.importc, nodecl, cdecl.} -proc gtk_widget_set_hexpand(w: pointer; expand: gboolean) {.importc, nodecl, cdecl.} -proc gtk_widget_set_vexpand(w: pointer; expand: gboolean) {.importc, nodecl, cdecl.} -proc gtk_widget_add_controller(w, ctrl: pointer) {.importc, nodecl, cdecl.} - -proc gtk_drawing_area_new(): pointer {.importc, nodecl, cdecl.} -proc gtk_drawing_area_set_content_width(area: ptr GtkDrawingArea; width: gint) {.importc, nodecl, cdecl.} -proc gtk_drawing_area_set_content_height(area: ptr GtkDrawingArea; height: gint) {.importc, nodecl, cdecl.} -proc gtk_drawing_area_get_content_width(area: ptr GtkDrawingArea): gint {.importc, nodecl, cdecl.} -proc gtk_drawing_area_get_content_height(area: ptr GtkDrawingArea): gint {.importc, nodecl, cdecl.} -proc gtk_drawing_area_set_draw_func( - area: pointer; - drawFunc: proc (area: ptr GtkDrawingArea; cr: ptr cairo_t; w, h: gint; - data: pointer) {.cdecl.}; - data: pointer; destroyNotify: pointer -) {.importc, nodecl, cdecl.} - -proc gtk_event_controller_key_new(): pointer {.importc, nodecl, cdecl.} -proc gtk_event_controller_key_set_im_context(KeyCtrl, im: pointer) {.importc, nodecl, cdecl.} -proc gtk_event_controller_get_current_event(ctrl: pointer): pointer {.importc, nodecl, cdecl.} -proc gtk_event_controller_motion_new(): pointer {.importc, nodecl, cdecl.} -proc gtk_event_controller_scroll_new(flags: guint): pointer {.importc, nodecl, cdecl.} -proc gtk_event_controller_focus_new(): pointer {.importc, nodecl, cdecl.} -proc gtk_gesture_click_new(): pointer {.importc, nodecl, cdecl.} -proc gtk_gesture_single_set_button(gesture: pointer; button: gint) {.importc, nodecl, cdecl.} -proc gtk_gesture_single_get_current_button(gesture: pointer): guint {.importc, nodecl, cdecl.} - -proc gtk_im_multicontext_new(): pointer {.importc, nodecl, cdecl.} -proc gtk_im_context_set_client_widget(im, widget: pointer) {.importc, nodecl, cdecl.} -proc gtk_im_context_focus_in(im: pointer) {.importc, nodecl, cdecl.} - -proc gdk_event_get_modifier_state(ev: pointer): guint {.importc, nodecl, cdecl.} -proc gdk_keyval_to_lower(k: guint): guint {.importc, nodecl, cdecl.} -proc gdk_cursor_new_from_name(name: cstring; fallback: pointer): pointer {.importc, nodecl, cdecl.} -proc gdk_display_get_default(): pointer {.importc, nodecl, cdecl.} -proc gdk_display_get_clipboard(disp: pointer): pointer {.importc, nodecl, cdecl.} -proc gdk_clipboard_set_text(clip: pointer; text: cstring) {.importc, nodecl, cdecl.} - -proc gdk_clipboard_read_text_async( - clip: pointer; cancellable: pointer; - callback: proc (sourceObj: ptr GObject; res: ptr GAsyncResult; data: pointer) {.cdecl.}; - data: pointer -) {.importc, nodecl, cdecl.} - -proc gdk_clipboard_read_text_finish(clip: pointer; res: pointer; err: ptr ptr GError): cstring {.importc, nodecl, cdecl.} - -proc cairo_create(surface: pointer): pointer {.importc, nodecl, cdecl.} -proc cairo_destroy(cr: pointer) {.importc, nodecl, cdecl.} -proc cairo_surface_destroy(surf: pointer) {.importc, nodecl, cdecl.} -proc cairo_surface_flush(surf: pointer) {.importc, nodecl, cdecl.} -proc cairo_image_surface_create(fmt, w, h: gint): pointer {.importc, nodecl, cdecl.} -proc cairo_save(cr: pointer) {.importc, nodecl, cdecl.} -proc cairo_restore(cr: pointer) {.importc, nodecl, cdecl.} -proc cairo_reset_clip(cr: pointer) {.importc, nodecl, cdecl.} -proc cairo_rectangle(cr: pointer; x, y, w, h: gdouble) {.importc, nodecl, cdecl.} -proc cairo_clip(cr: pointer) {.importc, nodecl, cdecl.} -proc cairo_set_source_rgba(cr: pointer; r, g, b, a: gdouble) {.importc, nodecl, cdecl.} -proc cairo_set_source_surface(cr, surf: pointer; x, y: gdouble) {.importc, nodecl, cdecl.} -proc cairo_paint(cr: pointer) {.importc, nodecl, cdecl.} -proc cairo_fill(cr: pointer) {.importc, nodecl, cdecl.} -proc cairo_stroke(cr: pointer) {.importc, nodecl, cdecl.} -proc cairo_move_to(cr: pointer; x, y: gdouble) {.importc, nodecl, cdecl.} -proc cairo_line_to(cr: pointer; x, y: gdouble) {.importc, nodecl, cdecl.} -proc cairo_set_line_width(cr: pointer; w: gdouble) {.importc, nodecl, cdecl.} -proc cairo_scale(cr: pointer; sx, sy: gdouble) {.importc, nodecl, cdecl.} - -proc pango_cairo_create_layout(cr: pointer): pointer {.importc, nodecl, cdecl.} -proc pango_cairo_update_layout(cr, layout: pointer) {.importc, nodecl, cdecl.} -proc pango_cairo_show_layout(cr, layout: pointer) {.importc, nodecl, cdecl.} -proc g_object_unref_layout(o: pointer) {.importc: "g_object_unref", nodecl, cdecl.} - -proc pango_layout_set_text(layout: pointer; text: cstring; len: gint) {.importc, nodecl, cdecl.} -proc pango_layout_set_font_description(layout, desc: pointer) {.importc, nodecl, cdecl.} -proc pango_layout_get_pixel_size(layout: pointer; w, h: ptr gint) {.importc, nodecl, cdecl.} - -proc pango_font_description_free(desc: pointer) {.importc, nodecl, cdecl.} -proc pango_font_description_set_absolute_size(desc: pointer; size: gint) {.importc, nodecl, cdecl.} - -proc pango_fc_font_description_from_pattern(pat: pointer; includeSize: gboolean): pointer {.importc, nodecl, cdecl.} - -proc pango_font_metrics_unref(m: pointer) {.importc, nodecl, cdecl.} -proc pango_layout_get_context(layout: pointer): pointer {.importc, nodecl, cdecl.} -proc pango_context_get_font_map(ctx: pointer): pointer {.importc, nodecl, cdecl.} -proc pango_font_map_load_font(map, ctx, desc: pointer): pointer {.importc, nodecl, cdecl.} -proc pango_font_get_metrics(font, lang: pointer): pointer {.importc, nodecl, cdecl.} -proc pango_font_metrics_get_ascent(m: pointer): gint {.importc, nodecl, cdecl.} -proc pango_font_metrics_get_descent(m: pointer): gint {.importc, nodecl, cdecl.} -proc pango_font_metrics_get_height(m: pointer): gint {.importc, nodecl, cdecl.} - -proc FcInit(): gboolean {.importc, nodecl, cdecl.} -proc FcPatternCreate(): pointer {.importc, nodecl, cdecl.} -proc FcPatternDestroy(p: pointer) {.importc, nodecl, cdecl.} -proc FcPatternAddString(p: pointer; obj: cstring; s: cstring): gboolean {.importc, nodecl, cdecl.} -proc FcConfigSubstitute(cfg, p: pointer; kind: gint): gboolean {.importc, nodecl, cdecl.} -proc FcDefaultSubstitute(p: pointer) {.importc, nodecl, cdecl.} -proc nimedit_fc_font_match(p, resultOut: pointer): pointer {.importc: "nimedit_fc_font_match", nodecl, cdecl.} - -# --- Font slots --- - -type - FontSlot = object - desc: pointer ## PangoFontDescription* - metrics: FontMetrics - -var fonts: seq[FontSlot] - -proc getDesc(f: Font): pointer = - let idx = f.int - 1 - if idx >= 0 and idx < fonts.len: fonts[idx].desc - else: nil - -# --- GTK state --- - -var - win: pointer - drawingArea: pointer - backingSurf: pointer - backingCr: pointer - backingW, backingH: int - imContext: pointer - eventQueue: seq[Event] - modState: set[Modifier] - clipboardBuf: string - clipboardLoop: pointer ## GMainLoop* — set during sync read - -proc pushEvent(e: Event) = - eventQueue.add e - -proc gdkModsToSet(st: guint): set[Modifier] = - if (st and (1'u32 shl 0)) != 0: result.incl ShiftPressed - if (st and (1'u32 shl 2)) != 0: result.incl CtrlPressed - if (st and (1'u32 shl 3)) != 0: result.incl AltPressed - if (st and (1'u32 shl 26)) != 0: result.incl GuiPressed - if (st and (1'u32 shl 28)) != 0: result.incl GuiPressed - -proc syncModsFromController(ctrl: pointer) = - let ev = gtk_event_controller_get_current_event(ctrl) - if ev != nil: - modState = gdkModsToSet(gdk_event_get_modifier_state(ev)) - -proc translateKeyval(kv: guint): KeyCode = - let k = gdk_keyval_to_lower(kv) - template ck(c: char, key: KeyCode): untyped = - if k == cast[guint](ord(c)): return key - ck('a', KeyA); ck('b', KeyB); ck('c', KeyC); ck('d', KeyD); ck('e', KeyE) - ck('f', KeyF); ck('g', KeyG); ck('h', KeyH); ck('i', KeyI); ck('j', KeyJ) - ck('k', KeyK); ck('l', KeyL); ck('m', KeyM); ck('n', KeyN); ck('o', KeyO) - ck('p', KeyP); ck('q', KeyQ); ck('r', KeyR); ck('s', KeyS); ck('t', KeyT) - ck('u', KeyU); ck('v', KeyV); ck('w', KeyW); ck('x', KeyX); ck('y', KeyY) - ck('z', KeyZ) - ck('0', Key0); ck('1', Key1); ck('2', Key2); ck('3', Key3); ck('4', Key4) - ck('5', Key5); ck('6', Key6); ck('7', Key7); ck('8', Key8); ck('9', Key9) - case k - of 0xff1b: KeyEsc - of 0xff09: KeyTab - of 0xff0d: KeyEnter - of 0x020: KeySpace - of 0xff08: KeyBackspace - of 0xffff: KeyDelete - of 0xff63: KeyInsert - of 0xff51: KeyLeft - of 0xff53: KeyRight - of 0xff52: KeyUp - of 0xff54: KeyDown - of 0xff55: KeyPageUp - of 0xff56: KeyPageDown - of 0xff50: KeyHome - of 0xff57: KeyEnd - of 0xffe5: KeyCapslock - of 0x02c: KeyComma - of 0x02e: KeyPeriod - of 0xffbe: KeyF1 - of 0xffbf: KeyF2 - of 0xffc0: KeyF3 - of 0xffc1: KeyF4 - of 0xffc2: KeyF5 - of 0xffc3: KeyF6 - of 0xffc4: KeyF7 - of 0xffc5: KeyF8 - of 0xffc6: KeyF9 - of 0xffc7: KeyF10 - of 0xffc8: KeyF11 - of 0xffc9: KeyF12 - else: KeyNone - -proc enqueueTextFromUtf8(s: string) = - for ch in s.toRunes: - var ev = Event(kind: TextInputEvent) - let u = toUtf8(ch) - for i in 0 ..< min(4, u.len): - ev.text[i] = u[i] - for i in u.len .. 3: - ev.text[i] = '\0' - pushEvent ev - -proc recreateBacking(w, h: int) = - ## GTK can emit resize with a transient 0 width or height; do not destroy a good buffer then bail. - if w <= 0 or h <= 0: - return - if backingCr != nil: - cairo_destroy(backingCr) - backingCr = nil - if backingSurf != nil: - cairo_surface_destroy(backingSurf) - backingSurf = nil - backingW = w - backingH = h - backingSurf = cairo_image_surface_create(gint(CAIRO_FORMAT_ARGB32), gint(w), gint(h)) - backingCr = cairo_create(backingSurf) - cairo_set_source_rgba(backingCr, 1, 1, 1, 1) - cairo_rectangle(backingCr, 0, 0, gdouble(w), gdouble(h)) - cairo_fill(backingCr) - cairo_surface_flush(backingSurf) - -proc ensureBackingCr() = - if backingCr == nil and win != nil and drawingArea != nil: - let w = gtk_widget_get_width(drawingArea).int - let h = gtk_widget_get_height(drawingArea).int - if w > 0 and h > 0: - recreateBacking(w, h) - -# --- Signal callbacks (cdecl) --- - -proc onCloseRequest(self: pointer; data: pointer): gboolean {.cdecl.} = - pushEvent(Event(kind: WindowCloseEvent)) - G_TRUE - -proc onResize(area: pointer; width, height: gint; data: pointer) {.cdecl.} = - recreateBacking(width.int, height.int) - pushEvent(Event(kind: WindowResizeEvent, x: width.int, y: height.int)) - -proc onDraw(area: ptr GtkDrawingArea; cr: ptr cairo_t; width, height: gint; - data: pointer) {.cdecl.} = - ## Blit only. Never call recreateBacking here: it clears the image surface and - ## drops NimEdit's pixels when the main loop skips redraws (events.len==0 && - ## doRedraw==false). Resize/`createWindow`/GtkDrawingArea::resize already size - ## the backing; if GTK reports a transient mismatch, scale the blit. - let w = width.int - let h = height.int - if w <= 0 or h <= 0 or backingSurf == nil: - return - cairo_surface_flush(backingSurf) - let crp = cast[pointer](cr) - cairo_save(crp) - if backingW != w or backingH != h: - cairo_scale(crp, gdouble(w) / gdouble(max(1, backingW)), - gdouble(h) / gdouble(max(1, backingH))) - cairo_set_source_surface(crp, backingSurf, 0, 0) - cairo_paint(crp) - cairo_restore(crp) - -proc onKeyPressed(ctrl: pointer; keyval, keycode: guint; state: guint; - data: pointer): gboolean {.cdecl.} = - modState = gdkModsToSet(state) - var ev = Event(kind: KeyDownEvent, key: translateKeyval(keyval), mods: modState) - pushEvent ev - G_FALSE - -proc onKeyReleased(ctrl: pointer; keyval, keycode: guint; state: guint; - data: pointer): gboolean {.cdecl.} = - modState = gdkModsToSet(state) - var ev = Event(kind: KeyUpEvent, key: translateKeyval(keyval), mods: modState) - pushEvent ev - G_FALSE - -proc onImCommit(ctx: pointer; str: cstring; data: pointer) {.cdecl.} = - if str != nil: - enqueueTextFromUtf8($str) - -proc onMotion(ctrl: pointer; x, y: gdouble; data: pointer) {.cdecl.} = - syncModsFromController(ctrl) - var ev = Event(kind: MouseMoveEvent, x: int(x), y: int(y), mods: modState) - pushEvent ev - -proc onScroll(ctrl: pointer; dx, dy: gdouble; data: pointer): gboolean {.cdecl.} = - syncModsFromController(ctrl) - pushEvent(Event(kind: MouseWheelEvent, x: int(-dx), y: int(-dy), mods: modState)) - G_TRUE - -proc onFocusEnter(ctrl: pointer; data: pointer) {.cdecl.} = - pushEvent(Event(kind: WindowFocusGainedEvent)) - -proc onFocusLeave(ctrl: pointer; data: pointer) {.cdecl.} = - pushEvent(Event(kind: WindowFocusLostEvent)) - -proc onClickPressed(gesture: pointer; nPress: gint; x, y: gdouble; data: pointer) {.cdecl.} = - let btn = gtk_gesture_single_get_current_button(gesture) - var b = LeftButton - if btn == GDK_BUTTON_SECONDARY: b = RightButton - elif btn == GDK_BUTTON_MIDDLE: b = MiddleButton - var ev = Event(kind: MouseDownEvent, x: int(x), y: int(y), button: b, - clicks: nPress.int) - pushEvent ev - -proc onClickReleased(gesture: pointer; nPress: gint; x, y: gdouble; data: pointer) {.cdecl.} = - let btn = gtk_gesture_single_get_current_button(gesture) - var b = LeftButton - if btn == GDK_BUTTON_SECONDARY: b = RightButton - elif btn == GDK_BUTTON_MIDDLE: b = MiddleButton - pushEvent(Event(kind: MouseUpEvent, x: int(x), y: int(y), button: b)) - -proc g_main_loop_new(ctx: pointer, isRunning: gboolean): pointer {.importc, nodecl, cdecl.} -proc g_main_loop_run(loop: pointer) {.importc, nodecl, cdecl.} -proc g_main_loop_quit(loop: pointer) {.importc, nodecl, cdecl.} -proc g_main_loop_unref(loop: pointer) {.importc, nodecl, cdecl.} - -proc clipboardReadCb(sourceObj: ptr GObject; res: ptr GAsyncResult; user: pointer) {.cdecl.} = - var err: ptr GError - let clip = cast[pointer](sourceObj) - let tres = cast[pointer](res) - let t = gdk_clipboard_read_text_finish(clip, tres, addr err) - clipboardBuf = if t != nil: $t else: "" - if t != nil: - g_free(cast[pointer](t)) - if err != nil: - g_error_free(err) - if clipboardLoop != nil: - g_main_loop_quit(clipboardLoop) - -proc readClipboardSync(): string = - let disp = gdk_display_get_default() - if disp == nil: return "" - let clip = gdk_display_get_clipboard(disp) - if clip == nil: return "" - clipboardBuf = "" - let loop = g_main_loop_new(nil, G_FALSE) - clipboardLoop = loop - gdk_clipboard_read_text_async(clip, nil, clipboardReadCb, nil) - g_main_loop_run(loop) - g_main_loop_unref(loop) - clipboardLoop = nil - result = clipboardBuf - -# --- Screen hooks --- - -proc gtkCreateWindow(layout: var ScreenLayout) = - if win != nil: - let da = cast[ptr GtkDrawingArea](drawingArea) - layout.width = max(1, gtk_drawing_area_get_content_width(da).int) - layout.height = max(1, gtk_drawing_area_get_content_height(da).int) - return - if gtk_init_check() == G_FALSE: - quit("GTK4 init failed") - win = gtk_window_new() - gtk_window_set_title(win, "NimEdit") - gtk_window_set_default_size(win, gint(layout.width), gint(layout.height)) - drawingArea = gtk_drawing_area_new() - gtk_window_set_child(win, drawingArea) - let da = cast[ptr GtkDrawingArea](drawingArea) - ## GtkDrawingArea content size defaults to 0; draw/blit is a no-op until set. - gtk_drawing_area_set_content_width(da, gint(layout.width)) - gtk_drawing_area_set_content_height(da, gint(layout.height)) - gtk_widget_set_hexpand(drawingArea, G_TRUE) - gtk_widget_set_vexpand(drawingArea, G_TRUE) - gtk_drawing_area_set_draw_func(drawingArea, onDraw, nil, nil) - discard g_signal_connect_data(drawingArea, "resize", - cast[GCallback](onResize), nil, nil, 0) - discard g_signal_connect_data(win, "close-request", - cast[GCallback](onCloseRequest), nil, nil, 0) - let keyc = gtk_event_controller_key_new() - gtk_widget_add_controller(drawingArea, keyc) - discard g_signal_connect_data(keyc, "key-pressed", - cast[GCallback](onKeyPressed), nil, nil, 0) - discard g_signal_connect_data(keyc, "key-released", - cast[GCallback](onKeyReleased), nil, nil, 0) - imContext = gtk_im_multicontext_new() - gtk_im_context_set_client_widget(imContext, drawingArea) - gtk_event_controller_key_set_im_context(keyc, imContext) - discard g_signal_connect_data(imContext, "commit", - cast[GCallback](onImCommit), nil, nil, 0) - let motion = gtk_event_controller_motion_new() - gtk_widget_add_controller(drawingArea, motion) - discard g_signal_connect_data(motion, "motion", - cast[GCallback](onMotion), nil, nil, 0) - let scroll = gtk_event_controller_scroll_new(GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES) - gtk_widget_add_controller(drawingArea, scroll) - discard g_signal_connect_data(scroll, "scroll", - cast[GCallback](onScroll), nil, nil, 0) - let focusc = gtk_event_controller_focus_new() - gtk_widget_add_controller(drawingArea, focusc) - discard g_signal_connect_data(focusc, "enter", - cast[GCallback](onFocusEnter), nil, nil, 0) - discard g_signal_connect_data(focusc, "leave", - cast[GCallback](onFocusLeave), nil, nil, 0) - let click = gtk_gesture_click_new() - gtk_gesture_single_set_button(click, 0) - gtk_widget_add_controller(drawingArea, click) - discard g_signal_connect_data(click, "pressed", - cast[GCallback](onClickPressed), nil, nil, 0) - discard g_signal_connect_data(click, "released", - cast[GCallback](onClickReleased), nil, nil, 0) - gtk_window_present(win) - var guard = 0 - while gtk_drawing_area_get_content_width(da) <= 0 and guard < 5000: - discard g_main_context_iteration(nil, G_FALSE) - inc guard - layout.width = max(1, gtk_drawing_area_get_content_width(da).int) - layout.height = max(1, gtk_drawing_area_get_content_height(da).int) - layout.scaleX = 1 - layout.scaleY = 1 - recreateBacking(layout.width, layout.height) - gtk_widget_set_focusable(drawingArea, G_TRUE) - gtk_widget_grab_focus(drawingArea) - gtk_im_context_focus_in(imContext) - -proc gtkRefresh() = - if backingSurf != nil: - cairo_surface_flush(backingSurf) - if drawingArea != nil: - gtk_widget_queue_draw(drawingArea) - -proc gtkSaveState() = discard -proc gtkRestoreState() = discard - -proc gtkSetClipRect(r: basetypes.Rect) = - ensureBackingCr() - if backingCr == nil: return - cairo_reset_clip(backingCr) - cairo_rectangle(backingCr, gdouble(r.x), gdouble(r.y), gdouble(r.w), gdouble(r.h)) - cairo_clip(backingCr) - -proc gtkOpenFont(path: string; size: int; metrics: var FontMetrics): Font = - discard FcInit() - var pat = FcPatternCreate() - if pat == nil: - return Font(0) - if FcPatternAddString(pat, FC_FILE, cstring(path)) == G_FALSE: - FcPatternDestroy(pat) - return Font(0) - discard FcConfigSubstitute(nil, pat, FcMatchPattern) - FcDefaultSubstitute(pat) - var fcRes: cint - let matched = nimedit_fc_font_match(pat, addr fcRes) - FcPatternDestroy(pat) - if matched == nil: - return Font(0) - let desc = pango_fc_font_description_from_pattern(matched, G_TRUE) - FcPatternDestroy(matched) - if desc == nil: - return Font(0) - pango_font_description_set_absolute_size(desc, gint(size * PANGO_SCALE)) - let surf = cairo_image_surface_create(0, 8, 8) - let cr = cairo_create(surf) - let layout = pango_cairo_create_layout(cr) - pango_layout_set_font_description(layout, desc) - pango_layout_set_text(layout, "Mg", -1) - let pctx = pango_layout_get_context(layout) - let fmap = pango_context_get_font_map(pctx) - let font = pango_font_map_load_font(fmap, pctx, desc) - if font != nil: - let m = pango_font_get_metrics(font, nil) - if m != nil: - metrics.ascent = pango_font_metrics_get_ascent(m) div PANGO_SCALE - metrics.descent = pango_font_metrics_get_descent(m) div PANGO_SCALE - metrics.lineHeight = pango_font_metrics_get_height(m) div PANGO_SCALE - pango_font_metrics_unref(m) - g_object_unref(font) - if metrics.lineHeight <= 0: - var tw, th: gint - pango_layout_get_pixel_size(layout, addr tw, addr th) - metrics.lineHeight = th.int - metrics.ascent = int(th.float64 * 0.75) - metrics.descent = th.int - metrics.ascent - g_object_unref_layout(layout) - cairo_destroy(cr) - cairo_surface_destroy(surf) - fonts.add FontSlot(desc: desc, metrics: metrics) - result = Font(fonts.len) - -proc gtkCloseFont(f: Font) = - let idx = f.int - 1 - if idx >= 0 and idx < fonts.len and fonts[idx].desc != nil: - pango_font_description_free(fonts[idx].desc) - fonts[idx].desc = nil - -proc gtkMeasureText(f: Font; text: string): TextExtent = - ensureBackingCr() - let desc = getDesc(f) - if desc == nil or text.len == 0: - return TextExtent() - let cr = backingCr - if cr == nil: - return TextExtent() - let layout = pango_cairo_create_layout(cr) - pango_layout_set_font_description(layout, desc) - pango_layout_set_text(layout, cstring(text), -1) - var w, h: gint - pango_layout_get_pixel_size(layout, addr w, addr h) - g_object_unref_layout(layout) - result = TextExtent(w: w.int, h: h.int) - -proc gtkDrawText(f: Font; x, y: int; text: string; fg, bg: screen.Color): TextExtent = - ensureBackingCr() - let desc = getDesc(f) - if desc == nil or text.len == 0 or backingCr == nil: - return - let ext = gtkMeasureText(f, text) - cairo_save(backingCr) - cairo_set_source_rgba(backingCr, - gdouble(bg.r) / 255.0, gdouble(bg.g) / 255.0, gdouble(bg.b) / 255.0, - gdouble(bg.a) / 255.0) - cairo_rectangle(backingCr, gdouble(x), gdouble(y), gdouble(ext.w), gdouble(ext.h)) - cairo_fill(backingCr) - let layout = pango_cairo_create_layout(backingCr) - pango_layout_set_font_description(layout, desc) - pango_layout_set_text(layout, cstring(text), -1) - cairo_set_source_rgba(backingCr, - gdouble(fg.r) / 255.0, gdouble(fg.g) / 255.0, gdouble(fg.b) / 255.0, - gdouble(fg.a) / 255.0) - pango_cairo_update_layout(backingCr, layout) - cairo_move_to(backingCr, gdouble(x), gdouble(y)) - pango_cairo_show_layout(backingCr, layout) - g_object_unref_layout(layout) - cairo_restore(backingCr) - result = ext - -proc gtkGetFontMetrics(f: Font): FontMetrics = - let idx = f.int - 1 - if idx >= 0 and idx < fonts.len: fonts[idx].metrics - else: FontMetrics() - -proc gtkFillRect(r: basetypes.Rect; color: screen.Color) = - ensureBackingCr() - if backingCr == nil: return - cairo_save(backingCr) - cairo_set_source_rgba(backingCr, - gdouble(color.r) / 255.0, gdouble(color.g) / 255.0, gdouble(color.b) / 255.0, - gdouble(color.a) / 255.0) - cairo_rectangle(backingCr, gdouble(r.x), gdouble(r.y), gdouble(r.w), gdouble(r.h)) - cairo_fill(backingCr) - cairo_restore(backingCr) - -proc gtkDrawLine(x1, y1, x2, y2: int; color: screen.Color) = - ensureBackingCr() - if backingCr == nil: return - cairo_save(backingCr) - cairo_set_source_rgba(backingCr, - gdouble(color.r) / 255.0, gdouble(color.g) / 255.0, gdouble(color.b) / 255.0, - gdouble(color.a) / 255.0) - cairo_set_line_width(backingCr, 1) - cairo_move_to(backingCr, gdouble(x1), gdouble(y1)) - cairo_line_to(backingCr, gdouble(x2), gdouble(y2)) - cairo_stroke(backingCr) - cairo_restore(backingCr) - -proc gtkDrawPoint(x, y: int; color: screen.Color) = - gtkFillRect(rect(x, y, 1, 1), color) - -proc gtkSetCursor(c: CursorKind) = - if drawingArea == nil: return - let name = case c - of curDefault, curArrow: "default" - of curIbeam: "text" - of curWait: "wait" - of curCrosshair: "crosshair" - of curHand: "pointer" - of curSizeNS: "ns-resize" - of curSizeWE: "ew-resize" - let cur = gdk_cursor_new_from_name(cstring(name), nil) - if cur != nil: - gtk_widget_set_cursor(drawingArea, cur) - g_object_unref(cur) - -proc gtkSetWindowTitle(title: string) = - if win != nil: - gtk_window_set_title(win, cstring(title)) - -# --- Input hooks --- - -proc pumpGtk() = - while g_main_context_iteration(nil, G_FALSE) != G_FALSE: - discard - -proc gtkPollEvent(e: var Event; flags: set[InputFlag]): bool = - pumpGtk() - if eventQueue.len > 0: - e = eventQueue[0] - eventQueue.delete(0) - return true - false - -proc gtkWaitEvent(e: var Event; timeoutMs: int; - flags: set[InputFlag]): bool = - if gtkPollEvent(e, flags): - return true - if timeoutMs < 0: - while true: - discard g_main_context_iteration(nil, G_TRUE) - if gtkPollEvent(e, flags): - return true - elif timeoutMs == 0: - return false - else: - let t0 = g_get_monotonic_time() div 1000 - while g_get_monotonic_time() div 1000 - t0 < timeoutMs: - discard g_main_context_iteration(nil, G_FALSE) - if gtkPollEvent(e, flags): - return true - g_usleep(5000) - return gtkPollEvent(e, flags) - -proc gtkGetClipboardText(): string = - readClipboardSync() - -proc gtkPutClipboardText(text: string) = - let disp = gdk_display_get_default() - if disp == nil: return - let clip = gdk_display_get_clipboard(disp) - if clip != nil: - gdk_clipboard_set_text(clip, cstring(text)) - -proc gtkGetTicks(): int = - int(g_get_monotonic_time() div 1000) - -proc gtkDelay(ms: int) = - g_usleep(culong(ms) * 1000) - -proc gtkQuitRequest() = - if win != nil: - gtk_window_destroy(win) - win = nil - drawingArea = nil - if backingCr != nil: - cairo_destroy(backingCr) - backingCr = nil - if backingSurf != nil: - cairo_surface_destroy(backingSurf) - backingSurf = nil - if imContext != nil: - g_object_unref(imContext) - imContext = nil - -proc initGtk4Driver*() = - windowRelays = WindowRelays( - createWindow: gtkCreateWindow, refresh: gtkRefresh, - saveState: gtkSaveState, restoreState: gtkRestoreState, - setClipRect: gtkSetClipRect, setCursor: gtkSetCursor, - setWindowTitle: gtkSetWindowTitle) - fontRelays = FontRelays( - openFont: gtkOpenFont, closeFont: gtkCloseFont, - getFontMetrics: gtkGetFontMetrics, measureText: gtkMeasureText, - drawText: gtkDrawText) - drawRelays = DrawRelays( - fillRect: gtkFillRect, drawLine: gtkDrawLine, drawPoint: gtkDrawPoint) - inputRelays = InputRelays( - pollEvent: gtkPollEvent, waitEvent: gtkWaitEvent, - getTicks: gtkGetTicks, delay: gtkDelay, - quitRequest: gtkQuitRequest) - clipboardRelays = ClipboardRelays( - getText: gtkGetClipboardText, putText: gtkPutClipboardText) diff --git a/app/nimedit.nim b/app/nimedit.nim index 98ad956..c42b39b 100644 --- a/app/nimedit.nim +++ b/app/nimedit.nim @@ -8,19 +8,7 @@ import std/[strutils, critbits, os, times, browsers, tables, hashes, intsets, exitprocs] from parseutils import parseInt -import basetypes, screen, input -when defined(cocoa): - import cocoa_driver -elif defined(gtk4): - import gtk4_driver -elif defined(x11): - import x11_driver -elif defined(winapi): - import winapi_driver -elif defined(sdl2): - import sdl2_driver -else: - import sdl3_driver +import uirelays/[coords, screen, input, backend] import buffertype except Action import buffer, styles, unicode, highlighters, console import nimscript/common, nimscript/keydefs, languages, themes, @@ -1166,17 +1154,6 @@ proc mainProc(ed: Editor) = -when defined(cocoa): - initCocoaDriver() -elif defined(gtk4): - initGtk4Driver() -elif defined(x11): - initX11Driver() -elif defined(winapi): - initWinapiDriver() -elif defined(sdl2): - initSdl2Driver() -else: - initSdl3Driver() +initBackend() mainProc(Editor()) input.quitRequest() diff --git a/app/prims.nim b/app/prims.nim index ea2879d..c12e954 100644 --- a/app/prims.nim +++ b/app/prims.nim @@ -1,5 +1,5 @@ -import basetypes, screen +import uirelays/[coords, screen] from math import sin, cos, PI type diff --git a/app/scrollbar.nim b/app/scrollbar.nim index d925247..009195d 100644 --- a/app/scrollbar.nim +++ b/app/scrollbar.nim @@ -1,7 +1,7 @@ ## Draws a vertical scrollbar for a buffer. import buffertype, themes -import basetypes, screen, input, tabbar +import uirelays/[coords, screen, input], tabbar const scrollBarWidth* = 15 diff --git a/app/sdl2_driver.nim b/app/sdl2_driver.nim deleted file mode 100644 index 74ac576..0000000 --- a/app/sdl2_driver.nim +++ /dev/null @@ -1,339 +0,0 @@ -# SDL2 backend driver. Sets all hooks from core/input and core/screen. -# This is the ONLY file that should import sdl2. - -import sdl2 except Rect, Point -import sdl2/ttf -import basetypes, input, screen - -# --- Font handle management --- - -type - FontSlot = object - sdlFont: FontPtr - metrics: FontMetrics - -var fonts: seq[FontSlot] - -proc toSdlColor(c: screen.Color): sdl2.Color = - result.r = c.r - result.g = c.g - result.b = c.b - result.a = c.a - -proc toSdlRect(r: basetypes.Rect): sdl2.Rect {.inline.} = - (r.x, r.y, r.w, r.h) - -proc getFontPtr(f: Font): FontPtr {.inline.} = - let idx = f.int - 1 - if idx >= 0 and idx < fonts.len: fonts[idx].sdlFont - else: nil - -# --- SDL driver state --- - -var - window: WindowPtr - renderer: RendererPtr - -# --- Screen hook implementations --- - -proc sdlCreateWindow(layout: var ScreenLayout) = - let flags = SDL_WINDOW_RESIZABLE or SDL_WINDOW_SHOWN - window = createWindow("NimEdit", - SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, - layout.width.cint, layout.height.cint, flags) - renderer = createRenderer(window, -1, Renderer_Software) - var w, h: cint - window.getSize(w, h) - layout.width = w - layout.height = h - layout.scaleX = 1 - layout.scaleY = 1 - sdl2.startTextInput() - -proc sdlRefresh() = - renderer.present() - -proc sdlSaveState() = - discard # TODO: push clip rect stack - -proc sdlRestoreState() = - discard # TODO: pop clip rect stack - -proc sdlSetClipRect(r: basetypes.Rect) = - var sdlRect = toSdlRect(r) - discard renderer.setClipRect(addr sdlRect) - -proc sdlOpenFont(path: string; size: int; - metrics: var FontMetrics): Font = - let f = openFont(cstring(path), size.cint) - if f == nil: return Font(0) - metrics.ascent = fontAscent(f) - metrics.descent = fontDescent(f) - metrics.lineHeight = fontLineSkip(f) - fonts.add FontSlot(sdlFont: f, metrics: metrics) - result = Font(fonts.len) # 1-based - -proc sdlCloseFont(f: Font) = - let idx = f.int - 1 - if idx >= 0 and idx < fonts.len and fonts[idx].sdlFont != nil: - close(fonts[idx].sdlFont) - fonts[idx].sdlFont = nil - -proc sdlMeasureText(f: Font; text: cstring): TextExtent = - let fp = getFontPtr(f) - if fp != nil and text[0] != '\0': - var w, h: cint - discard sizeUtf8(fp, text, addr w, addr h) - result = TextExtent(w: w, h: h) - -proc sdlDrawTextShaded(f: Font; x, y: cint; text: cstring; - fg, bg: screen.Color): TextExtent = - let fp = getFontPtr(f) - if fp == nil or text[0] == '\0': return - let surf = renderUtf8Shaded(fp, text, toSdlColor(fg), toSdlColor(bg)) - if surf == nil: return - let tex = renderer.createTextureFromSurface(surf) - if tex == nil: - freeSurface(surf) - return - var src: sdl2.Rect = (0.cint, 0.cint, surf.w, surf.h) - var dst: sdl2.Rect = (x, y, surf.w, surf.h) - renderer.copy(tex, addr src, addr dst) - result = TextExtent(w: surf.w, h: surf.h) - freeSurface(surf) - destroy(tex) - -proc sdlGetFontMetrics(f: Font): FontMetrics = - let idx = f.int - 1 - if idx >= 0 and idx < fonts.len: fonts[idx].metrics - else: FontMetrics() - -proc sdlFillRect(r: basetypes.Rect; color: screen.Color) = - renderer.setDrawColor(color.r, color.g, color.b, color.a) - var sdlRect = toSdlRect(r) - discard renderer.fillRect(sdlRect) - -proc sdlDrawLine(x1, y1, x2, y2: cint; color: screen.Color) = - renderer.setDrawColor(color.r, color.g, color.b, color.a) - renderer.drawLine(x1, y1, x2, y2) - -proc sdlDrawPoint(x, y: cint; color: screen.Color) = - renderer.setDrawColor(color.r, color.g, color.b, color.a) - renderer.drawPoint(x, y) - -proc sdlSetCursor(c: CursorKind) = - let sdlCursor = case c - of curDefault, curArrow: SDL_SYSTEM_CURSOR_ARROW - of curIbeam: SDL_SYSTEM_CURSOR_IBEAM - of curWait: SDL_SYSTEM_CURSOR_WAIT - of curCrosshair: SDL_SYSTEM_CURSOR_CROSSHAIR - of curHand: SDL_SYSTEM_CURSOR_HAND - of curSizeNS: SDL_SYSTEM_CURSOR_SIZENS - of curSizeWE: SDL_SYSTEM_CURSOR_SIZEWE - let cur = createSystemCursor(sdlCursor) - setCursor(cur) - -proc sdlSetWindowTitle(title: string) = - if window != nil: - window.setTitle(cstring(title)) - -# --- Input hook implementations --- - -proc sdlGetClipboardText(): string = - let t = sdl2.getClipboardText() - result = $t - freeClipboardText(t) - -proc sdlPutClipboardText(text: string) = - discard sdl2.setClipboardText(cstring(text)) - -proc translateScancode(sc: Scancode): KeyCode = - case sc - of SDL_SCANCODE_A: KeyA - of SDL_SCANCODE_B: KeyB - of SDL_SCANCODE_C: KeyC - of SDL_SCANCODE_D: KeyD - of SDL_SCANCODE_E: KeyE - of SDL_SCANCODE_F: KeyF - of SDL_SCANCODE_G: KeyG - of SDL_SCANCODE_H: KeyH - of SDL_SCANCODE_I: KeyI - of SDL_SCANCODE_J: KeyJ - of SDL_SCANCODE_K: KeyK - of SDL_SCANCODE_L: KeyL - of SDL_SCANCODE_M: KeyM - of SDL_SCANCODE_N: KeyN - of SDL_SCANCODE_O: KeyO - of SDL_SCANCODE_P: KeyP - of SDL_SCANCODE_Q: KeyQ - of SDL_SCANCODE_R: KeyR - of SDL_SCANCODE_S: KeyS - of SDL_SCANCODE_T: KeyT - of SDL_SCANCODE_U: KeyU - of SDL_SCANCODE_V: KeyV - of SDL_SCANCODE_W: KeyW - of SDL_SCANCODE_X: KeyX - of SDL_SCANCODE_Y: KeyY - of SDL_SCANCODE_Z: KeyZ - of SDL_SCANCODE_1: Key1 - of SDL_SCANCODE_2: Key2 - of SDL_SCANCODE_3: Key3 - of SDL_SCANCODE_4: Key4 - of SDL_SCANCODE_5: Key5 - of SDL_SCANCODE_6: Key6 - of SDL_SCANCODE_7: Key7 - of SDL_SCANCODE_8: Key8 - of SDL_SCANCODE_9: Key9 - of SDL_SCANCODE_0: Key0 - of SDL_SCANCODE_F1: KeyF1 - of SDL_SCANCODE_F2: KeyF2 - of SDL_SCANCODE_F3: KeyF3 - of SDL_SCANCODE_F4: KeyF4 - of SDL_SCANCODE_F5: KeyF5 - of SDL_SCANCODE_F6: KeyF6 - of SDL_SCANCODE_F7: KeyF7 - of SDL_SCANCODE_F8: KeyF8 - of SDL_SCANCODE_F9: KeyF9 - of SDL_SCANCODE_F10: KeyF10 - of SDL_SCANCODE_F11: KeyF11 - of SDL_SCANCODE_F12: KeyF12 - of SDL_SCANCODE_RETURN: KeyEnter - of SDL_SCANCODE_SPACE: KeySpace - of SDL_SCANCODE_ESCAPE: KeyEsc - of SDL_SCANCODE_TAB: KeyTab - of SDL_SCANCODE_BACKSPACE: KeyBackspace - of SDL_SCANCODE_DELETE: KeyDelete - of SDL_SCANCODE_INSERT: KeyInsert - of SDL_SCANCODE_LEFT: KeyLeft - of SDL_SCANCODE_RIGHT: KeyRight - of SDL_SCANCODE_UP: KeyUp - of SDL_SCANCODE_DOWN: KeyDown - of SDL_SCANCODE_PAGEUP: KeyPageUp - of SDL_SCANCODE_PAGEDOWN: KeyPageDown - of SDL_SCANCODE_HOME: KeyHome - of SDL_SCANCODE_END: KeyEnd - of SDL_SCANCODE_CAPSLOCK: KeyCapslock - of SDL_SCANCODE_COMMA: KeyComma - of SDL_SCANCODE_PERIOD: KeyPeriod - else: KeyNone - -proc translateMods(m: int16): set[Modifier] = - let m = m.int32 - if (m and KMOD_SHIFT) != 0: result.incl ShiftPressed - if (m and KMOD_CTRL) != 0: result.incl CtrlPressed - if (m and KMOD_ALT) != 0: result.incl AltPressed - if (m and KMOD_GUI) != 0: result.incl GuiPressed - -proc sdlPollEvent(e: var input.Event; flags: set[InputFlag]): bool = - var sdlEvent: sdl2.Event - if not sdl2.pollEvent(sdlEvent): - return false - result = true - e = input.Event(kind: NoEvent) - case sdlEvent.kind - of QuitEvent: - e.kind = input.QuitEvent - of WindowEvent: - let wev = sdlEvent.window - case wev.event - of WindowEvent_Resized, WindowEvent_SizeChanged: - e.kind = WindowResizeEvent - e.x = wev.data1 - e.y = wev.data2 - of WindowEvent_Close: - e.kind = WindowCloseEvent - of WindowEvent_FocusGained: - e.kind = WindowFocusGainedEvent - of WindowEvent_FocusLost: - e.kind = WindowFocusLostEvent - else: - e.kind = NoEvent - of KeyDown: - e.kind = KeyDownEvent - e.key = translateScancode(sdlEvent.key.keysym.scancode) - e.mods = translateMods(sdlEvent.key.keysym.modstate) - of KeyUp: - e.kind = KeyUpEvent - e.key = translateScancode(sdlEvent.key.keysym.scancode) - e.mods = translateMods(sdlEvent.key.keysym.modstate) - of TextInput: - e.kind = TextInputEvent - for i in 0..3: - e.text[i] = sdlEvent.text.text[i] - of MouseButtonDown: - e.kind = MouseDownEvent - e.x = sdlEvent.button.x - e.y = sdlEvent.button.y - e.clicks = sdlEvent.button.clicks.int - case sdlEvent.button.button - of BUTTON_LEFT: e.button = LeftButton - of BUTTON_RIGHT: e.button = RightButton - of BUTTON_MIDDLE: e.button = MiddleButton - else: e.button = LeftButton - of MouseButtonUp: - e.kind = MouseUpEvent - e.x = sdlEvent.button.x - e.y = sdlEvent.button.y - case sdlEvent.button.button - of BUTTON_LEFT: e.button = LeftButton - of BUTTON_RIGHT: e.button = RightButton - of BUTTON_MIDDLE: e.button = MiddleButton - else: e.button = LeftButton - of MouseMotion: - e.kind = MouseMoveEvent - e.x = sdlEvent.motion.x - e.y = sdlEvent.motion.y - of MouseWheel: - e.kind = MouseWheelEvent - e.x = sdlEvent.wheel.x - e.y = sdlEvent.wheel.y - else: - e.kind = NoEvent - -proc sdlWaitEvent(e: var input.Event; timeoutMs: int; - flags: set[InputFlag]): bool = - # Use pollEvent with delay for timeout behavior - if timeoutMs < 0: - # Block until event - while true: - if sdlPollEvent(e, flags): return true - sdl2.delay(10) - elif timeoutMs == 0: - return sdlPollEvent(e, flags) - else: - let start = sdl2.getTicks() - while sdl2.getTicks() - start < timeoutMs.uint32: - if sdlPollEvent(e, flags): return true - sdl2.delay(10) - return false - -proc sdlGetTicks(): int = sdl2.getTicks().int - -proc sdlDelay(ms: int) = sdl2.delay(ms.uint32) - -proc sdlQuitRequest() = sdl2.quit() - -# --- Init --- - -proc initSdl2Driver*() = - if sdl2.init(INIT_VIDEO or INIT_EVENTS) != SdlSuccess: - quit("SDL init failed") - if ttfInit() != SdlSuccess: - quit("TTF init failed") - windowRelays = WindowRelays( - createWindow: sdlCreateWindow, refresh: sdlRefresh, - saveState: sdlSaveState, restoreState: sdlRestoreState, - setClipRect: sdlSetClipRect, setCursor: sdlSetCursor, - setWindowTitle: sdlSetWindowTitle) - fontRelays = FontRelays( - openFont: sdlOpenFont, closeFont: sdlCloseFont, - getFontMetrics: sdlGetFontMetrics, measureText: sdlMeasureText, - drawText: sdlDrawTextShaded) - drawRelays = DrawRelays( - fillRect: sdlFillRect, drawLine: sdlDrawLine, drawPoint: sdlDrawPoint) - inputRelays = InputRelays( - pollEvent: sdlPollEvent, waitEvent: sdlWaitEvent, - getTicks: sdlGetTicks, delay: sdlDelay, - quitRequest: sdlQuitRequest) - clipboardRelays = ClipboardRelays( - getText: sdlGetClipboardText, putText: sdlPutClipboardText) diff --git a/app/sdl3_driver.nim b/app/sdl3_driver.nim deleted file mode 100644 index f01dc93..0000000 --- a/app/sdl3_driver.nim +++ /dev/null @@ -1,327 +0,0 @@ -# SDL3 backend driver. Sets all hooks from core/input and core/screen. - -import sdl3 -import sdl3_ttf -import basetypes, input, screen - -# --- Font handle management --- - -type - FontSlot = object - ttfFont: sdl3_ttf.Font - metrics: FontMetrics - -var fonts: seq[FontSlot] - -proc toColor(c: screen.Color): sdl3.Color {.inline.} = - sdl3.Color(r: c.r, g: c.g, b: c.b, a: c.a) - -proc getFontPtr(f: screen.Font): sdl3_ttf.Font {.inline.} = - let idx = f.int - 1 - if idx >= 0 and idx < fonts.len: fonts[idx].ttfFont - else: nil - -# --- SDL driver state --- - -var - win: sdl3.Window - ren: sdl3.Renderer - -# --- Screen hook implementations --- - -proc sdlCreateWindow(layout: var ScreenLayout) = - discard createWindowAndRenderer(cstring"NimEdit", - layout.width.cint, layout.height.cint, WINDOW_RESIZABLE, win, ren) - discard startTextInput(win) - var w, h: cint - discard getWindowSize(win, w, h) - layout.width = w - layout.height = h - layout.scaleX = 1 - layout.scaleY = 1 - -proc sdlRefresh() = - discard renderPresent(ren) - -proc sdlSaveState() = discard -proc sdlRestoreState() = discard - -proc sdlSetClipRect(r: basetypes.Rect) = - var sr = sdl3.Rect(x: r.x.cint, y: r.y.cint, w: r.w.cint, h: r.h.cint) - discard setRenderClipRect(ren, addr sr) - -proc sdlOpenFont(path: string; size: int; - metrics: var FontMetrics): screen.Font = - let f = sdl3_ttf.openFont(cstring(path), size.cfloat) - if f == nil: return screen.Font(0) - sdl3_ttf.setFontHinting(f, sdl3_ttf.hintingLightSubpixel) - metrics.ascent = sdl3_ttf.getFontAscent(f) - metrics.descent = sdl3_ttf.getFontDescent(f) - metrics.lineHeight = sdl3_ttf.getFontLineSkip(f) - fonts.add FontSlot(ttfFont: f, metrics: metrics) - result = screen.Font(fonts.len) - -proc sdlCloseFont(f: screen.Font) = - let idx = f.int - 1 - if idx >= 0 and idx < fonts.len and fonts[idx].ttfFont != nil: - sdl3_ttf.closeFont(fonts[idx].ttfFont) - fonts[idx].ttfFont = nil - -proc sdlMeasureText(f: screen.Font; text: string): TextExtent = - let fp = getFontPtr(f) - if fp != nil and text != "": - var w, h: cint - discard sdl3_ttf.getStringSize(fp, cstring(text), 0, w, h) - result = TextExtent(w: w, h: h) - -proc sdlDrawText(f: screen.Font; x, y: int; text: string; - fg, bg: screen.Color): TextExtent = - let fp = getFontPtr(f) - if fp == nil or text == "": return - # Fill background, then draw blended text on top - let ext0 = sdlMeasureText(f, text) - var bgRect = FRect(x: x.cfloat, y: y.cfloat, - w: ext0.w.cfloat, h: ext0.h.cfloat) - discard setRenderDrawColor(ren, bg.r, bg.g, bg.b, bg.a) - discard renderFillRect(ren, addr bgRect) - let surf = sdl3_ttf.renderTextBlended(fp, cstring(text), 0, toColor(fg)) - if surf == nil: return - let tex = createTextureFromSurface(ren, surf) - if tex == nil: - destroySurface(surf) - return - discard setTextureBlendMode(tex, BLENDMODE_BLEND) - var tw, th: cfloat - discard getTextureSize(tex, tw, th) - var src = FRect(x: 0, y: 0, w: tw, h: th) - var dst = FRect(x: x.cfloat, y: y.cfloat, w: tw, h: th) - discard renderTexture(ren, tex, addr src, addr dst) - result = TextExtent(w: tw.int, h: th.int) - destroySurface(surf) - destroyTexture(tex) - -proc sdlGetFontMetrics(f: screen.Font): FontMetrics = - let idx = f.int - 1 - if idx >= 0 and idx < fonts.len: fonts[idx].metrics - else: screen.FontMetrics() - -proc sdlFillRect(r: basetypes.Rect; color: screen.Color) = - discard setRenderDrawColor(ren, color.r, color.g, color.b, color.a) - var fr = FRect(x: r.x.cfloat, y: r.y.cfloat, - w: r.w.cfloat, h: r.h.cfloat) - discard renderFillRect(ren, addr fr) - -proc sdlDrawLine(x1, y1, x2, y2: int; color: screen.Color) = - discard setRenderDrawColor(ren, color.r, color.g, color.b, color.a) - discard renderLine(ren, x1.cfloat, y1.cfloat, x2.cfloat, y2.cfloat) - -proc sdlDrawPoint(x, y: int; color: screen.Color) = - discard setRenderDrawColor(ren, color.r, color.g, color.b, color.a) - discard renderPoint(ren, x.cfloat, y.cfloat) - -proc sdlSetCursor(c: CursorKind) = - let sc = case c - of curDefault, curArrow: SYSTEM_CURSOR_DEFAULT - of curIbeam: SYSTEM_CURSOR_TEXT - of curWait: SYSTEM_CURSOR_WAIT - of curCrosshair: SYSTEM_CURSOR_CROSSHAIR - of curHand: SYSTEM_CURSOR_POINTER - of curSizeNS: SYSTEM_CURSOR_NS_RESIZE - of curSizeWE: SYSTEM_CURSOR_EW_RESIZE - let cur = sdl3.createSystemCursor(sc) - discard sdl3.setCursor(cur) - -proc sdlSetWindowTitle(title: string) = - if win != nil: - discard setWindowTitle(win, cstring(title)) - -# --- Input hook implementations --- - -proc sdlGetClipboardText(): string = - let t = sdl3.getClipboardText() - if t != nil: result = $t - else: result = "" - -proc sdlPutClipboardText(text: string) = - discard setClipboardText(cstring(text)) - -proc translateScancode(sc: Scancode): input.KeyCode = - case sc - of SCANCODE_A: KeyA - of SCANCODE_B: KeyB - of SCANCODE_C: KeyC - of SCANCODE_D: KeyD - of SCANCODE_E: KeyE - of SCANCODE_F: KeyF - of SCANCODE_G: KeyG - of SCANCODE_H: KeyH - of SCANCODE_I: KeyI - of SCANCODE_J: KeyJ - of SCANCODE_K: KeyK - of SCANCODE_L: KeyL - of SCANCODE_M: KeyM - of SCANCODE_N: KeyN - of SCANCODE_O: KeyO - of SCANCODE_P: KeyP - of SCANCODE_Q: KeyQ - of SCANCODE_R: KeyR - of SCANCODE_S: KeyS - of SCANCODE_T: KeyT - of SCANCODE_U: KeyU - of SCANCODE_V: KeyV - of SCANCODE_W: KeyW - of SCANCODE_X: KeyX - of SCANCODE_Y: KeyY - of SCANCODE_Z: KeyZ - of SCANCODE_1: Key1 - of SCANCODE_2: Key2 - of SCANCODE_3: Key3 - of SCANCODE_4: Key4 - of SCANCODE_5: Key5 - of SCANCODE_6: Key6 - of SCANCODE_7: Key7 - of SCANCODE_8: Key8 - of SCANCODE_9: Key9 - of SCANCODE_0: Key0 - of SCANCODE_F1: KeyF1 - of SCANCODE_F2: KeyF2 - of SCANCODE_F3: KeyF3 - of SCANCODE_F4: KeyF4 - of SCANCODE_F5: KeyF5 - of SCANCODE_F6: KeyF6 - of SCANCODE_F7: KeyF7 - of SCANCODE_F8: KeyF8 - of SCANCODE_F9: KeyF9 - of SCANCODE_F10: KeyF10 - of SCANCODE_F11: KeyF11 - of SCANCODE_F12: KeyF12 - of SCANCODE_RETURN: KeyEnter - of SCANCODE_SPACE: KeySpace - of SCANCODE_ESCAPE: KeyEsc - of SCANCODE_TAB: KeyTab - of SCANCODE_BACKSPACE: KeyBackspace - of SCANCODE_DELETE: KeyDelete - of SCANCODE_INSERT: KeyInsert - of SCANCODE_LEFT: KeyLeft - of SCANCODE_RIGHT: KeyRight - of SCANCODE_UP: KeyUp - of SCANCODE_DOWN: KeyDown - of SCANCODE_PAGEUP: KeyPageUp - of SCANCODE_PAGEDOWN: KeyPageDown - of SCANCODE_HOME: KeyHome - of SCANCODE_END: KeyEnd - of SCANCODE_CAPSLOCK: KeyCapslock - of SCANCODE_COMMA: KeyComma - of SCANCODE_PERIOD: KeyPeriod - else: KeyNone - -proc translateMods(m: Keymod): set[Modifier] = - let m = m.uint32 - if (m and KMOD_SHIFT) != 0: result.incl ShiftPressed - if (m and KMOD_CTRL) != 0: result.incl CtrlPressed - if (m and KMOD_ALT) != 0: result.incl AltPressed - if (m and KMOD_GUI) != 0: result.incl GuiPressed - -proc translateEvent(sdlEvent: sdl3.Event; e: var input.Event) = - e = input.Event(kind: NoEvent) - let evType = uint32(sdlEvent.common.`type`) - if evType == uint32(EVENT_QUIT): - e.kind = QuitEvent - elif evType == uint32(EVENT_WINDOW_RESIZED): - e.kind = WindowResizeEvent - e.x = sdlEvent.window.data1 - e.y = sdlEvent.window.data2 - elif evType == uint32(EVENT_WINDOW_CLOSE_REQUESTED): - e.kind = WindowCloseEvent - elif evType == uint32(EVENT_WINDOW_FOCUS_GAINED): - e.kind = WindowFocusGainedEvent - elif evType == uint32(EVENT_WINDOW_FOCUS_LOST): - e.kind = WindowFocusLostEvent - elif evType == uint32(EVENT_KEY_DOWN): - e.kind = KeyDownEvent - e.key = translateScancode(sdlEvent.key.scancode) - e.mods = translateMods(sdlEvent.key.`mod`) - elif evType == uint32(EVENT_KEY_UP): - e.kind = KeyUpEvent - e.key = translateScancode(sdlEvent.key.scancode) - e.mods = translateMods(sdlEvent.key.`mod`) - elif evType == uint32(EVENT_TEXT_INPUT): - e.kind = TextInputEvent - if sdlEvent.text.text != nil: - for i in 0..3: - if sdlEvent.text.text[i] == '\0': - e.text[i] = '\0' - break - e.text[i] = sdlEvent.text.text[i] - elif evType == uint32(EVENT_MOUSE_BUTTON_DOWN): - e.kind = MouseDownEvent - e.x = sdlEvent.button.x.int - e.y = sdlEvent.button.y.int - e.clicks = sdlEvent.button.clicks.int - case sdlEvent.button.button - of BUTTON_LEFT: e.button = LeftButton - of BUTTON_RIGHT: e.button = RightButton - of BUTTON_MIDDLE: e.button = MiddleButton - else: e.button = LeftButton - elif evType == uint32(EVENT_MOUSE_BUTTON_UP): - e.kind = MouseUpEvent - e.x = sdlEvent.button.x.int - e.y = sdlEvent.button.y.int - case sdlEvent.button.button - of BUTTON_LEFT: e.button = LeftButton - of BUTTON_RIGHT: e.button = RightButton - of BUTTON_MIDDLE: e.button = MiddleButton - else: e.button = LeftButton - elif evType == uint32(EVENT_MOUSE_MOTION): - e.kind = MouseMoveEvent - e.x = sdlEvent.motion.x.int - e.y = sdlEvent.motion.y.int - elif evType == uint32(EVENT_MOUSE_WHEEL): - e.kind = MouseWheelEvent - e.x = sdlEvent.wheel.x.int - e.y = sdlEvent.wheel.y.int - -proc sdlPollEvent(e: var input.Event; flags: set[InputFlag]): bool = - var sdlEvent: sdl3.Event - if not pollEvent(sdlEvent): - return false - translateEvent(sdlEvent, e) - result = true - -proc sdlWaitEvent(e: var input.Event; timeoutMs: int; - flags: set[InputFlag]): bool = - var sdlEvent: sdl3.Event - let ok = if timeoutMs < 0: waitEvent(sdlEvent) - else: waitEventTimeout(sdlEvent, timeoutMs.int32) - if not ok: return false - translateEvent(sdlEvent, e) - result = true - -proc sdlGetTicks(): int = sdl3.getTicks().int -proc sdlDelay(ms: int) = sdl3.delay(ms.uint32) -proc sdlQuitRequest() = sdl3.quit() - -# --- Init --- - -proc initSdl3Driver*() = - if not sdl3.init(INIT_VIDEO or INIT_EVENTS): - quit("SDL3 init failed") - if not sdl3_ttf.init(): - quit("TTF3 init failed") - windowRelays = WindowRelays( - createWindow: sdlCreateWindow, refresh: sdlRefresh, - saveState: sdlSaveState, restoreState: sdlRestoreState, - setClipRect: sdlSetClipRect, setCursor: sdlSetCursor, - setWindowTitle: sdlSetWindowTitle) - fontRelays = FontRelays( - openFont: sdlOpenFont, closeFont: sdlCloseFont, - getFontMetrics: sdlGetFontMetrics, measureText: sdlMeasureText, - drawText: sdlDrawText) - drawRelays = DrawRelays( - fillRect: sdlFillRect, drawLine: sdlDrawLine, drawPoint: sdlDrawPoint) - inputRelays = InputRelays( - pollEvent: sdlPollEvent, waitEvent: sdlWaitEvent, - getTicks: sdlGetTicks, delay: sdlDelay, - quitRequest: sdlQuitRequest) - clipboardRelays = ClipboardRelays( - getText: sdlGetClipboardText, putText: sdlPutClipboardText) diff --git a/app/styles.nim b/app/styles.nim index 4181910..faaac99 100644 --- a/app/styles.nim +++ b/app/styles.nim @@ -2,7 +2,7 @@ # Handling of styles. import std/[strformat, strutils, decls] -import screen, input +import uirelays/[screen, input] import nimscript/common when NimMajor >= 2: diff --git a/app/tabbar.nim b/app/tabbar.nim index 2dc4b2d..bc6238a 100644 --- a/app/tabbar.nim +++ b/app/tabbar.nim @@ -1,7 +1,7 @@ import buffertype, themes -import basetypes, screen, input, prims +import uirelays/[coords, screen, input], prims type TabBar* = object diff --git a/app/themes.nim b/app/themes.nim index 13bc619..4e680f8 100644 --- a/app/themes.nim +++ b/app/themes.nim @@ -1,5 +1,5 @@ -import screen +import uirelays/screen type InternalTheme* = object diff --git a/app/winapi_driver.nim b/app/winapi_driver.nim deleted file mode 100644 index 40ffd68..0000000 --- a/app/winapi_driver.nim +++ /dev/null @@ -1,921 +0,0 @@ -# WinAPI (GDI) backend driver. Sets all hooks from core/input and core/screen. -# Uses a double-buffered GDI approach: draw to an off-screen bitmap, -# BitBlt to window on refresh. - -import basetypes, input, screen -import std/[widestrs, strutils, os] - -{.passL: "-lgdi32 -luser32 -lkernel32".} - -# ---- Win32 type definitions ---- - -type - UINT = uint32 - DWORD = uint32 - LONG = int32 - BOOL = int32 - BYTE = uint8 - WORD = uint16 - WPARAM = uint - LPARAM = int - LRESULT = int - ATOM = WORD - HANDLE = pointer - HWND = HANDLE - HDC = HANDLE - HINSTANCE = HANDLE - HBRUSH = HANDLE - HBITMAP = HANDLE - HFONT = HANDLE - HCURSOR = HANDLE - HICON = HANDLE - HGDIOBJ = HANDLE - HMENU = HANDLE - HRGN = HANDLE - HGLOBAL = HANDLE - COLORREF = DWORD - - WNDCLASSEXW {.pure.} = object - cbSize: UINT - style: UINT - lpfnWndProc: proc (hwnd: HWND; msg: UINT; wp: WPARAM; lp: LPARAM): LRESULT {.stdcall.} - cbClsExtra: int32 - cbWndExtra: int32 - hInstance: HINSTANCE - hIcon: HICON - hCursor: HCURSOR - hbrBackground: HBRUSH - lpszMenuName: ptr uint16 - lpszClassName: ptr uint16 - hIconSm: HICON - - MSG {.pure.} = object - hwnd: HWND - message: UINT - wParam: WPARAM - lParam: LPARAM - time: DWORD - x: LONG - y: LONG - - WINAPIPOINT {.pure.} = object - x, y: LONG - - WINAPIRECT {.pure.} = object - left, top, right, bottom: LONG - - PAINTSTRUCT {.pure.} = object - hdc: HDC - fErase: BOOL - rcPaint: WINAPIRECT - fRestore: BOOL - fIncUpdate: BOOL - rgbReserved: array[32, BYTE] - - TEXTMETRICW {.pure.} = object - tmHeight: LONG - tmAscent: LONG - tmDescent: LONG - tmInternalLeading: LONG - tmExternalLeading: LONG - tmAveCharWidth: LONG - tmMaxCharWidth: LONG - tmWeight: LONG - tmOverhang: LONG - tmDigitizedAspectX: LONG - tmDigitizedAspectY: LONG - tmFirstChar: uint16 - tmLastChar: uint16 - tmDefaultChar: uint16 - tmBreakChar: uint16 - tmItalic: BYTE - tmUnderlined: BYTE - tmStruckOut: BYTE - tmPitchAndFamily: BYTE - tmCharSet: BYTE - - SIZE {.pure.} = object - cx, cy: LONG - -# ---- Win32 constants ---- - -const - CS_HREDRAW = 0x0002'u32 - CS_VREDRAW = 0x0001'u32 - WS_OVERLAPPEDWINDOW = 0x00CF0000'u32 - WS_VISIBLE = 0x10000000'u32 - - WM_DESTROY = 0x0002'u32 - WM_SIZE = 0x0005'u32 - WM_PAINT = 0x000F'u32 - WM_CLOSE = 0x0010'u32 - WM_QUIT = 0x0012'u32 - WM_ERASEBKGND = 0x0014'u32 - WM_KEYDOWN = 0x0100'u32 - WM_KEYUP = 0x0101'u32 - WM_CHAR = 0x0102'u32 - WM_MOUSEMOVE = 0x0200'u32 - WM_LBUTTONDOWN = 0x0201'u32 - WM_LBUTTONUP = 0x0202'u32 - WM_RBUTTONDOWN = 0x0204'u32 - WM_RBUTTONUP = 0x0205'u32 - WM_MBUTTONDOWN = 0x0207'u32 - WM_MBUTTONUP = 0x0208'u32 - WM_MOUSEWHEEL = 0x020A'u32 - WM_SETFOCUS = 0x0007'u32 - WM_KILLFOCUS = 0x0008'u32 - - PM_REMOVE = 0x0001'u32 - INFINITE = 0xFFFFFFFF'u32 - - MK_LBUTTON = 0x0001'u32 - MK_RBUTTON = 0x0002'u32 - MK_MBUTTON = 0x0010'u32 - - SW_SHOW = 5'i32 - TRANSPARENT = 1 - OPAQUE = 2 - - SRCCOPY = 0x00CC0020'u32 - DIB_RGB_COLORS = 0'u32 - - IDC_ARROW = cast[ptr uint16](32512) - IDC_IBEAM = cast[ptr uint16](32513) - IDC_WAIT = cast[ptr uint16](32514) - IDC_CROSS = cast[ptr uint16](32515) - IDC_HAND = cast[ptr uint16](32649) - IDC_SIZENS = cast[ptr uint16](32645) - IDC_SIZEWE = cast[ptr uint16](32644) - - VK_BACK = 0x08'u32 - VK_TAB = 0x09'u32 - VK_RETURN = 0x0D'u32 - VK_ESCAPE = 0x1B'u32 - VK_SPACE = 0x20'u32 - VK_DELETE = 0x2E'u32 - VK_INSERT = 0x2D'u32 - VK_LEFT = 0x25'u32 - VK_UP = 0x26'u32 - VK_RIGHT = 0x27'u32 - VK_DOWN = 0x28'u32 - VK_PRIOR = 0x21'u32 # Page Up - VK_NEXT = 0x22'u32 # Page Down - VK_HOME = 0x24'u32 - VK_END = 0x23'u32 - VK_CAPITAL = 0x14'u32 - VK_F1 = 0x70'u32 - VK_F12 = 0x7B'u32 - VK_OEM_COMMA = 0xBC'u32 - VK_OEM_PERIOD = 0xBE'u32 - VK_SHIFT = 0x10'u32 - VK_CONTROL = 0x11'u32 - VK_MENU = 0x12'u32 # Alt - - FW_NORMAL = 400'i32 - DEFAULT_CHARSET = 1'u8 - OUT_TT_PRECIS = 4'u32 - CLIP_DEFAULT_PRECIS = 0'u32 - CLEARTYPE_QUALITY = 5'u32 - FF_DONTCARE = 0'u32 - DEFAULT_PITCH = 0'u32 - - CF_UNICODETEXT = 13'u32 - GMEM_MOVEABLE = 0x0002'u32 - - WAIT_TIMEOUT = 258'u32 - QS_ALLINPUT = 0x04FF'u32 - -# ---- Win32 API imports ---- - -proc GetModuleHandleW(lpModuleName: ptr uint16): HINSTANCE - {.stdcall, dynlib: "kernel32", importc.} -proc GetLastError(): DWORD - {.stdcall, dynlib: "kernel32", importc.} -proc GetTickCount(): DWORD - {.stdcall, dynlib: "kernel32", importc.} -proc Sleep(dwMilliseconds: DWORD) - {.stdcall, dynlib: "kernel32", importc.} -proc GlobalAlloc(uFlags: UINT; dwBytes: uint): HGLOBAL - {.stdcall, dynlib: "kernel32", importc.} -proc GlobalLock(hMem: HGLOBAL): pointer - {.stdcall, dynlib: "kernel32", importc.} -proc GlobalUnlock(hMem: HGLOBAL): BOOL - {.stdcall, dynlib: "kernel32", importc.} -proc GlobalSize(hMem: HGLOBAL): uint - {.stdcall, dynlib: "kernel32", importc.} - -proc RegisterClassExW(lpwcx: ptr WNDCLASSEXW): ATOM - {.stdcall, dynlib: "user32", importc.} -proc CreateWindowExW(dwExStyle: DWORD; lpClassName, lpWindowName: ptr uint16; - dwStyle: DWORD; x, y, nWidth, nHeight: int32; - hWndParent: HWND; hMenu: HMENU; hInstance: HINSTANCE; - lpParam: pointer): HWND - {.stdcall, dynlib: "user32", importc.} -proc ShowWindow(hWnd: HWND; nCmdShow: int32): BOOL - {.stdcall, dynlib: "user32", importc.} -proc UpdateWindow(hWnd: HWND): BOOL - {.stdcall, dynlib: "user32", importc.} -proc DestroyWindow(hWnd: HWND): BOOL - {.stdcall, dynlib: "user32", importc.} -proc PostQuitMessage(nExitCode: int32) - {.stdcall, dynlib: "user32", importc.} -proc DefWindowProcW(hWnd: HWND; msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT - {.stdcall, dynlib: "user32", importc.} -proc PeekMessageW(lpMsg: ptr MSG; hWnd: HWND; wMsgFilterMin, wMsgFilterMax: UINT; - wRemoveMsg: UINT): BOOL - {.stdcall, dynlib: "user32", importc.} -proc TranslateMessage(lpMsg: ptr MSG): BOOL - {.stdcall, dynlib: "user32", importc.} -proc DispatchMessageW(lpMsg: ptr MSG): LRESULT - {.stdcall, dynlib: "user32", importc.} -proc GetClientRect(hWnd: HWND; lpRect: ptr WINAPIRECT): BOOL - {.stdcall, dynlib: "user32", importc.} -proc InvalidateRect(hWnd: HWND; lpRect: ptr WINAPIRECT; bErase: BOOL): BOOL - {.stdcall, dynlib: "user32", importc.} -proc BeginPaint(hWnd: HWND; lpPaint: ptr PAINTSTRUCT): HDC - {.stdcall, dynlib: "user32", importc.} -proc EndPaint(hWnd: HWND; lpPaint: ptr PAINTSTRUCT): BOOL - {.stdcall, dynlib: "user32", importc.} -proc GetDC(hWnd: HWND): HDC - {.stdcall, dynlib: "user32", importc.} -proc ReleaseDC(hWnd: HWND; hDC: HDC): int32 - {.stdcall, dynlib: "user32", importc.} -proc SetWindowTextW(hWnd: HWND; lpString: ptr uint16): BOOL - {.stdcall, dynlib: "user32", importc.} -proc LoadCursorW(hInstance: HINSTANCE; lpCursorName: ptr uint16): HCURSOR - {.stdcall, dynlib: "user32", importc.} -proc SetCursorWin(hCursor: HCURSOR): HCURSOR - {.stdcall, dynlib: "user32", importc: "SetCursor".} -proc GetKeyState(nVirtKey: int32): int16 - {.stdcall, dynlib: "user32", importc.} -proc OpenClipboard(hWndNewOwner: HWND): BOOL - {.stdcall, dynlib: "user32", importc.} -proc CloseClipboard(): BOOL - {.stdcall, dynlib: "user32", importc.} -proc EmptyClipboard(): BOOL - {.stdcall, dynlib: "user32", importc.} -proc GetClipboardData(uFormat: UINT): HANDLE - {.stdcall, dynlib: "user32", importc.} -proc SetClipboardData(uFormat: UINT; hMem: HANDLE): HANDLE - {.stdcall, dynlib: "user32", importc.} -proc MsgWaitForMultipleObjects(nCount: DWORD; pHandles: pointer; - fWaitAll: BOOL; dwMilliseconds: DWORD; dwWakeMask: DWORD): DWORD - {.stdcall, dynlib: "user32", importc.} - -proc CreateCompatibleDC(hdc: HDC): HDC - {.stdcall, dynlib: "gdi32", importc.} -proc CreateCompatibleBitmap(hdc: HDC; cx, cy: int32): HBITMAP - {.stdcall, dynlib: "gdi32", importc.} -proc SelectObject(hdc: HDC; h: HGDIOBJ): HGDIOBJ - {.stdcall, dynlib: "gdi32", importc.} -proc DeleteObject(ho: HGDIOBJ): BOOL - {.stdcall, dynlib: "gdi32", importc.} -proc DeleteDC(hdc: HDC): BOOL - {.stdcall, dynlib: "gdi32", importc.} -proc BitBlt(hdc: HDC; x, y, cx, cy: int32; hdcSrc: HDC; - x1, y1: int32; rop: DWORD): BOOL - {.stdcall, dynlib: "gdi32", importc.} -proc SetBkMode(hdc: HDC; mode: int32): int32 - {.stdcall, dynlib: "gdi32", importc.} -proc SetBkColor(hdc: HDC; color: COLORREF): COLORREF - {.stdcall, dynlib: "gdi32", importc.} -proc SetTextColor(hdc: HDC; color: COLORREF): COLORREF - {.stdcall, dynlib: "gdi32", importc.} -proc TextOutW(hdc: HDC; x, y: int32; lpString: ptr uint16; c: int32): BOOL - {.stdcall, dynlib: "gdi32", importc.} -proc GetTextExtentPoint32W(hdc: HDC; lpString: ptr uint16; c: int32; - lpSize: ptr SIZE): BOOL - {.stdcall, dynlib: "gdi32", importc.} -proc GetTextMetricsW(hdc: HDC; lptm: ptr TEXTMETRICW): BOOL - {.stdcall, dynlib: "gdi32", importc.} -proc CreateFontW(cHeight, cWidth, cEscapement, cOrientation, cWeight: int32; - bItalic, bUnderline, bStrikeOut: DWORD; - iCharSet: DWORD; iOutPrecision, iClipPrecision, iQuality: DWORD; - iPitchAndFamily: DWORD; pszFaceName: ptr uint16): HFONT - {.stdcall, dynlib: "gdi32", importc.} -proc CreateSolidBrush(color: COLORREF): HBRUSH - {.stdcall, dynlib: "gdi32", importc.} -proc FillRectGdi(hDC: HDC; lprc: ptr WINAPIRECT; hbr: HBRUSH): int32 - {.stdcall, dynlib: "user32", importc: "FillRect".} -proc MoveToEx(hdc: HDC; x, y: int32; lppt: ptr WINAPIPOINT): BOOL - {.stdcall, dynlib: "gdi32", importc.} -proc LineTo(hdc: HDC; x, y: int32): BOOL - {.stdcall, dynlib: "gdi32", importc.} -proc CreatePen(iStyle, cWidth: int32; color: COLORREF): HGDIOBJ - {.stdcall, dynlib: "gdi32", importc.} -proc SetPixel(hdc: HDC; x, y: int32; color: COLORREF): COLORREF - {.stdcall, dynlib: "gdi32", importc.} -proc IntersectClipRect(hdc: HDC; left, top, right, bottom: int32): int32 - {.stdcall, dynlib: "gdi32", importc.} -proc SelectClipRgn(hdc: HDC; hrgn: HRGN): int32 - {.stdcall, dynlib: "gdi32", importc.} -proc CreateRectRgn(x1, y1, x2, y2: int32): HRGN - {.stdcall, dynlib: "gdi32", importc.} -proc AddFontResourceExW(name: ptr uint16; fl: DWORD; res: pointer): int32 - {.stdcall, dynlib: "gdi32", importc.} - -# ---- Helpers ---- - -proc rgb(c: screen.Color): COLORREF {.inline.} = - COLORREF(c.r.uint32 or (c.g.uint32 shl 8) or (c.b.uint32 shl 16)) - -proc loWord(lp: LPARAM): int {.inline.} = int(lp and 0xFFFF) -proc hiWord(lp: LPARAM): int {.inline.} = int((lp shr 16) and 0xFFFF) -proc signedHiWord(wp: WPARAM): int {.inline.} = - ## For WM_MOUSEWHEEL: wParam high word is signed - cast[int16](uint16((wp shr 16) and 0xFFFF)).int - -# ---- Font handle management ---- - -type - FontSlot = object - hFont: HFONT - metrics: FontMetrics - faceName: string - size: int - -var fonts: seq[FontSlot] - -proc getFontHandle(f: screen.Font): HFONT {.inline.} = - let idx = f.int - 1 - if idx >= 0 and idx < fonts.len: fonts[idx].hFont - else: nil - -# ---- Driver state ---- - -var - gHwnd: HWND - gHinstance: HINSTANCE - gBackDC: HDC # off-screen DC for double buffering - gBackBmp: HBITMAP # off-screen bitmap - gOldBmp: HGDIOBJ # original bitmap selected into gBackDC - gWidth, gHeight: int32 - gQuitFlag: bool - gSavedClipRgn: HRGN - -# Event queue: WndProc pushes events, pollEvent/waitEvent consumes them -var eventQueue: seq[input.Event] - -proc pushEvent(e: input.Event) = - eventQueue.add e - -# ---- Back-buffer management ---- - -proc recreateBackBuffer() = - let screenDC = GetDC(gHwnd) - let newBmp = CreateCompatibleBitmap(screenDC, gWidth, gHeight) - if gBackDC == nil: - gBackDC = CreateCompatibleDC(screenDC) - if gBackBmp != nil: - discard SelectObject(gBackDC, cast[HGDIOBJ](newBmp)) - discard DeleteObject(cast[HGDIOBJ](gBackBmp)) - else: - gOldBmp = SelectObject(gBackDC, cast[HGDIOBJ](newBmp)) - gBackBmp = newBmp - # Clear to opaque black -- CreateCompatibleBitmap inits to all zeros which - # DWM interprets as fully transparent on 32-bit displays. - var rc = WINAPIRECT(left: 0, top: 0, right: gWidth, bottom: gHeight) - let blackBrush = CreateSolidBrush(0x00000000'u32) - discard FillRectGdi(gBackDC, addr rc, blackBrush) - discard DeleteObject(cast[HGDIOBJ](blackBrush)) - discard ReleaseDC(gHwnd, screenDC) - -# ---- WndProc ---- - -proc translateVK(vk: WPARAM): input.KeyCode = - let vk = vk.uint32 - if vk >= ord('A').uint32 and vk <= ord('Z').uint32: - return input.KeyCode(ord(KeyA) + (vk.int - ord('A'))) - if vk >= ord('0').uint32 and vk <= ord('9').uint32: - return input.KeyCode(ord(Key0) + (vk.int - ord('0'))) - if vk >= VK_F1 and vk <= VK_F12: - return input.KeyCode(ord(KeyF1) + (vk.int - VK_F1.int)) - case vk - of VK_RETURN: KeyEnter - of VK_SPACE: KeySpace - of VK_ESCAPE: KeyEsc - of VK_TAB: KeyTab - of VK_BACK: KeyBackspace - of VK_DELETE: KeyDelete - of VK_INSERT: KeyInsert - of VK_LEFT: KeyLeft - of VK_RIGHT: KeyRight - of VK_UP: KeyUp - of VK_DOWN: KeyDown - of VK_PRIOR: KeyPageUp - of VK_NEXT: KeyPageDown - of VK_HOME: KeyHome - of VK_END: KeyEnd - of VK_CAPITAL: KeyCapslock - of VK_OEM_COMMA: KeyComma - of VK_OEM_PERIOD: KeyPeriod - else: KeyNone - -proc getModifiers(): set[Modifier] = - if GetKeyState(VK_SHIFT.int32) < 0: result.incl ShiftPressed - if GetKeyState(VK_CONTROL.int32) < 0: result.incl CtrlPressed - if GetKeyState(VK_MENU.int32) < 0: result.incl AltPressed - -var lastClickTime: DWORD -var lastClickX, lastClickY: int -var clickCount: int - -proc pumpMessages() = - ## Drain Win32 message queue. Must be called frequently to prevent - ## the "Not Responding" ghost window (Windows triggers it after ~5s - ## of not processing sent messages). - var msg: MSG - while PeekMessageW(addr msg, nil, 0, 0, PM_REMOVE) != 0: - discard TranslateMessage(addr msg) - discard DispatchMessageW(addr msg) - if msg.message == WM_QUIT: - pushEvent(input.Event(kind: QuitEvent)) - -proc wndProc(hwnd: HWND; msg: UINT; wp: WPARAM; lp: LPARAM): LRESULT {.stdcall.} = - # Capture gHwnd from the first message -- WM_SIZE etc. arrive - # during CreateWindowExW, *before* it returns and assigns gHwnd. - if gHwnd == nil and hwnd != nil: - gHwnd = hwnd - case msg - of WM_DESTROY: - PostQuitMessage(0) - pushEvent(input.Event(kind: QuitEvent)) - return 0 - - of WM_CLOSE: - pushEvent(input.Event(kind: WindowCloseEvent)) - return 0 # don't call DestroyWindow yet; let the app decide - - of WM_ERASEBKGND: - return 1 # we handle erasing via double buffer - - of WM_SIZE: - let newW = loWord(lp).int32 - let newH = hiWord(lp).int32 - if newW > 0 and newH > 0 and (newW != gWidth or newH != gHeight): - gWidth = newW - gHeight = newH - recreateBackBuffer() - var e = input.Event(kind: WindowResizeEvent) - e.x = gWidth - e.y = gHeight - pushEvent(e) - return 0 - - of WM_PAINT: - var ps: PAINTSTRUCT - let hdc = BeginPaint(hwnd, addr ps) - if gBackDC != nil: - discard BitBlt(hdc, 0, 0, gWidth, gHeight, gBackDC, 0, 0, SRCCOPY) - discard EndPaint(hwnd, addr ps) - return 0 - - of WM_KEYDOWN: - var e = input.Event(kind: KeyDownEvent) - e.key = translateVK(wp) - e.mods = getModifiers() - pushEvent(e) - return 0 - - of WM_KEYUP: - var e = input.Event(kind: KeyUpEvent) - e.key = translateVK(wp) - e.mods = getModifiers() - pushEvent(e) - return 0 - - of WM_CHAR: - # wp is a UTF-16 code unit. For BMP characters, emit TextInputEvent. - let ch = wp.uint16 - if ch >= 32 and ch != 127: - var e = input.Event(kind: TextInputEvent) - # Convert UTF-16 to UTF-8 into e.text[0..3] - let codepoint = ch.uint32 - if codepoint < 0x80: - e.text[0] = chr(codepoint) - elif codepoint < 0x800: - e.text[0] = chr(0xC0 or (codepoint shr 6)) - e.text[1] = chr(0x80 or (codepoint and 0x3F)) - else: - e.text[0] = chr(0xE0 or (codepoint shr 12)) - e.text[1] = chr(0x80 or ((codepoint shr 6) and 0x3F)) - e.text[2] = chr(0x80 or (codepoint and 0x3F)) - pushEvent(e) - return 0 - - of WM_SETFOCUS: - pushEvent(input.Event(kind: WindowFocusGainedEvent)) - return 0 - - of WM_KILLFOCUS: - pushEvent(input.Event(kind: WindowFocusLostEvent)) - return 0 - - of WM_LBUTTONDOWN, WM_RBUTTONDOWN, WM_MBUTTONDOWN: - var e = input.Event(kind: MouseDownEvent) - e.x = loWord(lp) - e.y = hiWord(lp) - e.mods = getModifiers() - case msg - of WM_LBUTTONDOWN: e.button = LeftButton - of WM_RBUTTONDOWN: e.button = RightButton - else: e.button = MiddleButton - # Track click count for double/triple click - let now = GetTickCount() - if now - lastClickTime < 500 and - abs(e.x - lastClickX) < 4 and abs(e.y - lastClickY) < 4: - inc clickCount - else: - clickCount = 1 - lastClickTime = now - lastClickX = e.x - lastClickY = e.y - e.clicks = clickCount - pushEvent(e) - return 0 - - of WM_LBUTTONUP, WM_RBUTTONUP, WM_MBUTTONUP: - var e = input.Event(kind: MouseUpEvent) - e.x = loWord(lp) - e.y = hiWord(lp) - case msg - of WM_LBUTTONUP: e.button = LeftButton - of WM_RBUTTONUP: e.button = RightButton - else: e.button = MiddleButton - pushEvent(e) - return 0 - - of WM_MOUSEMOVE: - var e = input.Event(kind: MouseMoveEvent) - e.x = loWord(lp) - e.y = hiWord(lp) - pushEvent(e) - return 0 - - of WM_MOUSEWHEEL: - var e = input.Event(kind: MouseWheelEvent) - let delta = signedHiWord(wp) - e.y = delta div 120 # standard wheel delta - var pt = WINAPIPOINT(x: loWord(lp).LONG, y: hiWord(lp).LONG) - # wheel coords are screen-relative; could ScreenToClient but - # Nimedit only uses e.y for scroll direction - pushEvent(e) - return 0 - - else: - discard - - return DefWindowProcW(hwnd, msg, wp, lp) - -# ---- Screen hook implementations ---- - -proc winCreateWindow(layout: var ScreenLayout) = - gHinstance = GetModuleHandleW(nil) - let className = newWideCString("NimEditWinAPI") - - var wc: WNDCLASSEXW - wc.cbSize = UINT(sizeof(WNDCLASSEXW)) - wc.style = CS_HREDRAW or CS_VREDRAW - wc.lpfnWndProc = wndProc - wc.hInstance = gHinstance - wc.hCursor = LoadCursorW(nil, IDC_ARROW) - wc.lpszClassName = cast[ptr uint16](className[0].addr) - - discard RegisterClassExW(addr wc) - - let title = newWideCString("NimEdit") - gHwnd = CreateWindowExW( - 0, - cast[ptr uint16](className[0].addr), - cast[ptr uint16](title[0].addr), - WS_OVERLAPPEDWINDOW or WS_VISIBLE, - 0x80000000'i32, 0x80000000'i32, # CW_USEDEFAULT - layout.width.int32, layout.height.int32, - nil, nil, gHinstance, nil) - - if gHwnd == nil: - quit("CreateWindowExW failed") - - discard ShowWindow(gHwnd, SW_SHOW) - discard UpdateWindow(gHwnd) - - var rc: WINAPIRECT - discard GetClientRect(gHwnd, addr rc) - gWidth = rc.right - rc.left - gHeight = rc.bottom - rc.top - layout.width = gWidth - layout.height = gHeight - layout.scaleX = 1 - layout.scaleY = 1 - - recreateBackBuffer() - -proc winRefresh() = - discard InvalidateRect(gHwnd, nil, 0) - discard UpdateWindow(gHwnd) - -proc winSaveState() = - # Save current clip region - gSavedClipRgn = CreateRectRgn(0, 0, 0, 0) - # GetClipRgn not easily available; we just reset to full on restore - discard - -proc winRestoreState() = - # Restore by removing clip region - if gBackDC != nil: - discard SelectClipRgn(gBackDC, nil) - if gSavedClipRgn != nil: - discard DeleteObject(cast[HGDIOBJ](gSavedClipRgn)) - gSavedClipRgn = nil - -proc winSetClipRect(r: basetypes.Rect) = - if gBackDC != nil: - # Reset clip region first, then set new one - discard SelectClipRgn(gBackDC, nil) - discard IntersectClipRect(gBackDC, - r.x.int32, r.y.int32, (r.x + r.w).int32, (r.y + r.h).int32) - -proc winOpenFont(path: string; size: int; - metrics: var FontMetrics): screen.Font = - # Ensure the font file is available as a private resource (needed for - # fonts outside C:\Windows\Fonts, harmless for system-installed ones). - let wpath = newWideCString(path) - let FR_PRIVATE = 0x10'u32 - discard AddFontResourceExW(cast[ptr uint16](wpath[0].addr), FR_PRIVATE, nil) - - # Detect bold/italic from filename - let lpath = path.toLowerAscii() - let isBold = "bold" in lpath - let isItalic = "italic" in lpath or "oblique" in lpath - let weight = if isBold: 700'i32 else: FW_NORMAL - let italic = if isItalic: 1'u32 else: 0'u32 - let FIXED_PITCH = 1'u32 - - # Map known font filenames to their GDI face names. - # Consolas is the safe default -- ships since Vista with excellent - # ClearType hinting. - var faceName = "Consolas" - let baseName = path.extractFilename.toLowerAscii - if "dejavu" in baseName and "mono" in baseName: - faceName = "DejaVu Sans Mono" - elif "dejavu" in baseName: - faceName = "DejaVu Sans" - elif "consola" in baseName: - faceName = "Consolas" - elif "courier" in baseName: - faceName = "Courier New" - elif "arial" in baseName: - faceName = "Arial" - elif "segoe" in baseName: - faceName = "Segoe UI" - elif "cascadia" in baseName: - if "mono" in baseName: faceName = "Cascadia Mono" - else: faceName = "Cascadia Code" - elif "hack" in baseName: - faceName = "Hack" - elif "fira" in baseName and "code" in baseName: - faceName = "Fira Code" - elif "roboto" in baseName and "mono" in baseName: - faceName = "Roboto Mono" - elif "source" in baseName and "code" in baseName: - faceName = "Source Code Pro" - elif "jetbrains" in baseName: - faceName = "JetBrains Mono" - else: - # Unknown font -- use the filename stem as face name - var stem = path.extractFilename - let dot = stem.rfind('.') - if dot >= 0: stem = stem[0 ..< dot] - for suffix in ["-BoldOblique", "-BoldItalic", "-Bold", "-Oblique", - "-Italic", "-Regular"]: - if stem.endsWith(suffix): - stem = stem[0 ..< stem.len - suffix.len] - break - faceName = stem - - let wface = newWideCString(faceName) - let hf = CreateFontW( - -size.int32, # negative = character height in pixels - 0, 0, 0, # width, escapement, orientation - weight, - italic, 0, 0, # italic, underline, strikeout - DEFAULT_CHARSET.DWORD, - OUT_TT_PRECIS, - CLIP_DEFAULT_PRECIS, - CLEARTYPE_QUALITY, - FIXED_PITCH or FF_DONTCARE, - cast[ptr uint16](wface[0].addr) - ) - - if hf == nil: - return screen.Font(0) - - # Get font metrics - let oldFont = SelectObject(gBackDC, cast[HGDIOBJ](hf)) - var tm: TEXTMETRICW - discard GetTextMetricsW(gBackDC, addr tm) - discard SelectObject(gBackDC, oldFont) - - metrics.ascent = tm.tmAscent.int - metrics.descent = tm.tmDescent.int - metrics.lineHeight = tm.tmHeight.int + tm.tmExternalLeading.int - - fonts.add FontSlot(hFont: hf, metrics: metrics, faceName: path, size: size) - result = screen.Font(fonts.len) - -proc winCloseFont(f: screen.Font) = - let idx = f.int - 1 - if idx >= 0 and idx < fonts.len and fonts[idx].hFont != nil: - discard DeleteObject(cast[HGDIOBJ](fonts[idx].hFont)) - fonts[idx].hFont = nil - -proc winMeasureText(f: screen.Font; text: string): TextExtent = - let hf = getFontHandle(f) - if hf == nil or text.len == 0: return - let oldFont = SelectObject(gBackDC, cast[HGDIOBJ](hf)) - let wtext = newWideCString(text) - var sz: SIZE - discard GetTextExtentPoint32W(gBackDC, cast[ptr uint16](wtext[0].addr), - wtext.len.int32, addr sz) - discard SelectObject(gBackDC, oldFont) - result = TextExtent(w: sz.cx.int, h: sz.cy.int) - -proc winDrawText(f: screen.Font; x, y: int; text: string; - fg, bg: screen.Color): TextExtent = - let hf = getFontHandle(f) - if hf == nil or text.len == 0: return - let oldFont = SelectObject(gBackDC, cast[HGDIOBJ](hf)) - discard SetBkMode(gBackDC, OPAQUE) - discard SetBkColor(gBackDC, rgb(bg)) - discard SetTextColor(gBackDC, rgb(fg)) - let wtext = newWideCString(text) - let wlen = wtext.len.int32 - discard TextOutW(gBackDC, x.int32, y.int32, - cast[ptr uint16](wtext[0].addr), wlen) - var sz: SIZE - discard GetTextExtentPoint32W(gBackDC, cast[ptr uint16](wtext[0].addr), - wlen, addr sz) - discard SelectObject(gBackDC, oldFont) - result = TextExtent(w: sz.cx.int, h: sz.cy.int) - -proc winGetFontMetrics(f: screen.Font): FontMetrics = - let idx = f.int - 1 - if idx >= 0 and idx < fonts.len: fonts[idx].metrics - else: screen.FontMetrics() - -proc winFillRect(r: basetypes.Rect; color: screen.Color) = - let brush = CreateSolidBrush(rgb(color)) - var wr = WINAPIRECT( - left: r.x.int32, top: r.y.int32, - right: (r.x + r.w).int32, bottom: (r.y + r.h).int32) - discard FillRectGdi(gBackDC, addr wr, brush) - discard DeleteObject(cast[HGDIOBJ](brush)) - -proc winDrawLine(x1, y1, x2, y2: int; color: screen.Color) = - let pen = CreatePen(0, 1, rgb(color)) # PS_SOLID = 0 - let oldPen = SelectObject(gBackDC, pen) - discard MoveToEx(gBackDC, x1.int32, y1.int32, nil) - discard LineTo(gBackDC, x2.int32, y2.int32) - discard SelectObject(gBackDC, oldPen) - discard DeleteObject(pen) - -proc winDrawPoint(x, y: int; color: screen.Color) = - discard SetPixel(gBackDC, x.int32, y.int32, rgb(color)) - -proc winSetCursor(c: CursorKind) = - let id = case c - of curDefault, curArrow: IDC_ARROW - of curIbeam: IDC_IBEAM - of curWait: IDC_WAIT - of curCrosshair: IDC_CROSS - of curHand: IDC_HAND - of curSizeNS: IDC_SIZENS - of curSizeWE: IDC_SIZEWE - let cur = LoadCursorW(nil, id) - discard SetCursorWin(cur) - -proc winSetWindowTitle(title: string) = - if gHwnd != nil: - let wtitle = newWideCString(title) - discard SetWindowTextW(gHwnd, cast[ptr uint16](wtitle[0].addr)) - -# ---- Input hook implementations ---- - -proc winPollEvent(e: var input.Event; flags: set[InputFlag]): bool = - pumpMessages() - if eventQueue.len > 0: - e = eventQueue[0] - eventQueue.delete(0) - return true - return false - -proc winWaitEvent(e: var input.Event; timeoutMs: int; - flags: set[InputFlag]): bool = - # Check already-queued events first - if eventQueue.len > 0: - e = eventQueue[0] - eventQueue.delete(0) - return true - # Drain any pending Win32 messages before blocking - if winPollEvent(e, flags): - return true - # Pump messages in a loop with short sleeps, like SDL does internally. - # A single MWFMO(INFINITE) would block the thread entirely, causing - # Windows to show the "Not Responding" ghost window after ~5 seconds - # which then intercepts all user input -- deadlock. - let deadline = if timeoutMs < 0: uint32.high - else: GetTickCount() + timeoutMs.uint32 - while true: - let now = GetTickCount() - if now >= deadline: return false - let remaining = if timeoutMs < 0: 100'u32 - else: min(deadline - now, 100'u32) - let res = MsgWaitForMultipleObjects(0, nil, 0, remaining, QS_ALLINPUT) - if res != WAIT_TIMEOUT: - if winPollEvent(e, flags): return true - elif timeoutMs >= 0: - # Finite timeout: check if expired - if GetTickCount() >= deadline: return false - -proc winGetClipboardText(): string = - if OpenClipboard(gHwnd) == 0: return "" - let hData = GetClipboardData(CF_UNICODETEXT) - if hData != nil: - let p = cast[ptr UncheckedArray[uint16]](GlobalLock(cast[HGLOBAL](hData))) - if p != nil: - var wlen = 0 - while p[wlen] != 0: inc wlen - var ws = newWideCString("", wlen) - copyMem(addr ws[0], p, wlen * 2) - result = $ws - discard GlobalUnlock(cast[HGLOBAL](hData)) - discard CloseClipboard() - -proc winPutClipboardText(text: string) = - if OpenClipboard(gHwnd) == 0: return - discard EmptyClipboard() - let ws = newWideCString(text) - let bytes = (ws.len + 1) * 2 - let hMem = GlobalAlloc(GMEM_MOVEABLE, bytes.uint) - if hMem != nil: - let p = GlobalLock(hMem) - if p != nil: - copyMem(p, addr ws[0], bytes) - discard GlobalUnlock(hMem) - discard SetClipboardData(CF_UNICODETEXT, cast[HANDLE](hMem)) - discard CloseClipboard() - -proc winGetTicks(): int = GetTickCount().int -proc winDelay(ms: int) = - let msU = ms.DWORD - let deadline = GetTickCount() + msU - while true: - let now = GetTickCount() - if now >= deadline: break - let remaining = min(deadline - now, msU) - discard MsgWaitForMultipleObjects(0, nil, 0, remaining, QS_ALLINPUT) - pumpMessages() -proc winQuitRequest() = - gQuitFlag = true - if gHwnd != nil: - discard DestroyWindow(gHwnd) - -# ---- Init ---- - -proc setDpiAware() = - # Try the modern API first (Windows 10 1703+), fall back to older ones. - # Without this, Windows bitmap-scales the window on high-DPI displays, - # causing blurry/pixelated rendering. - type DPI_AWARENESS_CONTEXT = HANDLE - try: - proc SetProcessDpiAwarenessContext(value: DPI_AWARENESS_CONTEXT): BOOL - {.stdcall, dynlib: "user32", importc.} - let DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = cast[DPI_AWARENESS_CONTEXT](-4) - if SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) != 0: - return - except: discard - try: - proc SetProcessDPIAware(): BOOL - {.stdcall, dynlib: "user32", importc.} - discard SetProcessDPIAware() - except: discard - -proc initWinapiDriver*() = - setDpiAware() - windowRelays = WindowRelays( - createWindow: winCreateWindow, refresh: winRefresh, - saveState: winSaveState, restoreState: winRestoreState, - setClipRect: winSetClipRect, setCursor: winSetCursor, - setWindowTitle: winSetWindowTitle) - fontRelays = FontRelays( - openFont: winOpenFont, closeFont: winCloseFont, - getFontMetrics: winGetFontMetrics, measureText: winMeasureText, - drawText: winDrawText) - drawRelays = DrawRelays( - fillRect: winFillRect, drawLine: winDrawLine, drawPoint: winDrawPoint) - inputRelays = InputRelays( - pollEvent: winPollEvent, waitEvent: winWaitEvent, - getTicks: winGetTicks, delay: winDelay, - quitRequest: winQuitRequest) - clipboardRelays = ClipboardRelays( - getText: winGetClipboardText, putText: winPutClipboardText) diff --git a/app/x11_driver.nim b/app/x11_driver.nim deleted file mode 100644 index 9a372a8..0000000 --- a/app/x11_driver.nim +++ /dev/null @@ -1,938 +0,0 @@ -# X11 + Xft backend driver. Sets all hooks from core/input and core/screen. -# Uses Xlib for windowing/events, Xft for antialiased font rendering. -# Double-buffered via X Pixmap. - -import basetypes, input, screen -import std/[strutils, os] - -{.passL: "-lX11 -lXft".} - -# ---- X11 type definitions ---- - -const - libX11 = "libX11.so(|.6)" - libXft = "libXft.so(|.2)" - -type - XID = culong - Atom = culong - XTime = culong - XKeySym = culong - XBool = cint - XStatus = cint - - XRectangle {.pure.} = object - x, y: cshort - width, height: cushort - - XColor {.pure.} = object - pixel: culong - red, green, blue: cushort - flags: uint8 - pad: uint8 - - # ---- Event types ---- - - XAnyEvent {.pure.} = object - theType: cint - serial: culong - send_event: XBool - display: pointer - window: XID - - XKeyEvent {.pure.} = object - theType: cint - serial: culong - send_event: XBool - display: pointer - window: XID - root: XID - subwindow: XID - time: XTime - x, y: cint - x_root, y_root: cint - state: cuint - keycode: cuint - same_screen: XBool - - XButtonEvent {.pure.} = object - theType: cint - serial: culong - send_event: XBool - display: pointer - window: XID - root: XID - subwindow: XID - time: XTime - x, y: cint - x_root, y_root: cint - state: cuint - button: cuint - same_screen: XBool - - XMotionEvent {.pure.} = object - theType: cint - serial: culong - send_event: XBool - display: pointer - window: XID - root: XID - subwindow: XID - time: XTime - x, y: cint - x_root, y_root: cint - state: cuint - is_hint: uint8 - same_screen: XBool - - XConfigureEvent {.pure.} = object - theType: cint - serial: culong - send_event: XBool - display: pointer - event: XID - window: XID - x, y: cint - width, height: cint - border_width: cint - above: XID - override_redirect: XBool - - XExposeEvent {.pure.} = object - theType: cint - serial: culong - send_event: XBool - display: pointer - window: XID - x, y: cint - width, height: cint - count: cint - - XClientMessageEvent {.pure.} = object - theType: cint - serial: culong - send_event: XBool - display: pointer - window: XID - message_type: Atom - format: cint - data: array[5, clong] - - XFocusChangeEvent {.pure.} = object - theType: cint - serial: culong - send_event: XBool - display: pointer - window: XID - mode: cint - detail: cint - - XSelectionRequestEvent {.pure.} = object - theType: cint - serial: culong - send_event: XBool - display: pointer - owner: XID - requestor: XID - selection: Atom - target: Atom - property: Atom - time: XTime - - XSelectionEvent {.pure.} = object - theType: cint - serial: culong - send_event: XBool - display: pointer - requestor: XID - selection: Atom - target: Atom - property: Atom - time: XTime - - XEvent {.union.} = object - theType: cint - xany: XAnyEvent - xkey: XKeyEvent - xbutton: XButtonEvent - xmotion: XMotionEvent - xconfigure: XConfigureEvent - xexpose: XExposeEvent - xclient: XClientMessageEvent - xfocus: XFocusChangeEvent - xselection: XSelectionEvent - xselectionrequest: XSelectionRequestEvent - pad: array[24, clong] # XEvent is 192 bytes on 64-bit - - # ---- Xft types ---- - - XRenderColor {.pure.} = object - red, green, blue, alpha: cushort - - XftColor {.pure.} = object - pixel: culong - color: XRenderColor - - XftFont {.pure.} = object - ascent: cint - descent: cint - height: cint - max_advance_width: cint - charset: pointer - pattern: pointer - - XGlyphInfo {.pure.} = object - width, height: cushort - x, y: cshort - xOff, yOff: cshort - -# ---- X11 constants ---- - -const - None = 0.XID - CurrentTime = 0.XTime - XA_ATOM = 4.Atom - XA_STRING = 31.Atom - PropModeReplace = 0.cint - - # Event types - KeyPress = 2.cint - KeyRelease = 3.cint - ButtonPress = 4.cint - ButtonRelease = 5.cint - MotionNotify = 6.cint - FocusIn = 9.cint - FocusOut = 10.cint - Expose = 12.cint - ConfigureNotify = 22.cint - SelectionNotify = 31.cint - SelectionRequest = 30.cint - ClientMessage = 33.cint - - # Event masks - ExposureMask = 1 shl 15 - KeyPressMask = 1 shl 0 - KeyReleaseMask = 1 shl 1 - ButtonPressMask = 1 shl 2 - ButtonReleaseMask = 1 shl 3 - PointerMotionMask = 1 shl 6 - StructureNotifyMask = 1 shl 17 - FocusChangeMask = 1 shl 21 - - # Modifier masks - ShiftMask = 1'u32 - ControlMask = 4'u32 - Mod1Mask = 8'u32 # Alt - Mod4Mask = 64'u32 # Super/GUI - - # Mouse buttons - Button1 = 1'u32 - Button2 = 2'u32 - Button3 = 3'u32 - Button4 = 4'u32 # scroll up - Button5 = 5'u32 # scroll down - Button1Mask = 1'u32 shl 8 - Button2Mask = 1'u32 shl 9 - Button3Mask = 1'u32 shl 10 - - # Cursor shapes - XC_left_ptr = 68'u32 - XC_xterm = 152'u32 - XC_watch = 150'u32 - XC_crosshair = 34'u32 - XC_hand2 = 60'u32 - XC_sb_v_double_arrow = 116'u32 - XC_sb_h_double_arrow = 108'u32 - - # KeySyms - XK_a = 0x61'u - XK_z = 0x7a'u - XK_0 = 0x30'u - XK_9 = 0x39'u - XK_F1 = 0xffbe'u - XK_F12 = 0xffc9'u - XK_Return = 0xff0d'u - XK_space = 0x20'u - XK_Escape = 0xff1b'u - XK_Tab = 0xff09'u - XK_BackSpace = 0xff08'u - XK_Delete = 0xffff'u - XK_Insert = 0xff63'u - XK_Left = 0xff51'u - XK_Up = 0xff52'u - XK_Right = 0xff53'u - XK_Down = 0xff54'u - XK_Page_Up = 0xff55'u - XK_Page_Down = 0xff56'u - XK_Home = 0xff50'u - XK_End = 0xff57'u - XK_Caps_Lock = 0xffe5'u - XK_comma = 0x2c'u - XK_period = 0x2e'u - -# ---- X11 function imports ---- - -proc XOpenDisplay(name: cstring): pointer - {.cdecl, dynlib: libX11, importc.} -proc XDefaultScreen(dpy: pointer): cint - {.cdecl, dynlib: libX11, importc.} -proc XRootWindow(dpy: pointer; screen: cint): XID - {.cdecl, dynlib: libX11, importc.} -proc XDefaultVisual(dpy: pointer; screen: cint): pointer - {.cdecl, dynlib: libX11, importc.} -proc XDefaultColormap(dpy: pointer; screen: cint): XID - {.cdecl, dynlib: libX11, importc.} -proc XDefaultDepth(dpy: pointer; screen: cint): cint - {.cdecl, dynlib: libX11, importc.} -proc XBlackPixel(dpy: pointer; screen: cint): culong - {.cdecl, dynlib: libX11, importc.} -proc XWhitePixel(dpy: pointer; screen: cint): culong - {.cdecl, dynlib: libX11, importc.} -proc XCreateSimpleWindow(dpy: pointer; parent: XID; - x, y: cint; w, h, border: cuint; borderColor, bgColor: culong): XID - {.cdecl, dynlib: libX11, importc.} -proc XMapWindow(dpy: pointer; w: XID): cint - {.cdecl, dynlib: libX11, importc.} -proc XDestroyWindow(dpy: pointer; w: XID): cint - {.cdecl, dynlib: libX11, importc.} -proc XSelectInput(dpy: pointer; w: XID; mask: clong): cint - {.cdecl, dynlib: libX11, importc.} -proc XCreatePixmap(dpy: pointer; d: XID; w, h, depth: cuint): XID - {.cdecl, dynlib: libX11, importc.} -proc XFreePixmap(dpy: pointer; p: XID): cint - {.cdecl, dynlib: libX11, importc.} -proc XCreateGC(dpy: pointer; d: XID; mask: culong; values: pointer): pointer - {.cdecl, dynlib: libX11, importc.} -proc XFreeGC(dpy: pointer; gc: pointer): cint - {.cdecl, dynlib: libX11, importc.} -proc XSetForeground(dpy: pointer; gc: pointer; pixel: culong): cint - {.cdecl, dynlib: libX11, importc.} -proc XFillRectangle(dpy: pointer; d: XID; gc: pointer; - x, y: cint; w, h: cuint): cint - {.cdecl, dynlib: libX11, importc.} -proc XDrawLine(dpy: pointer; d: XID; gc: pointer; - x1, y1, x2, y2: cint): cint - {.cdecl, dynlib: libX11, importc.} -proc XDrawPoint(dpy: pointer; d: XID; gc: pointer; x, y: cint): cint - {.cdecl, dynlib: libX11, importc.} -proc XCopyArea(dpy: pointer; src, dst: XID; gc: pointer; - srcX, srcY: cint; w, h: cuint; dstX, dstY: cint): cint - {.cdecl, dynlib: libX11, importc.} -proc XSetClipRectangles(dpy: pointer; gc: pointer; - x, y: cint; rects: ptr XRectangle; n, ordering: cint): cint - {.cdecl, dynlib: libX11, importc.} -proc XSetClipMask(dpy: pointer; gc: pointer; pixmap: XID): cint - {.cdecl, dynlib: libX11, importc.} -proc XNextEvent(dpy: pointer; ev: ptr XEvent): cint - {.cdecl, dynlib: libX11, importc.} -proc XPending(dpy: pointer): cint - {.cdecl, dynlib: libX11, importc.} -proc XFlush(dpy: pointer): cint - {.cdecl, dynlib: libX11, importc.} -proc XStoreName(dpy: pointer; w: XID; name: cstring): cint - {.cdecl, dynlib: libX11, importc.} -proc XInternAtom(dpy: pointer; name: cstring; onlyIfExists: XBool): Atom - {.cdecl, dynlib: libX11, importc.} -proc XSetWMProtocols(dpy: pointer; w: XID; protocols: ptr Atom; count: cint): XStatus - {.cdecl, dynlib: libX11, importc.} -proc XCreateFontCursor(dpy: pointer; shape: cuint): XID - {.cdecl, dynlib: libX11, importc.} -proc XDefineCursor(dpy: pointer; w: XID; cursor: XID): cint - {.cdecl, dynlib: libX11, importc.} -proc XLookupString(ev: ptr XKeyEvent; buf: cstring; bufSize: cint; - keysym: ptr XKeySym; compose: pointer): cint - {.cdecl, dynlib: libX11, importc.} -proc XSetSelectionOwner(dpy: pointer; selection: Atom; owner: XID; time: XTime): cint - {.cdecl, dynlib: libX11, importc.} -proc XConvertSelection(dpy: pointer; selection, target, property: Atom; - requestor: XID; time: XTime): cint - {.cdecl, dynlib: libX11, importc.} -proc XChangeProperty(dpy: pointer; w: XID; property, propType: Atom; - format, mode: cint; data: pointer; nelements: cint): cint - {.cdecl, dynlib: libX11, importc.} -proc XGetWindowProperty(dpy: pointer; w: XID; property: Atom; - offset, length: clong; delete: XBool; reqType: Atom; - actualType: ptr Atom; actualFormat: ptr cint; - nitems, bytesAfter: ptr culong; prop: ptr pointer): cint - {.cdecl, dynlib: libX11, importc.} -proc XSendEvent(dpy: pointer; w: XID; propagate: XBool; - mask: clong; ev: ptr XEvent): XStatus - {.cdecl, dynlib: libX11, importc.} -proc XFree(data: pointer): cint - {.cdecl, dynlib: libX11, importc.} -proc XCloseDisplay(dpy: pointer): cint - {.cdecl, dynlib: libX11, importc.} - -# ---- Xft function imports ---- - -proc XftFontOpenName(dpy: pointer; screen: cint; name: cstring): ptr XftFont - {.cdecl, dynlib: libXft, importc.} -proc XftFontClose(dpy: pointer; font: ptr XftFont): void - {.cdecl, dynlib: libXft, importc.} -proc XftDrawCreate(dpy: pointer; d: XID; visual: pointer; cmap: XID): pointer - {.cdecl, dynlib: libXft, importc.} -proc XftDrawDestroy(draw: pointer): void - {.cdecl, dynlib: libXft, importc.} -proc XftDrawChange(draw: pointer; d: XID): void - {.cdecl, dynlib: libXft, importc.} -proc XftDrawStringUtf8(draw: pointer; color: ptr XftColor; font: ptr XftFont; - x, y: cint; text: cstring; len: cint): void - {.cdecl, dynlib: libXft, importc.} -proc XftTextExtentsUtf8(dpy: pointer; font: ptr XftFont; - text: cstring; len: cint; extents: ptr XGlyphInfo): void - {.cdecl, dynlib: libXft, importc.} -proc XftDrawRect(draw: pointer; color: ptr XftColor; - x, y: cint; w, h: cuint): void - {.cdecl, dynlib: libXft, importc.} -proc XftDrawSetClipRectangles(draw: pointer; x, y: cint; - rects: ptr XRectangle; n: cint): XBool - {.cdecl, dynlib: libXft, importc.} -proc XftDrawSetClip(draw: pointer; region: pointer): XBool - {.cdecl, dynlib: libXft, importc.} - -# ---- Helpers ---- - -proc toXftColor(c: screen.Color): XftColor = - result.pixel = (c.r.culong shl 16) or (c.g.culong shl 8) or c.b.culong - result.color = XRenderColor( - red: c.r.cushort * 257, - green: c.g.cushort * 257, - blue: c.b.cushort * 257, - alpha: c.a.cushort * 257) - -proc toPixel(c: screen.Color): culong {.inline.} = - (c.r.culong shl 16) or (c.g.culong shl 8) or c.b.culong - -# ---- Font handle management ---- - -type - FontSlot = object - xftFont: ptr XftFont - metrics: FontMetrics - -var fonts: seq[FontSlot] - -proc getFontPtr(f: screen.Font): ptr XftFont {.inline.} = - let idx = f.int - 1 - if idx >= 0 and idx < fonts.len: fonts[idx].xftFont - else: nil - -# ---- Driver state ---- - -var - gDisplay: pointer - gScreen: cint - gVisual: pointer - gColormap: XID - gDepth: cint - gWindow: XID - gGC: pointer # Xlib GC for primitives - gBackPixmap: XID # double buffer - gXftDraw: pointer # Xft draw context on back pixmap - gWidth, gHeight: cint - gWmDeleteWindow: Atom - gClipboard: Atom - gUtf8String: Atom - gTargets: Atom - gClipboardText: string - gClipProperty: Atom - -var eventQueue: seq[input.Event] - -proc pushEvent(e: input.Event) = - eventQueue.add e - -# ---- Back-buffer management ---- - -proc recreateBackBuffer() = - if gBackPixmap != None: - XftDrawDestroy(gXftDraw) - discard XFreePixmap(gDisplay, gBackPixmap) - gBackPixmap = XCreatePixmap(gDisplay, gWindow, - gWidth.cuint, gHeight.cuint, gDepth.cuint) - gXftDraw = XftDrawCreate(gDisplay, gBackPixmap, gVisual, gColormap) - # Clear to black - discard XSetForeground(gDisplay, gGC, 0) - discard XFillRectangle(gDisplay, gBackPixmap, gGC, 0, 0, - gWidth.cuint, gHeight.cuint) - -# ---- Key translation ---- - -proc translateKeySym(ks: XKeySym): input.KeyCode = - if ks >= XK_a and ks <= XK_z: - return input.KeyCode(ord(KeyA) + (ks.int - XK_a.int)) - if ks >= XK_0 and ks <= XK_9: - return input.KeyCode(ord(Key0) + (ks.int - XK_0.int)) - if ks >= XK_F1 and ks <= XK_F12: - return input.KeyCode(ord(KeyF1) + (ks.int - XK_F1.int)) - case ks.uint - of XK_Return: KeyEnter - of XK_space: KeySpace - of XK_Escape: KeyEsc - of XK_Tab: KeyTab - of XK_BackSpace: KeyBackspace - of XK_Delete: KeyDelete - of XK_Insert: KeyInsert - of XK_Left: KeyLeft - of XK_Right: KeyRight - of XK_Up: KeyUp - of XK_Down: KeyDown - of XK_Page_Up: KeyPageUp - of XK_Page_Down: KeyPageDown - of XK_Home: KeyHome - of XK_End: KeyEnd - of XK_Caps_Lock: KeyCapslock - of XK_comma: KeyComma - of XK_period: KeyPeriod - else: KeyNone - -proc translateMods(state: cuint): set[Modifier] = - if (state and ShiftMask) != 0: result.incl ShiftPressed - if (state and ControlMask) != 0: result.incl CtrlPressed - if (state and Mod1Mask) != 0: result.incl AltPressed - if (state and Mod4Mask) != 0: result.incl GuiPressed - -proc translateButton(button: cuint): MouseButton = - case button - of Button1: LeftButton - of Button3: RightButton - of Button2: MiddleButton - else: LeftButton - -# ---- Clipboard handling ---- - -proc handleSelectionRequest(req: XSelectionRequestEvent) = - var ev: XEvent - zeroMem(addr ev, sizeof(XEvent)) - ev.xselection.theType = SelectionNotify - ev.xselection.requestor = req.requestor - ev.xselection.selection = req.selection - ev.xselection.target = req.target - ev.xselection.time = req.time - - if req.target == gUtf8String or req.target == XA_STRING: - discard XChangeProperty(gDisplay, req.requestor, req.property, - gUtf8String, 8, PropModeReplace, - cstring(gClipboardText), gClipboardText.len.cint) - ev.xselection.property = req.property - elif req.target == gTargets: - var targets = [gUtf8String, XA_STRING, gTargets] - discard XChangeProperty(gDisplay, req.requestor, req.property, - XA_ATOM, 32, PropModeReplace, - addr targets[0], 3) - ev.xselection.property = req.property - else: - ev.xselection.property = None - - discard XSendEvent(gDisplay, req.requestor, 0, 0, addr ev) - -# ---- Event processing ---- - -var lastClickTime: XTime -var lastClickX, lastClickY: int -var clickCount: int - -proc processXEvent(xev: XEvent) = - case xev.theType - of Expose: - if xev.xexpose.count == 0 and gBackPixmap != None: - discard XCopyArea(gDisplay, gBackPixmap, gWindow, gGC, - 0, 0, gWidth.cuint, gHeight.cuint, 0, 0) - - of ConfigureNotify: - let newW = xev.xconfigure.width - let newH = xev.xconfigure.height - if newW > 0 and newH > 0 and (newW != gWidth or newH != gHeight): - gWidth = newW - gHeight = newH - recreateBackBuffer() - var e = input.Event(kind: WindowResizeEvent) - e.x = gWidth - e.y = gHeight - pushEvent(e) - - of ClientMessage: - if xev.xclient.data[0] == gWmDeleteWindow.clong: - pushEvent(input.Event(kind: WindowCloseEvent)) - - of FocusIn: - pushEvent(input.Event(kind: WindowFocusGainedEvent)) - - of FocusOut: - pushEvent(input.Event(kind: WindowFocusLostEvent)) - - of KeyPress: - var buf: array[8, char] - var ks: XKeySym - let textLen = XLookupString(unsafeAddr xev.xkey, cast[cstring](addr buf[0]), - 8, addr ks, nil) - # Key event - var e = input.Event(kind: KeyDownEvent) - e.key = translateKeySym(ks) - e.mods = translateMods(xev.xkey.state) - pushEvent(e) - # Text input (if printable) - if textLen > 0 and buf[0].uint8 >= 32 and buf[0].uint8 != 127: - var te = input.Event(kind: TextInputEvent) - for i in 0 ..< min(textLen, 4): - te.text[i] = buf[i] - pushEvent(te) - - of KeyRelease: - var ks: XKeySym - discard XLookupString(unsafeAddr xev.xkey, nil, 0, addr ks, nil) - var e = input.Event(kind: KeyUpEvent) - e.key = translateKeySym(ks) - e.mods = translateMods(xev.xkey.state) - pushEvent(e) - - of ButtonPress: - let btn = xev.xbutton.button - if btn == Button4 or btn == Button5: - # Scroll wheel - var e = input.Event(kind: MouseWheelEvent) - e.y = if btn == Button4: 1 else: -1 - pushEvent(e) - else: - var e = input.Event(kind: MouseDownEvent) - e.x = xev.xbutton.x - e.y = xev.xbutton.y - e.button = translateButton(btn) - e.mods = translateMods(xev.xbutton.state) - # Click counting for double/triple click - let now = xev.xbutton.time - if now - lastClickTime < 500 and - abs(e.x - lastClickX) < 4 and abs(e.y - lastClickY) < 4: - inc clickCount - else: - clickCount = 1 - lastClickTime = now - lastClickX = e.x - lastClickY = e.y - e.clicks = clickCount - pushEvent(e) - - of ButtonRelease: - let btn = xev.xbutton.button - if btn != Button4 and btn != Button5: - var e = input.Event(kind: MouseUpEvent) - e.x = xev.xbutton.x - e.y = xev.xbutton.y - e.button = translateButton(btn) - pushEvent(e) - - of MotionNotify: - var e = input.Event(kind: MouseMoveEvent) - e.x = xev.xmotion.x - e.y = xev.xmotion.y - pushEvent(e) - - of SelectionRequest: - handleSelectionRequest(xev.xselectionrequest) - - else: - discard - -proc drainXEvents() = - while XPending(gDisplay) > 0: - var xev: XEvent - discard XNextEvent(gDisplay, addr xev) - processXEvent(xev) - -# ---- Screen hook implementations ---- - -proc x11CreateWindow(layout: var ScreenLayout) = - gDisplay = XOpenDisplay(nil) - if gDisplay == nil: - quit("Cannot open X11 display") - gScreen = XDefaultScreen(gDisplay) - gVisual = XDefaultVisual(gDisplay, gScreen) - gColormap = XDefaultColormap(gDisplay, gScreen) - gDepth = XDefaultDepth(gDisplay, gScreen) - - gWindow = XCreateSimpleWindow(gDisplay, XRootWindow(gDisplay, gScreen), - 0, 0, layout.width.cuint, layout.height.cuint, 0, - XBlackPixel(gDisplay, gScreen), XBlackPixel(gDisplay, gScreen)) - - discard XSelectInput(gDisplay, gWindow, - (ExposureMask or KeyPressMask or KeyReleaseMask or - ButtonPressMask or ButtonReleaseMask or PointerMotionMask or - StructureNotifyMask or FocusChangeMask).clong) - - # Register WM_DELETE_WINDOW - gWmDeleteWindow = XInternAtom(gDisplay, "WM_DELETE_WINDOW", 0) - discard XSetWMProtocols(gDisplay, gWindow, addr gWmDeleteWindow, 1) - - # Clipboard atoms - gClipboard = XInternAtom(gDisplay, "CLIPBOARD", 0) - gUtf8String = XInternAtom(gDisplay, "UTF8_STRING", 0) - gTargets = XInternAtom(gDisplay, "TARGETS", 0) - gClipProperty = XInternAtom(gDisplay, "NIMEDIT_CLIP", 0) - - discard XStoreName(gDisplay, gWindow, "NimEdit") - discard XMapWindow(gDisplay, gWindow) - - gGC = XCreateGC(gDisplay, gWindow, 0, nil) - - # Wait for the first Expose/ConfigureNotify to get actual size - gWidth = layout.width.cint - gHeight = layout.height.cint - recreateBackBuffer() - - layout.scaleX = 1 - layout.scaleY = 1 - -proc x11Refresh() = - if gBackPixmap != None: - discard XCopyArea(gDisplay, gBackPixmap, gWindow, gGC, - 0, 0, gWidth.cuint, gHeight.cuint, 0, 0) - discard XFlush(gDisplay) - -proc x11SaveState() = discard -proc x11RestoreState() = - # Reset clip on both GC and XftDraw - discard XSetClipMask(gDisplay, gGC, None) - discard XftDrawSetClip(gXftDraw, nil) - -proc x11SetClipRect(r: basetypes.Rect) = - var xr = XRectangle( - x: r.x.cshort, y: r.y.cshort, - width: r.w.cushort, height: r.h.cushort) - discard XSetClipRectangles(gDisplay, gGC, 0, 0, addr xr, 1, 0) - discard XftDrawSetClipRectangles(gXftDraw, 0, 0, addr xr, 1) - -proc x11OpenFont(path: string; size: int; - metrics: var FontMetrics): screen.Font = - # Detect bold/italic from filename - let lpath = path.toLowerAscii() - let isBold = "bold" in lpath - let isItalic = "italic" in lpath or "oblique" in lpath - - # Map known font filenames to fontconfig names - var faceName = "monospace" # safe default - let baseName = path.extractFilename.toLowerAscii - if "dejavu" in baseName and "mono" in baseName: - faceName = "DejaVu Sans Mono" - elif "dejavu" in baseName: - faceName = "DejaVu Sans" - elif "consola" in baseName: - faceName = "Consolas" - elif "courier" in baseName: - faceName = "Courier New" - elif "arial" in baseName: - faceName = "Arial" - elif "cascadia" in baseName: - if "mono" in baseName: faceName = "Cascadia Mono" - else: faceName = "Cascadia Code" - elif "hack" in baseName: - faceName = "Hack" - elif "fira" in baseName and "code" in baseName: - faceName = "Fira Code" - elif "roboto" in baseName and "mono" in baseName: - faceName = "Roboto Mono" - elif "source" in baseName and "code" in baseName: - faceName = "Source Code Pro" - elif "jetbrains" in baseName: - faceName = "JetBrains Mono" - - # Build Xft/fontconfig pattern - var pattern = faceName & ":pixelsize=" & $size - if isBold: pattern &= ":weight=bold" - if isItalic: pattern &= ":slant=italic" - - let f = XftFontOpenName(gDisplay, gScreen, cstring(pattern)) - if f == nil: return screen.Font(0) - - metrics.ascent = f.ascent - metrics.descent = f.descent - metrics.lineHeight = f.height - fonts.add FontSlot(xftFont: f, metrics: metrics) - result = screen.Font(fonts.len) - -proc x11CloseFont(f: screen.Font) = - let idx = f.int - 1 - if idx >= 0 and idx < fonts.len and fonts[idx].xftFont != nil: - XftFontClose(gDisplay, fonts[idx].xftFont) - fonts[idx].xftFont = nil - -proc x11MeasureText(f: screen.Font; text: string): TextExtent = - let fp = getFontPtr(f) - if fp != nil and text.len > 0: - var extents: XGlyphInfo - XftTextExtentsUtf8(gDisplay, fp, cstring(text), text.len.cint, addr extents) - result = TextExtent(w: extents.xOff.int, h: fp.height.int) - -proc x11DrawText(f: screen.Font; x, y: int; text: string; - fg, bg: screen.Color): TextExtent = - let fp = getFontPtr(f) - if fp == nil or text.len == 0: return - # Measure first for background fill - var extents: XGlyphInfo - XftTextExtentsUtf8(gDisplay, fp, cstring(text), text.len.cint, addr extents) - result = TextExtent(w: extents.xOff.int, h: fp.height.int) - # Fill background - var bgColor = toXftColor(bg) - XftDrawRect(gXftDraw, addr bgColor, x.cint, y.cint, - extents.xOff.cuint, fp.height.cuint) - # Draw text (y is baseline, not top) - var fgColor = toXftColor(fg) - XftDrawStringUtf8(gXftDraw, addr fgColor, fp, - x.cint, (y + fp.ascent).cint, cstring(text), text.len.cint) - -proc x11GetFontMetrics(f: screen.Font): FontMetrics = - let idx = f.int - 1 - if idx >= 0 and idx < fonts.len: fonts[idx].metrics - else: screen.FontMetrics() - -proc x11FillRect(r: basetypes.Rect; color: screen.Color) = - var c = toXftColor(color) - XftDrawRect(gXftDraw, addr c, r.x.cint, r.y.cint, r.w.cuint, r.h.cuint) - -proc x11DrawLine(x1, y1, x2, y2: int; color: screen.Color) = - discard XSetForeground(gDisplay, gGC, toPixel(color)) - discard XDrawLine(gDisplay, gBackPixmap, gGC, - x1.cint, y1.cint, x2.cint, y2.cint) - -proc x11DrawPoint(x, y: int; color: screen.Color) = - discard XSetForeground(gDisplay, gGC, toPixel(color)) - discard XDrawPoint(gDisplay, gBackPixmap, gGC, x.cint, y.cint) - -proc x11SetCursor(c: CursorKind) = - let shape = case c - of curDefault, curArrow: XC_left_ptr - of curIbeam: XC_xterm - of curWait: XC_watch - of curCrosshair: XC_crosshair - of curHand: XC_hand2 - of curSizeNS: XC_sb_v_double_arrow - of curSizeWE: XC_sb_h_double_arrow - let cur = XCreateFontCursor(gDisplay, shape) - discard XDefineCursor(gDisplay, gWindow, cur) - -proc x11SetWindowTitle(title: string) = - discard XStoreName(gDisplay, gWindow, cstring(title)) - -# ---- Input hook implementations ---- - -proc x11PollEvent(e: var input.Event; flags: set[InputFlag]): bool = - drainXEvents() - if eventQueue.len > 0: - e = eventQueue[0] - eventQueue.delete(0) - return true - return false - -proc x11WaitEvent(e: var input.Event; timeoutMs: int; - flags: set[InputFlag]): bool = - if eventQueue.len > 0: - e = eventQueue[0] - eventQueue.delete(0) - return true - if x11PollEvent(e, flags): return true - - if timeoutMs < 0: - # Block efficiently until an X11 event arrives - var xev: XEvent - discard XNextEvent(gDisplay, addr xev) - processXEvent(xev) - # Drain any remaining - drainXEvents() - if eventQueue.len > 0: - e = eventQueue[0] - eventQueue.delete(0) - return true - return false - else: - # Poll with short sleeps - let deadline = getTicks() + timeoutMs - while true: - let now = getTicks() - if now >= deadline: return false - os.sleep(10) - if x11PollEvent(e, flags): return true - -proc x11GetClipboardText(): string = - discard XConvertSelection(gDisplay, gClipboard, gUtf8String, - gClipProperty, gWindow, CurrentTime) - discard XFlush(gDisplay) - # Wait for SelectionNotify (with timeout) - let deadline = getTicks() + 500 # 500ms timeout - while getTicks() < deadline: - if XPending(gDisplay) > 0: - var xev: XEvent - discard XNextEvent(gDisplay, addr xev) - if xev.theType == SelectionNotify: - if xev.xselection.property != None: - var actualType: Atom - var actualFormat: cint - var nitems, bytesAfter: culong - var data: pointer - discard XGetWindowProperty(gDisplay, gWindow, gClipProperty, - 0, 1024*1024, 1, 0, # delete=True, AnyPropertyType - addr actualType, addr actualFormat, - addr nitems, addr bytesAfter, addr data) - if data != nil: - result = $cast[cstring](data) - discard XFree(data) - return - else: - processXEvent(xev) - else: - os.sleep(5) - -proc x11PutClipboardText(text: string) = - gClipboardText = text - discard XSetSelectionOwner(gDisplay, gClipboard, gWindow, CurrentTime) - -# ---- POSIX imports for getTicks ---- - -type - ClockId {.importc: "clockid_t", header: "".} = distinct cint - Timespec {.importc: "struct timespec", header: "".} = object - tv_sec: clong - tv_nsec: clong - -proc clock_gettime(clk: ClockId; tp: var Timespec): cint - {.importc, header: "".} - -proc x11GetTicks(): int = - # Use POSIX clock - var ts: Timespec - discard clock_gettime(0.ClockId, ts) # CLOCK_REALTIME = 0 - result = int(ts.tv_sec.int64 * 1000 + ts.tv_nsec.int64 div 1_000_000) - -proc x11Delay(ms: int) = - # Drain events during delay to stay responsive - let deadline = x11GetTicks() + ms - while true: - let now = x11GetTicks() - if now >= deadline: break - drainXEvents() - os.sleep(min(deadline - now, 10)) - -proc x11QuitRequest() = - if gDisplay != nil: - discard XDestroyWindow(gDisplay, gWindow) - discard XCloseDisplay(gDisplay) - - -# ---- Init ---- - -proc initX11Driver*() = - windowRelays = WindowRelays( - createWindow: x11CreateWindow, refresh: x11Refresh, - saveState: x11SaveState, restoreState: x11RestoreState, - setClipRect: x11SetClipRect, setCursor: x11SetCursor, - setWindowTitle: x11SetWindowTitle) - fontRelays = FontRelays( - openFont: x11OpenFont, closeFont: x11CloseFont, - getFontMetrics: x11GetFontMetrics, measureText: x11MeasureText, - drawText: x11DrawText) - drawRelays = DrawRelays( - fillRect: x11FillRect, drawLine: x11DrawLine, drawPoint: x11DrawPoint) - inputRelays = InputRelays( - pollEvent: x11PollEvent, waitEvent: x11WaitEvent, - getTicks: x11GetTicks, delay: x11Delay, - quitRequest: x11QuitRequest) - clipboardRelays = ClipboardRelays( - getText: x11GetClipboardText, putText: x11PutClipboardText) diff --git a/core/basetypes.nim b/core/basetypes.nim deleted file mode 100644 index 1a6cd81..0000000 --- a/core/basetypes.nim +++ /dev/null @@ -1,22 +0,0 @@ -# Base types for the UI layer. No SDL or platform dependencies. - -type - Rect* = object - x*, y*: int - w*, h*: int - - Point* = object - x*, y*: int - - GlobalPos* = object - x*, y*, z*: int - t*: int - -proc rect*(x, y, w, h: int): Rect = - Rect(x: x, y: y, w: w, h: h) - -proc point*(x, y: int): Point = - Point(x: x, y: y) - -proc contains*(r: Rect; p: Point): bool = - p.x >= r.x and p.x < r.x + r.w and p.y >= r.y and p.y < r.y + r.h diff --git a/core/input.nim b/core/input.nim deleted file mode 100644 index 368bd1a..0000000 --- a/core/input.nim +++ /dev/null @@ -1,78 +0,0 @@ -# Platform-independent input events and relays. -# Part of the core stdlib abstraction. - -type - KeyCode* = enum - KeyNone, - KeyA, KeyB, KeyC, KeyD, KeyE, KeyF, KeyG, KeyH, KeyI, KeyJ, - KeyK, KeyL, KeyM, KeyN, KeyO, KeyP, KeyQ, KeyR, KeyS, KeyT, - KeyU, KeyV, KeyW, KeyX, KeyY, KeyZ, - Key0, Key1, Key2, Key3, Key4, Key5, Key6, Key7, Key8, Key9, - KeyF1, KeyF2, KeyF3, KeyF4, KeyF5, KeyF6, - KeyF7, KeyF8, KeyF9, KeyF10, KeyF11, KeyF12, - KeyEnter, KeySpace, KeyEsc, KeyTab, - KeyBackspace, KeyDelete, KeyInsert, - KeyLeft, KeyRight, KeyUp, KeyDown, - KeyPageUp, KeyPageDown, KeyHome, KeyEnd, - KeyCapslock, KeyComma, KeyPeriod, - - EventKind* = enum - NoEvent, - KeyDownEvent, KeyUpEvent, TextInputEvent, - MouseDownEvent, MouseUpEvent, MouseMoveEvent, MouseWheelEvent, - WindowResizeEvent, WindowCloseEvent, - WindowFocusGainedEvent, WindowFocusLostEvent, - QuitEvent - - Modifier* = enum - ShiftPressed, CtrlPressed, AltPressed, GuiPressed - - MouseButton* = enum - LeftButton, RightButton, MiddleButton - - InputFlag* = enum - WantTextInput ## show on-screen keyboard / enable IME - - Event* = object - kind*: EventKind - key*: KeyCode - mods*: set[Modifier] - text*: array[4, char] ## TextInputEvent: one UTF-8 codepoint, no alloc - x*, y*: int ## mouse position, scroll delta, or new window size - button*: MouseButton ## MouseDownEvent/MouseUpEvent: which button - clicks*: int ## number of consecutive clicks (double-click = 2) - - ClipboardRelays* = object - getText*: proc (): string {.nimcall.} - putText*: proc (text: string) {.nimcall.} - - InputRelays* = object - pollEvent*: proc (e: var Event; flags: set[InputFlag]): bool {.nimcall.} - waitEvent*: proc (e: var Event; timeoutMs: int; - flags: set[InputFlag]): bool {.nimcall.} - getTicks*: proc (): int {.nimcall.} - delay*: proc (ms: int) {.nimcall.} - quitRequest*: proc () {.nimcall.} - -var clipboardRelays* = ClipboardRelays( - getText: proc (): string = "", - putText: proc (text: string) = discard) - -var inputRelays* = InputRelays( - pollEvent: proc (e: var Event; flags: set[InputFlag]): bool = false, - waitEvent: proc (e: var Event; timeoutMs: int; - flags: set[InputFlag]): bool = false, - getTicks: proc (): int = 0, - delay: proc (ms: int) = discard, - quitRequest: proc () = discard) - -proc pollEvent*(e: var Event; flags: set[InputFlag] = {}): bool = - inputRelays.pollEvent(e, flags) -proc waitEvent*(e: var Event; timeoutMs: int = -1; - flags: set[InputFlag] = {}): bool = - inputRelays.waitEvent(e, timeoutMs, flags) -proc getClipboardText*(): string = clipboardRelays.getText() -proc putClipboardText*(text: string) = clipboardRelays.putText(text) -proc getTicks*(): int = inputRelays.getTicks() -proc delay*(ms: int) = inputRelays.delay(ms) -proc quitRequest*() = inputRelays.quitRequest() diff --git a/core/screen.nim b/core/screen.nim deleted file mode 100644 index cbdc631..0000000 --- a/core/screen.nim +++ /dev/null @@ -1,115 +0,0 @@ -# Platform-independent screen/drawing relays. -# Part of the core stdlib abstraction (plan.md). - -import basetypes - -type - Color* = object - r*, g*, b*, a*: uint8 - - Font* = distinct int ## opaque handle; 0 = invalid - Image* = distinct int ## opaque handle; 0 = invalid - - TextExtent* = object - w*, h*: int - - FontMetrics* = object - ascent*, descent*, lineHeight*: int - - ScreenLayout* = object - width*, height*: int - pitch*: int - scaleX*, scaleY*: int - fullScreen*: bool - - CursorKind* = enum - curDefault, curArrow, curIbeam, curWait, - curCrosshair, curHand, curSizeNS, curSizeWE - - WindowRelays* = object - createWindow*: proc (layout: var ScreenLayout) {.nimcall.} - refresh*: proc () {.nimcall.} - saveState*: proc () {.nimcall.} - restoreState*: proc () {.nimcall.} - setClipRect*: proc (r: Rect) {.nimcall.} - setCursor*: proc (c: CursorKind) {.nimcall.} - setWindowTitle*: proc (title: string) {.nimcall.} - - FontRelays* = object - openFont*: proc (path: string; size: int; - metrics: var FontMetrics): Font {.nimcall.} - closeFont*: proc (f: Font) {.nimcall.} - getFontMetrics*: proc (f: Font): FontMetrics {.nimcall.} - measureText*: proc (f: Font; text: string): TextExtent {.nimcall.} - drawText*: proc (f: Font; x, y: int; text: string; - fg, bg: Color): TextExtent {.nimcall.} - - DrawRelays* = object - fillRect*: proc (r: Rect; color: Color) {.nimcall.} - drawLine*: proc (x1, y1, x2, y2: int; color: Color) {.nimcall.} - drawPoint*: proc (x, y: int; color: Color) {.nimcall.} - loadImage*: proc (path: string): Image {.nimcall.} - freeImage*: proc (img: Image) {.nimcall.} - drawImage*: proc (img: Image; src, dst: Rect) {.nimcall.} - -proc `==`*(a, b: Font): bool {.borrow.} -proc `==`*(a, b: Image): bool {.borrow.} - -var windowRelays* = WindowRelays( - createWindow: proc (layout: var ScreenLayout) = discard, - refresh: proc () = discard, - saveState: proc () = discard, - restoreState: proc () = discard, - setClipRect: proc (r: Rect) = discard, - setCursor: proc (c: CursorKind) = discard, - setWindowTitle: proc (title: string) = discard) - -var fontRelays* = FontRelays( - openFont: proc (path: string; size: int; metrics: var FontMetrics): Font = Font(0), - closeFont: proc (f: Font) = discard, - getFontMetrics: proc (f: Font): FontMetrics = FontMetrics(), - measureText: proc (f: Font; text: string): TextExtent = TextExtent(), - drawText: proc (f: Font; x, y: int; text: string; - fg, bg: Color): TextExtent = TextExtent()) - -var drawRelays* = DrawRelays( - fillRect: proc (r: Rect; color: Color) = discard, - drawLine: proc (x1, y1, x2, y2: int; color: Color) = discard, - drawPoint: proc (x, y: int; color: Color) = discard, - loadImage: proc (path: string): Image = Image(0), - freeImage: proc (img: Image) = discard, - drawImage: proc (img: Image; src, dst: Rect) = discard) - -# Convenience wrappers -proc createWindow*(requestedW, requestedH: int): ScreenLayout = - result = ScreenLayout(width: requestedW, height: requestedH) - windowRelays.createWindow(result) - -proc refresh*() = windowRelays.refresh() -proc saveState*() = windowRelays.saveState() -proc restoreState*() = windowRelays.restoreState() -proc setClipRect*(r: Rect) = windowRelays.setClipRect(r) -proc setCursor*(c: CursorKind) = windowRelays.setCursor(c) -proc setWindowTitle*(title: string) = windowRelays.setWindowTitle(title) - -proc openFont*(path: string; size: int; metrics: var FontMetrics): Font = - fontRelays.openFont(path, size, metrics) -proc closeFont*(f: Font) = fontRelays.closeFont(f) -proc getFontMetrics*(f: Font): FontMetrics = fontRelays.getFontMetrics(f) -proc fontLineSkip*(f: Font): int = fontRelays.getFontMetrics(f).lineHeight -proc measureText*(f: Font; text: string): TextExtent = - fontRelays.measureText(f, text) -proc drawText*(f: Font; x, y: int; text: string; fg, bg: Color): TextExtent = - fontRelays.drawText(f, x, y, text, fg, bg) - -proc fillRect*(r: Rect; color: Color) = drawRelays.fillRect(r, color) -proc drawLine*(x1, y1, x2, y2: int; color: Color) = - drawRelays.drawLine(x1, y1, x2, y2, color) -proc drawPoint*(x, y: int; color: Color) = drawRelays.drawPoint(x, y, color) -proc loadImage*(path: string): Image = drawRelays.loadImage(path) -proc freeImage*(img: Image) = drawRelays.freeImage(img) -proc drawImage*(img: Image; src, dst: Rect) = drawRelays.drawImage(img, src, dst) - -# Color constructors -proc color*(r, g, b: uint8; a: uint8 = 255): Color = - Color(r: r, g: g, b: b, a: a) diff --git a/editor/buffer.nim b/editor/buffer.nim index 4b5d216..ccf2374 100644 --- a/editor/buffer.nim +++ b/editor/buffer.nim @@ -2,7 +2,7 @@ import strutils, unicode, intsets, compiler/ast, tables import styles, highlighters, nimscript/common, themes -import basetypes, screen, prims +import uirelays/[coords, screen], prims import buffertype, unihelp, languages from os import splitFile diff --git a/editor/buffertype.nim b/editor/buffertype.nim index f25bca7..281426c 100644 --- a/editor/buffertype.nim +++ b/editor/buffertype.nim @@ -1,7 +1,7 @@ import styles, nimscript/common, intsets, compiler/ast, tables from times import Time -import core/basetypes +import uirelays/coords type ActionKind* = enum diff --git a/nim.cfg b/nim.cfg index 4f7c01f..0751b02 100644 --- a/nim.cfg +++ b/nim.cfg @@ -2,7 +2,6 @@ # we need access to the compiler, so we'll switch our compilation path to its # source: --path: "$nim" ---path: "core" --path: "editor" --path: "app" --path: "." From 70c14fec0c9787e270356910dd7cfd943d988713 Mon Sep 17 00:00:00 2001 From: araq Date: Sun, 12 Apr 2026 17:28:44 +0200 Subject: [PATCH 11/14] now uses the uirelays based backend --- README.md | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6dd3a2d..289250b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,40 @@ NimEdit is the new upcoming slim IDE/editor for the Nim programming language. -![shot1.png](https://bitbucket.org/repo/ee5daK/images/675518804-shot1.png) \ No newline at end of file +![shot1.png](https://bitbucket.org/repo/ee5daK/images/675518804-shot1.png) + +# Installation + +NimEdit now uses the new [uirelays]() library for drawing and font rendering. This library talks directly to your +OS and has no dependencies. But it can also use an SDL 3 backend via `-d:sdl3`. + +To install the required dependencies use: + +```nim +nimble install +``` + +## Windows + +NimEdit uses the Windows API for drawing and font rendering. To the best of my knowledge nothing special +needs to be installed. (Corrections welcome!) + + +## Linux + +On Linux we use X11: + +``` +sudo apt install libx11-dev libxft-dev +``` + +You may need to install good fonts via: + +``` +sudo apt install fonts-freefont-ttf fonts-dejavu-core +``` + + +## OSX + +NimEdit uses a Cocoa-based wrapper which does not require anything beyond the typical frameworks. To the best of my knowledge nothing special needs to be installed. (Corrections welcome!) + From 28afca3f384ce080e94bff4394a142c1752ee80d Mon Sep 17 00:00:00 2001 From: araq Date: Sun, 12 Apr 2026 17:32:42 +0200 Subject: [PATCH 12/14] fixed link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 289250b..98954d2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ NimEdit is the new upcoming slim IDE/editor for the Nim programming language. # Installation -NimEdit now uses the new [uirelays]() library for drawing and font rendering. This library talks directly to your +NimEdit now uses the new [uirelays](https://github.com/nim-lang/uirelay) library for drawing and font rendering. This library talks directly to your OS and has no dependencies. But it can also use an SDL 3 backend via `-d:sdl3`. To install the required dependencies use: From 82cb2a1c5b2f50fa52398256c4de92101320c07d Mon Sep 17 00:00:00 2001 From: araq Date: Sun, 12 Apr 2026 17:49:49 +0200 Subject: [PATCH 13/14] typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 98954d2..e2ef174 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ NimEdit is the new upcoming slim IDE/editor for the Nim programming language. # Installation -NimEdit now uses the new [uirelays](https://github.com/nim-lang/uirelay) library for drawing and font rendering. This library talks directly to your +NimEdit now uses the new [uirelays](https://github.com/nim-lang/uirelays) library for drawing and font rendering. This library talks directly to your OS and has no dependencies. But it can also use an SDL 3 backend via `-d:sdl3`. To install the required dependencies use: From d613a8da242a36495742d068f31e528c75e7e144 Mon Sep 17 00:00:00 2001 From: araq Date: Sun, 12 Apr 2026 18:01:08 +0200 Subject: [PATCH 14/14] adapted to new API name --- app/nimedit.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/nimedit.nim b/app/nimedit.nim index c42b39b..be6bd70 100644 --- a/app/nimedit.nim +++ b/app/nimedit.nim @@ -1132,7 +1132,7 @@ proc mainProc(ed: Editor) = else: DefaultTimeOut # reduce CPU usage: - delay(20) + sleep(20) let newTicks = getTicks() if newTicks - oldTicks > timeout: oldTicks = newTicks