/*
 * main.cpp
 * MontaukOS Character Map - standalone Window Server app
 *
 * Browse curated sets of characters, click (or use the keyboard) to select,
 * and copy to the system clipboard for pasting into other apps.
 *
 * Character model: MontaukOS's GUI text stack is single-byte (Windows-1252,
 * with the 0x80-0x9F block mapped to typographic punctuation in
 * gui::decode_single_byte_codepoint). The glyph cache only holds codepoints
 * 0-255, so that byte range is exactly the set of glyphs the system font can
 * render. We therefore present that set and copy the *raw byte* to the
 * clipboard: it renders identically here and in every other Montauk app
 * (copying multi-byte UTF-8 would render as mojibake in the single-byte stack).
 *
 * Copyright (c) 2026 Daniel Hammer
 */

#include <cstdint>
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <gui/gui.hpp>
#include <gui/canvas.hpp>
#include <gui/font.hpp>
#include <gui/clipboard.hpp>
#include <gui/standalone.hpp>
#include <gui/truetype.hpp>
#include <gui/mtk/theme.hpp>
#include <gui/mtk/widgets.hpp>

extern "C" {
#include <stdio.h>
}

using namespace gui;

// ==== Character data ====

struct Glyph {
    uint8_t byte;       // Windows-1252 byte value (what gets copied / rendered)
    const char* name;   // human-readable description
};

struct CharSet {
    const char* label;
    const Glyph* glyphs;
    int count;
};

// Accented Latin letters and ligatures (things you can't easily type).
static const Glyph LATIN[] = {
    {0xC0, "Latin Capital Letter A With Grave"},
    {0xC1, "Latin Capital Letter A With Acute"},
    {0xC2, "Latin Capital Letter A With Circumflex"},
    {0xC3, "Latin Capital Letter A With Tilde"},
    {0xC4, "Latin Capital Letter A With Diaeresis"},
    {0xC5, "Latin Capital Letter A With Ring Above"},
    {0xC6, "Latin Capital Letter AE"},
    {0xC7, "Latin Capital Letter C With Cedilla"},
    {0xC8, "Latin Capital Letter E With Grave"},
    {0xC9, "Latin Capital Letter E With Acute"},
    {0xCA, "Latin Capital Letter E With Circumflex"},
    {0xCB, "Latin Capital Letter E With Diaeresis"},
    {0xCC, "Latin Capital Letter I With Grave"},
    {0xCD, "Latin Capital Letter I With Acute"},
    {0xCE, "Latin Capital Letter I With Circumflex"},
    {0xCF, "Latin Capital Letter I With Diaeresis"},
    {0xD0, "Latin Capital Letter Eth"},
    {0xD1, "Latin Capital Letter N With Tilde"},
    {0xD2, "Latin Capital Letter O With Grave"},
    {0xD3, "Latin Capital Letter O With Acute"},
    {0xD4, "Latin Capital Letter O With Circumflex"},
    {0xD5, "Latin Capital Letter O With Tilde"},
    {0xD6, "Latin Capital Letter O With Diaeresis"},
    {0xD8, "Latin Capital Letter O With Stroke"},
    {0xD9, "Latin Capital Letter U With Grave"},
    {0xDA, "Latin Capital Letter U With Acute"},
    {0xDB, "Latin Capital Letter U With Circumflex"},
    {0xDC, "Latin Capital Letter U With Diaeresis"},
    {0xDD, "Latin Capital Letter Y With Acute"},
    {0xDE, "Latin Capital Letter Thorn"},
    {0x8A, "Latin Capital Letter S With Caron"},
    {0x8C, "Latin Capital Ligature OE"},
    {0x8E, "Latin Capital Letter Z With Caron"},
    {0x9F, "Latin Capital Letter Y With Diaeresis"},
    {0xDF, "Latin Small Letter Sharp S"},
    {0xE0, "Latin Small Letter A With Grave"},
    {0xE1, "Latin Small Letter A With Acute"},
    {0xE2, "Latin Small Letter A With Circumflex"},
    {0xE3, "Latin Small Letter A With Tilde"},
    {0xE4, "Latin Small Letter A With Diaeresis"},
    {0xE5, "Latin Small Letter A With Ring Above"},
    {0xE6, "Latin Small Letter AE"},
    {0xE7, "Latin Small Letter C With Cedilla"},
    {0xE8, "Latin Small Letter E With Grave"},
    {0xE9, "Latin Small Letter E With Acute"},
    {0xEA, "Latin Small Letter E With Circumflex"},
    {0xEB, "Latin Small Letter E With Diaeresis"},
    {0xEC, "Latin Small Letter I With Grave"},
    {0xED, "Latin Small Letter I With Acute"},
    {0xEE, "Latin Small Letter I With Circumflex"},
    {0xEF, "Latin Small Letter I With Diaeresis"},
    {0xF0, "Latin Small Letter Eth"},
    {0xF1, "Latin Small Letter N With Tilde"},
    {0xF2, "Latin Small Letter O With Grave"},
    {0xF3, "Latin Small Letter O With Acute"},
    {0xF4, "Latin Small Letter O With Circumflex"},
    {0xF5, "Latin Small Letter O With Tilde"},
    {0xF6, "Latin Small Letter O With Diaeresis"},
    {0xF8, "Latin Small Letter O With Stroke"},
    {0xF9, "Latin Small Letter U With Grave"},
    {0xFA, "Latin Small Letter U With Acute"},
    {0xFB, "Latin Small Letter U With Circumflex"},
    {0xFC, "Latin Small Letter U With Diaeresis"},
    {0xFD, "Latin Small Letter Y With Acute"},
    {0xFE, "Latin Small Letter Thorn"},
    {0xFF, "Latin Small Letter Y With Diaeresis"},
    {0x9A, "Latin Small Letter S With Caron"},
    {0x9C, "Latin Small Ligature OE"},
    {0x9E, "Latin Small Letter Z With Caron"},
    {0x83, "Latin Small Letter F With Hook"},
};

