- tty1-tty12 all run login; the image has only root. /dev/tty7..12 could not be written (cdev.c knew minors 10-16 only), which sent their jobs into a restart storm that took the supervisor down; fixed, and stat reports them as Linux's tty7..12. - Erasing text at a prompt did nothing: erase_one sent a bare backspace, which only moves; it and the echo path now send back-blank-back through vt_feed. - A white cursor block was left at the end of every line of a verbose boot: a line feed at the bottom scrolls the pixels, cursor included, and the cursor was erased after the scroll. It comes off first now. A blank cell is also marked dirty, which it was not. - ldlsd died with a general protection fault on real hardware in its variadic log function: Linux module code calls with an 8-byte-aligned stack and an unset %al, and the prologue's movaps faults. LDLS guest code is built with -mstackrealign. - make iso took over a minute for any change, because staging r8169 rewrote the same files and every mtime moved; it now replaces files only if their contents differ (about 10 s). - The loader's splash is redrawn in the boot manager's style, and the boot manager shows its cursor whenever a pointer device exists. - xf86-input-libinput is out of the image again: with it Xorg took both keyboard and pointer from libinput and, on the user's laptop, neither worked. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
1010 lines
31 KiB
C
1010 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 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;
|
|
}
|