From b31939913904ceaf88bc0816073eff906d888adf Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Sat, 18 Jul 2026 11:55:11 +0200 Subject: [PATCH] feat: revamp Installer UI --- kernel/src/Api/BuildNo.hpp | 2 +- programs/src/installer/installer.h | 33 ++++- programs/src/installer/main.cpp | 62 ++++++++- programs/src/installer/render.cpp | 212 ++++++++++++++--------------- 4 files changed, 193 insertions(+), 116 deletions(-) diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 2ed68c4..46dfbd9 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 34 +#define MONTAUK_BUILD_NUMBER 42 diff --git a/programs/src/installer/installer.h b/programs/src/installer/installer.h index 8561898..5e193ef 100644 --- a/programs/src/installer/installer.h +++ b/programs/src/installer/installer.h @@ -26,9 +26,8 @@ using namespace gui; static constexpr int INIT_W = 560; static constexpr int INIT_H = 500; -static constexpr int HEADER_H = 44; -static constexpr int STEP_BAR_H = 30; -static constexpr int CONTENT_TOP = HEADER_H + STEP_BAR_H; +static constexpr int STEP_BAR_H = 44; +static constexpr int CONTENT_TOP = STEP_BAR_H; static constexpr int BOTTOM_H = 48; static constexpr int PAD = 16; static constexpr int BTN_H = 30; @@ -164,6 +163,10 @@ struct InstallerState { int mouse_x; int mouse_y; int comp_scroll; + + // Software-step scrollbar drag + bool comp_sb_dragging; + int comp_sb_drag_offset; }; // ============================================================================ @@ -182,9 +185,10 @@ inline int content_x() { return PAD; } inline int content_w() { return g_win_w - PAD * 2; } inline int content_bottom() { return g_win_h - BOTTOM_H; } -// Heading block: title line + smaller subtitle line +// Content subtitle line sits just below the top bar; the per-step title now +// lives in the top bar itself (see render_top_bar). inline int heading_bottom() { - return CONTENT_TOP + 14 + system_font_height() * 2 + 16; + return CONTENT_TOP + 12 + system_font_height() + 16; } inline Rect bottom_bar_rect() { @@ -235,6 +239,25 @@ inline Rect component_row_rect(int visible_index) { list.w, COMP_ROW_H}; } +// Software-step scrollbar geometry (shared by render + input) +inline bool components_scrollbar_visible(int visible_count) { + return mtk::scrollbar_needed(visible_count * COMP_ROW_H, + components_list_rect().h); +} + +inline Rect components_scrollbar_track(int visible_count) { + if (!components_scrollbar_visible(visible_count)) return {0, 0, 0, 0}; + return mtk::scrollbar_track_rect(components_list_rect()); +} + +inline Rect components_scrollbar_thumb(int visible_count) { + Rect track = components_scrollbar_track(visible_count); + if (track.empty()) return {0, 0, 0, 0}; + return mtk::scrollbar_thumb_rect(track, visible_count * COMP_ROW_H, + components_list_rect().h, + g_state.comp_scroll); +} + // ============================================================================ // Function declarations — helpers (main.cpp) // ============================================================================ diff --git a/programs/src/installer/main.cpp b/programs/src/installer/main.cpp index 41b7dd9..032147d 100644 --- a/programs/src/installer/main.cpp +++ b/programs/src/installer/main.cpp @@ -257,6 +257,63 @@ static bool handle_click(int mx, int my) { return true; } +// ============================================================================ +// Software-step scrollbar drag — mirrors the Time Zones implementation. +// Returns true if the event was consumed (suppress normal click handling). +// ============================================================================ + +static bool handle_comp_scrollbar(const montauk::abi::WinEvent& ev, bool* redraw) { + if (g_state.step != STEP_COMPONENTS) return false; + + int vis[32]; + int n = component_visible_rows(vis, 32); + Rect track = components_scrollbar_track(n); + Rect thumb = components_scrollbar_thumb(n); + + int mx = ev.mouse.x; + int my = ev.mouse.y; + bool left_held = (ev.mouse.buttons & 1) != 0; + bool just_pressed = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); + bool just_released = !(ev.mouse.buttons & 1) && (ev.mouse.prev_buttons & 1); + + if (g_state.comp_sb_dragging) { + if (left_held && !track.empty()) { + int thumb_top = my - g_state.comp_sb_drag_offset; + int off = mtk::scrollbar_offset_from_thumb_top( + track, n * COMP_ROW_H, components_list_rect().h, + thumb.h, thumb_top); + if (off != g_state.comp_scroll) { + g_state.comp_scroll = off; + clamp_comp_scroll(); + *redraw = true; + } + } + if (just_released) { + g_state.comp_sb_dragging = false; + *redraw = true; + } + return true; + } + + if (just_pressed && !track.empty() && track.contains(mx, my)) { + if (thumb.contains(mx, my)) { + g_state.comp_sb_dragging = true; + g_state.comp_sb_drag_offset = my - thumb.y; + } else { + // Page towards the click position. + int page = components_list_rect().h - COMP_ROW_H; + if (page < COMP_ROW_H) page = COMP_ROW_H; + if (my < thumb.y) g_state.comp_scroll -= page; + else g_state.comp_scroll += page; + clamp_comp_scroll(); + } + *redraw = true; + return true; + } + + return false; +} + // ============================================================================ // Key handling — returns false to signal quit // ============================================================================ @@ -375,7 +432,10 @@ extern "C" void _start() { redraw = true; } - bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); + bool consumed = handle_comp_scrollbar(ev, &redraw); + + bool clicked = !consumed && + (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); if (clicked) { if (!handle_click(ev.mouse.x, ev.mouse.y)) break; diff --git a/programs/src/installer/render.cpp b/programs/src/installer/render.cpp index 1ed65bf..3f4c717 100644 --- a/programs/src/installer/render.cpp +++ b/programs/src/installer/render.cpp @@ -60,37 +60,45 @@ static void draw_centered_text(Canvas& c, int y, const char* text, Color color) c.text((g_win_w - text_width(text)) / 2, y, text, color); } -static void draw_heading(Canvas& c, const mtk::Theme& theme, - const char* title, const char* subtitle) { - int fh = system_font_height(); - int y = CONTENT_TOP + 14; - c.text(content_x(), y, title, theme.text); +// The per-step title now lives in the top bar; the content region opens with +// just the descriptive subtitle line. +static void draw_subtitle(Canvas& c, const mtk::Theme& theme, + const char* subtitle) { if (subtitle && subtitle[0]) - c.text(content_x(), y + fh + 2, subtitle, theme.text_subtle); + c.text(content_x(), CONTENT_TOP + 12, subtitle, theme.text_subtle); } // Radio-style two-line list row shared by disk/scheme/partition steps +// Single-select option row using the canonical MontaukOS selection idiom +// (matching Character Map tiles / Time Zones rows): the selected row is filled +// solid with the accent and its text flips to accent_fg -- no radio bubble and +// no contrasting border. Unselected rows are quiet surface cards. static void draw_option_row(Canvas& c, const mtk::Theme& theme, const Rect& row, bool selected, const char* title, const char* info) { - Color fill = selected ? theme.accent_soft - : mouse_in_rect(row) ? mtk::mix(theme.surface_alt, theme.accent, 14) - : theme.surface_alt; - Color border = selected ? theme.accent : theme.border; + Color fill, border, title_col, info_col; + if (selected) { + fill = theme.selection; + border = theme.selection; + title_col = theme.accent_fg; + info_col = mtk::mix(theme.selection, theme.accent_fg, 190); + } else { + fill = mouse_in_rect(row) ? theme.surface_hover : theme.surface; + border = theme.border; + title_col = theme.text; + info_col = theme.text_subtle; + } mtk::draw_rounded_frame(c, row, theme.radius_md, fill, border); - Rect bubble = {row.x + 14, row.y, 14, row.h}; - mtk::draw_radio(c, bubble, "", selected, theme); - int fh = system_font_height(); - int tx = row.x + 40; - int tw = row.w - 40 - 12; + int tx = row.x + 16; + int tw = row.w - 16 - 12; if (info && info[0]) { - draw_text_fit(c, tx, row.y + (row.h - fh * 2 - 4) / 2, title, tw, theme.text); + draw_text_fit(c, tx, row.y + (row.h - fh * 2 - 4) / 2, title, tw, title_col); draw_text_fit(c, tx, row.y + (row.h - fh * 2 - 4) / 2 + fh + 4, info, tw, - theme.text_subtle); + info_col); } else { - draw_text_fit(c, tx, row.y + (row.h - fh) / 2, title, tw, theme.text); + draw_text_fit(c, tx, row.y + (row.h - fh) / 2, title, tw, title_col); } } @@ -98,75 +106,71 @@ static void draw_option_row(Canvas& c, const mtk::Theme& theme, // Header and step bar // ============================================================================ -static void render_header(Canvas& c, const mtk::Theme& theme) { - c.fill_rect(0, 0, g_win_w, HEADER_H, theme.surface); - mtk::draw_separator(c, 0, HEADER_H - 1, g_win_w, theme); - - int fh = system_font_height(); - c.text(PAD, (HEADER_H - fh) / 2, "MontaukOS Installer", theme.text); - - const char* tag = "Setup"; - c.text(g_win_w - PAD - text_width(tag), (HEADER_H - fh) / 2, tag, - theme.text_muted); +// Title shown in the top bar for the current step (folds in what used to be a +// separate in-content heading). +static const char* step_title() { + switch (g_state.step) { + case STEP_MODE_SELECT: return "Welcome to MontaukOS"; + case STEP_SELECT_DISK: return "Select a target disk"; + case STEP_PARTITION_SCHEME: return "Partition scheme"; + case STEP_COMPONENTS: return "Choose software"; + case STEP_CONFIRM: return "Confirm installation"; + case STEP_UPDATE_SELECT_PART: return "Select a root partition"; + case STEP_UPDATE_CONFIRM: return "Confirm update"; + default: // installing / updating / done / error + return (g_state.mode == MODE_UPDATE) ? "Update" : "Install"; + } } -static void render_step_bar(Canvas& c, const mtk::Theme& theme) { - int bar_y = HEADER_H; - c.fill_rect(0, bar_y, g_win_w, STEP_BAR_H, theme.surface_alt); - mtk::draw_separator(c, 0, bar_y + STEP_BAR_H - 1, g_win_w, theme); +// Position of the current step within its flow. Returns false for the welcome +// screen, which is outside the numbered flow. +static bool flow_step_index(int* cur, int* count) { + auto& st = g_state; + if (st.step == STEP_MODE_SELECT) return false; - if (g_state.step == STEP_MODE_SELECT) return; - - static const char* install_labels[] = { "Disk", "Partition", "Software", "Confirm", "Install" }; - static const char* update_labels[] = { "Partition", "Confirm", "Update" }; - - const char** labels; - int count; - int cur = 0; - - if (g_state.mode == MODE_UPDATE) { - labels = update_labels; - count = 3; - if (g_state.step == STEP_UPDATE_CONFIRM) cur = 1; - else if (g_state.step == STEP_UPDATING || - g_state.step == STEP_DONE || - g_state.step == STEP_ERROR) cur = 2; + if (st.mode == MODE_UPDATE) { + *count = 3; + if (st.step == STEP_UPDATE_SELECT_PART) *cur = 0; + else if (st.step == STEP_UPDATE_CONFIRM) *cur = 1; + else *cur = 2; // updating/done/error } else { - labels = install_labels; - count = 5; - if (g_state.step == STEP_PARTITION_SCHEME) cur = 1; - else if (g_state.step == STEP_COMPONENTS) cur = 2; - else if (g_state.step == STEP_CONFIRM) cur = 3; - else if (g_state.step >= STEP_INSTALLING) cur = 4; + *count = 5; + if (st.step == STEP_SELECT_DISK) *cur = 0; + else if (st.step == STEP_PARTITION_SCHEME) *cur = 1; + else if (st.step == STEP_COMPONENTS) *cur = 2; + else if (st.step == STEP_CONFIRM) *cur = 3; + else *cur = 4; // installing/done/error } + return true; +} + +static void render_top_bar(Canvas& c, const mtk::Theme& theme) { + c.fill_rect(0, 0, g_win_w, STEP_BAR_H, theme.surface); + mtk::draw_separator(c, 0, STEP_BAR_H - 1, g_win_w, theme); int fh = system_font_height(); - int arrow_w = text_width(">"); + int text_y = (STEP_BAR_H - 4 - fh) / 2; // vertically center above the strip - int total_w = 0; - int label_ws[8]; + // Step title, left-aligned. + c.text(PAD, text_y, step_title(), theme.text); + + int cur, count; + if (!flow_step_index(&cur, &count)) return; + + // "Step N of M", right-aligned. + char prog[24]; + snprintf(prog, sizeof(prog), "Step %d of %d", cur + 1, count); + c.text(g_win_w - PAD - text_width(prog), text_y, prog, theme.text_subtle); + + // Segmented progress stepper along the bottom edge of the bar. + int strip_y = STEP_BAR_H - 4; + int seg_gap = 4; + int usable = g_win_w - PAD * 2; + int seg_w = (usable - seg_gap * (count - 1)) / count; + if (seg_w < 1) seg_w = 1; for (int i = 0; i < count; i++) { - label_ws[i] = text_width(labels[i]); - total_w += label_ws[i]; - if (i < count - 1) total_w += arrow_w + 16; - } - - int x = (g_win_w - total_w) / 2; - int ty = bar_y + (STEP_BAR_H - fh) / 2; - - for (int i = 0; i < count; i++) { - Color col; - if (i == cur) col = theme.accent; - else if (i < cur) col = mtk::mix(theme.text_muted, theme.accent, 96); - else col = theme.text_muted; - - c.text(x, ty, labels[i], col); - x += label_ws[i] + 8; - - if (i < count - 1) { - c.text(x, ty, ">", theme.border); - x += arrow_w + 8; - } + int sx = PAD + i * (seg_w + seg_gap); + c.fill_rect(sx, strip_y, seg_w, 3, i <= cur ? theme.accent : theme.border); } } @@ -175,8 +179,7 @@ static void render_step_bar(Canvas& c, const mtk::Theme& theme) { // ============================================================================ static void render_mode_select(Canvas& c, const mtk::Theme& theme) { - draw_heading(c, theme, "Welcome to MontaukOS", - "Choose what you would like to do."); + draw_subtitle(c, theme, "Choose what you would like to do."); static const char* titles[] = { "Install", "Update" }; static const char* descs[] = { @@ -188,24 +191,19 @@ static void render_mode_select(Canvas& c, const mtk::Theme& theme) { for (int i = 0; i < 2; i++) { Rect card = mode_card_rect(i); bool hov = mouse_in_rect(card); - Color fill = hov ? mtk::mix(theme.surface_alt, theme.accent, 14) - : theme.surface_alt; - mtk::draw_rounded_frame(c, card, theme.radius_md, fill, - hov ? theme.accent : theme.border); + // Action rows, not selectable options: quiet surface card that lifts to + // surface_hover, neutral border throughout (matches an unselected + // option row). The chevron carries the "this navigates" affordance. + Color fill = hov ? theme.surface_hover : theme.surface; + mtk::draw_rounded_frame(c, card, theme.radius_md, fill, theme.border); c.text(card.x + 16, card.y + 9, titles[i], theme.text); c.text(card.x + 16, card.y + 9 + fh + 4, descs[i], theme.text_subtle); - - const char* chevron = ">"; - c.text(card.x + card.w - 16 - text_width(chevron), - card.y + (card.h - fh) / 2, chevron, - hov ? theme.accent : theme.text_muted); } } static void render_select_disk(Canvas& c, const mtk::Theme& theme) { auto& st = g_state; - draw_heading(c, theme, "Select a target disk", - "MontaukOS will be installed on this disk."); + draw_subtitle(c, theme, "MontaukOS will be installed on this disk."); if (st.disk_count == 0) c.text(content_x(), heading_bottom() + 8, "No disks detected", @@ -229,8 +227,7 @@ static void render_select_disk(Canvas& c, const mtk::Theme& theme) { static void render_partition_scheme(Canvas& c, const mtk::Theme& theme) { auto& st = g_state; - draw_heading(c, theme, "Partition scheme", - "How should the disk be partitioned?"); + draw_subtitle(c, theme, "How should the disk be partitioned?"); for (int i = 0; i < SCHEME_COUNT; i++) { Rect row = list_row_rect(i, SCHEME_ROW_H); @@ -240,8 +237,8 @@ static void render_partition_scheme(Canvas& c, const mtk::Theme& theme) { } static void render_components(Canvas& c, const mtk::Theme& theme) { - draw_heading(c, theme, "Choose software", - "Optional components. The base system is always installed."); + draw_subtitle(c, theme, + "Optional components. The base system is always installed."); Rect list = components_list_rect(); @@ -291,19 +288,18 @@ static void render_components(Canvas& c, const mtk::Theme& theme) { } // Scrollbar - int content_extent = n * COMP_ROW_H; - if (mtk::scrollbar_needed(content_extent, list.h)) { - Rect track = mtk::scrollbar_track_rect(list); - Rect thumb = mtk::scrollbar_thumb_rect(track, content_extent, list.h, - g_state.comp_scroll); - mtk::draw_scrollbar(c, track, thumb, mouse_in_rect(track), false, theme); + if (components_scrollbar_visible(n)) { + Rect track = components_scrollbar_track(n); + Rect thumb = components_scrollbar_thumb(n); + bool hovered = mouse_in_rect(track) || g_state.comp_sb_dragging; + mtk::draw_scrollbar(c, track, thumb, hovered, + g_state.comp_sb_dragging, theme); } } static void render_confirm(Canvas& c, const mtk::Theme& theme) { auto& st = g_state; - draw_heading(c, theme, "Confirm installation", - "Review your choices before continuing."); + draw_subtitle(c, theme, "Review your choices before continuing."); int fh = system_font_height(); int line_h = fh + 8; @@ -343,8 +339,8 @@ static void render_confirm(Canvas& c, const mtk::Theme& theme) { static void render_update_select_part(Canvas& c, const mtk::Theme& theme) { auto& st = g_state; - draw_heading(c, theme, "Select a root partition", - "OS, apps and SDK will be updated on this partition."); + draw_subtitle(c, theme, + "OS, apps and SDK will be updated on this partition."); if (st.part_count == 0) c.text(content_x(), heading_bottom() + 8, "No partitions detected", @@ -366,8 +362,7 @@ static void render_update_select_part(Canvas& c, const mtk::Theme& theme) { static void render_update_confirm(Canvas& c, const mtk::Theme& theme) { auto& st = g_state; - draw_heading(c, theme, "Confirm update", - "Review your choices before continuing."); + draw_subtitle(c, theme, "Review your choices before continuing."); int fh = system_font_height(); char desc[128], sz[24]; @@ -544,8 +539,7 @@ void render(uint32_t* pixels) { mtk::Theme theme = installer_theme(); c.fill(theme.window_bg); - render_header(c, theme); - render_step_bar(c, theme); + render_top_bar(c, theme); switch (g_state.step) { case STEP_MODE_SELECT: render_mode_select(c, theme); break;