/* * render.cpp * MontaukOS Disk Tool - rendering * Copyright (c) 2026 Daniel Hammer */ #include "disks.h" #include #include static mtk::Theme disks_theme() { mtk::Theme theme = mtk::make_theme(g_accent); theme.selection = theme.accent_soft; theme.disabled_bg = Color::from_rgb(0xE4, 0xE4, 0xE4); theme.disabled_fg = Color::from_rgb(0x9A, 0x9A, 0x9A); return theme; } static mtk::TableStyle partition_table_style(const mtk::Theme& theme) { mtk::TableStyle style = mtk::make_table_style(theme); style.row_selected_bg = theme.accent_soft; style.row_hover_bg = mtk::mix(theme.window_bg, theme.accent, 12); return style; } static bool mouse_in_rect(const Rect& rect) { return rect.contains(g_state.mouse_x, g_state.mouse_y); } static mtk::WidgetState toolbar_button_state(const Rect& rect, bool enabled, bool active = false) { return mtk::widget_state(active, mouse_in_rect(rect), enabled); } static void draw_text_fit(Canvas& c, int x, int y, const char* text, int max_w, Color color) { if (!text || !text[0] || max_w <= 4) return; if (text_width(text) <= max_w) { c.text(x, y, text, color); return; } const char* ellipsis = "..."; int ell_w = text_width(ellipsis); if (ell_w >= max_w) return; char buf[96]; int out = 0; while (text[out] && out < (int)sizeof(buf) - 4) { buf[out] = text[out]; buf[out + 1] = '\0'; if (text_width(buf) + ell_w > max_w) break; out++; } if (out <= 0) return; buf[out] = '.'; buf[out + 1] = '.'; buf[out + 2] = '.'; buf[out + 3] = '\0'; c.text(x, y, buf, color); } static int hovered_partition_row() { if (g_state.disk_count == 0 || g_state.selected_disk < 0) return -1; if (g_state.mouse_y < partition_list_y() || g_state.mouse_y >= g_win_h - STATUS_H) return -1; int part_indices[MAX_PARTS]; int nparts = get_disk_parts(part_indices, MAX_PARTS); int row = (g_state.mouse_y - partition_list_y() + g_state.scroll_y) / ITEM_H; if (row < 0 || row >= nparts) return -1; return row; } static void draw_centered_text(Canvas& c, int width, int y, const char* text, Color color) { int tw = text_width(text); c.text((width - tw) / 2, y, text, color); } static void draw_dialog_footer(Canvas& c, int dialog_w, int dialog_h, const mtk::Theme& theme) { constexpr int FOOTER_H = 54; int y = dialog_h - FOOTER_H; c.fill_rect(0, y, dialog_w, FOOTER_H, theme.surface); mtk::draw_separator(c, 0, y, dialog_w, theme); } static void render_toolbar(Canvas& c, const mtk::Theme& theme) { auto& dt = g_state; int fh = system_font_height(); c.fill_rect(0, 0, g_win_w, TOOLBAR_H, theme.surface); mtk::draw_separator(c, 0, TOOLBAR_H - 1, g_win_w, theme); for (int i = 0; i < dt.disk_count; i++) { Rect button = toolbar_disk_button_rect(i); char label[16]; snprintf(label, sizeof(label), "Disk %d", disk_block_dev(i)); mtk::draw_button(c, button, label, mtk::BUTTON_SECONDARY, toolbar_button_state(button, true, i == dt.selected_disk), theme); } if (dt.disk_count == 0) { c.text(8, (TOOLBAR_H - fh) / 2, "No disks detected", theme.text); return; } } static void render_content(Canvas& c, const mtk::Theme& theme) { auto& dt = g_state; int fh = system_font_height(); if (dt.disk_count == 0 || dt.selected_disk < 0 || dt.selected_disk >= dt.disk_count) return; montauk::abi::DiskInfo& disk = dt.disks[dt.selected_disk]; int y = content_title_y(); char info_str[64]; char size_str[24]; format_disk_size(size_str, sizeof(size_str), disk.sectorCount, disk.sectorSizeLog); const char* dtype = disk.rpm == 1 ? "SSD" : "HDD"; snprintf(info_str, sizeof(info_str), "%s %s", size_str, dtype); int info_w = text_width(info_str); int info_x = g_win_w - info_w - MAP_PAD; int model_max_w = info_x - MAP_PAD - 12; draw_text_fit(c, MAP_PAD, y, disk.model, model_max_w, theme.text); c.text(info_x, y, info_str, theme.text_subtle); y = partition_map_y(); Rect map_rect = {MAP_PAD, y, g_win_w - MAP_PAD * 2, MAP_H}; mtk::draw_rounded_frame(c, map_rect, theme.radius_md, theme.surface_alt, theme.border); int part_indices[MAX_PARTS]; int nparts = get_disk_parts(part_indices, MAX_PARTS); uint64_t total_sectors = disk.sectorCount; if (total_sectors > 0 && nparts > 0) { for (int pi = 0; pi < nparts; pi++) { montauk::abi::PartInfo& p = dt.parts[part_indices[pi]]; int part_x = map_rect.x + (int)((p.startLba * (uint64_t)map_rect.w) / total_sectors); int part_w = (int)((p.sectorCount * (uint64_t)map_rect.w) / total_sectors); if (part_w < 2) part_w = 2; if (part_x + part_w > map_rect.x + map_rect.w) part_w = map_rect.x + map_rect.w - part_x; Rect segment = {part_x, map_rect.y + 3, part_w, MAP_H - 6}; if (pi == dt.selected_part) { c.fill_rounded_rect(segment.x - 1, segment.y - 1, segment.w + 2, segment.h + 2, theme.radius_sm, theme.accent); } Color col = part_colors[pi % NUM_PART_COLORS]; c.fill_rounded_rect(segment.x, segment.y, segment.w, segment.h, theme.radius_sm, col); char label[8]; snprintf(label, sizeof(label), "%d", pi); int label_w = text_width(label); if (part_w > label_w + 4) { c.text(segment.x + (segment.w - label_w) / 2, map_rect.y + (MAP_H - fh) / 2, label, theme.text_inverse); } } } y = partition_header_y(); int list_y = partition_list_y(); int list_bottom = g_win_h - STATUS_H; int hovered_row = hovered_partition_row(); mtk::TableStyle table_style = partition_table_style(theme); mtk::TableLayout table = { {0, y, g_win_w, HEADER_H}, {0, list_y, g_win_w, list_bottom - list_y}, ITEM_H }; int col_idx = 12; int col_name = 40; int col_type = g_win_w / 2 - 20; int col_size = g_win_w - 160; int col_lba = g_win_w - 80; mtk::TableColumn cols[5] = { {"#", col_idx, col_name - col_idx - 8}, {"Name", col_name, col_type - col_name - 10}, {"Type", col_type, col_size - col_type - 10}, {"Size", col_size, col_lba - col_size - 10}, {"LBA", col_lba, g_win_w - col_lba - 12}, }; c.fill_rect(table.header.x, table.header.y, table.header.w, table.header.h, table_style.header_bg); c.hline(table.header.x, table.header.y + table.header.h - 1, table.header.w, table_style.header_border); int ty = table.header.y + (table.header.h - fh) / 2; for (int i = 0; i < 5; i++) c.text(table.header.x + cols[i].x, ty, cols[i].label, table_style.header_text); for (int pi = 0; pi < nparts; pi++) { int row_y = list_y + pi * ITEM_H - dt.scroll_y; if (row_y + ITEM_H <= list_y) continue; if (row_y >= list_bottom) break; Rect row_rect = {0, row_y, g_win_w, ITEM_H}; if (pi == dt.selected_part) { c.fill_rect(row_rect.x, row_rect.y, row_rect.w, row_rect.h, table_style.row_selected_bg); } else if (pi == hovered_row) { c.fill_rect(row_rect.x, row_rect.y, row_rect.w, row_rect.h, table_style.row_hover_bg); } montauk::abi::PartInfo& p = dt.parts[part_indices[pi]]; c.fill_rounded_rect(col_idx, row_y + (ITEM_H - 10) / 2, 10, 10, 5, part_colors[pi % NUM_PART_COLORS]); int ry = row_y + (ITEM_H - fh) / 2; draw_text_fit(c, col_name, ry, p.name[0] ? p.name : "(unnamed)", cols[1].w, theme.text); draw_text_fit(c, col_type, ry, p.typeName, cols[2].w, theme.text_subtle); char sz[24]; format_disk_size(sz, sizeof(sz), p.sectorCount, disk.sectorSizeLog); draw_text_fit(c, col_size, ry, sz, cols[3].w, theme.text); char lba_str[24]; snprintf(lba_str, sizeof(lba_str), "%lu", (unsigned)p.startLba); draw_text_fit(c, col_lba, ry, lba_str, cols[4].w, theme.text_muted); } if (nparts == 0) c.text(col_name, list_y + 8, "No partitions found", theme.text_muted); } static void render_status(Canvas& c, const mtk::Theme& theme) { auto& dt = g_state; int fh = system_font_height(); int sy = g_win_h - STATUS_H; c.fill_rect(0, sy, g_win_w, STATUS_H, theme.surface); mtk::draw_separator(c, 0, sy, g_win_w, theme); bool has_disks = dt.disk_count > 0; bool has_sel = dt.selected_part >= 0; int text_right = g_win_w - 16; if (has_disks) { Rect newpart = toolbar_newpart_button_rect(); Rect format = toolbar_format_button_rect(); Rect mount = toolbar_mount_button_rect(); Rect refresh = toolbar_refresh_button_rect(); text_right = newpart.x - 12; mtk::draw_button(c, newpart, "New Part", mtk::BUTTON_PRIMARY, toolbar_button_state(newpart, true), theme); mtk::draw_button(c, format, "Format", mtk::BUTTON_TONAL, toolbar_button_state(format, has_sel), theme); mtk::draw_button(c, mount, "Mount", mtk::BUTTON_TONAL, toolbar_button_state(mount, has_sel), theme); mtk::draw_button(c, refresh, "Refresh", mtk::BUTTON_SECONDARY, toolbar_button_state(refresh, true), theme); } if (dt.status[0]) { uint64_t age = montauk::get_milliseconds() - dt.status_time; Color color = age < 5000 ? theme.text : theme.text_muted; draw_text_fit(c, 16, sy + (STATUS_H - fh) / 2, dt.status, text_right - 16, color); } } void render_format_window() { auto& dlg = g_state.fmt_dlg; if (!dlg.open || !dlg.pixels) return; Canvas c(dlg.pixels, FMT_DLG_W, FMT_DLG_H); mtk::Theme theme = disks_theme(); c.fill(theme.window_bg); int fh = system_font_height(); draw_centered_text(c, FMT_DLG_W, 12, "Format Partition", theme.text); draw_centered_text(c, FMT_DLG_W, 12 + fh + 6, dlg.part_desc, theme.text_subtle); int label_y = 12 + fh * 2 + 18; c.text(16, label_y, "Filesystem", theme.text_muted); for (int i = 0; i < NUM_FS_TYPES; i++) { Rect option = format_option_rect(FMT_DLG_W, i); if (i == dlg.selected_fs) { mtk::draw_rounded_frame(c, option, theme.radius_md, theme.accent_soft, theme.accent_soft); } mtk::draw_radio(c, option, g_fsTypes[i].name, i == dlg.selected_fs, theme); } draw_dialog_footer(c, FMT_DLG_W, FMT_DLG_H, theme); mtk::draw_modal_actions(c, dialog_primary_button_rect(FMT_DLG_W, FMT_DLG_H), "Format", mtk::BUTTON_DANGER, mtk::widget_state(false, dlg.hover_format), dialog_secondary_button_rect(FMT_DLG_W, FMT_DLG_H), "Cancel", mtk::widget_state(false, dlg.hover_cancel), theme); } void render_newpart_window() { auto& dlg = g_state.np_dlg; if (!dlg.open || !dlg.pixels) return; Canvas c(dlg.pixels, NP_DLG_W, NP_DLG_H); mtk::Theme theme = disks_theme(); c.fill(theme.window_bg); int fh = system_font_height(); const char* title = dlg.will_init_gpt ? "Initialize Disk" : "New Partition"; draw_centered_text(c, NP_DLG_W, 12, title, theme.text); draw_centered_text(c, NP_DLG_W, 12 + fh + 6, dlg.disk_desc, theme.text_subtle); Color warn_color = dlg.will_init_gpt ? theme.danger : theme.text; int warn_y = 12 + fh * 2 + 20; Rect warn_panel = {16, warn_y - 8, NP_DLG_W - 32, fh * 2 + 20}; Color warn_bg = dlg.will_init_gpt ? theme.danger_soft : theme.surface_alt; mtk::draw_rounded_frame(c, warn_panel, theme.radius_md, warn_bg, warn_bg); draw_centered_text(c, NP_DLG_W, warn_y, dlg.warn_line1, warn_color); draw_centered_text(c, NP_DLG_W, warn_y + fh + 4, dlg.warn_line2, warn_color); const char* confirm_label = dlg.will_init_gpt ? "Initialize" : "Create"; mtk::ButtonVariant confirm_variant = dlg.will_init_gpt ? mtk::BUTTON_DANGER : mtk::BUTTON_PRIMARY; draw_dialog_footer(c, NP_DLG_W, NP_DLG_H, theme); mtk::draw_modal_actions(c, dialog_primary_button_rect(NP_DLG_W, NP_DLG_H), confirm_label, confirm_variant, mtk::widget_state(false, dlg.hover_confirm), dialog_secondary_button_rect(NP_DLG_W, NP_DLG_H), "Cancel", mtk::widget_state(false, dlg.hover_cancel), theme); } void render(uint32_t* pixels) { if (!pixels) return; Canvas c(pixels, g_win_w, g_win_h); mtk::Theme theme = disks_theme(); c.fill(theme.window_bg); render_toolbar(c, theme); render_content(c, theme); render_status(c, theme); }