// Typographic punctuation.
static const Glyph PUNCT[] = {
    {0xA1, "Inverted Exclamation Mark"},
    {0xBF, "Inverted Question Mark"},
    {0x91, "Left Single Quotation Mark"},
    {0x92, "Right Single Quotation Mark"},
    {0x93, "Left Double Quotation Mark"},
    {0x94, "Right Double Quotation Mark"},
    {0x82, "Single Low-9 Quotation Mark"},
    {0x84, "Double Low-9 Quotation Mark"},
    {0xAB, "Left-Pointing Double Angle Quotation Mark"},
    {0xBB, "Right-Pointing Double Angle Quotation Mark"},
    {0x8B, "Single Left-Pointing Angle Quotation Mark"},
    {0x9B, "Single Right-Pointing Angle Quotation Mark"},
    {0x96, "En Dash"},
    {0x97, "Em Dash"},
    {0x85, "Horizontal Ellipsis"},
    {0x95, "Bullet"},
    {0xB7, "Middle Dot"},
    {0x86, "Dagger"},
    {0x87, "Double Dagger"},
    {0xB6, "Pilcrow Sign"},
    {0xA7, "Section Sign"},
    {0x89, "Per Mille Sign"},
};

// Currency symbols.
static const Glyph CURRENCY[] = {
    {0x24, "Dollar Sign"},
    {0xA2, "Cent Sign"},
    {0xA3, "Pound Sign"},
    {0xA5, "Yen Sign"},
    {0x80, "Euro Sign"},
    {0xA4, "Currency Sign"},
    {0x83, "Latin Small Letter F With Hook (Florin)"},
};

// Math, technical, and miscellaneous symbols.
static const Glyph SYMBOLS[] = {
    {0xA9, "Copyright Sign"},
    {0xAE, "Registered Sign"},
    {0x99, "Trade Mark Sign"},
    {0xB0, "Degree Sign"},
    {0xB1, "Plus-Minus Sign"},
    {0xD7, "Multiplication Sign"},
    {0xF7, "Division Sign"},
    {0xB5, "Micro Sign"},
    {0xAC, "Not Sign"},
    {0xBC, "Vulgar Fraction One Quarter"},
    {0xBD, "Vulgar Fraction One Half"},
    {0xBE, "Vulgar Fraction Three Quarters"},
    {0xB9, "Superscript One"},
    {0xB2, "Superscript Two"},
    {0xB3, "Superscript Three"},
    {0xAA, "Feminine Ordinal Indicator"},
    {0xBA, "Masculine Ordinal Indicator"},
    {0xA6, "Broken Bar"},
    {0xB4, "Acute Accent"},
    {0xA8, "Diaeresis"},
    {0xAF, "Macron"},
    {0xB8, "Cedilla"},
    {0x88, "Modifier Letter Circumflex Accent"},
    {0x98, "Small Tilde"},
};

