/* * main.cpp * MontaukOS Time Zone configuration applet * Copyright (c) 2026 Daniel Hammer */ #include #include #include #include #include #include #include #include #include #include extern "C" { #include } using namespace gui; static constexpr int INIT_W = 640; static constexpr int INIT_H = 460; static constexpr int HEADER_H = 14; static constexpr int FOOTER_H = 58; static constexpr int PAD = 16; static constexpr int GAP = 12; static constexpr int LABEL_H = 24; static constexpr int ROW_H = 30; static constexpr int MAX_COUNTRIES = 320; struct CountryEntry { char key[64]; // Full TOML table key, e.g. countries.NO char code[24]; // Country code suffix, e.g. NO const char* name; const char* default_tz; const char* note; bool dst_enabled; char dst_rule[32]; montauk::toml::Value* zones; }; static WsWindow g_win; static montauk::toml::Doc g_data; static bool g_data_loaded = false; static CountryEntry g_countries[MAX_COUNTRIES]; static int g_country_count = 0; static int g_selected_country = 0; static int g_selected_zone = 0; static int g_saved_country = 0; static int g_saved_zone = 0; static int g_country_scroll = 0; static int g_zone_scroll = 0; static bool g_country_sb_dragging = false; static int g_country_sb_drag_offset = 0; static int g_focus_pane = 0; // 0=country list, 1=zone list static int g_mouse_x = -1; static int g_mouse_y = -1; static bool g_dirty = false; static char g_status[160] = {}; static uint64_t g_status_time = 0; static Color g_accent = colors::ACCENT; static void safe_copy(char* dst, int cap, const char* src) { montauk::strncpy(dst, src ? src : "", cap); } static int str_compare_ci(const char* a, const char* b) { while (*a && *b) { char ca = (*a >= 'A' && *a <= 'Z') ? (*a + 32) : *a; char cb = (*b >= 'A' && *b <= 'Z') ? (*b + 32) : *b; if (ca != cb) return ca - cb; a++; b++; } return (unsigned char)*a - (unsigned char)*b; } static void set_status(const char* msg) { safe_copy(g_status, sizeof(g_status), msg); g_status_time = montauk::get_milliseconds(); } static bool status_visible() { return g_status[0] && (montauk::get_milliseconds() - g_status_time < 5000); } static mtk::Theme app_theme() { mtk::Theme theme = mtk::make_theme(g_accent); theme.disabled_bg = Color::from_rgb(0xE4, 0xE4, 0xE4); theme.disabled_fg = Color::from_rgb(0x9A, 0x9A, 0x9A); return theme; } static void apply_scale(int scale) { switch (scale) { case 0: fonts::UI_SIZE = 14; fonts::TITLE_SIZE = 14; fonts::TERM_SIZE = 14; fonts::LARGE_SIZE = 22; break; case 2: fonts::UI_SIZE = 22; fonts::TITLE_SIZE = 22; fonts::TERM_SIZE = 22; fonts::LARGE_SIZE = 34; break; default: fonts::UI_SIZE = 18; fonts::TITLE_SIZE = 18; fonts::TERM_SIZE = 18; fonts::LARGE_SIZE = 28; break; } } static void load_accent() { g_accent = mtk::load_system_accent(); } static void build_key(char* out, int cap, const char* base, const char* field) { snprintf(out, cap, "%s.%s", base, field); } static const char* zone_name(const CountryEntry& country, int index) { if (!country.zones || index < 0 || index >= country.zones->array.count) return ""; montauk::toml::Value* item = country.zones->array.items[index]; if (!item || item->type != montauk::toml::Type::String) return ""; if (country.zones->type == montauk::toml::Type::Array) return item->str ? item->str : ""; char prefix[96]; snprintf(prefix, sizeof(prefix), "%s.timezones.", country.key); return (item->key && montauk::starts_with(item->key, prefix)) ? item->key + montauk::slen(prefix) : ""; } static int zone_count(const CountryEntry& country) { return country.zones ? country.zones->array.count : 0; } static int find_zone_index(const CountryEntry& country, const char* zone) { if (!zone || !zone[0]) return -1; int count = zone_count(country); for (int i = 0; i < count; i++) { if (montauk::streq(zone_name(country, i), zone)) return i; } return -1; } static int default_zone_index(const CountryEntry& country) { int index = find_zone_index(country, country.default_tz); return index >= 0 ? index : 0; } static const char* zone_offset_string(const CountryEntry& country, int index) { if (!country.zones || country.zones->type != montauk::toml::Type::Table) return ""; if (index < 0 || index >= country.zones->array.count) return ""; montauk::toml::Value* item = country.zones->array.items[index]; if (!item || item->type != montauk::toml::Type::String) return ""; return item->str ? item->str : ""; } static bool parse_offset_minutes(const char* value, int* out_minutes) { if (!value || !value[0]) return false; int sign = 1; int pos = 0; if (value[pos] == '+') { pos++; } else if (value[pos] == '-') { sign = -1; pos++; } if (value[pos] < '0' || value[pos] > '9') return false; int hours = 0; while (value[pos] >= '0' && value[pos] <= '9') { hours = hours * 10 + (value[pos] - '0'); pos++; } int minutes = 0; if (value[pos] == ':') { pos++; if (value[pos] < '0' || value[pos] > '9') return false; while (value[pos] >= '0' && value[pos] <= '9') { minutes = minutes * 10 + (value[pos] - '0'); pos++; } } if (value[pos] != '\0') return false; if (hours > 24 || minutes > 59) return false; if (out_minutes) *out_minutes = sign * (hours * 60 + minutes); return true; } static int visible_rows(const Rect& list) { int rows = list.h / ROW_H; return rows > 0 ? rows : 1; } static void clamp_scrolls() { Rect country_list = {}; Rect zone_list = {}; int left_w = gui_min(250, (g_win.width - PAD * 2 - GAP) / 2); int list_y = HEADER_H + LABEL_H; int list_h = gui_max(g_win.height - HEADER_H - FOOTER_H - LABEL_H - PAD, ROW_H); country_list = {PAD, list_y, left_w, list_h}; zone_list = {PAD + left_w + GAP, list_y, g_win.width - PAD * 2 - GAP - left_w, list_h}; int country_max = gui_max(0, g_country_count - visible_rows(country_list)); if (g_country_scroll < 0) g_country_scroll = 0; if (g_country_scroll > country_max) g_country_scroll = country_max; int zcount = (g_selected_country >= 0 && g_selected_country < g_country_count) ? zone_count(g_countries[g_selected_country]) : 0; int zone_max = gui_max(0, zcount - visible_rows(zone_list)); if (g_zone_scroll < 0) g_zone_scroll = 0; if (g_zone_scroll > zone_max) g_zone_scroll = zone_max; } static void ensure_country_visible() { Rect list = {PAD, HEADER_H + LABEL_H, 250, gui_max(g_win.height - HEADER_H - FOOTER_H - LABEL_H - PAD, ROW_H)}; int rows = visible_rows(list); if (g_selected_country < g_country_scroll) g_country_scroll = g_selected_country; else if (g_selected_country >= g_country_scroll + rows) g_country_scroll = g_selected_country - rows + 1; } static void ensure_zone_visible() { int left_w = gui_min(250, (g_win.width - PAD * 2 - GAP) / 2); Rect list = {PAD + left_w + GAP, HEADER_H + LABEL_H, g_win.width - PAD * 2 - GAP - left_w, gui_max(g_win.height - HEADER_H - FOOTER_H - LABEL_H - PAD, ROW_H)}; int rows = visible_rows(list); if (g_selected_zone < g_zone_scroll) g_zone_scroll = g_selected_zone; else if (g_selected_zone >= g_zone_scroll + rows) g_zone_scroll = g_selected_zone - rows + 1; } static int find_country_by_code(const char* code) { if (!code || !code[0]) return -1; for (int i = 0; i < g_country_count; i++) { if (montauk::streq(g_countries[i].code, code)) return i; } return -1; } static void sort_countries() { for (int i = 1; i < g_country_count; i++) { CountryEntry tmp = g_countries[i]; int j = i - 1; while (j >= 0 && str_compare_ci(tmp.name, g_countries[j].name) < 0) { g_countries[j + 1] = g_countries[j]; j--; } g_countries[j + 1] = tmp; } } static void load_timezone_data() { g_data = montauk::config::load("timezonedata"); g_data_loaded = true; g_country_count = 0; static constexpr const char* prefix = "countries."; int prefix_len = montauk::slen(prefix); for (int i = 0; i < g_data.entries.count && g_country_count < MAX_COUNTRIES; i++) { montauk::toml::Value* item = g_data.entries.items[i]; if (!item || item->type != montauk::toml::Type::Table || !item->key) continue; if (!montauk::starts_with(item->key, prefix)) continue; CountryEntry entry = {}; safe_copy(entry.key, sizeof(entry.key), item->key); safe_copy(entry.code, sizeof(entry.code), item->key + prefix_len); char key[128]; build_key(key, sizeof(key), item->key, "name"); entry.name = g_data.get_string(key, entry.code); build_key(key, sizeof(key), item->key, "timezones"); entry.zones = g_data.get(key); if (!entry.zones || entry.zones->array.count <= 0) continue; if (entry.zones->type != montauk::toml::Type::Array && entry.zones->type != montauk::toml::Type::Table) { continue; } build_key(key, sizeof(key), item->key, "default_tz"); entry.default_tz = g_data.get_string(key, zone_name(entry, 0)); build_key(key, sizeof(key), item->key, "dst_enabled"); entry.dst_enabled = g_data.get_bool(key, false); build_key(key, sizeof(key), item->key, "dst_rules.rule"); safe_copy(entry.dst_rule, sizeof(entry.dst_rule), g_data.get_string(key, "")); build_key(key, sizeof(key), item->key, "note"); entry.note = g_data.get_string(key, ""); g_countries[g_country_count++] = entry; } sort_countries(); } static void load_saved_selection() { char saved_code[48] = "NO"; char saved_zone[96] = ""; auto cfg = montauk::config::load("timezone"); safe_copy(saved_code, sizeof(saved_code), cfg.get_string("timezone.country_code", saved_code)); safe_copy(saved_zone, sizeof(saved_zone), cfg.get_string("timezone.zone", "")); cfg.destroy(); int country = find_country_by_code(saved_code); if (country < 0) country = find_country_by_code("NO"); if (country < 0 && g_country_count > 0) country = 0; g_selected_country = country >= 0 ? country : 0; if (g_country_count > 0) { CountryEntry& c = g_countries[g_selected_country]; int zone = find_zone_index(c, saved_zone); if (zone < 0) zone = default_zone_index(c); g_selected_zone = zone; } else { g_selected_zone = 0; } g_saved_country = g_selected_country; g_saved_zone = g_selected_zone; g_dirty = false; ensure_country_visible(); ensure_zone_visible(); clamp_scrolls(); } static void fit_text(char* out, int cap, const char* text, int max_w) { safe_copy(out, cap, text); if (text_width(out) <= max_w) return; static constexpr const char* ellipsis = "..."; int len = montauk::slen(out); while (len > 0) { out[--len] = '\0'; char candidate[160]; snprintf(candidate, sizeof(candidate), "%s%s", out, ellipsis); if (text_width(candidate) <= max_w) { safe_copy(out, cap, candidate); return; } } safe_copy(out, cap, ellipsis); } static void format_offset(int minutes, char* out, int cap) { int h = minutes / 60; int m = minutes % 60; if (m < 0) m = -m; if (m) snprintf(out, cap, "UTC%s%d:%02d", h >= 0 ? "+" : "", h, m); else snprintf(out, cap, "UTC%s%d", h >= 0 ? "+" : "", h); } static bool selected_offset_minutes(int* out_minutes) { if (g_selected_country < 0 || g_selected_country >= g_country_count) return false; CountryEntry& country = g_countries[g_selected_country]; char key[128]; const char* offset = zone_offset_string(country, g_selected_zone); if (parse_offset_minutes(offset, out_minutes)) return true; build_key(key, sizeof(key), country.key, "offsets"); montauk::toml::Value* offsets = g_data.get_array(key); if (offsets && g_selected_zone >= 0 && g_selected_zone < offsets->array.count) { montauk::toml::Value* item = offsets->array.items[g_selected_zone]; if (item && item->type == montauk::toml::Type::Int) { if (out_minutes) *out_minutes = (int)item->ival; return true; } } build_key(key, sizeof(key), country.key, "offset_minutes"); montauk::toml::Value* single = g_data.get(key); if (single && single->type == montauk::toml::Type::Int) { if (out_minutes) *out_minutes = (int)single->ival; return true; } return false; } static int dst_offset_change_minutes(const CountryEntry& country) { if (!country.dst_enabled || !country.dst_rule[0]) return 0; char key[96]; snprintf(key, sizeof(key), "dst_rules.%s.offset_change", country.dst_rule); return (int)(g_data.get_int(key, 0) / 60); } static void save_selection() { if (g_country_count <= 0) return; CountryEntry& country = g_countries[g_selected_country]; const char* zone = zone_name(country, g_selected_zone); auto cfg = montauk::config::load("timezone"); montauk::config::set_string(&cfg, "timezone.country_code", country.code); montauk::config::set_string(&cfg, "timezone.country", country.name); montauk::config::set_string(&cfg, "timezone.zone", zone); montauk::config::set_bool(&cfg, "timezone.dst_enabled", country.dst_enabled); if (country.dst_rule[0]) montauk::config::set_string(&cfg, "timezone.dst_rule", country.dst_rule); else montauk::config::unset(&cfg, "timezone.dst_rule"); int dst_change = dst_offset_change_minutes(country); if (country.dst_enabled && dst_change != 0) montauk::config::set_int(&cfg, "timezone.dst_offset_minutes", dst_change); else montauk::config::unset(&cfg, "timezone.dst_offset_minutes"); int offset = 0; bool has_offset = selected_offset_minutes(&offset); if (has_offset) { const char* offset_text = zone_offset_string(country, g_selected_zone); if (offset_text[0]) montauk::config::set_string(&cfg, "timezone.offset", offset_text); montauk::config::set_int(&cfg, "timezone.offset_minutes", offset); montauk::settz(offset); } else { montauk::config::unset(&cfg, "timezone.offset"); } int ret = montauk::config::save("timezone", &cfg); cfg.destroy(); if (ret < 0) { set_status("Could not save time zone"); return; } g_saved_country = g_selected_country; g_saved_zone = g_selected_zone; g_dirty = false; set_status(has_offset ? "Saved and applied" : "Saved selection; clock offset unchanged"); } static void revert_selection() { g_selected_country = g_saved_country; g_selected_zone = g_saved_zone; g_dirty = false; ensure_country_visible(); ensure_zone_visible(); clamp_scrolls(); set_status("Selection reverted"); } static Rect country_list_rect() { int left_w = gui_min(250, (g_win.width - PAD * 2 - GAP) / 2); return {PAD, HEADER_H + LABEL_H, left_w, gui_max(g_win.height - HEADER_H - FOOTER_H - LABEL_H - PAD, ROW_H)}; } static Rect zone_list_rect() { Rect country = country_list_rect(); return {country.x + country.w + GAP, country.y, g_win.width - PAD * 2 - GAP - country.w, country.h}; } static Rect apply_button_rect() { return {g_win.width - PAD - 104, g_win.height - FOOTER_H + 14, 104, 30}; } static Rect revert_button_rect() { return {g_win.width - PAD - 104 - GAP - 92, g_win.height - FOOTER_H + 14, 92, 30}; } static bool mouse_in_rect(const Rect& rect) { return rect.contains(g_mouse_x, g_mouse_y); } static mtk::WidgetState button_state(const Rect& rect, bool enabled, bool active = false) { return mtk::widget_state(active, mouse_in_rect(rect), enabled); } static bool hover_changed(const Rect& rect, bool enabled, int prev_x, int prev_y, int next_x, int next_y) { if (!enabled) return false; return rect.contains(prev_x, prev_y) != rect.contains(next_x, next_y); } static void draw_list_frame(Canvas& canvas, const Rect& list, const mtk::Theme& theme) { canvas.fill_rect(list.x, list.y, list.w, list.h, theme.surface_alt); canvas.rect(list.x, list.y, list.w, list.h, theme.border); } static int country_content_extent() { return g_country_count * ROW_H; } static int country_view_extent(const Rect& list) { return visible_rows(list) * ROW_H; } static bool country_scrollbar_visible(const Rect& list) { return mtk::scrollbar_needed(country_content_extent(), country_view_extent(list)); } static Rect country_scrollbar_track(const Rect& list) { if (!country_scrollbar_visible(list)) return {0, 0, 0, 0}; Rect inner = {list.x + 1, list.y + 2, list.w - 2, list.h - 4}; return mtk::scrollbar_track_rect(inner); } static Rect country_scrollbar_thumb(const Rect& list) { Rect track = country_scrollbar_track(list); if (track.empty()) return {0, 0, 0, 0}; return mtk::scrollbar_thumb_rect(track, country_content_extent(), country_view_extent(list), g_country_scroll * ROW_H); } static void draw_country_list(Canvas& canvas, const Rect& list, const mtk::Theme& theme) { draw_list_frame(canvas, list, theme); bool sb_visible = country_scrollbar_visible(list); int item_w = list.w - 4 - (sb_visible ? mtk::SCROLLBAR_W : 0); int rows = visible_rows(list); for (int row = 0; row < rows; row++) { int index = g_country_scroll + row; if (index >= g_country_count) break; Rect item = {list.x + 2, list.y + 2 + row * ROW_H, item_w, ROW_H - 2}; bool selected = index == g_selected_country; mtk::draw_list_row(canvas, item, selected, (index & 1) != 0, theme); char label[128]; fit_text(label, sizeof(label), g_countries[index].name, item.w - 16); canvas.text(item.x + 8, item.y + (item.h - system_font_height()) / 2, label, selected ? theme.accent_fg : theme.text); } if (sb_visible) { Rect track = country_scrollbar_track(list); Rect thumb = country_scrollbar_thumb(list); bool hovered = track.contains(g_mouse_x, g_mouse_y) || g_country_sb_dragging; mtk::draw_scrollbar(canvas, track, thumb, hovered, g_country_sb_dragging, theme); } } static void draw_zone_list(Canvas& canvas, const Rect& list, const mtk::Theme& theme) { draw_list_frame(canvas, list, theme); if (g_selected_country < 0 || g_selected_country >= g_country_count) return; CountryEntry& country = g_countries[g_selected_country]; int count = zone_count(country); int rows = visible_rows(list); for (int row = 0; row < rows; row++) { int index = g_zone_scroll + row; if (index >= count) break; Rect item = {list.x + 2, list.y + 2 + row * ROW_H, list.w - 4, ROW_H - 2}; bool selected = index == g_selected_zone; mtk::draw_list_row(canvas, item, selected, (index & 1) != 0, theme); char label[128]; fit_text(label, sizeof(label), zone_name(country, index), item.w - 16); canvas.text(item.x + 8, item.y + (item.h - system_font_height()) / 2, label, selected ? theme.accent_fg : theme.text); } } static void draw_details(Canvas& canvas, const mtk::Theme& theme) { if (g_country_count <= 0) return; Rect country = country_list_rect(); Rect zone = zone_list_rect(); int x = zone.x; int y = zone.y + zone.h + 8; if (y > g_win.height - FOOTER_H - 24) return; CountryEntry& entry = g_countries[g_selected_country]; char line[192]; snprintf(line, sizeof(line), "%s / %s", entry.name, zone_name(entry, g_selected_zone)); fit_text(line, sizeof(line), line, g_win.width - x - PAD); canvas.text(x, y, line, theme.text); int offset = 0; char right[96]; if (selected_offset_minutes(&offset)) { format_offset(offset, right, sizeof(right)); } else { format_offset(montauk::gettz(), right, sizeof(right)); char tmp[96]; snprintf(tmp, sizeof(tmp), "Clock: %s", right); safe_copy(right, sizeof(right), tmp); } canvas.text(country.x, y, right, theme.text_subtle); y += system_font_height() + 6; if (entry.dst_enabled) { int dst_change = dst_offset_change_minutes(entry); if (dst_change) snprintf(line, sizeof(line), "DST: %s (+%d min)", entry.dst_rule[0] ? entry.dst_rule : "enabled", dst_change); else snprintf(line, sizeof(line), "DST: %s", entry.dst_rule[0] ? entry.dst_rule : "enabled"); } else { safe_copy(line, sizeof(line), "DST: not used"); } canvas.text(x, y, line, theme.text_subtle); if (entry.note && entry.note[0]) { char note[192]; fit_text(note, sizeof(note), entry.note, g_win.width - x - PAD); canvas.text(x, y + system_font_height() + 4, note, theme.text_subtle); } } static void render() { mtk::StandaloneHost host(&g_win); Canvas canvas = host.canvas(); mtk::Theme theme = app_theme(); canvas.fill(theme.window_bg); if (g_country_count <= 0) { canvas.text(PAD, HEADER_H + 24, "No time zone data was found in 0:/config/timezonedata.toml", theme.danger); host.present(); return; } Rect country = country_list_rect(); Rect zone = zone_list_rect(); canvas.text(country.x, HEADER_H + 6, "Country", theme.text_subtle); canvas.text(zone.x, HEADER_H + 6, "Time Zone", theme.text_subtle); draw_country_list(canvas, country, theme); draw_zone_list(canvas, zone, theme); draw_details(canvas, theme); Rect footer = {0, g_win.height - FOOTER_H, g_win.width, FOOTER_H}; canvas.fill_rect(footer.x, footer.y, footer.w, footer.h, theme.surface); mtk::draw_separator(canvas, 0, footer.y, g_win.width, theme); const char* footer_text = status_visible() ? g_status : (g_dirty ? "Unsaved time zone selection" : "Selection saved"); int max_footer_text = revert_button_rect().x - PAD - GAP; char footer_label[160]; fit_text(footer_label, sizeof(footer_label), footer_text, max_footer_text); canvas.text(PAD, footer.y + (FOOTER_H - system_font_height()) / 2, footer_label, g_dirty ? theme.text : theme.text_subtle); Rect revert = revert_button_rect(); Rect apply = apply_button_rect(); mtk::draw_button(canvas, revert, "Revert", mtk::BUTTON_SECONDARY, button_state(revert, g_dirty), theme); mtk::draw_button(canvas, apply, "Apply", mtk::BUTTON_PRIMARY, button_state(apply, g_dirty), theme); host.present(); } static bool handle_mouse(const Montauk::WinEvent& ev) { int prev_x = g_mouse_x; int prev_y = g_mouse_y; g_mouse_x = ev.mouse.x; g_mouse_y = ev.mouse.y; Rect country = country_list_rect(); Rect zone = zone_list_rect(); Rect revert = revert_button_rect(); Rect apply = apply_button_rect(); bool redraw = false; redraw |= hover_changed(revert, g_dirty, prev_x, prev_y, g_mouse_x, g_mouse_y); redraw |= hover_changed(apply, g_dirty, prev_x, prev_y, g_mouse_x, g_mouse_y); if (ev.mouse.scroll != 0) { if (country.contains(g_mouse_x, g_mouse_y)) { g_country_scroll -= ev.mouse.scroll * 3; clamp_scrolls(); redraw = true; } else if (zone.contains(g_mouse_x, g_mouse_y)) { g_zone_scroll -= ev.mouse.scroll * 3; clamp_scrolls(); redraw = true; } } // Country scrollbar drag Rect csb_track = country_scrollbar_track(country); Rect csb_thumb = country_scrollbar_thumb(country); bool left_held = (ev.mouse.buttons & 1) != 0; bool just_released = !(ev.mouse.buttons & 1) && (ev.mouse.prev_buttons & 1); if (g_country_sb_dragging) { if (left_held && !csb_track.empty()) { int thumb_top = g_mouse_y - g_country_sb_drag_offset; int px_offset = mtk::scrollbar_offset_from_thumb_top( csb_track, country_content_extent(), country_view_extent(country), csb_thumb.h, thumb_top); int new_scroll = px_offset / ROW_H; if (new_scroll != g_country_scroll) { g_country_scroll = new_scroll; clamp_scrolls(); redraw = true; } return redraw; } if (just_released) { g_country_sb_dragging = false; return true; } } bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); if (!clicked) return redraw; if (!csb_track.empty() && csb_track.contains(g_mouse_x, g_mouse_y)) { if (csb_thumb.contains(g_mouse_x, g_mouse_y)) { g_country_sb_dragging = true; g_country_sb_drag_offset = g_mouse_y - csb_thumb.y; } else { int rows = visible_rows(country); int delta = rows > 1 ? rows - 1 : 1; if (g_mouse_y < csb_thumb.y) g_country_scroll -= delta; else g_country_scroll += delta; clamp_scrolls(); } return true; } if (country.contains(g_mouse_x, g_mouse_y)) { int row = (g_mouse_y - country.y - 2) / ROW_H; int index = g_country_scroll + row; if (index >= 0 && index < g_country_count) { g_selected_country = index; g_selected_zone = default_zone_index(g_countries[g_selected_country]); g_zone_scroll = 0; g_focus_pane = 0; g_dirty = (g_selected_country != g_saved_country || g_selected_zone != g_saved_zone); clamp_scrolls(); redraw = true; } return redraw; } if (zone.contains(g_mouse_x, g_mouse_y)) { int row = (g_mouse_y - zone.y - 2) / ROW_H; int index = g_zone_scroll + row; if (g_selected_country >= 0 && g_selected_country < g_country_count && index >= 0 && index < zone_count(g_countries[g_selected_country])) { g_selected_zone = index; g_focus_pane = 1; g_dirty = (g_selected_country != g_saved_country || g_selected_zone != g_saved_zone); redraw = true; } return redraw; } if (apply.contains(g_mouse_x, g_mouse_y)) { if (g_dirty) save_selection(); return true; } if (revert.contains(g_mouse_x, g_mouse_y)) { if (g_dirty) revert_selection(); return true; } return redraw; } static bool handle_key(const Montauk::KeyEvent& key) { if (!key.pressed || g_country_count <= 0) return false; if (key.ascii == '\n' || key.ascii == '\r') { if (g_dirty) save_selection(); return true; } if (key.ascii == '\033') { if (g_dirty) revert_selection(); return true; } int* selected = g_focus_pane == 0 ? &g_selected_country : &g_selected_zone; int count = g_focus_pane == 0 ? g_country_count : zone_count(g_countries[g_selected_country]); int old_country = g_selected_country; if (key.scancode == 0x4B) g_focus_pane = 0; // Left else if (key.scancode == 0x4D) g_focus_pane = 1; // Right else if (key.scancode == 0x48 && *selected > 0) (*selected)--; // Up else if (key.scancode == 0x50 && *selected < count - 1) (*selected)++; // Down else if (key.scancode == 0x49) *selected = gui_max(0, *selected - 8); // Page Up else if (key.scancode == 0x51) *selected = gui_min(count - 1, *selected + 8); // Page Down else return false; if (old_country != g_selected_country) { g_selected_zone = default_zone_index(g_countries[g_selected_country]); g_zone_scroll = 0; } ensure_country_visible(); ensure_zone_visible(); clamp_scrolls(); g_dirty = (g_selected_country != g_saved_country || g_selected_zone != g_saved_zone); return true; } extern "C" void _start() { if (!fonts::init()) montauk::exit(1); apply_scale(montauk::win_getscale()); load_accent(); load_timezone_data(); if (!g_win.create("Time Zone", INIT_W, INIT_H)) montauk::exit(1); load_saved_selection(); render(); while (g_win.id >= 0 && !g_win.closed) { Montauk::WinEvent ev; int r = g_win.poll(&ev); bool redraw = false; if (r < 0) break; if (r == 0) { montauk::sleep_ms(16); continue; } if (ev.type == 3) break; if (ev.type == 2) { redraw = true; } else if (ev.type == 4) { apply_scale(g_win.scale_factor); redraw = true; } else if (ev.type == 1) { redraw = handle_mouse(ev); } else if (ev.type == 0) { redraw = handle_key(ev.key); } if (redraw) render(); } if (g_data_loaded) g_data.destroy(); g_win.destroy(); montauk::exit(0); }