feat: revamp Installer UI
This commit is contained in:
@@ -12,4 +12,4 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#define MONTAUK_BUILD_NUMBER 34
|
||||
#define MONTAUK_BUILD_NUMBER 42
|
||||
|
||||
@@ -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)
|
||||
// ============================================================================
|
||||
|
||||
@@ -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;
|
||||
|
||||
+101
-107
@@ -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,7 +237,7 @@ 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",
|
||||
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,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) {
|
||||
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.");
|
||||
|
||||
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) {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user