feat: revamp Installer UI

This commit is contained in:
2026-07-18 11:55:11 +02:00
parent 3ac25b7abd
commit b319399139
4 changed files with 193 additions and 116 deletions
+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once #pragma once
#define MONTAUK_BUILD_NUMBER 34 #define MONTAUK_BUILD_NUMBER 42
+28 -5
View File
@@ -26,9 +26,8 @@ using namespace gui;
static constexpr int INIT_W = 560; static constexpr int INIT_W = 560;
static constexpr int INIT_H = 500; static constexpr int INIT_H = 500;
static constexpr int HEADER_H = 44; static constexpr int STEP_BAR_H = 44;
static constexpr int STEP_BAR_H = 30; static constexpr int CONTENT_TOP = STEP_BAR_H;
static constexpr int CONTENT_TOP = HEADER_H + STEP_BAR_H;
static constexpr int BOTTOM_H = 48; static constexpr int BOTTOM_H = 48;
static constexpr int PAD = 16; static constexpr int PAD = 16;
static constexpr int BTN_H = 30; static constexpr int BTN_H = 30;
@@ -164,6 +163,10 @@ struct InstallerState {
int mouse_x; int mouse_x;
int mouse_y; int mouse_y;
int comp_scroll; 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_w() { return g_win_w - PAD * 2; }
inline int content_bottom() { return g_win_h - BOTTOM_H; } 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() { 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() { inline Rect bottom_bar_rect() {
@@ -235,6 +239,25 @@ inline Rect component_row_rect(int visible_index) {
list.w, COMP_ROW_H}; 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) // Function declarations — helpers (main.cpp)
// ============================================================================ // ============================================================================
+61 -1
View File
@@ -257,6 +257,63 @@ static bool handle_click(int mx, int my) {
return true; 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 // Key handling — returns false to signal quit
// ============================================================================ // ============================================================================
@@ -375,7 +432,10 @@ extern "C" void _start() {
redraw = true; 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 (clicked) {
if (!handle_click(ev.mouse.x, ev.mouse.y)) if (!handle_click(ev.mouse.x, ev.mouse.y))
break; break;
+101 -107
View File
@@ -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); c.text((g_win_w - text_width(text)) / 2, y, text, color);
} }
static void draw_heading(Canvas& c, const mtk::Theme& theme, // The per-step title now lives in the top bar; the content region opens with
const char* title, const char* subtitle) { // just the descriptive subtitle line.
int fh = system_font_height(); static void draw_subtitle(Canvas& c, const mtk::Theme& theme,
int y = CONTENT_TOP + 14; const char* subtitle) {
c.text(content_x(), y, title, theme.text);
if (subtitle && subtitle[0]) 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 // 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, static void draw_option_row(Canvas& c, const mtk::Theme& theme,
const Rect& row, bool selected, const Rect& row, bool selected,
const char* title, const char* info) { const char* title, const char* info) {
Color fill = selected ? theme.accent_soft Color fill, border, title_col, info_col;
: mouse_in_rect(row) ? mtk::mix(theme.surface_alt, theme.accent, 14) if (selected) {
: theme.surface_alt; fill = theme.selection;
Color border = selected ? theme.accent : theme.border; 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); 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 fh = system_font_height();
int tx = row.x + 40; int tx = row.x + 16;
int tw = row.w - 40 - 12; int tw = row.w - 16 - 12;
if (info && info[0]) { 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, draw_text_fit(c, tx, row.y + (row.h - fh * 2 - 4) / 2 + fh + 4, info, tw,
theme.text_subtle); info_col);
} else { } 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 // Header and step bar
// ============================================================================ // ============================================================================
static void render_header(Canvas& c, const mtk::Theme& theme) { // Title shown in the top bar for the current step (folds in what used to be a
c.fill_rect(0, 0, g_win_w, HEADER_H, theme.surface); // separate in-content heading).
mtk::draw_separator(c, 0, HEADER_H - 1, g_win_w, theme); static const char* step_title() {
switch (g_state.step) {
int fh = system_font_height(); case STEP_MODE_SELECT: return "Welcome to MontaukOS";
c.text(PAD, (HEADER_H - fh) / 2, "MontaukOS Installer", theme.text); case STEP_SELECT_DISK: return "Select a target disk";
case STEP_PARTITION_SCHEME: return "Partition scheme";
const char* tag = "Setup"; case STEP_COMPONENTS: return "Choose software";
c.text(g_win_w - PAD - text_width(tag), (HEADER_H - fh) / 2, tag, case STEP_CONFIRM: return "Confirm installation";
theme.text_muted); 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) { // Position of the current step within its flow. Returns false for the welcome
int bar_y = HEADER_H; // screen, which is outside the numbered flow.
c.fill_rect(0, bar_y, g_win_w, STEP_BAR_H, theme.surface_alt); static bool flow_step_index(int* cur, int* count) {
mtk::draw_separator(c, 0, bar_y + STEP_BAR_H - 1, g_win_w, theme); auto& st = g_state;
if (st.step == STEP_MODE_SELECT) return false;
if (g_state.step == STEP_MODE_SELECT) return; if (st.mode == MODE_UPDATE) {
*count = 3;
static const char* install_labels[] = { "Disk", "Partition", "Software", "Confirm", "Install" }; if (st.step == STEP_UPDATE_SELECT_PART) *cur = 0;
static const char* update_labels[] = { "Partition", "Confirm", "Update" }; else if (st.step == STEP_UPDATE_CONFIRM) *cur = 1;
else *cur = 2; // updating/done/error
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;
} else { } else {
labels = install_labels; *count = 5;
count = 5; if (st.step == STEP_SELECT_DISK) *cur = 0;
if (g_state.step == STEP_PARTITION_SCHEME) cur = 1; else if (st.step == STEP_PARTITION_SCHEME) *cur = 1;
else if (g_state.step == STEP_COMPONENTS) cur = 2; else if (st.step == STEP_COMPONENTS) *cur = 2;
else if (g_state.step == STEP_CONFIRM) cur = 3; else if (st.step == STEP_CONFIRM) *cur = 3;
else if (g_state.step >= STEP_INSTALLING) cur = 4; 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 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; // Step title, left-aligned.
int label_ws[8]; 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++) { for (int i = 0; i < count; i++) {
label_ws[i] = text_width(labels[i]); int sx = PAD + i * (seg_w + seg_gap);
total_w += label_ws[i]; c.fill_rect(sx, strip_y, seg_w, 3, i <= cur ? theme.accent : theme.border);
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;
}
} }
} }
@@ -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) { static void render_mode_select(Canvas& c, const mtk::Theme& theme) {
draw_heading(c, theme, "Welcome to MontaukOS", draw_subtitle(c, theme, "Choose what you would like to do.");
"Choose what you would like to do.");
static const char* titles[] = { "Install", "Update" }; static const char* titles[] = { "Install", "Update" };
static const char* descs[] = { 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++) { for (int i = 0; i < 2; i++) {
Rect card = mode_card_rect(i); Rect card = mode_card_rect(i);
bool hov = mouse_in_rect(card); bool hov = mouse_in_rect(card);
Color fill = hov ? mtk::mix(theme.surface_alt, theme.accent, 14) // Action rows, not selectable options: quiet surface card that lifts to
: theme.surface_alt; // surface_hover, neutral border throughout (matches an unselected
mtk::draw_rounded_frame(c, card, theme.radius_md, fill, // option row). The chevron carries the "this navigates" affordance.
hov ? theme.accent : theme.border); 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, titles[i], theme.text);
c.text(card.x + 16, card.y + 9 + fh + 4, descs[i], theme.text_subtle); 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) { static void render_select_disk(Canvas& c, const mtk::Theme& theme) {
auto& st = g_state; auto& st = g_state;
draw_heading(c, theme, "Select a target disk", draw_subtitle(c, theme, "MontaukOS will be installed on this disk.");
"MontaukOS will be installed on this disk.");
if (st.disk_count == 0) if (st.disk_count == 0)
c.text(content_x(), heading_bottom() + 8, "No disks detected", 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) { static void render_partition_scheme(Canvas& c, const mtk::Theme& theme) {
auto& st = g_state; auto& st = g_state;
draw_heading(c, theme, "Partition scheme", draw_subtitle(c, theme, "How should the disk be partitioned?");
"How should the disk be partitioned?");
for (int i = 0; i < SCHEME_COUNT; i++) { for (int i = 0; i < SCHEME_COUNT; i++) {
Rect row = list_row_rect(i, SCHEME_ROW_H); Rect row = list_row_rect(i, SCHEME_ROW_H);
@@ -240,7 +237,7 @@ static void render_partition_scheme(Canvas& c, const mtk::Theme& theme) {
} }
static void render_components(Canvas& c, const mtk::Theme& theme) { static void render_components(Canvas& c, const mtk::Theme& theme) {
draw_heading(c, theme, "Choose software", draw_subtitle(c, theme,
"Optional components. The base system is always installed."); "Optional components. The base system is always installed.");
Rect list = components_list_rect(); Rect list = components_list_rect();
@@ -291,19 +288,18 @@ static void render_components(Canvas& c, const mtk::Theme& theme) {
} }
// Scrollbar // Scrollbar
int content_extent = n * COMP_ROW_H; if (components_scrollbar_visible(n)) {
if (mtk::scrollbar_needed(content_extent, list.h)) { Rect track = components_scrollbar_track(n);
Rect track = mtk::scrollbar_track_rect(list); Rect thumb = components_scrollbar_thumb(n);
Rect thumb = mtk::scrollbar_thumb_rect(track, content_extent, list.h, bool hovered = mouse_in_rect(track) || g_state.comp_sb_dragging;
g_state.comp_scroll); mtk::draw_scrollbar(c, track, thumb, hovered,
mtk::draw_scrollbar(c, track, thumb, mouse_in_rect(track), false, theme); g_state.comp_sb_dragging, theme);
} }
} }
static void render_confirm(Canvas& c, const mtk::Theme& theme) { static void render_confirm(Canvas& c, const mtk::Theme& theme) {
auto& st = g_state; auto& st = g_state;
draw_heading(c, theme, "Confirm installation", draw_subtitle(c, theme, "Review your choices before continuing.");
"Review your choices before continuing.");
int fh = system_font_height(); int fh = system_font_height();
int line_h = fh + 8; int line_h = fh + 8;
@@ -343,7 +339,7 @@ static void render_confirm(Canvas& c, const mtk::Theme& theme) {
static void render_update_select_part(Canvas& c, const mtk::Theme& theme) { static void render_update_select_part(Canvas& c, const mtk::Theme& theme) {
auto& st = g_state; auto& st = g_state;
draw_heading(c, theme, "Select a root partition", draw_subtitle(c, theme,
"OS, apps and SDK will be updated on this partition."); "OS, apps and SDK will be updated on this partition.");
if (st.part_count == 0) if (st.part_count == 0)
@@ -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) { static void render_update_confirm(Canvas& c, const mtk::Theme& theme) {
auto& st = g_state; auto& st = g_state;
draw_heading(c, theme, "Confirm update", draw_subtitle(c, theme, "Review your choices before continuing.");
"Review your choices before continuing.");
int fh = system_font_height(); int fh = system_font_height();
char desc[128], sz[24]; char desc[128], sz[24];
@@ -544,8 +539,7 @@ void render(uint32_t* pixels) {
mtk::Theme theme = installer_theme(); mtk::Theme theme = installer_theme();
c.fill(theme.window_bg); c.fill(theme.window_bg);
render_header(c, theme); render_top_bar(c, theme);
render_step_bar(c, theme);
switch (g_state.step) { switch (g_state.step) {
case STEP_MODE_SELECT: render_mode_select(c, theme); break; case STEP_MODE_SELECT: render_mode_select(c, theme); break;