#define SET(arr, label) { label, arr, (int)(sizeof(arr) / sizeof(arr[0])) }
static const CharSet SETS[] = {
    SET(LATIN,    "Latin"),
    SET(PUNCT,    "Punctuation"),
    SET(CURRENCY, "Currency"),
    SET(SYMBOLS,  "Symbols"),
};
static constexpr int SET_COUNT = (int)(sizeof(SETS) / sizeof(SETS[0]));

// ==== Layout constants ====

static constexpr int INIT_W    = 680;
static constexpr int INIT_H    = 448;
static constexpr int TAB_H     = 36;   // matches mtk theme.tab_h / Settings TAB_BAR_H
static constexpr int FOOTER_H  = 84;
static constexpr int GRID_PAD  = 12;
static constexpr int CELL_GAP  = 6;
static constexpr int CELL_TARGET = 54;   // desired tile edge in px

static constexpr int CELL_SIZE  = 26;    // glyph font size inside a tile
static constexpr int PREVIEW_SIZE = 40;
static constexpr int NAME_SIZE  = 19;
static constexpr int META_SIZE  = 15;

static constexpr uint64_t COPIED_MS = 1600;

// ==== App state ====

static int   g_set = 0;         // active character set index
static int   g_sel = 0;         // selected glyph index within active set
static int   g_hover = -1;      // hovered glyph index, or -1
static bool  g_copy_hover = false;
static bool  g_copy_pressed = false;
static int   g_scroll = 0;      // vertical scroll offset in px
static bool  g_sb_hover = false;    // pointer over the scrollbar track
static bool  g_sb_dragging = false; // dragging the scrollbar thumb
static int   g_sb_drag_offset = 0;  // grab point within the thumb
static uint64_t g_copied_until = 0;  // show the "copied" status until this time

static mtk::Theme g_theme;
static const char* g_tab_labels[SET_COUNT] = {};  // populated from SETS in _start

// ==== Derived layout ====

struct Layout {
    Rect tabs;
    Rect grid;      // interior area available for the tile grid (with margins)
    Rect panel;     // full content region between tabs and footer (scrollbar viewport)
    Rect footer;
    Rect copy_btn;
    int  cols;
    int  cell_w;    // tile slot width  (includes gap handling)
    int  cell_h;    // tile slot height
    int  content_h; // full scrollable height
    int  max_scroll;
};

static const CharSet& cur_set() { return SETS[g_set]; }

static Layout compute_layout(int w, int h) {
    Layout L {};
    L.tabs   = { 0, 0, w, TAB_H };
    int grid_top = TAB_H;
    L.footer = { 0, h - FOOTER_H, w, FOOTER_H };

    // Scrollbar viewport: the whole panel between tabs and footer, so the
    // shared MTK scrollbar hugs the window's right edge and spans full height
    // (matching Devices/Music) — no inset above or beside it.
    L.panel  = { 0, grid_top, w, (h - FOOTER_H) - grid_top };

    // Tile grid keeps left/top/bottom margins; its right edge leaves the
    // scrollbar's width plus a GRID_PAD gap.
    L.grid   = { GRID_PAD, grid_top + GRID_PAD,
                 w - mtk::SCROLLBAR_W - GRID_PAD * 2,
                 (h - FOOTER_H) - grid_top - GRID_PAD * 2 };
    if (L.grid.w < CELL_TARGET) L.grid.w = CELL_TARGET;
    if (L.grid.h < 1) L.grid.h = 1;

    int avail = L.grid.w;
    L.cols = (avail + CELL_GAP) / (CELL_TARGET + CELL_GAP);
    if (L.cols < 1) L.cols = 1;
    L.cell_w = (avail - CELL_GAP * (L.cols - 1)) / L.cols;
    if (L.cell_w < 8) L.cell_w = 8;
    L.cell_h = L.cell_w;

    int rows = (cur_set().count + L.cols - 1) / L.cols;
    L.content_h = rows > 0 ? rows * L.cell_h + (rows - 1) * CELL_GAP : 0;
    L.max_scroll = L.content_h - L.grid.h;
    if (L.max_scroll < 0) L.max_scroll = 0;

    // Copy button in the footer, right-aligned.
    int btn_w = 96, btn_h = 34;
    L.copy_btn = { w - GRID_PAD - btn_w,
                   L.footer.y + (FOOTER_H - btn_h) / 2, btn_w, btn_h };
    return L;
}

