Nexoniarz is the sole author and SchOS was never published under Apache-2.0, so this is a straight relicence. LICENSE is the FSF GPLv2 text, every first-party SPDX header moves to GPL-2.0-or-later, and NOTICE, README.md, docs/licensing.md and the site say so. projects/schfs-linux follows the rest of the tree. The GPL gives copyleft: a fork has to ship its source. It also lifts the old "Linux drivers, never" rule, since GPL-2.0-only code combines with GPL-2.0-or-later. "Or later" leaves room for GPLv3, but one binary still has to pick one version, so docs/licensing.md now keeps Apache-2.0 and GPLv3 code out of the kernel to keep Linux code possible there. BSD 4-clause code goes from "avoid" to "no", because the advertising clause is GPL-incompatible. LDLS still ships no .ko. That boundary is now project policy rather than a legal requirement, and ldls.md, devices.md and licensing.md section 6 say so. refmod declares license=GPL, and ldls-check passes (27 checks). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
478 lines
15 KiB
C
478 lines
15 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/theme.h"
|
|
#include "../../include/font.h"
|
|
#include "../../include/logo.h"
|
|
|
|
/* "Draw the glyph, leave the background alone." Not a colour anyone can mean:
|
|
* the palette is 24-bit, so the high byte is free for a sentinel. */
|
|
#define SCH_BG_NONE 0xFF000000u
|
|
|
|
static struct ui_fb FB;
|
|
static int ACTIVE;
|
|
|
|
/* Progress state. Kept here rather than passed around because the caller ticks
|
|
* it from inside a read loop that has no business carrying UI state. */
|
|
static ui_u32 PROG_TOTAL, PROG_DONE;
|
|
static int PROG_X, PROG_Y, PROG_W, PROG_H;
|
|
static int PROG_LAST_PX; /* pixels already filled */
|
|
static int PROG_ACTIVE;
|
|
|
|
/* Layout constants. One place, because a splash that is laid out by scattered
|
|
* literals is one nobody dares move.
|
|
*
|
|
* The mark and the bar are the same width, and that is the arrangement: a small
|
|
* mark with a short bar directly beneath it, both centred. Everything else --
|
|
* gaps, panel sizes -- is expressed against that width so the whole thing
|
|
* scales as one object.
|
|
*/
|
|
#define MARK_W_FRAC 8 /* the mark is width/8 ... */
|
|
#define MARK_W_MIN 72 /* ... clamped, because a mark that tracks the */
|
|
#define MARK_W_MAX 128 /* mode exactly is tiny at 800 and soft at 1600 */
|
|
#define MARK_GAP 30 /* mark bottom to the bar */
|
|
#define SUB_GAP 20 /* bar to the line under it */
|
|
#define BAR_H 4 /* thin: it is a hint, not a gauge */
|
|
#define GLYPH_W 8
|
|
#define GLYPH_H 16
|
|
|
|
/* --- the metal ------------------------------------------------------------
|
|
*
|
|
* Brushed metal, done the cheap way that actually survives a 4-bit mask and a
|
|
* 32bpp framebuffer: a vertical gradient from a light top to a darker bottom,
|
|
* with a brighter band near the top where a highlight would fall. There is no
|
|
* texture and no noise -- at this size they read as dirt rather than as metal,
|
|
* and the loader has no business carrying a texture asset.
|
|
*
|
|
* The mark is not a flat colour any more for the same reason a real one would
|
|
* not be: a single grey on a dark ground looks printed, and the gradient is
|
|
* what makes it look like an object with a light above it.
|
|
*/
|
|
#define METAL_TOP 0xF2F4F8u /* near-white, where the light lands */
|
|
#define METAL_MID 0xB9BEC7u
|
|
#define METAL_BOT 0x6E747Fu /* shadowed underside */
|
|
#define METAL_EDGE 0x3A3F49u /* panel border */
|
|
#define PANEL_BG 0x151821u /* panel ground, a shade off SCH_BG */
|
|
#define PANEL_TOP 0x232838u /* bevel: one row lighter along the top */
|
|
|
|
static ui_u32
|
|
mix(ui_u32 a, ui_u32 b, int num, int den)
|
|
{
|
|
if (den <= 0) return a;
|
|
if (num < 0) num = 0;
|
|
if (num > den) num = den;
|
|
ui_u32 r = ((((a >> 16) & 0xFF) * (den - num)) + (((b >> 16) & 0xFF) * num)) / den;
|
|
ui_u32 g = ((((a >> 8) & 0xFF) * (den - num)) + (((b >> 8) & 0xFF) * num)) / den;
|
|
ui_u32 bl = ((((a ) & 0xFF) * (den - num)) + (((b ) & 0xFF) * num)) / den;
|
|
return (r << 16) | (g << 8) | bl;
|
|
}
|
|
|
|
/* The metal colour at row `i` of an object `h` rows tall. */
|
|
static ui_u32
|
|
metal(int i, int h)
|
|
{
|
|
if (h <= 1)
|
|
return METAL_MID;
|
|
/* Two segments: a fast lift into the highlight over the top third, then a
|
|
* long fall to the shadow. A single linear ramp looks like a gradient; the
|
|
* knee is what makes it look lit. */
|
|
int knee = h / 3;
|
|
if (i <= knee)
|
|
return mix(METAL_MID, METAL_TOP, i, knee ? knee : 1);
|
|
return mix(METAL_TOP, METAL_BOT, i - knee, h - knee);
|
|
}
|
|
|
|
void
|
|
ui_init(const struct ui_fb *fb)
|
|
{
|
|
ACTIVE = 0;
|
|
if (!fb || !fb->base || !fb->width || !fb->height)
|
|
return;
|
|
FB = *fb;
|
|
if (!FB.stride)
|
|
FB.stride = FB.width * 4;
|
|
ACTIVE = 1;
|
|
}
|
|
|
|
int ui_active(void) { return ACTIVE; }
|
|
ui_u32 ui_width(void) { return ACTIVE ? FB.width : 0; }
|
|
ui_u32 ui_height(void) { return ACTIVE ? FB.height : 0; }
|
|
|
|
static void
|
|
put(int x, int y, ui_u32 colour)
|
|
{
|
|
if (!ACTIVE || x < 0 || y < 0 || (ui_u32)x >= FB.width || (ui_u32)y >= FB.height)
|
|
return;
|
|
*(ui_u32 *)(FB.base + (ui_u64)y * FB.stride + (ui_u64)x * 4) = colour;
|
|
}
|
|
|
|
void
|
|
ui_fill_rect(int x, int y, int w, int h, ui_u32 colour)
|
|
{
|
|
if (!ACTIVE)
|
|
return;
|
|
for (int r = 0; r < h; r++) {
|
|
int yy = y + r;
|
|
if (yy < 0 || (ui_u32)yy >= FB.height)
|
|
continue;
|
|
/* One row at a time through a row pointer: put() per pixel costs a
|
|
* bounds check and a multiply for every one of them, and this is used
|
|
* to clear a whole screen. */
|
|
ui_u32 *row = (ui_u32 *)(FB.base + (ui_u64)yy * FB.stride);
|
|
for (int c = 0; c < w; c++) {
|
|
int xx = x + c;
|
|
if (xx >= 0 && (ui_u32)xx < FB.width)
|
|
row[xx] = colour;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
ui_clear(void)
|
|
{
|
|
ui_fill_rect(0, 0, (int)FB.width, (int)FB.height, SCH_BG);
|
|
}
|
|
|
|
static void
|
|
glyph(int x, int y, ui_u8 c, ui_u32 colour, ui_u32 bg)
|
|
{
|
|
for (int row = 0; row < GLYPH_H; row++) {
|
|
ui_u8 bits = font_data[c][row];
|
|
for (int col = 0; col < GLYPH_W; col++) {
|
|
if (bits & (0x80 >> col))
|
|
put(x + col, y + row, colour);
|
|
else if (bg != SCH_BG_NONE)
|
|
put(x + col, y + row, bg);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
ui_text(int x, int y, const char *s, ui_u32 colour, ui_u32 bg)
|
|
{
|
|
if (!ACTIVE || !s)
|
|
return;
|
|
for (int i = 0; s[i]; i++)
|
|
glyph(x + i * GLYPH_W, y, (ui_u8)s[i], colour, bg);
|
|
}
|
|
|
|
static int
|
|
slen(const char *s)
|
|
{
|
|
int n = 0;
|
|
while (s && s[n])
|
|
n++;
|
|
return n;
|
|
}
|
|
|
|
void
|
|
ui_text_centered(int y, const char *s, ui_u32 colour)
|
|
{
|
|
if (!ACTIVE)
|
|
return;
|
|
int w = slen(s) * GLYPH_W;
|
|
ui_text(((int)FB.width - w) / 2, y, s, colour, SCH_BG_NONE);
|
|
}
|
|
|
|
int
|
|
ui_mark_width(void)
|
|
{
|
|
if (!ACTIVE)
|
|
return MARK_W_MIN;
|
|
int w = (int)FB.width / MARK_W_FRAC;
|
|
if (w < MARK_W_MIN) w = MARK_W_MIN;
|
|
if (w > MARK_W_MAX) w = MARK_W_MAX;
|
|
return w;
|
|
}
|
|
|
|
void
|
|
ui_logo(int y, int w)
|
|
{
|
|
if (!ACTIVE)
|
|
return;
|
|
if (w <= 0 || w > LOGO_W)
|
|
w = LOGO_W; /* never upscale: it only goes soft */
|
|
|
|
int h = (LOGO_H * w) / LOGO_W;
|
|
int x0 = ((int)FB.width - w) / 2;
|
|
|
|
/* Box-filter down from the source mask.
|
|
*
|
|
* Point-sampling a coverage mask drops half the edge pixels and gives a
|
|
* mark with holes in the thin strokes -- exactly where a logo can least
|
|
* afford them. Averaging the coverage of every source pixel that lands in a
|
|
* destination pixel is what keeps the shape intact at 72 px.
|
|
*/
|
|
for (int dy = 0; dy < h; dy++) {
|
|
int sy0 = (dy * LOGO_H) / h, sy1 = ((dy + 1) * LOGO_H) / h;
|
|
if (sy1 <= sy0) sy1 = sy0 + 1;
|
|
|
|
ui_u32 shade = metal(dy, h);
|
|
|
|
for (int dx = 0; dx < w; dx++) {
|
|
int sx0 = (dx * LOGO_W) / w, sx1 = ((dx + 1) * LOGO_W) / w;
|
|
if (sx1 <= sx0) sx1 = sx0 + 1;
|
|
|
|
unsigned sum = 0, n = 0;
|
|
for (int sy = sy0; sy < sy1; sy++) {
|
|
for (int sx = sx0; sx < sx1; sx++) {
|
|
unsigned idx = (unsigned)sy * LOGO_W + (unsigned)sx;
|
|
sum += (logo_mask[idx >> 1] >> ((idx & 1) ? 0 : 4)) & 0xF;
|
|
n++;
|
|
}
|
|
}
|
|
unsigned cov = n ? sum / n : 0;
|
|
if (!cov)
|
|
continue;
|
|
|
|
/* Coverage blends the shaded metal against the ground, so the mark
|
|
* has soft edges and a lit face at the same time. */
|
|
put(x0 + dx, y + dy, mix(SCH_BG, shade, (int)cov, 15));
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Where the bar goes. Derived, not stored, so nothing can set it and then have
|
|
* the screen change underneath it.
|
|
*
|
|
* The bar is exactly the mark's width and sits directly beneath it: the two are
|
|
* one object, and a bar wider than the mark it belongs to reads as a separate
|
|
* thing that happens to be nearby.
|
|
*/
|
|
static void
|
|
bar_geometry(void)
|
|
{
|
|
PROG_W = ui_mark_width();
|
|
PROG_H = BAR_H;
|
|
PROG_X = ((int)FB.width - PROG_W) / 2;
|
|
|
|
int mark_h = (LOGO_H * PROG_W) / LOGO_W;
|
|
int group_h = mark_h + MARK_GAP + PROG_H;
|
|
int top = ((int)FB.height - group_h) / 2;
|
|
PROG_Y = top + mark_h + MARK_GAP;
|
|
}
|
|
|
|
void
|
|
ui_splash(const char *subtitle)
|
|
{
|
|
if (!ACTIVE)
|
|
return;
|
|
|
|
ui_clear();
|
|
bar_geometry();
|
|
|
|
int w = PROG_W;
|
|
int mark_h = (LOGO_H * w) / LOGO_W;
|
|
int top = PROG_Y - MARK_GAP - mark_h;
|
|
|
|
ui_logo(top, w);
|
|
|
|
if (subtitle)
|
|
ui_text_centered(PROG_Y + PROG_H + SUB_GAP, subtitle, SCH_DIM);
|
|
}
|
|
|
|
/* --- panels ---------------------------------------------------------------
|
|
*
|
|
* A panel is a plate: a slightly lighter ground than the screen, one lighter
|
|
* row along the top and a darker border, which is the whole of the bevel. Two
|
|
* rows of shading do more for the impression of a raised surface than any
|
|
* amount of gradient inside it, and cost nothing to draw.
|
|
*/
|
|
static void
|
|
panel(int x, int y, int w, int h)
|
|
{
|
|
ui_fill_rect(x, y, w, h, PANEL_BG);
|
|
ui_fill_rect(x, y, w, 1, PANEL_TOP); /* lit top edge */
|
|
ui_fill_rect(x, y + h - 1, w, 1, METAL_EDGE); /* shadowed bottom */
|
|
ui_fill_rect(x, y, 1, h, METAL_EDGE);
|
|
ui_fill_rect(x + w - 1, y, 1, h, METAL_EDGE);
|
|
}
|
|
|
|
/* A heading in metal: the same shading the mark uses, applied per row of the
|
|
* glyph cell, so a title looks struck out of the same material. */
|
|
static void
|
|
metal_text_centered(int y, const char *s)
|
|
{
|
|
int len = slen(s);
|
|
int x = ((int)FB.width - len * GLYPH_W) / 2;
|
|
|
|
for (int row = 0; row < GLYPH_H; row++) {
|
|
ui_u32 shade = metal(row, GLYPH_H);
|
|
for (int i = 0; i < len; i++) {
|
|
ui_u8 bits = font_data[(ui_u8)s[i]][row];
|
|
for (int col = 0; col < GLYPH_W; col++)
|
|
if (bits & (0x80 >> col))
|
|
put(x + i * GLYPH_W + col, y + row, shade);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Panel metrics.
|
|
*
|
|
* TITLE_H is the whole title band -- the padding above the heading, the
|
|
* heading itself, and the air beneath it before the rule. It was 34 while the
|
|
* heading alone needs 18 + 16 = 34, so the rule at TITLE_H - 6 was drawn
|
|
* *through* the lettering and the first row started on the heading's baseline.
|
|
* Derived from its parts now, so it cannot drift from them again.
|
|
*/
|
|
#define ROW_H 24
|
|
#define PANEL_PAD 18
|
|
#define TITLE_GAP 14 /* heading to the rule */
|
|
#define TITLE_H (PANEL_PAD + GLYPH_H + TITLE_GAP) /* = 48 */
|
|
#define ROW_LIFT ((ROW_H - GLYPH_H) / 2) /* highlight overhang */
|
|
|
|
static void
|
|
panel_frame(const char *title, int rows, const char *hint,
|
|
int *out_x, int *out_y, int *out_w)
|
|
{
|
|
int w = (int)FB.width / 2;
|
|
if (w < 360) w = 360;
|
|
if (w > (int)FB.width - 60) w = (int)FB.width - 60;
|
|
|
|
int h = TITLE_H + rows * ROW_H + PANEL_PAD * 2;
|
|
int x = ((int)FB.width - w) / 2;
|
|
int y = ((int)FB.height - h) / 2;
|
|
|
|
ui_clear();
|
|
|
|
/* The mark, small, above the plate: it is the same screen, not a new one. */
|
|
int mw = ui_mark_width() * 2 / 3;
|
|
int mh = (LOGO_H * mw) / LOGO_W;
|
|
if (y - mh - 24 > 0)
|
|
ui_logo(y - mh - 24, mw);
|
|
|
|
panel(x, y, w, h);
|
|
metal_text_centered(y + PANEL_PAD, title);
|
|
ui_fill_rect(x + PANEL_PAD, y + PANEL_PAD + GLYPH_H + TITLE_GAP / 2,
|
|
w - PANEL_PAD * 2, 1, METAL_EDGE);
|
|
|
|
if (hint)
|
|
ui_text_centered(y + h + 16, hint, SCH_DIM);
|
|
|
|
*out_x = x; *out_y = y + TITLE_H; *out_w = w;
|
|
}
|
|
|
|
void
|
|
ui_menu(const char *title, const char *const *items, int n, int selected,
|
|
const char *hint)
|
|
{
|
|
if (!ACTIVE)
|
|
return;
|
|
|
|
int x, y, w;
|
|
panel_frame(title, n, hint, &x, &y, &w);
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
int ry = y + i * ROW_H;
|
|
|
|
if (i == selected) {
|
|
/* The selected row is the only lit thing on the plate. Filling it
|
|
* with the accent and putting the label in the ground colour reads
|
|
* at a glance; an outline does not, at this size. */
|
|
/* Centred on the glyph box: (ROW_H - GLYPH_H) / 2 above it, the
|
|
* same below. Off-by-a-few here is what makes a highlight look
|
|
* like it is sitting on the text rather than around it. */
|
|
ui_fill_rect(x + PANEL_PAD / 2, ry - ROW_LIFT, w - PANEL_PAD, ROW_H,
|
|
SCH_ACCENT);
|
|
ui_text(x + PANEL_PAD, ry, items[i], PANEL_BG, SCH_BG_NONE);
|
|
} else {
|
|
ui_text(x + PANEL_PAD, ry, items[i], SCH_FG, SCH_BG_NONE);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
ui_panel(const char *title, const char *const *labels,
|
|
const char *const *values, int n, const char *hint)
|
|
{
|
|
if (!ACTIVE)
|
|
return;
|
|
|
|
int x, y, w;
|
|
panel_frame(title, n, hint, &x, &y, &w);
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
int ry = y + i * ROW_H;
|
|
ui_text(x + PANEL_PAD, ry, labels[i], SCH_DIM, SCH_BG_NONE);
|
|
|
|
/* Values right-aligned, so the column reads down rather than ragged. */
|
|
int vw = slen(values[i]) * GLYPH_W;
|
|
ui_text(x + w - PANEL_PAD - vw, ry, values[i], SCH_FG, SCH_BG_NONE);
|
|
}
|
|
}
|
|
|
|
void
|
|
ui_progress_begin(const char *label, ui_u32 total)
|
|
{
|
|
if (!ACTIVE)
|
|
return;
|
|
|
|
bar_geometry();
|
|
PROG_TOTAL = total ? total : 1;
|
|
PROG_DONE = 0;
|
|
PROG_LAST_PX = 0;
|
|
PROG_ACTIVE = 1;
|
|
|
|
/* The unfilled track: a recess, not a colour. Drawn once -- repainting it
|
|
* on every tick is what makes a progress bar flicker. */
|
|
ui_fill_rect(PROG_X, PROG_Y, PROG_W, PROG_H, 0x1B1F28u);
|
|
ui_fill_rect(PROG_X, PROG_Y, PROG_W, 1, 0x101319u);
|
|
if (label)
|
|
ui_progress_label(label);
|
|
}
|
|
|
|
void
|
|
ui_progress_label(const char *label)
|
|
{
|
|
if (!ACTIVE || !label)
|
|
return;
|
|
|
|
/* Clear the line first. A shorter label after a longer one would otherwise
|
|
* leave the tail of the old one behind. */
|
|
int y = PROG_Y + PROG_H + SUB_GAP;
|
|
ui_fill_rect(0, y, (int)FB.width, GLYPH_H, SCH_BG);
|
|
ui_text_centered(y, label, SCH_DIM);
|
|
}
|
|
|
|
void
|
|
ui_progress_tick(ui_u32 done)
|
|
{
|
|
if (!ACTIVE || !PROG_ACTIVE)
|
|
return;
|
|
|
|
if (done > PROG_TOTAL)
|
|
done = PROG_TOTAL;
|
|
PROG_DONE = done;
|
|
|
|
int px = (int)(((ui_u64)PROG_DONE * (ui_u64)PROG_W) / PROG_TOTAL);
|
|
|
|
/* Only the newly filled part. Filling from zero each time is O(width) per
|
|
* tick and visibly slower than the thing it is reporting on. */
|
|
if (px > PROG_LAST_PX) {
|
|
/* The fill is metal too, shaded across the bar's height, so it looks
|
|
* like the same material as the mark rather than a coloured rectangle
|
|
* that happens to be underneath it. */
|
|
for (int r = 0; r < PROG_H; r++)
|
|
ui_fill_rect(PROG_X + PROG_LAST_PX, PROG_Y + r,
|
|
px - PROG_LAST_PX, 1, metal(r, PROG_H));
|
|
PROG_LAST_PX = px;
|
|
}
|
|
}
|
|
|
|
void
|
|
ui_progress_end(void)
|
|
{
|
|
if (!ACTIVE || !PROG_ACTIVE)
|
|
return;
|
|
ui_progress_tick(PROG_TOTAL);
|
|
PROG_ACTIVE = 0;
|
|
}
|