SchOS/boot/common/ui.c
Nexoniarz dee3affa3b Boot manager: Niveus window, keys and wells, Instrument Sans, pointer, other systems, working power entries
The F8 menu is redrawn in the Niveus language: a window with three lights,
bevelled keys with a lit edge, wells for panels, anti-aliased corners and a
soft shadow, set in Instrument Sans rasterised into include/gfx/nvfont.h
(tools/python/mknvfont.py, make nvfont). Scale doubles on screens 1400 px tall
or more. Lists taller than the screen scroll.

The keyboard still drives everything, and a mouse, touchpad or tablet does
too: both UEFI pointer protocols are polled, hover moves the highlight, a
click activates. The cursor stays hidden until the pointer has really done
something, since the firmware installs a pointer protocol with or without a
mouse. The 'keep this resolution?' countdown is one ten-second budget however
many times the pointer moves.

Loaders found at well-known paths on any volume (Windows, Ubuntu, Debian,
Fedora, Arch, openSUSE, systemd-boot, rEFInd, other BOOTX64.EFI) are listed
and chain-loaded with LoadImage/StartImage. 'Reboot to firmware' used to halt;
it now sets OsIndications and restarts, and Restart and Shut Down call
ResetSystem.

Checked under QEMU+OVMF: menu, resolution list (incl. 2560x1600 at double
scale), system information, shut down, firmware setup, chain-loading a copy of
the loader from a second disk, and a full boot of the ISO to the login prompt.
Pointer hover and click were checked only with synthetic events: in this setup
a USB mouse or tablet delivered no event to the loader (one USB I/O handle
appeared but no pointer protocol came from it), and why was not established.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 15:31:39 +01:00

999 lines
31 KiB
C

/* 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 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);
}
/* --- Niveus -----------------------------------------------------------------
*
* The boot manager is drawn in the same language as the rest of the system
* (docs/Website/niveus): windows are solid and carry three lights, 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 NV_LIGHT_CLOSE 0xFF5F57u
#define NV_LIGHT_MIN 0xFEBC2Eu
#define NV_LIGHT_ZOOM 0x28C840u
#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);
}
}
static void
light(int x, int y, int d, ui_u32 colour, int tb_y, int tb_h)
{
ui_u32 top = mix(colour, WHITE, 28, 100);
ui_u32 bot = mix(colour, BLACK, 22, 100);
ui_u32 rim = mix(colour, BLACK, 62, 100);
rrect(x - SC, y - SC, d + 2 * SC, d + 2 * SC, d / 2 + SC, rim, rim,
NV_TITLE_TOP, NV_TITLE_BOT, tb_y, tb_h);
rrect(x, y, d, d, d / 2, top, bot, rim, rim, 0, 1);
}
/* --- 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 d = S(12), ly = y + SC + (tb - d) / 2, lx = x + S(18);
light(lx, ly, d, NV_LIGHT_CLOSE, y + SC, tb);
light(lx + S(20), ly, d, NV_LIGHT_MIN, y + SC, tb);
light(lx + S(40), ly, d, NV_LIGHT_ZOOM, y + SC, tb);
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);
}
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;
}