// Screen rect of tile `i` (may be off-screen; caller clips).
static Rect tile_rect(const Layout& L, int i) {
    int row = i / L.cols;
    int col = i % L.cols;
    int x = L.grid.x + col * (L.cell_w + CELL_GAP);
    int y = L.grid.y + row * (L.cell_h + CELL_GAP) - g_scroll;
    return { x, y, L.cell_w, L.cell_h };
}

static int hit_tile(const Layout& L, int mx, int my) {
    if (my < L.grid.y || my >= L.grid.y + L.grid.h) return -1;
    if (mx < L.grid.x || mx >= L.grid.x + L.grid.w) return -1;
    for (int i = 0; i < cur_set().count; i++) {
        Rect r = tile_rect(L, i);
        if (r.contains(mx, my)) return i;
    }
    return -1;
}

static void clamp_scroll(const Layout& L) {
    if (g_scroll > L.max_scroll) g_scroll = L.max_scroll;
    if (g_scroll < 0) g_scroll = 0;
}

// Keep the selected tile fully visible (used after keyboard navigation).
static void ensure_visible(const Layout& L) {
    int row = g_sel / L.cols;
    int top = row * (L.cell_h + CELL_GAP);
    int bot = top + L.cell_h;
    if (top - g_scroll < 0) g_scroll = top;
    else if (bot - g_scroll > L.grid.h) g_scroll = bot - L.grid.h;
    clamp_scroll(L);
}

// ==== Helpers ====

static void glyph_str(uint8_t byte, char out[2]) {
    out[0] = (char)byte;
    out[1] = '\0';
}

static int codepoint_of(uint8_t byte) {
    return decode_single_byte_codepoint((int)byte);
}

static void copy_selected() {
    const Glyph& g = cur_set().glyphs[g_sel];
    char s[2];
    glyph_str(g.byte, s);
    gui::clipboard_set_text(s, 1);
    g_copied_until = montauk::get_milliseconds() + COPIED_MS;
}

// ==== Rendering ====

static void draw_centered_glyph(Canvas& c, const Rect& r, uint8_t byte,
                                Color color, int size) {
    char s[2];
    glyph_str(byte, s);
    int tw = text_width(fonts::system_font, s, size);
    int th = text_height(fonts::system_font, size);
    draw_text(c, fonts::system_font, r.x + (r.w - tw) / 2,
              r.y + (r.h - th) / 2, s, color, size);
}

