feat: add time zone configuration applet to System Configuration

This commit is contained in:
2026-04-24 16:37:17 +02:00
parent b4bb2932cf
commit b4be0fcc0f
17 changed files with 1404 additions and 471 deletions
@@ -53,32 +53,6 @@ void settings_draw_display(Canvas& c, SettingsState* st) {
mtk::widget_state(s.ui_scale == 1), theme);
mtk::draw_button(c, {bx + (sbw + 8) * 2, y, sbw, btn_h}, "Large", mtk::BUTTON_SECONDARY,
mtk::widget_state(s.ui_scale == 2), theme);
y += btn_h + 20;
mtk::draw_separator(c, x, y, c.w - 2 * x, theme);
y += 16;
c.text(x, y + 6, "UTC Offset", theme.text);
Rect minus_btn = {bx, y, 36, btn_h};
Rect plus_btn = {bx + 36 + 90, y, 36, btn_h};
mtk::draw_button(c, minus_btn, "-", mtk::BUTTON_SECONDARY, mtk::widget_state(), theme);
mtk::draw_button(c, plus_btn, "+", mtk::BUTTON_SECONDARY, mtk::widget_state(), theme);
int off = s.tz_offset_minutes;
int off_h = off / 60;
int off_m = off % 60;
if (off_m < 0) off_m = -off_m;
char tz_label[16];
if (off_m) {
snprintf(tz_label, sizeof(tz_label), "UTC%s%d:%02d", off_h >= 0 ? "+" : "", off_h, off_m);
} else {
snprintf(tz_label, sizeof(tz_label), "UTC%s%d", off_h >= 0 ? "+" : "", off_h);
}
int tw = text_width(tz_label);
int label_w = 90;
c.text(bx + 36 + (label_w - tw) / 2, y + 6, tz_label, theme.text);
}
bool settings_handle_display_click(SettingsState* st, int mx, int cy) {
@@ -142,26 +116,6 @@ bool settings_handle_display_click(SettingsState* st, int mx, int cy) {
settings_persist(st);
return true;
}
y += btn_h + 20 + 16;
Rect minus_btn = {bx, y, 36, btn_h};
Rect plus_btn = {bx + 36 + 90, y, 36, btn_h};
if (minus_btn.contains(mx, cy)) {
if (s.tz_offset_minutes > -720) {
s.tz_offset_minutes -= 60;
montauk::settz(s.tz_offset_minutes);
settings_persist_tz(st);
}
return true;
}
if (plus_btn.contains(mx, cy)) {
if (s.tz_offset_minutes < 840) {
s.tz_offset_minutes += 60;
montauk::settz(s.tz_offset_minutes);
settings_persist_tz(st);
}
return true;
}
return false;
}
@@ -45,12 +45,4 @@ void settings_persist(SettingsState* st) {
doc.destroy();
}
void settings_persist_tz(SettingsState* st) {
montauk::toml::Doc doc;
doc.init();
montauk::config::set_int(&doc, "timezone.offset_minutes", st->desktop->settings.tz_offset_minutes);
montauk::config::save("timezone", &doc);
doc.destroy();
}
} // namespace settings_app
@@ -91,6 +91,5 @@ bool settings_handle_display_click(SettingsState* st, int mx, int cy);
void settings_draw_about(Canvas& c, SettingsState* st);
void settings_persist(SettingsState* st);
void settings_persist_tz(SettingsState* st);
} // namespace settings_app
+78
View File
@@ -0,0 +1,78 @@
# Makefile for timezone (standalone Time Zone configuration app) on MontaukOS
# Copyright (c) 2026 Daniel Hammer
MAKEFLAGS += -rR
.SUFFIXES:
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
CXX := $(TOOLCHAIN_PREFIX)g++
else
CXX := g++
endif
PROG_INC := ../../include
LINK_LD := ../../link.ld
BINDIR := ../../bin
OBJDIR := obj
LIBDIR := ../../lib
CXXFLAGS := \
-std=gnu++20 \
-g -O2 -pipe \
-Wall \
-Wextra \
-Wno-unused-parameter \
-Wno-unused-function \
-nostdinc \
-ffreestanding \
-fno-stack-protector \
-fno-stack-check \
-fno-PIC \
-fno-rtti \
-fno-exceptions \
-ffunction-sections \
-fdata-sections \
-m64 \
-march=x86-64 \
-msse \
-msse2 \
-mno-red-zone \
-mcmodel=small \
-MMD -MP \
-I $(PROG_INC) \
-isystem $(PROG_INC)/libc \
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
LDFLAGS := \
-nostdlib \
-static \
-Wl,--build-id=none \
-Wl,--gc-sections \
-Wl,-m,elf_x86_64 \
-z max-page-size=0x1000 \
-T $(LINK_LD)
SRCS := main.cpp stb_truetype_impl.cpp font_data.cpp
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
TARGET := $(BINDIR)/apps/timezone/timezone.elf
LIBS := $(LIBDIR)/libc/liblibc.a
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBS)
mkdir -p $(BINDIR)/apps/timezone
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
$(OBJDIR)/%.o: %.cpp Makefile
mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) -c $< -o $@
-include $(OBJS:.o=.d)
clean:
rm -rf $(OBJDIR) $(TARGET)
+1
View File
@@ -0,0 +1 @@
#include "../desktop/font_data.cpp"
+832
View File
@@ -0,0 +1,832 @@
/*
* main.cpp
* MontaukOS Time Zone configuration applet
* Copyright (c) 2026 Daniel Hammer
*/
#include <montauk/config.h>
#include <montauk/heap.h>
#include <montauk/string.h>
#include <montauk/syscall.h>
#include <gui/gui.hpp>
#include <gui/canvas.hpp>
#include <gui/mtk.hpp>
#include <gui/standalone.hpp>
#include <gui/truetype.hpp>
extern "C" {
#include <stdio.h>
}
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 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 = colors::ACCENT;
char user[64] = {};
if (montauk::getuser(user, sizeof(user)) > 0 && user[0]) {
auto doc = montauk::config::load_user(user, "desktop");
int64_t accent = doc.get_int("appearance.accent_color", -1);
if (accent >= 0) {
g_accent = Color::from_rgb((uint8_t)((accent >> 16) & 0xFF),
(uint8_t)((accent >> 8) & 0xFF),
(uint8_t)(accent & 0xFF));
doc.destroy();
return;
}
doc.destroy();
}
auto doc = montauk::config::load("desktop");
int64_t accent = doc.get_int("appearance.accent_color", -1);
if (accent >= 0) {
g_accent = Color::from_rgb((uint8_t)((accent >> 16) & 0xFF),
(uint8_t)((accent >> 8) & 0xFF),
(uint8_t)(accent & 0xFF));
}
doc.destroy();
}
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 void draw_country_list(Canvas& canvas, const Rect& list, const mtk::Theme& theme) {
draw_list_frame(canvas, list, theme);
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, list.w - 4, 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);
}
}
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;
}
}
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
if (!clicked) return redraw;
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);
}
+8
View File
@@ -0,0 +1,8 @@
[app]
name = "Time Zone"
binary = "timezone.elf"
icon = "preferences-system-time.svg"
[menu]
category = "System"
visible = false
@@ -0,0 +1 @@
#include "../desktop/stb_truetype_impl.cpp"