/* SPDX-License-Identifier: Apache-2.0 */ /* * The loader's user interface. See ui.h for why this can be shared at all. * * The look is deliberately quiet: a dark ground, the mark centred, one thin * bar beneath it, and no text that is not load-bearing. A loader is not a * place to be interesting -- it is looked at for four seconds by someone who * wants to be somewhere else, and the useful thing it can do is stay still and * tell them it is working. */ #include "ui.h" #include "../../include/theme.h" #include "../../include/font.h" #include "../../include/logo.h" /* "Draw the glyph, leave the background alone." Not a colour anyone can mean: * the palette is 24-bit, so the high byte is free for a sentinel. */ #define SCH_BG_NONE 0xFF000000u static struct ui_fb FB; static int ACTIVE; /* Progress state. Kept here rather than passed around because the caller ticks * it from inside a read loop that has no business carrying UI state. */ static ui_u32 PROG_TOTAL, PROG_DONE; static int PROG_X, PROG_Y, PROG_W, PROG_H; static int PROG_LAST_PX; /* pixels already filled */ static int PROG_ACTIVE; /* Layout constants. One place, because a splash that is laid out by scattered * literals is one nobody dares move. * * The mark and the bar are the same width, and that is the arrangement: a small * mark with a short bar directly beneath it, both centred. Everything else -- * gaps, panel sizes -- is expressed against that width so the whole thing * scales as one object. */ #define MARK_W_FRAC 8 /* the mark is width/8 ... */ #define MARK_W_MIN 72 /* ... clamped, because a mark that tracks the */ #define MARK_W_MAX 128 /* mode exactly is tiny at 800 and soft at 1600 */ #define MARK_GAP 30 /* mark bottom to the bar */ #define SUB_GAP 20 /* bar to the line under it */ #define BAR_H 4 /* thin: it is a hint, not a gauge */ #define GLYPH_W 8 #define GLYPH_H 16 /* --- the metal ------------------------------------------------------------ * * Brushed metal, done the cheap way that actually survives a 4-bit mask and a * 32bpp framebuffer: a vertical gradient from a light top to a darker bottom, * with a brighter band near the top where a highlight would fall. There is no * texture and no noise -- at this size they read as dirt rather than as metal, * and the loader has no business carrying a texture asset. * * The mark is not a flat colour any more for the same reason a real one would * not be: a single grey on a dark ground looks printed, and the gradient is * what makes it look like an object with a light above it. */ #define METAL_TOP 0xF2F4F8u /* near-white, where the light lands */ #define METAL_MID 0xB9BEC7u #define METAL_BOT 0x6E747Fu /* shadowed underside */ #define METAL_EDGE 0x3A3F49u /* panel border */ #define PANEL_BG 0x151821u /* panel ground, a shade off SCH_BG */ #define PANEL_TOP 0x232838u /* bevel: one row lighter along the top */ static ui_u32 mix(ui_u32 a, ui_u32 b, int num, int den) { if (den <= 0) return a; if (num < 0) num = 0; if (num > den) num = den; ui_u32 r = ((((a >> 16) & 0xFF) * (den - num)) + (((b >> 16) & 0xFF) * num)) / den; ui_u32 g = ((((a >> 8) & 0xFF) * (den - num)) + (((b >> 8) & 0xFF) * num)) / den; ui_u32 bl = ((((a ) & 0xFF) * (den - num)) + (((b ) & 0xFF) * num)) / den; return (r << 16) | (g << 8) | bl; } /* The metal colour at row `i` of an object `h` rows tall. */ static ui_u32 metal(int i, int h) { if (h <= 1) return METAL_MID; /* Two segments: a fast lift into the highlight over the top third, then a * long fall to the shadow. A single linear ramp looks like a gradient; the * knee is what makes it look lit. */ int knee = h / 3; if (i <= knee) return mix(METAL_MID, METAL_TOP, i, knee ? knee : 1); return mix(METAL_TOP, METAL_BOT, i - knee, h - knee); } void ui_init(const struct ui_fb *fb) { ACTIVE = 0; if (!fb || !fb->base || !fb->width || !fb->height) return; FB = *fb; if (!FB.stride) FB.stride = FB.width * 4; ACTIVE = 1; } int ui_active(void) { return ACTIVE; } ui_u32 ui_width(void) { return ACTIVE ? FB.width : 0; } ui_u32 ui_height(void) { return ACTIVE ? FB.height : 0; } static void put(int x, int y, ui_u32 colour) { if (!ACTIVE || x < 0 || y < 0 || (ui_u32)x >= FB.width || (ui_u32)y >= FB.height) return; *(ui_u32 *)(FB.base + (ui_u64)y * FB.stride + (ui_u64)x * 4) = colour; } void ui_fill_rect(int x, int y, int w, int h, ui_u32 colour) { if (!ACTIVE) return; for (int r = 0; r < h; r++) { int yy = y + r; if (yy < 0 || (ui_u32)yy >= FB.height) continue; /* One row at a time through a row pointer: put() per pixel costs a * bounds check and a multiply for every one of them, and this is used * to clear a whole screen. */ ui_u32 *row = (ui_u32 *)(FB.base + (ui_u64)yy * FB.stride); for (int c = 0; c < w; c++) { int xx = x + c; if (xx >= 0 && (ui_u32)xx < FB.width) row[xx] = colour; } } } void ui_clear(void) { ui_fill_rect(0, 0, (int)FB.width, (int)FB.height, SCH_BG); } static void glyph(int x, int y, ui_u8 c, ui_u32 colour, ui_u32 bg) { for (int row = 0; row < GLYPH_H; row++) { ui_u8 bits = font_data[c][row]; for (int col = 0; col < GLYPH_W; col++) { if (bits & (0x80 >> col)) put(x + col, y + row, colour); else if (bg != SCH_BG_NONE) put(x + col, y + row, bg); } } } void ui_text(int x, int y, const char *s, ui_u32 colour, ui_u32 bg) { if (!ACTIVE || !s) return; for (int i = 0; s[i]; i++) glyph(x + i * GLYPH_W, y, (ui_u8)s[i], colour, bg); } static int slen(const char *s) { int n = 0; while (s && s[n]) n++; return n; } void ui_text_centered(int y, const char *s, ui_u32 colour) { if (!ACTIVE) return; int w = slen(s) * GLYPH_W; ui_text(((int)FB.width - w) / 2, y, s, colour, SCH_BG_NONE); } int ui_mark_width(void) { if (!ACTIVE) return MARK_W_MIN; int w = (int)FB.width / MARK_W_FRAC; if (w < MARK_W_MIN) w = MARK_W_MIN; if (w > MARK_W_MAX) w = MARK_W_MAX; return w; } void ui_logo(int y, int w) { if (!ACTIVE) return; if (w <= 0 || w > LOGO_W) w = LOGO_W; /* never upscale: it only goes soft */ int h = (LOGO_H * w) / LOGO_W; int x0 = ((int)FB.width - w) / 2; /* Box-filter down from the source mask. * * Point-sampling a coverage mask drops half the edge pixels and gives a * mark with holes in the thin strokes -- exactly where a logo can least * afford them. Averaging the coverage of every source pixel that lands in a * destination pixel is what keeps the shape intact at 72 px. */ for (int dy = 0; dy < h; dy++) { int sy0 = (dy * LOGO_H) / h, sy1 = ((dy + 1) * LOGO_H) / h; if (sy1 <= sy0) sy1 = sy0 + 1; ui_u32 shade = metal(dy, h); for (int dx = 0; dx < w; dx++) { int sx0 = (dx * LOGO_W) / w, sx1 = ((dx + 1) * LOGO_W) / w; if (sx1 <= sx0) sx1 = sx0 + 1; unsigned sum = 0, n = 0; for (int sy = sy0; sy < sy1; sy++) { for (int sx = sx0; sx < sx1; sx++) { unsigned idx = (unsigned)sy * LOGO_W + (unsigned)sx; sum += (logo_mask[idx >> 1] >> ((idx & 1) ? 0 : 4)) & 0xF; n++; } } unsigned cov = n ? sum / n : 0; if (!cov) continue; /* Coverage blends the shaded metal against the ground, so the mark * has soft edges and a lit face at the same time. */ put(x0 + dx, y + dy, mix(SCH_BG, shade, (int)cov, 15)); } } } /* Where the bar goes. Derived, not stored, so nothing can set it and then have * the screen change underneath it. * * The bar is exactly the mark's width and sits directly beneath it: the two are * one object, and a bar wider than the mark it belongs to reads as a separate * thing that happens to be nearby. */ static void bar_geometry(void) { PROG_W = ui_mark_width(); PROG_H = BAR_H; PROG_X = ((int)FB.width - PROG_W) / 2; int mark_h = (LOGO_H * PROG_W) / LOGO_W; int group_h = mark_h + MARK_GAP + PROG_H; int top = ((int)FB.height - group_h) / 2; PROG_Y = top + mark_h + MARK_GAP; } void ui_splash(const char *subtitle) { if (!ACTIVE) return; ui_clear(); bar_geometry(); int w = PROG_W; int mark_h = (LOGO_H * w) / LOGO_W; int top = PROG_Y - MARK_GAP - mark_h; ui_logo(top, w); if (subtitle) ui_text_centered(PROG_Y + PROG_H + SUB_GAP, subtitle, SCH_DIM); } /* --- panels --------------------------------------------------------------- * * A panel is a plate: a slightly lighter ground than the screen, one lighter * row along the top and a darker border, which is the whole of the bevel. Two * rows of shading do more for the impression of a raised surface than any * amount of gradient inside it, and cost nothing to draw. */ static void panel(int x, int y, int w, int h) { ui_fill_rect(x, y, w, h, PANEL_BG); ui_fill_rect(x, y, w, 1, PANEL_TOP); /* lit top edge */ ui_fill_rect(x, y + h - 1, w, 1, METAL_EDGE); /* shadowed bottom */ ui_fill_rect(x, y, 1, h, METAL_EDGE); ui_fill_rect(x + w - 1, y, 1, h, METAL_EDGE); } /* A heading in metal: the same shading the mark uses, applied per row of the * glyph cell, so a title looks struck out of the same material. */ static void metal_text_centered(int y, const char *s) { int len = slen(s); int x = ((int)FB.width - len * GLYPH_W) / 2; for (int row = 0; row < GLYPH_H; row++) { ui_u32 shade = metal(row, GLYPH_H); for (int i = 0; i < len; i++) { ui_u8 bits = font_data[(ui_u8)s[i]][row]; for (int col = 0; col < GLYPH_W; col++) if (bits & (0x80 >> col)) put(x + i * GLYPH_W + col, y + row, shade); } } } #define ROW_H 22 #define PANEL_PAD 18 #define TITLE_H 34 static void panel_frame(const char *title, int rows, const char *hint, int *out_x, int *out_y, int *out_w) { int w = (int)FB.width / 2; if (w < 360) w = 360; if (w > (int)FB.width - 60) w = (int)FB.width - 60; int h = TITLE_H + rows * ROW_H + PANEL_PAD * 2; int x = ((int)FB.width - w) / 2; int y = ((int)FB.height - h) / 2; ui_clear(); /* The mark, small, above the plate: it is the same screen, not a new one. */ int mw = ui_mark_width() * 2 / 3; int mh = (LOGO_H * mw) / LOGO_W; if (y - mh - 24 > 0) ui_logo(y - mh - 24, mw); panel(x, y, w, h); metal_text_centered(y + PANEL_PAD, title); ui_fill_rect(x + PANEL_PAD, y + TITLE_H - 6, w - PANEL_PAD * 2, 1, METAL_EDGE); if (hint) ui_text_centered(y + h + 16, hint, SCH_DIM); *out_x = x; *out_y = y + TITLE_H; *out_w = w; } void ui_menu(const char *title, const char *const *items, int n, int selected, const char *hint) { if (!ACTIVE) return; int x, y, w; panel_frame(title, n, hint, &x, &y, &w); for (int i = 0; i < n; i++) { int ry = y + i * ROW_H; if (i == selected) { /* The selected row is the only lit thing on the plate. Filling it * with the accent and putting the label in the ground colour reads * at a glance; an outline does not, at this size. */ ui_fill_rect(x + PANEL_PAD / 2, ry - 3, w - PANEL_PAD, ROW_H, SCH_ACCENT); ui_text(x + PANEL_PAD, ry, items[i], PANEL_BG, SCH_BG_NONE); } else { ui_text(x + PANEL_PAD, ry, items[i], SCH_FG, SCH_BG_NONE); } } } void ui_panel(const char *title, const char *const *labels, const char *const *values, int n, const char *hint) { if (!ACTIVE) return; int x, y, w; panel_frame(title, n, hint, &x, &y, &w); for (int i = 0; i < n; i++) { int ry = y + i * ROW_H; ui_text(x + PANEL_PAD, ry, labels[i], SCH_DIM, SCH_BG_NONE); /* Values right-aligned, so the column reads down rather than ragged. */ int vw = slen(values[i]) * GLYPH_W; ui_text(x + w - PANEL_PAD - vw, ry, values[i], SCH_FG, SCH_BG_NONE); } } void ui_progress_begin(const char *label, ui_u32 total) { if (!ACTIVE) return; bar_geometry(); PROG_TOTAL = total ? total : 1; PROG_DONE = 0; PROG_LAST_PX = 0; PROG_ACTIVE = 1; /* The unfilled track: a recess, not a colour. Drawn once -- repainting it * on every tick is what makes a progress bar flicker. */ ui_fill_rect(PROG_X, PROG_Y, PROG_W, PROG_H, 0x1B1F28u); ui_fill_rect(PROG_X, PROG_Y, PROG_W, 1, 0x101319u); if (label) ui_progress_label(label); } void ui_progress_label(const char *label) { if (!ACTIVE || !label) return; /* Clear the line first. A shorter label after a longer one would otherwise * leave the tail of the old one behind. */ int y = PROG_Y + PROG_H + SUB_GAP; ui_fill_rect(0, y, (int)FB.width, GLYPH_H, SCH_BG); ui_text_centered(y, label, SCH_DIM); } void ui_progress_tick(ui_u32 done) { if (!ACTIVE || !PROG_ACTIVE) return; if (done > PROG_TOTAL) done = PROG_TOTAL; PROG_DONE = done; int px = (int)(((ui_u64)PROG_DONE * (ui_u64)PROG_W) / PROG_TOTAL); /* Only the newly filled part. Filling from zero each time is O(width) per * tick and visibly slower than the thing it is reporting on. */ if (px > PROG_LAST_PX) { /* The fill is metal too, shaded across the bar's height, so it looks * like the same material as the mark rather than a coloured rectangle * that happens to be underneath it. */ for (int r = 0; r < PROG_H; r++) ui_fill_rect(PROG_X + PROG_LAST_PX, PROG_Y + r, px - PROG_LAST_PX, 1, metal(r, PROG_H)); PROG_LAST_PX = px; } } void ui_progress_end(void) { if (!ACTIVE || !PROG_ACTIVE) return; ui_progress_tick(PROG_TOTAL); PROG_ACTIVE = 0; }