SchOS/boot/common/ui.c
Nexoniarz a1c965da1e include: one folder per subsystem, and includes that name it
Sixty-five headers sat side by side in include/, so `#include "irq.h"`
said nothing about where irq.h was or what it belonged to, and errno.h,
stat.h and fcntl.h at the top shadowed the system's for any host tool
built with -I./include. They are in folders now -- arch/ core/ fs/
dev/ gfx/ net/ uapi/ (and ldls/ for the Linux driver layer) -- and
every include names the folder, as Linux's do: "core/proc.h".

Done mechanically and checked the hard way: the kernel built from this
tree disassembles identically to the one built from the tree before
it. The full image boots, and userland is the same set of programs.
Paths in docs and comments follow; docs/guides/layout.md has a table
of what is where, and tools/fix-includes.py rewrites old-style
includes from the folders in the tree, for code written before this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-23 15:05:12 +02:00

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/gfx/theme.h"
#include "../../include/gfx/font.h"
#include "../../include/gfx/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;
}