/* * 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 #include #include #include #include #include #include #include #include #include extern "C" { #include } 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 = 40; 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 SCROLLBAR_W = 8; static constexpr int TAB_SIZE = 16; static constexpr int CELL_SIZE = 26; // glyph font size inside a tile static constexpr int PREVIEW_SIZE = 40; static constexpr int NAME_SIZE = 16; static constexpr int META_SIZE = 14; static constexpr int TOAST_SIZE = 14; static constexpr uint64_t TOAST_MS = 1400; // ==== 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 int g_tab_hover = -1; // hovered tab 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 uint64_t g_toast_until = 0; static char g_toast[48] = {}; static mtk::Theme g_theme; // ==== Derived layout ==== struct Layout { Rect tabs; Rect grid; // interior area available for the tile grid 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 }; L.grid = { GRID_PAD, grid_top + GRID_PAD, 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 - SCROLLBAR_W; if (avail < CELL_TARGET) avail = CELL_TARGET; 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 - SCROLLBAR_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 int hit_tab(const Layout& L, int mx, int my) { if (my < L.tabs.y || my >= L.tabs.y + L.tabs.h) return -1; int tab_w = L.tabs.w / SET_COUNT; if (tab_w <= 0) return -1; int idx = mx / tab_w; if (idx < 0 || idx >= SET_COUNT) return -1; return idx; } 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); // Toast: Copied "" int n = 0; const char* pfx = "Copied "; while (pfx[n]) { g_toast[n] = pfx[n]; n++; } g_toast[n++] = '"'; g_toast[n++] = (char)g.byte; g_toast[n++] = '"'; g_toast[n] = '\0'; g_toast_until = montauk::get_milliseconds() + TOAST_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 ---- if (L.max_scroll > 0 && L.content_h > 0) { int track_x = L.grid.x + L.grid.w - SCROLLBAR_W + 2; int track_h = L.grid.h; int thumb_h = track_h * L.grid.h / L.content_h; if (thumb_h < 24) thumb_h = 24; int thumb_y = L.grid.y + (track_h - thumb_h) * g_scroll / L.max_scroll; c.fill_rounded_rect(track_x, thumb_y, SCROLLBAR_W - 4, thumb_h, (SCROLLBAR_W - 4) / 2, g_theme.border); } // ---- Tab bar (opaque) ---- c.fill_rect(L.tabs.x, L.tabs.y, L.tabs.w, L.tabs.h, g_theme.window_bg); int tab_w = L.tabs.w / SET_COUNT; for (int t = 0; t < SET_COUNT; t++) { int tx = t * tab_w; bool active = (t == g_set); if (t == g_tab_hover && !active) c.fill_rect(tx, L.tabs.y, tab_w, L.tabs.h, g_theme.surface_hover); Color tc = active ? g_theme.accent : (t == g_tab_hover ? g_theme.text : g_theme.text_muted); int lw = text_width(fonts::system_font, SETS[t].label, TAB_SIZE); int lh = text_height(fonts::system_font, TAB_SIZE); draw_text(c, fonts::system_font, tx + (tab_w - lw) / 2, L.tabs.y + (L.tabs.h - lh) / 2 - 1, SETS[t].label, tc, TAB_SIZE); if (active) { int uw = lw + 16; c.fill_rounded_rect(tx + (tab_w - uw) / 2, L.tabs.y + L.tabs.h - 3, uw, 3, 1, g_theme.accent); } } c.fill_rect(L.tabs.x, L.tabs.y + L.tabs.h - 1, L.tabs.w, 1, g_theme.border); // 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. int cp = codepoint_of(sg.byte); int text_x = preview.x + preview.w + 14; draw_text(c, title_font, text_x, L.footer.y + 16, sg.name, g_theme.text, NAME_SIZE); char meta[64]; snprintf(meta, sizeof(meta), "U+%04X dec %d Alt 0%d", cp, cp, (int)sg.byte); draw_text(c, fonts::system_font, text_x, L.footer.y + 16 + NAME_SIZE + 8, meta, g_theme.text_subtle, META_SIZE); // Copy button. Color btn_bg = g_theme.accent; if (g_copy_pressed) btn_bg = g_theme.accent_hover; else if (g_copy_hover) btn_bg = mtk::darken(g_theme.accent, 16); draw_button(c, title_font, L.copy_btn.x, L.copy_btn.y, L.copy_btn.w, L.copy_btn.h, "Copy", btn_bg, g_theme.accent_fg, g_theme.radius_md, TAB_SIZE); // ---- Toast (drawn last; floats over the lower-right of the grid) ---- if (g_toast[0] && montauk::get_milliseconds() < g_toast_until) { int tw = text_width(fonts::system_font, g_toast, TOAST_SIZE); int pill_w = tw + 24, pill_h = 28; int px = c.w - GRID_PAD - pill_w; int py = L.footer.y - GRID_PAD - pill_h; c.fill_rounded_rect(px, py, pill_w, pill_h, pill_h / 2, g_theme.accent); draw_text(c, fonts::system_font, px + 12, py + (pill_h - text_height(fonts::system_font, TOAST_SIZE)) / 2, g_toast, g_theme.accent_fg, TOAST_SIZE); } } // ==== 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; // Wheel scroll. if (ev.mouse.scroll != 0) { g_scroll -= ev.mouse.scroll * (L.cell_h + CELL_GAP) / 2; clamp_scroll(L); redraw = true; } int tile = hit_tile(L, mx, my); if (tile != g_hover) { g_hover = tile; redraw = true; } int tab = hit_tab(L, mx, my); if (tab != g_tab_hover) { g_tab_hover = tab; redraw = true; } bool copy_hov = L.copy_btn.contains(mx, my); if (copy_hov != g_copy_hover) { g_copy_hover = copy_hov; redraw = true; } bool pressed = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); bool released = !(ev.mouse.buttons & 1) && (ev.mouse.prev_buttons & 1); if (pressed) { 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(); 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) { // Expire the toast even when idle so the pill disappears. if (toast_shown && g_toast[0] && montauk::get_milliseconds() >= g_toast_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_tab_hover = -1; 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); }