static void render(Canvas& c) {
    Layout L = compute_layout(c.w, c.h);
    clamp_scroll(L);

    c.fill(g_theme.window_bg);

    // ---- Tile grid (drawn first; header/tabs/footer are painted over any
    // spill from partially-scrolled edge rows) ----
    const CharSet& set = cur_set();
    for (int i = 0; i < set.count; i++) {
        Rect r = tile_rect(L, i);
        if (r.y + r.h <= L.grid.y || r.y >= L.grid.y + L.grid.h) continue;

        bool selected = (i == g_sel);
        bool hovered = (i == g_hover);
        Color fill = g_theme.surface;
        Color glyph = g_theme.text;
        Color border = g_theme.border;
        if (selected) {
            fill = g_theme.accent;
            glyph = g_theme.accent_fg;
            border = g_theme.accent;
        } else if (hovered) {
            fill = g_theme.surface_hover;
        }
        c.fill_rounded_rect(r.x, r.y, r.w, r.h, g_theme.radius_md, border);
        c.fill_rounded_rect(r.x + 1, r.y + 1, r.w - 2, r.h - 2,
                            g_theme.radius_md > 0 ? g_theme.radius_md - 1 : 0, fill);
        draw_centered_glyph(c, r, set.glyphs[i].byte, glyph, CELL_SIZE);
    }

    // ---- Scrollbar (shared MTK widget; no-ops when the set fits) ----
    Rect sb_track = mtk::scrollbar_track_rect(L.panel);
    Rect sb_thumb = mtk::scrollbar_thumb_rect(sb_track, L.content_h, L.grid.h, g_scroll);
    mtk::draw_scrollbar(c, sb_track, sb_thumb, g_sb_hover, g_sb_dragging, g_theme);

    // ---- Tab bar (opaque) — canonical MontaukOS top-tab style, matching the
    // Desktop Settings panel (surface bar, window_bg active tab, accent
    // underline). Covers any grid spill above the grid top. ----
    mtk::draw_tab_bar(c, L.tabs, g_tab_labels, SET_COUNT, g_set, g_theme);

    // Bold font for the footer name and Copy button.
    TrueTypeFont* title_font =
        (fonts::system_bold && fonts::system_bold->valid) ? fonts::system_bold
                                                          : fonts::system_font;

    // ---- Footer (opaque) ----
    c.fill_rect(L.footer.x, L.footer.y, L.footer.w, L.footer.h, g_theme.surface_alt);
    c.fill_rect(L.footer.x, L.footer.y, L.footer.w, 1, g_theme.border);

    const Glyph& sg = set.glyphs[g_sel];

    // Preview box.
    int pv = FOOTER_H - 20;
    Rect preview = { GRID_PAD, L.footer.y + 10, pv, pv };
    c.fill_rounded_rect(preview.x, preview.y, preview.w, preview.h,
                        g_theme.radius_md, g_theme.border);
    c.fill_rounded_rect(preview.x + 1, preview.y + 1, preview.w - 2, preview.h - 2,
                        g_theme.radius_md - 1, colors::WHITE);
    draw_centered_glyph(c, preview, sg.byte, g_theme.text, PREVIEW_SIZE);

    // Name + metadata, as a two-line block vertically centered on the preview.
    int cp = codepoint_of(sg.byte);
    int text_x = preview.x + preview.w + 14;
    char meta[64];
    snprintf(meta, sizeof(meta), "U+%04X    dec %d    Alt 0%d",
             cp, cp, (int)sg.byte);

    int name_h = text_height(title_font, NAME_SIZE);
    int meta_h = text_height(fonts::system_font, META_SIZE);
    int line_gap = 6;
    int block_y = preview.y + (preview.h - (name_h + line_gap + meta_h)) / 2;

    draw_text(c, title_font, text_x, block_y, sg.name, g_theme.text, NAME_SIZE);
    draw_text(c, fonts::system_font, text_x, block_y + name_h + line_gap,
              meta, g_theme.text_subtle, META_SIZE);

    // Copy button — canonical MTK primary button, like other Montauk apps.
    // After a copy it flips to a greyed-out "Copied" for a moment (the disabled
    // state, matching the Bluetooth app's "Scanning..." button) as confirmation.
    bool just_copied = g_copied_until && montauk::get_milliseconds() < g_copied_until;
    mtk::WidgetState copy_state = mtk::widget_state(false, g_copy_hover, !just_copied);
    mtk::draw_button(c, L.copy_btn, just_copied ? "Copied" : "Copy",
                     mtk::BUTTON_PRIMARY, copy_state, g_theme);
}

// ==== Event handling ====

static bool handle_mouse(const montauk::abi::WinEvent& ev, int w, int h) {
    Layout L = compute_layout(w, h);
    clamp_scroll(L);
    bool redraw = false;

    int mx = ev.mouse.x, my = ev.mouse.y;

    bool left_down = (ev.mouse.buttons & 1) != 0;
    bool pressed = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
    bool released = !(ev.mouse.buttons & 1) && (ev.mouse.prev_buttons & 1);

    // Scrollbar geometry (shared MTK widget).
    Rect sb_track = mtk::scrollbar_track_rect(L.panel);
    Rect sb_thumb = mtk::scrollbar_thumb_rect(sb_track, L.content_h, L.grid.h, g_scroll);
    bool scrollable = !sb_track.empty() && !sb_thumb.empty();

    // Wheel scroll.
    if (ev.mouse.scroll != 0) {
        g_scroll -= ev.mouse.scroll * (L.cell_h + CELL_GAP) / 2;
        clamp_scroll(L);
        redraw = true;
    }

    // Release ends any active thumb drag.
    if (released && g_sb_dragging) { g_sb_dragging = false; redraw = true; }

    // Active drag follows the pointer.
    if (g_sb_dragging && left_down && scrollable) {
        g_scroll = mtk::scrollbar_offset_from_thumb_top(
            sb_track, L.content_h, L.grid.h, sb_thumb.h, my - g_sb_drag_offset);
        clamp_scroll(L);
        redraw = true;
    }

    // Thumb hover highlight (only meaningful when the set scrolls).
    bool sb_hover_now = scrollable && sb_track.contains(mx, my);
    if (sb_hover_now != g_sb_hover) { g_sb_hover = sb_hover_now; redraw = true; }

    int tile = hit_tile(L, mx, my);
    if (tile != g_hover) { g_hover = tile; redraw = true; }

    int tab = mtk::hit_tab_bar(L.tabs, SET_COUNT, mx, my);

    bool copy_hov = L.copy_btn.contains(mx, my);
    if (copy_hov != g_copy_hover) { g_copy_hover = copy_hov; redraw = true; }

    if (pressed) {
        if (scrollable && sb_thumb.contains(mx, my)) {
            // Grab the thumb.
            g_sb_dragging = true;
            g_sb_drag_offset = my - sb_thumb.y;
            redraw = true;
        } else if (scrollable && sb_track.contains(mx, my)) {
            // Click the track: jump the thumb under the pointer, then drag.
            g_sb_dragging = true;
            g_sb_drag_offset = sb_thumb.h / 2;
            g_scroll = mtk::scrollbar_offset_from_thumb_top(
                sb_track, L.content_h, L.grid.h, sb_thumb.h, my - sb_thumb.h / 2);
            clamp_scroll(L);
            redraw = true;
        } else if (tab >= 0 && tab != g_set) {
            g_set = tab;
            g_sel = 0;
            g_scroll = 0;
            redraw = true;
        } else if (tile >= 0) {
            g_sel = tile;
            copy_selected();          // click a tile = select + copy
            redraw = true;
        } else if (copy_hov) {
            g_copy_pressed = true;
            redraw = true;
        }
    }

    if (released && g_copy_pressed) {
        g_copy_pressed = false;
        if (copy_hov) copy_selected();
        redraw = true;
    }

    return redraw;
}

