feat: port weather to MTK toolkit

This commit is contained in:
2026-04-18 17:50:25 +02:00
parent 3b89ab8a75
commit ad68f99b74
+61 -43
View File
@@ -13,6 +13,7 @@
#include <gui/standalone.hpp> #include <gui/standalone.hpp>
#include <gui/svg.hpp> #include <gui/svg.hpp>
#include <gui/truetype.hpp> #include <gui/truetype.hpp>
#include <gui/mtk.hpp>
#include <tls/tls.hpp> #include <tls/tls.hpp>
extern "C" { extern "C" {
@@ -304,47 +305,63 @@ static void do_fetch() {
// Theme colors // Theme colors
// ============================================================================ // ============================================================================
static constexpr Color CONTENT_BG = Color::from_rgb(0xFF, 0xFF, 0xFF); // white
static constexpr Color FOOTER_BG = Color::from_rgb(0xF5, 0xF5, 0xF5); // light gray
static constexpr Color DIVIDER = Color::from_rgb(0xCC, 0xCC, 0xCC); // subtle border
static constexpr Color DARK_TEXT = Color::from_rgb(0x33, 0x33, 0x33);
static constexpr Color MID_TEXT = Color::from_rgb(0x88, 0x88, 0x88);
static constexpr Color HINT_TEXT = Color::from_rgb(0x99, 0x99, 0x99);
static constexpr Color ERR_TEXT = Color::from_rgb(0xCC, 0x22, 0x22); static constexpr Color ERR_TEXT = Color::from_rgb(0xCC, 0x22, 0x22);
static constexpr Color BTN_BG = Color::from_rgb(0x36, 0x7B, 0xF0); // accent blue
static constexpr Color WHITE_TEXT = Color::from_rgb(0xFF, 0xFF, 0xFF); static mtk::Theme weather_theme() {
mtk::Theme theme = mtk::make_theme();
theme.window_bg = WHITE;
theme.surface = Color::from_rgb(0xF5, 0xF5, 0xF5);
theme.surface_alt = Color::from_rgb(0xF8, 0xF8, 0xF8);
theme.surface_hover = mtk::mix(theme.surface, theme.accent, 18);
theme.border = Color::from_rgb(0xCC, 0xCC, 0xCC);
theme.text = Color::from_rgb(0x33, 0x33, 0x33);
theme.text_muted = Color::from_rgb(0x88, 0x88, 0x88);
theme.text_subtle = Color::from_rgb(0x99, 0x99, 0x99);
return theme;
}
// ============================================================================ // ============================================================================
// Rendering // Rendering
// ============================================================================ // ============================================================================
static constexpr int BTN_W = 110; static constexpr int BTN_W = 110;
static constexpr int BTN_H = 28; static constexpr int BTN_H = 30;
static constexpr int WEATHER_BTN_RADIUS = 6;
static int g_mouse_x = -1;
static int g_mouse_y = -1;
static Rect refresh_button_rect() {
int btn_x = (g_win_w - BTN_W) / 2;
int btn_y = g_win_h - FOOTER_H + (FOOTER_H - BTN_H) / 2;
return {btn_x, btn_y, BTN_W, BTN_H};
}
static void render(Canvas& canvas) { static void render(Canvas& canvas) {
mtk::Theme theme = weather_theme();
Rect btn = refresh_button_rect();
// ── Background ─────────────────────────────────────────────────────────── // ── Background ───────────────────────────────────────────────────────────
canvas.fill_rect(0, 0, g_win_w, g_win_h - FOOTER_H, CONTENT_BG); canvas.fill_rect(0, 0, g_win_w, g_win_h - FOOTER_H, theme.window_bg);
canvas.fill_rect(0, g_win_h - FOOTER_H, g_win_w, FOOTER_H, FOOTER_BG); canvas.fill_rect(0, g_win_h - FOOTER_H, g_win_w, FOOTER_H, theme.surface);
// Divider between content and location strip // Divider between content and location strip
canvas.hline(0, HEADER_H, g_win_w, DIVIDER); canvas.hline(0, HEADER_H, g_win_w, theme.border);
// Divider above footer // Divider above footer
canvas.hline(0, g_win_h - FOOTER_H, g_win_w, DIVIDER); canvas.hline(0, g_win_h - FOOTER_H, g_win_w, theme.border);
if (!g_font) return; if (!g_font) return;
// ── Main content area (y=0..HEADER_H) ──────────────────────────────────── // ── Main content area (y=0..HEADER_H) ────────────────────────────────────
if (g_phase == AppPhase::LOADING) { if (g_phase == AppPhase::LOADING) {
draw_text(canvas, g_font, 20, HEADER_H / 2 - 9, draw_text(canvas, g_font, 20, HEADER_H / 2 - 9,
"Fetching weather data...", HINT_TEXT, 18); "Fetching weather data...", theme.text_subtle, 18);
} else if (g_phase == AppPhase::ERR) { } else if (g_phase == AppPhase::ERR) {
draw_text(canvas, g_font, 20, 20, g_status, ERR_TEXT, 15); draw_text(canvas, g_font, 20, 20, g_status, ERR_TEXT, 15);
} else if (g_phase == AppPhase::IDLE) { } else if (g_phase == AppPhase::IDLE) {
draw_text(canvas, g_font, 20, HEADER_H / 2 - 9, draw_text(canvas, g_font, 20, HEADER_H / 2 - 9,
"Click Refresh to check weather.", HINT_TEXT, 18); "Click Refresh to check weather.", theme.text_subtle, 18);
} else { // DONE } else { // DONE
// Weather icon // Weather icon
@@ -352,31 +369,26 @@ static void render(Canvas& canvas) {
// Temperature (large bold) // Temperature (large bold)
TrueTypeFont* temp_font = g_font_bold ? g_font_bold : g_font; TrueTypeFont* temp_font = g_font_bold ? g_font_bold : g_font;
draw_text(canvas, temp_font, INFO_X, TEMP_Y, g_temp, DARK_TEXT, TEMP_SIZE); draw_text(canvas, temp_font, INFO_X, TEMP_Y, g_temp, theme.text, TEMP_SIZE);
// Weather description // Weather description
draw_text(canvas, g_font, INFO_X, DESC_Y, g_desc, DARK_TEXT, DESC_SIZE); draw_text(canvas, g_font, INFO_X, DESC_Y, g_desc, theme.text, DESC_SIZE);
// Feels like // Feels like
draw_text(canvas, g_font, INFO_X, FEELS_Y, g_feels, MID_TEXT, LABEL_SIZE); draw_text(canvas, g_font, INFO_X, FEELS_Y, g_feels, theme.text_muted, LABEL_SIZE);
} }
// ── Location strip (y=HEADER_H..g_win_h-FOOTER_H) ──────────────────────── // ── Location strip (y=HEADER_H..g_win_h-FOOTER_H) ────────────────────────
if (g_phase == AppPhase::DONE) { if (g_phase == AppPhase::DONE) {
draw_text(canvas, g_font, 20, HEADER_H + 14, g_location, DARK_TEXT, LABEL_SIZE); draw_text(canvas, g_font, 20, HEADER_H + 14, g_location, theme.text, LABEL_SIZE);
} }
// ── Refresh button (rounded, in footer) ─────────────────────────────────── // ── Refresh button (rounded, in footer) ───────────────────────────────────
int btn_x = (g_win_w - BTN_W) / 2; bool hovered = btn.contains(g_mouse_x, g_mouse_y);
int btn_y = g_win_h - FOOTER_H + (FOOTER_H - BTN_H) / 2; bool loading = (g_phase == AppPhase::LOADING);
mtk::WidgetState state = mtk::widget_state(false, hovered, !loading);
if (g_phase == AppPhase::LOADING) { mtk::draw_button(canvas, btn, loading ? "Loading..." : "Refresh",
draw_button(canvas, g_font, btn_x, btn_y, BTN_W, BTN_H, mtk::BUTTON_PRIMARY, state, theme);
"Loading...", BTN_BG, WHITE_TEXT, WEATHER_BTN_RADIUS, 14);
} else {
draw_button(canvas, g_font, btn_x, btn_y, BTN_W, BTN_H,
"Refresh", BTN_BG, WHITE_TEXT, WEATHER_BTN_RADIUS, 15);
}
} }
// ============================================================================ // ============================================================================
@@ -384,6 +396,9 @@ static void render(Canvas& canvas) {
// ============================================================================ // ============================================================================
extern "C" void _start() { extern "C" void _start() {
if (!fonts::init())
montauk::exit(1);
// Allocate response buffer from heap // Allocate response buffer from heap
g_resp_buf = (char*)malloc(RESP_MAX + 1); g_resp_buf = (char*)malloc(RESP_MAX + 1);
if (!g_resp_buf) montauk::exit(1); if (!g_resp_buf) montauk::exit(1);
@@ -406,12 +421,13 @@ extern "C" void _start() {
if (!win.create("Weather", INIT_W, INIT_H)) if (!win.create("Weather", INIT_W, INIT_H))
montauk::exit(1); montauk::exit(1);
Canvas canvas = win.canvas(); mtk::StandaloneHost host(&win);
Canvas canvas = host.canvas();
// Initial fetch on startup // Initial fetch on startup
g_phase = AppPhase::LOADING; g_phase = AppPhase::LOADING;
render(canvas); render(canvas);
win.present(); host.present();
do_fetch(); do_fetch();
// Event loop // Event loop
@@ -423,9 +439,9 @@ extern "C" void _start() {
if (r == 0) { if (r == 0) {
montauk::sleep_ms(16); montauk::sleep_ms(16);
canvas = win.canvas(); canvas = host.canvas();
render(canvas); render(canvas);
win.present(); host.present();
continue; continue;
} }
@@ -438,30 +454,32 @@ extern "C" void _start() {
if (ev.type == 2) { if (ev.type == 2) {
g_win_w = win.width; g_win_w = win.width;
g_win_h = win.height; g_win_h = win.height;
canvas = win.canvas(); canvas = host.canvas();
} }
if (ev.type == 1) { if (ev.type == 1) {
// Track mouse position for hover states
g_mouse_x = ev.mouse.x;
g_mouse_y = ev.mouse.y;
// Mouse — check for Refresh button click // Mouse — check for Refresh button click
bool just_clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); bool just_clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
if (just_clicked && g_phase != AppPhase::LOADING) { if (just_clicked && g_phase != AppPhase::LOADING) {
int btn_x = (g_win_w - BTN_W) / 2; Rect btn = refresh_button_rect();
int btn_y = g_win_h - FOOTER_H + (FOOTER_H - BTN_H) / 2;
int mx = ev.mouse.x, my = ev.mouse.y; int mx = ev.mouse.x, my = ev.mouse.y;
if (mx >= btn_x && mx < btn_x + BTN_W && if (btn.contains(mx, my)) {
my >= btn_y && my < btn_y + BTN_H) {
g_phase = AppPhase::LOADING; g_phase = AppPhase::LOADING;
canvas = win.canvas(); canvas = host.canvas();
render(canvas); render(canvas);
win.present(); host.present();
do_fetch(); do_fetch();
} }
} }
} }
canvas = win.canvas(); canvas = host.canvas();
render(canvas); render(canvas);
win.present(); host.present();
} }
if (g_icon.pixels) svg_free(g_icon); if (g_icon.pixels) svg_free(g_icon);