/* 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. */ #define LOGO_GAP 28 /* logo bottom to product name */ #define NAME_GAP 22 /* product name to the bar */ #define SUB_GAP 18 /* bar to the subtitle under it */ #define BAR_W_FRAC 4 /* bar is width/4 ... */ #define BAR_W_MIN 220 /* ... but never narrower than this */ #define BAR_H 4 /* thin: it is a hint, not a gauge */ #define GLYPH_W 8 #define GLYPH_H 16 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); } void ui_logo(int y) { if (!ACTIVE) return; int x0 = ((int)FB.width - LOGO_W) / 2; /* The logo is a 4-bit coverage mask, not a bitmap: each nibble says how * much of that pixel the mark covers, and the colour is blended against the * background by that amount. Drawing it as on/off would give a mark with * stepped edges that looks wrong at every size. */ for (int y1 = 0; y1 < LOGO_H; y1++) { for (int x1 = 0; x1 < LOGO_W; x1++) { unsigned idx = (unsigned)y1 * LOGO_W + (unsigned)x1; unsigned cov = (logo_mask[idx >> 1] >> ((idx & 1) ? 0 : 4)) & 0xF; if (!cov) continue; ui_u32 fg = SCH_FG, bg = SCH_BG; ui_u32 r = (((fg >> 16) & 0xFF) * cov + ((bg >> 16) & 0xFF) * (15 - cov)) / 15; ui_u32 g = (((fg >> 8) & 0xFF) * cov + ((bg >> 8) & 0xFF) * (15 - cov)) / 15; ui_u32 b = (((fg ) & 0xFF) * cov + ((bg ) & 0xFF) * (15 - cov)) / 15; put(x0 + x1, y + y1, (r << 16) | (g << 8) | b); } } } /* Where the bar goes. Derived, not stored, so nothing can set it and then have * the screen change underneath it. */ static void bar_geometry(void) { PROG_W = (int)FB.width / BAR_W_FRAC; if (PROG_W < BAR_W_MIN) PROG_W = BAR_W_MIN; if (PROG_W > (int)FB.width - 80) PROG_W = (int)FB.width - 80; PROG_H = BAR_H; PROG_X = ((int)FB.width - PROG_W) / 2; /* Vertically: the whole group is centred, and the bar sits below the logo * and the name. Anchoring to the centre rather than to the top is what * keeps the arrangement identical from 800x600 up to 1600x1200. */ int group_h = LOGO_H + LOGO_GAP + GLYPH_H + NAME_GAP + PROG_H; int top = ((int)FB.height - group_h) / 2; PROG_Y = top + LOGO_H + LOGO_GAP + GLYPH_H + NAME_GAP; } void ui_splash(const char *product, const char *subtitle) { if (!ACTIVE) return; ui_clear(); bar_geometry(); int group_h = LOGO_H + LOGO_GAP + GLYPH_H + NAME_GAP + PROG_H; int top = ((int)FB.height - group_h) / 2; ui_logo(top); ui_text_centered(top + LOGO_H + LOGO_GAP, product, SCH_FG); if (subtitle) ui_text_centered(PROG_Y + PROG_H + SUB_GAP, subtitle, SCH_DIM); } 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. 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, SCH_ACCENT_DIM); 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) { ui_fill_rect(PROG_X + PROG_LAST_PX, PROG_Y, px - PROG_LAST_PX, PROG_H, SCH_ACCENT); PROG_LAST_PX = px; } } void ui_progress_end(void) { if (!ACTIVE || !PROG_ACTIVE) return; ui_progress_tick(PROG_TOTAL); PROG_ACTIVE = 0; }