/* SPDX-License-Identifier: GPL-2.0-or-later */ /* * 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/gfx/theme.h" #include "../../include/gfx/font.h" #include "../../include/gfx/logo.h" #include "../../include/gfx/nvfont.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; /* What the mark is blended against: the splash ground, or a window body. */ static ui_u32 LOGO_BG = SCH_BG; /* The boot manager's scale and faces. 2 on a screen big enough that a 15 px * face would be a speck; the faces are rasterised at both sizes, not doubled. */ static int SC = 1; static const struct nvfont *F_BODY, *F_SMALL, *F_TITLE; static int CUR_ON; /* the pointer is drawn right now */ static void ui_set_scale(void) { SC = (FB.height >= 1400 || FB.width >= 2400) ? 2 : 1; F_BODY = SC == 2 ? &nvfont_body2 : &nvfont_body1; F_SMALL = SC == 2 ? &nvfont_small2 : &nvfont_small1; F_TITLE = SC == 2 ? &nvfont_title2 : &nvfont_title1; CUR_ON = 0; /* a new framebuffer holds no pointer */ } /* 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; ui_set_scale(); } 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(LOGO_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 track is wider than the mark and sits beneath it: a mark with a recessed * track and one line of type, centred, on the same graphite the boot manager * uses. Laid out from the middle outwards so it reads the same at 800x600 and * at 4K. S() is defined further down with the rest of the Niveus code, so the * scale is applied through SC directly. */ static void bar_geometry(void) { PROG_W = 220 * SC; PROG_H = 6 * SC; PROG_X = ((int)FB.width - PROG_W) / 2; int mw = ui_mark_width() * SC; if (mw > LOGO_W) mw = LOGO_W; int mark_h = (LOGO_H * mw) / LOGO_W; int gap = 34 * SC; int group_h = mark_h + gap + PROG_H; int top = ((int)FB.height - group_h) / 2; PROG_Y = top + mark_h + gap; } static ui_u32 bg_at(int y); static void nv_backdrop(void); static int nv_width(const struct nvfont *f, const char *s); static void nv_text(const struct nvfont *f, int x, int base, const char *s, ui_u32 fg, ui_u32 bgtop, ui_u32 bgbot, int y0, int h); static void rrect(int x, int y, int w, int h, int r, ui_u32 top, ui_u32 bot, ui_u32 bgtop, ui_u32 bgbot, int bgy, int bgh); void ui_splash(const char *subtitle) { if (!ACTIVE) return; nv_backdrop(); bar_geometry(); int w = ui_mark_width() * SC; if (w > LOGO_W) w = LOGO_W; int mark_h = (LOGO_H * w) / LOGO_W; int top = PROG_Y - 34 * SC - mark_h; ui_u32 saved = LOGO_BG; LOGO_BG = bg_at(top + mark_h / 2); ui_logo(top, w); LOGO_BG = saved; if (subtitle) ui_progress_label(subtitle); } /* --- Niveus ----------------------------------------------------------------- * * The boot manager is drawn in the same language as the rest of the system * (docs/Website/niveus): windows are solid, controls are * bevelled keys with light falling across them, fields are wells cut into the * surface, and the ground under all of it is warm graphite rather than blue. * * Every shape here is a rounded rectangle with a vertical gradient, which is * all that skeuomorphism needs at this size. Corners are anti-aliased by * supersampling the corner pixels only, and the AA blends against a colour the * caller *names*, never against what is in the framebuffer: reading the * framebuffer back is uncached on real hardware and ruinously slow. * * The typeface is Instrument Sans, rasterised at build time (nvfont.h). */ #define NV_BG_TOP 0x1B1B1Eu /* the ground, light at the top */ #define NV_BG_BOT 0x0B0B0Du #define NV_WIN_BODY 0x232326u #define NV_WIN_EDGE 0x050507u #define NV_TITLE_TOP 0x45444Au #define NV_TITLE_BOT 0x2C2B30u #define NV_HILITE 0x5E5D64u /* the lit upper edge of a raised thing */ #define NV_KEY_TOP 0x3F3E44u #define NV_KEY_BOT 0x2D2C32u #define NV_KEY_EDGE 0x111113u #define NV_KEY_SHADOW 0x141416u #define NV_WELL_TOP 0x101012u #define NV_WELL_BOT 0x171719u #define NV_WELL_EDGE 0x0A0A0Bu #define NV_WELL_LIP 0x3A393Eu /* light catching the lower lip of a well */ #define NV_TXT 0xECEAE5u #define NV_TXT2 0xA9A7A1u #define NV_TXT3 0x74726Du #define S(n) ((n) * SC) #define WHITE 0xFFFFFFu #define BLACK 0x000000u static ui_u32 bg_at(int y) { return mix(NV_BG_TOP, NV_BG_BOT, y, (int)FB.height > 1 ? (int)FB.height - 1 : 1); } static void nv_backdrop(void) { for (int y = 0; y < (int)FB.height; y++) ui_fill_rect(0, y, (int)FB.width, 1, bg_at(y)); } static int nv_glyph_index(unsigned c) { if (c < NVFONT_FIRST || c > NVFONT_LAST) c = '?'; return (int)(c - NVFONT_FIRST); } static int nv_width(const struct nvfont *f, const char *s) { int w = 0; for (; *s && *s != '\n'; s++) w += f->g[nv_glyph_index((ui_u8)*s)].adv; return (w + 32) >> 6; } static int nv_height(const struct nvfont *f) { return f->ascent + f->descent; } /* Draw `s` (up to a newline) with its origin on baseline `base`. The colour * under it is a vertical gradient from bgtop at row y0 to bgbot at y0+h. */ static void nv_text(const struct nvfont *f, int x, int base, const char *s, ui_u32 fg, ui_u32 bgtop, ui_u32 bgbot, int y0, int h) { int pen = 0; /* 1/64 px: the face's own spacing */ for (; *s && *s != '\n'; s++) { const struct nvglyph *g = &f->g[nv_glyph_index((ui_u8)*s)]; const ui_u8 *bits = f->bits + g->off; int gx = x + ((pen + 32) >> 6); for (int r = 0; r < g->h; r++) { int py = base + g->oy + r; ui_u32 bg = mix(bgtop, bgbot, py - y0, h > 1 ? h - 1 : 1); for (int c = 0; c < g->w; c++) { ui_u8 a = bits[r * g->w + c]; if (a) put(gx + g->ox + c, py, mix(bg, fg, a, 255)); } } pen += g->adv; } } /* How much of the pixel at (dx,dy) from a corner's centre lies inside a circle * of radius r, in sixteenths. */ static int corner_cov(int dx, int dy, int r) { int n = 0, R = r * 8; for (int j = 0; j < 4; j++) for (int i = 0; i < 4; i++) { int sx = dx * 8 + 2 * i + 1, sy = dy * 8 + 2 * j + 1; if (sx * sx + sy * sy <= R * R) n++; } return n; } static void rrect(int x, int y, int w, int h, int r, ui_u32 top, ui_u32 bot, ui_u32 bgtop, ui_u32 bgbot, int bgy, int bgh) { if (w <= 0 || h <= 0) return; if (r * 2 > h) r = h / 2; if (r * 2 > w) r = w / 2; for (int rr = 0; rr < h; rr++) { int yy = y + rr; ui_u32 col = mix(top, bot, rr, h > 1 ? h - 1 : 1); int cy, corner = 1; if (rr < r) cy = y + r; else if (rr >= h - r) cy = y + h - r; else corner = 0; if (!corner) { ui_fill_rect(x, yy, w, 1, col); continue; } ui_u32 bg = mix(bgtop, bgbot, yy - bgy, bgh > 1 ? bgh - 1 : 1); ui_fill_rect(x + r, yy, w - 2 * r, 1, col); for (int k = 0; k < r; k++) { int dy = yy - cy; int cl = corner_cov(k - r, dy, r); int cr = corner_cov(k, dy, r); if (cl) put(x + k, yy, mix(bg, col, cl, 16)); if (cr) put(x + w - r + k, yy, mix(bg, col, cr, 16)); } } } /* A raised object: an edge, a gradient face, and a bright line just inside the * top where the light lands. */ static void bevel(int x, int y, int w, int h, int r, ui_u32 top, ui_u32 bot, ui_u32 edge, ui_u32 hi, ui_u32 bgtop, ui_u32 bgbot, int bgy, int bgh) { rrect(x, y, w, h, r, edge, edge, bgtop, bgbot, bgy, bgh); rrect(x + SC, y + SC, w - 2 * SC, h - 2 * SC, r > SC ? r - SC : r, top, bot, edge, edge, 0, 1); if (hi) ui_fill_rect(x + r, y + SC, w - 2 * r, SC, hi); } /* A recess: the same thing with the light reversed. Darker at the top where the * rim shades it, with a pale lip along the bottom edge. */ static void well(int x, int y, int w, int h, ui_u32 bgcol) { int r = S(8); bevel(x, y, w, h, r, NV_WELL_TOP, NV_WELL_BOT, NV_WELL_EDGE, 0, bgcol, bgcol, 0, 1); ui_fill_rect(x + r, y + h - SC, w - 2 * r, SC, NV_WELL_LIP); } /* The soft shadow beneath a window, as stacked translucent layers laid down * from the outermost in, each one blended against the backdrop's own gradient. */ static void shadow(int x, int y, int w, int h, int r) { int spread = S(16), dy = S(8); for (int i = spread; i > 0; i -= SC) { int t = spread - i; int a = 60 * t * t / (spread * spread); /* percent black */ int ty = y - i + dy, th = h + 2 * i; ui_u32 top = mix(bg_at(ty), BLACK, a, 100); ui_u32 bot = mix(bg_at(ty + th), BLACK, a, 100); rrect(x - i, ty, w + 2 * i, th, r + i, top, bot, bg_at(ty), bg_at(ty + th), ty, th); } } /* --- the pointer ----------------------------------------------------------- */ #define CUR_W 12 #define CUR_H 19 static const char *const CUR_SHAPE[CUR_H] = { "X ", "XX ", "X.X ", "X..X ", "X...X ", "X....X ", "X.....X ", "X......X ", "X.......X ", "X........X ", "X.........X ", "X......XXXXX", "X...X..X ", "X..X X..X ", "X.X X..X ", "XX X..X ", "X X..X ", " X..X ", " XXX ", }; static ui_u32 CUR_SAVE[CUR_W * 2 * CUR_H * 2]; static int CUR_X, CUR_Y, CUR_WANT; static void cursor_hide(void) { if (!CUR_ON || !ACTIVE) return; int w = CUR_W * SC, h = CUR_H * SC; for (int r = 0; r < h; r++) for (int c = 0; c < w; c++) { int px = CUR_X + c, py = CUR_Y + r; if (px >= 0 && py >= 0 && (ui_u32)px < FB.width && (ui_u32)py < FB.height) *(ui_u32 *)(FB.base + (ui_u64)py * FB.stride + (ui_u64)px * 4) = CUR_SAVE[r * w + c]; } CUR_ON = 0; } static void cursor_show(void) { if (!CUR_WANT || CUR_ON || !ACTIVE) return; int w = CUR_W * SC, h = CUR_H * SC; for (int r = 0; r < h; r++) for (int c = 0; c < w; c++) { int px = CUR_X + c, py = CUR_Y + r; if (px < 0 || py < 0 || (ui_u32)px >= FB.width || (ui_u32)py >= FB.height) continue; ui_u32 *p = (ui_u32 *)(FB.base + (ui_u64)py * FB.stride + (ui_u64)px * 4); CUR_SAVE[r * w + c] = *p; char ch = CUR_SHAPE[r / SC][c / SC]; if (ch == 'X') *p = BLACK; else if (ch == '.') *p = WHITE; } CUR_ON = 1; } void ui_cursor(int x, int y, int show) { cursor_hide(); CUR_X = x; CUR_Y = y; CUR_WANT = show; cursor_show(); } /* --- window chrome --------------------------------------------------------- */ #define TB_H S(34) #define BODY_PAD S(18) #define HINT_H S(36) static void window(int x, int y, int w, int h, const char *title) { int r = S(12), tb = TB_H; shadow(x, y, w, h, r); rrect(x, y, w, h, r, NV_WIN_EDGE, NV_WIN_EDGE, bg_at(y), bg_at(y + h), y, h); rrect(x + SC, y + SC, w - 2 * SC, h - 2 * SC, r - SC, NV_WIN_BODY, NV_WIN_BODY, NV_WIN_EDGE, NV_WIN_EDGE, 0, 1); /* The titlebar is rounded on top only: draw it tall enough that its bottom * corners fall below the line, then lay the body colour over the excess. */ rrect(x + SC, y + SC, w - 2 * SC, tb + r, r - SC, NV_TITLE_TOP, NV_TITLE_BOT, NV_WIN_EDGE, NV_WIN_EDGE, 0, 1); ui_fill_rect(x + SC, y + SC + tb, w - 2 * SC, r, NV_WIN_BODY); ui_fill_rect(x + SC, y + SC + tb - SC, w - 2 * SC, SC, NV_WIN_EDGE); ui_fill_rect(x + r, y + SC, w - 2 * r, SC, NV_HILITE); int tw = nv_width(F_BODY, title); int base = y + SC + (tb - nv_height(F_BODY)) / 2 + F_BODY->ascent; nv_text(F_BODY, x + (w - tw) / 2, base, title, NV_TXT2, NV_TITLE_TOP, NV_TITLE_BOT, y + SC, tb); } static void footer(int x, int y, int w, int h, const char *hint) { if (!hint) return; int fy = y + h - HINT_H; ui_fill_rect(x + S(14), fy, w - S(28), SC, 0x1A1A1Cu); ui_fill_rect(x + S(14), fy + SC, w - S(28), SC, 0x2E2E32u); int tw = nv_width(F_SMALL, hint); int base = fy + (HINT_H - nv_height(F_SMALL)) / 2 + F_SMALL->ascent + SC; nv_text(F_SMALL, x + (w - tw) / 2, base, hint, NV_TXT3, NV_WIN_BODY, NV_WIN_BODY, fy, HINT_H); } /* --- keys ------------------------------------------------------------------ */ #define MAXKEYS 40 static struct { int x, y, w, h; } KEY[MAXKEYS]; static struct { const char *title, *hint; const char *const *items; int n, sel, scroll, vis, kind; /* kind: 0 a list of keys, 1 a panel */ int has_detail, kh, gap, logo; int wx, wy, ww, wh; int lx, ly, lw; /* the list's box */ int ypos[MAXKEYS]; } M; static int has_nl(const char *s) { for (; *s; s++) if (*s == '\n') return 1; return 0; } static const char * label_of(const char *s) { return s[0] == 1 ? s + 1 : s; } static const char * detail_of(const char *s) { for (; *s; s++) if (*s == '\n') return s + 1; return 0; } static void paint_key(int i, int sel) { if (i < M.scroll || i >= M.scroll + M.vis || i >= MAXKEYS) return; int x = KEY[i].x, y = KEY[i].y, w = KEY[i].w, h = KEY[i].h, r = S(8); const char *item = M.items[i]; const char *label = label_of(item), *detail = detail_of(item); /* Wipe the ring/shadow a previous state left, then draw this one. */ ui_fill_rect(x - S(3), y - S(3), w + S(6), h + S(7), NV_WIN_BODY); ui_u32 top, bot, edge, hi, fg, fg2; if (sel) { ui_u32 ring = mix(NV_WIN_BODY, SCH_ACCENT, 70, 100); rrect(x - S(2), y - S(2), w + S(4), h + S(4), r + S(2), ring, ring, NV_WIN_BODY, NV_WIN_BODY, 0, 1); top = mix(SCH_ACCENT, WHITE, 20, 100); bot = mix(SCH_ACCENT, BLACK, 24, 100); edge = mix(SCH_ACCENT, BLACK, 62, 100); hi = mix(top, WHITE, 45, 100); fg = WHITE; fg2 = 0xE2DBFFu; } else { rrect(x, y + SC, w, h, r, NV_KEY_SHADOW, NV_KEY_SHADOW, NV_WIN_BODY, NV_WIN_BODY, 0, 1); top = NV_KEY_TOP; bot = NV_KEY_BOT; edge = NV_KEY_EDGE; hi = NV_HILITE; fg = NV_TXT; fg2 = NV_TXT3; } bevel(x, y, w, h, r, top, bot, edge, hi, NV_WIN_BODY, NV_WIN_BODY, 0, 1); int ty0 = y + SC, th = h - 2 * SC, tx = x + S(18); if (detail) { int lh = nv_height(F_BODY), dh = nv_height(F_SMALL); int total = lh + S(2) + dh; int t = y + (h - total) / 2; nv_text(F_BODY, tx, t + F_BODY->ascent, label, fg, top, bot, ty0, th); nv_text(F_SMALL, tx, t + lh + S(2) + F_SMALL->ascent, detail, sel ? fg2 : NV_TXT2, top, bot, ty0, th); } else { int base = y + (h - nv_height(F_BODY)) / 2 + F_BODY->ascent; nv_text(F_BODY, tx, base, label, fg, top, bot, ty0, th); } if (M.kind == 0 && M.n <= 9 && M.vis == M.n) { char num[2] = { (char)('1' + i), 0 }; int nw = nv_width(F_SMALL, num); int base = y + (h - nv_height(F_SMALL)) / 2 + F_SMALL->ascent; nv_text(F_SMALL, x + w - S(18) - nw, base, num, sel ? fg2 : NV_TXT3, top, bot, ty0, th); } } static void menu_layout(void) { int n = M.n > MAXKEYS ? MAXKEYS : M.n; M.n = n; M.has_detail = 0; for (int i = 0; i < n; i++) if (has_nl(M.items[i])) M.has_detail = 1; M.gap = S(8); M.kh = M.has_detail ? S(50) : S(34); M.ww = S(520); if (M.ww > (int)FB.width - S(24)) M.ww = (int)FB.width - S(24); int maxh = (int)FB.height - S(24); int chrome = TB_H + BODY_PAD * 2 + HINT_H + 2 * SC; int logoh = S(56) + S(16); int full; for (int pass = 0; pass < 2; pass++) { int y = 0; for (int i = 0; i < n; i++) { if (pass == 0 && i && M.items[i][0] == 1) y += S(14); M.ypos[i] = y; y += M.kh + M.gap; } full = n ? y - M.gap : 0; if (pass == 0 && chrome + full <= maxh) break; if (pass == 0 && chrome + logoh + full <= maxh) break; } M.logo = 1; M.vis = n; int list_h = full; if (chrome + logoh + full > maxh) M.logo = 0; if (chrome + full > maxh) { int avail = maxh - chrome; M.vis = (avail + M.gap) / (M.kh + M.gap); if (M.vis < 1) M.vis = 1; if (M.vis > n) M.vis = n; list_h = M.vis * M.kh + (M.vis - 1) * M.gap; } M.wh = chrome + (M.logo ? logoh : 0) + list_h; M.wx = ((int)FB.width - M.ww) / 2; M.wy = ((int)FB.height - M.wh) / 2; if (M.wy < 0) M.wy = 0; M.lx = M.wx + S(20); M.lw = M.ww - S(40) - (M.vis < n ? S(14) : 0); M.ly = M.wy + SC + TB_H + BODY_PAD + (M.logo ? logoh : 0); if (M.sel < M.scroll) M.scroll = M.sel; if (M.sel >= M.scroll + M.vis) M.scroll = M.sel - M.vis + 1; if (M.scroll < 0) M.scroll = 0; if (M.scroll > n - M.vis) M.scroll = n - M.vis; if (M.scroll < 0) M.scroll = 0; for (int i = 0; i < n; i++) { KEY[i].x = M.lx; KEY[i].w = M.lw; KEY[i].h = M.kh; KEY[i].y = M.ly + M.ypos[i] - M.ypos[M.scroll]; } } static void menu_paint(void) { cursor_hide(); nv_backdrop(); window(M.wx, M.wy, M.ww, M.wh, M.title); if (M.logo) { ui_u32 saved = LOGO_BG; LOGO_BG = NV_WIN_BODY; ui_logo(M.wy + SC + TB_H + BODY_PAD, S(56)); LOGO_BG = saved; } for (int i = M.scroll; i < M.scroll + M.vis && i < M.n; i++) paint_key(i, i == M.sel); if (M.vis < M.n) { int tx = M.lx + M.lw + S(8), tw = S(4); int th = M.vis * M.kh + (M.vis - 1) * M.gap; rrect(tx, M.ly, tw, th, tw / 2, NV_WELL_TOP, NV_WELL_BOT, NV_WIN_BODY, NV_WIN_BODY, 0, 1); int thumb = th * M.vis / M.n; if (thumb < S(16)) thumb = S(16); int ty = M.ly + (th - thumb) * M.scroll / (M.n - M.vis); rrect(tx, ty, tw, thumb, tw / 2, NV_KEY_TOP, NV_KEY_BOT, NV_WIN_BODY, NV_WIN_BODY, 0, 1); } footer(M.wx, M.wy, M.ww, M.wh, M.hint); cursor_show(); } void ui_menu(const char *title, const char *const *items, int n, int selected, const char *hint) { if (!ACTIVE) return; M.title = title; M.items = items; M.n = n; M.sel = selected; M.hint = hint; M.scroll = 0; M.kind = 0; menu_layout(); menu_paint(); } void ui_menu_select(int selected) { if (!ACTIVE || M.kind != 0 || selected < 0 || selected >= M.n || selected == M.sel) return; int old = M.sel; M.sel = selected; if (selected < M.scroll || selected >= M.scroll + M.vis) { menu_layout(); menu_paint(); return; } cursor_hide(); paint_key(old, 0); paint_key(selected, 1); cursor_show(); } int ui_menu_hit(int x, int y) { if (!ACTIVE) return -1; for (int i = M.scroll; i < M.scroll + M.vis && i < M.n; i++) if (x >= KEY[i].x && x < KEY[i].x + KEY[i].w && y >= KEY[i].y && y < KEY[i].y + KEY[i].h) return i; return -1; } /* A panel: label/value rows in a well, and one button underneath. With no * values, the labels are a message and are centred. */ static void box(const char *title, const char *const *labels, const char *const *values, int n, const char *button, const char *hint) { if (!ACTIVE) return; int rowh = S(28), wpad = S(12); int well_h = n * rowh + 2 * wpad; int btn_h = S(34), btn_w = S(180); M.title = title; M.hint = hint; M.kind = 1; M.items = &button; M.n = 1; M.sel = 0; M.scroll = 0; M.vis = 1; M.ww = S(520); if (M.ww > (int)FB.width - S(24)) M.ww = (int)FB.width - S(24); M.wh = TB_H + 2 * SC + BODY_PAD + well_h + S(18) + btn_h + BODY_PAD + HINT_H; M.wx = ((int)FB.width - M.ww) / 2; M.wy = ((int)FB.height - M.wh) / 2; if (M.wy < 0) M.wy = 0; cursor_hide(); nv_backdrop(); window(M.wx, M.wy, M.ww, M.wh, title); int wx = M.wx + S(20), ww = M.ww - S(40); int wy = M.wy + SC + TB_H + BODY_PAD; well(wx, wy, ww, well_h, NV_WIN_BODY); for (int i = 0; i < n; i++) { int ry = wy + wpad + i * rowh; int base = ry + (rowh - nv_height(F_BODY)) / 2 + F_BODY->ascent; if (i) ui_fill_rect(wx + S(14), ry, ww - S(28), SC, 0x1E1E21u); if (values) { nv_text(F_BODY, wx + S(16), base, labels[i], NV_TXT2, NV_WELL_TOP, NV_WELL_BOT, wy, well_h); int vw = nv_width(F_BODY, values[i]); nv_text(F_BODY, wx + ww - S(16) - vw, base, values[i], NV_TXT, NV_WELL_TOP, NV_WELL_BOT, wy, well_h); } else { int lw = nv_width(F_BODY, labels[i]); nv_text(F_BODY, wx + (ww - lw) / 2, base, labels[i], NV_TXT, NV_WELL_TOP, NV_WELL_BOT, wy, well_h); } } KEY[0].w = btn_w; KEY[0].h = btn_h; KEY[0].x = M.wx + (M.ww - btn_w) / 2; KEY[0].y = wy + well_h + S(18); M.has_detail = 0; paint_key(0, 1); footer(M.wx, M.wy, M.ww, M.wh, hint); cursor_show(); } void ui_panel(const char *title, const char *const *labels, const char *const *values, int n, const char *hint) { box(title, labels, values, n, "Back", hint); } void ui_notice(const char *title, const char *const *lines, int n, const char *button, const char *hint) { box(title, lines, 0, n, button, hint); } /* The track is a recess, the fill is the accent, lit from above -- the same two * materials as a well and a selected key in the boot manager. The fill is * redrawn from the left edge on every tick rather than extended, because its * rounded end moves; the bar is small enough that this costs nothing. */ static void progress_paint(int px) { int r = PROG_H / 2; ui_u32 bg = bg_at(PROG_Y + PROG_H / 2); rrect(PROG_X, PROG_Y, PROG_W, PROG_H, r, 0x0A0A0Bu, 0x151517u, bg, bg, 0, 1); if (px >= PROG_H) { ui_u32 top = mix(SCH_ACCENT, 0xFFFFFFu, 28, 100); ui_u32 bot = mix(SCH_ACCENT, 0x000000u, 24, 100); rrect(PROG_X + 1, PROG_Y + 1, px - 2, PROG_H - 2, r - 1, top, bot, 0x0A0A0Bu, 0x151517u, PROG_Y, PROG_H); } } 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; progress_paint(0); if (label) ui_progress_label(label); } void ui_progress_label(const char *label) { if (!ACTIVE || !label) return; /* Repaint the backdrop under the line first. A shorter label after a longer * one would otherwise leave the tail of the old one behind. */ int h = nv_height(F_BODY) + 4 * SC; int y = PROG_Y + PROG_H + 18 * SC; for (int r = 0; r < h; r++) ui_fill_rect(0, y + r, (int)FB.width, 1, bg_at(y + r)); int tw = nv_width(F_BODY, label); nv_text(F_BODY, ((int)FB.width - tw) / 2, y + F_BODY->ascent, label, NV_TXT3, bg_at(y), bg_at(y + h), y, h); } 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); if (px > PROG_LAST_PX) { progress_paint(px); PROG_LAST_PX = px; } } void ui_progress_end(void) { if (!ACTIVE || !PROG_ACTIVE) return; ui_progress_tick(PROG_TOTAL); PROG_ACTIVE = 0; }