static bool handle_key(const montauk::abi::KeyEvent& key, int w, int h) {
    if (!key.pressed) return false;
    Layout L = compute_layout(w, h);
    clamp_scroll(L);
    const int count = cur_set().count;
    bool redraw = false;

    switch (key.scancode) {
    case 0x4B: // Left
        if (g_sel > 0) { g_sel--; ensure_visible(L); redraw = true; }
        break;
    case 0x4D: // Right
        if (g_sel < count - 1) { g_sel++; ensure_visible(L); redraw = true; }
        break;
    case 0x48: // Up
        if (g_sel - L.cols >= 0) { g_sel -= L.cols; ensure_visible(L); redraw = true; }
        break;
    case 0x50: // Down
        if (g_sel + L.cols < count) { g_sel += L.cols; ensure_visible(L); redraw = true; }
        break;
    case 0x0F: // Tab -> next character set
        g_set = (g_set + (key.shift ? SET_COUNT - 1 : 1)) % SET_COUNT;
        g_sel = 0; g_scroll = 0; redraw = true;
        break;
    case 0x1C: // Enter
    case 0x39: // Space
        copy_selected();
        redraw = true;
        break;
    default:
        break;
    }
    return redraw;
}

extern "C" void _start() {
    fonts::init();
    g_theme = mtk::make_theme();
    for (int i = 0; i < SET_COUNT; i++) g_tab_labels[i] = SETS[i].label;

    WsWindow win;
    if (!win.create("Character Map", INIT_W, INIT_H))
        montauk::exit(1);

    {
        Canvas canvas = win.canvas();
        render(canvas);
    }
    win.present();

    bool toast_shown = true;
    while (win.id >= 0 && !win.closed) {
        montauk::abi::WinEvent ev;
        int r = win.poll(&ev);

        if (r < 0) break;
        if (r == 0) {
            // Clear the "copied" status once it expires, even when idle.
            if (toast_shown && g_copied_until &&
                montauk::get_milliseconds() >= g_copied_until) {
                Canvas canvas = win.canvas();
                render(canvas);
                win.present();
                toast_shown = false;
            }
            montauk::sleep_ms(16);
            continue;
        }

        bool redraw = false;
        if (ev.type == 3) break;                 // close
        if (ev.type == 2 || ev.type == 4) {      // resize / scale
            g_hover = -1;
            g_sb_hover = false;
            g_sb_dragging = false;
            redraw = true;
        } else if (ev.type == 1) {
            redraw = handle_mouse(ev, win.width, win.height);
        } else if (ev.type == 0) {
            redraw = handle_key(ev.key, win.width, win.height);
        }

        if (redraw) {
            Canvas canvas = win.canvas();
            render(canvas);
            win.present();
            toast_shown = true;
        }
    }

    win.destroy();
    montauk::exit(0);
}
