Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions examples/inline-regions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ const GREEN = rgba(80, 250, 123);
const GREEN_BG = rgba(20, 70, 38);
const GRAY = rgba(100, 100, 100);
const CYAN = rgba(139, 233, 253);
const DARK_BG = rgba(30, 30, 40);

const RED = rgba(255, 0, 0);
const ORANGE = rgba(255, 153, 0);
Expand Down Expand Up @@ -85,7 +84,7 @@ await main(function* () {
);

let first = term.render(
box("Press any key to compile modules.", CYAN, GRAY, DARK_BG),
box("Press any key to compile modules.", CYAN, GRAY),
{ row },
);
write(new Uint8Array(first.output));
Expand All @@ -102,7 +101,6 @@ await main(function* () {
`${icon} ${label} ${time}`,
done ? GREEN : CYAN,
done ? GREEN : GRAY,
DARK_BG,
),
{ row },
);
Expand Down Expand Up @@ -350,7 +348,7 @@ function waitKey(): void {
}
}

function box(msg: string, fg: number, border: number, bg: number): Op[] {
function box(msg: string, fg: number, border: number, bg?: number): Op[] {
return [
open("root", {
layout: { width: grow(), height: grow(), direction: "ttb" },
Expand Down
60 changes: 45 additions & 15 deletions ops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,26 @@ function packAxis(view: DataView, offset: number, axis: SizingAxis): number {
return o;
}

function sideWidth(side: BorderSide | undefined): number {
return typeof side === "number" ? side : side?.width ?? 0;
}

function sideFg(side: BorderSide | undefined, shared: number): number {
let color = typeof side === "object" && side.color !== undefined
? side.color
: shared;
return color & 0x00FFFFFF;
}

function sideBg(
side: BorderSide | undefined,
shared: number | undefined,
): number {
let bg = typeof side === "object" && side.bg !== undefined ? side.bg : shared;
// ATTR_DEFAULT sentinel (bit 31 set) means "keep the existing cell bg"
return bg === undefined ? 0x80000000 : bg & 0x00FFFFFF;
}

function packString(
view: DataView,
bytes: Uint8Array,
Expand Down Expand Up @@ -154,21 +174,23 @@ export function pack(

if (op.border) {
let b = op.border;
view.setUint32(o, b.color, true);
o += 4;

// ATTR_DEFAULT sentinel (bit 31 set) means "use terminal default bg"
let bg = b.bg === undefined ? 0x80000000 : b.bg & 0x00FFFFFF;
view.setUint32(o, bg, true);
o += 4;

view.setUint32(
o,
(b.left ?? 0) | ((b.right ?? 0) << 8) | ((b.top ?? 0) << 16) |
((b.bottom ?? 0) << 24),
sideWidth(b.left) | (sideWidth(b.right) << 8) |
(sideWidth(b.top) << 16) | (sideWidth(b.bottom) << 24),
true,
);
o += 4;

// Resolved per-side attributes (CSS-like fallback expansion done
// here, not in C): fg/bg word pairs in top, right, bottom, left
// order. The C renderer consumes these as explicit values.
for (let side of [b.top, b.right, b.bottom, b.left]) {
view.setUint32(o, sideFg(side, b.color), true);
o += 4;
view.setUint32(o, sideBg(side, b.bg), true);
o += 4;
}
}

if (op.clip) {
Expand Down Expand Up @@ -280,6 +302,14 @@ export interface CloseElement {
directive: typeof OP_CLOSE_ELEMENT;
}

export type BorderSide =
| number
| {
width: number;
color?: number;
bg?: number;
};

export interface OpenElement {
directive: typeof OP_OPEN_ELEMENT;
id: string;
Expand All @@ -297,10 +327,10 @@ export interface OpenElement {
border?: {
color: number;
bg?: number;
left?: number;
right?: number;
top?: number;
bottom?: number;
left?: BorderSide;
right?: BorderSide;
top?: BorderSide;
bottom?: BorderSide;
};
clip?: { horizontal?: boolean; vertical?: boolean };
floating?: {
Expand Down Expand Up @@ -366,7 +396,7 @@ function packSize(ops: Op[]): number {
if (op.layout) n += 6 * 4 + 4 + 4 + 4; // 2 axes (3 words each) + pad + gap + align
if (op.bg !== undefined) n += 4;
if (op.cornerRadius) n += 4;
if (op.border) n += 12;
if (op.border) n += 36; // widths word + 4 sides × (fg + bg)
if (op.clip) n += 4;
if (op.floating) n += 16;
break;
Expand Down
54 changes: 46 additions & 8 deletions specs/renderer-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -644,8 +644,11 @@ The `open()` constructor currently accepts the following property groups in its
padding (per-side), alignment (currently numeric enum values, with a planned
transition to string literals), direction (top-to-bottom or left-to-right),
and gap
- **`border`** — per-side border widths, border color, and border background
color
- **`border`** — per-side border configuration. Each side field (`top`, `right`,
`bottom`, `left`) accepts either a scalar width or a structured object
`{ width, color?, bg? }`. The shared `color` field is required and is the
fallback foreground for every side; the optional shared `bg` field is the
fallback border-cell background for every side
- **`cornerRadius`** — per-corner radius values, producing rounded box-drawing
characters
- **`clip`** — clip region configuration for scroll containers
Expand All @@ -661,12 +664,47 @@ These property groups represent the current implementation surface. New groups
and fields have been added incrementally and more may follow. Alignment values
are expected to transition from numeric to string-literal form.

**Border background.** When `border.bg` is provided, the renderer MUST apply
that background color to all cells occupied by border glyphs (corners,
horizontal edges, and vertical edges). When `border.bg` is omitted, border
rendering MUST NOT override the background already present in each border cell;
element backgrounds established by `open({ bg })` remain in effect, and the
terminal default remains in effect where no element background applies.
**Border sides.** Each border side is declared independently as either a scalar
width (`top: 1`) or a structured object (`top: { width: 1, color?, bg? }`). The
two forms are equivalent when the object form provides only `width`. A side is
enabled when its resolved width is greater than zero; an omitted side or a side
with width `0` MUST NOT be drawn. Scalar side declarations MUST keep their
pre-existing behavior.

**Border side colors (fallback resolution).** Side attributes resolve in a
CSS-like shorthand/longhand fashion before rendering:

- A structured side with `color` MUST render with that foreground color. A
scalar side, or a structured side that omits `color`, MUST fall back to the
shared `border.color`. The shared `color` remains required.
- A structured side with `bg` MUST render border cells of that side with that
background color. A scalar side, or a structured side that omits `bg`, MUST
fall back to the shared `border.bg` when it is provided.
- When neither the side nor the shared border provides `bg`, border rendering
MUST NOT override the background already present in each border cell of that
side; element backgrounds established by `open({ bg })` remain in effect, and
the terminal default remains in effect where no element background applies.

Fallback resolution is performed on the TypeScript side before the frame is
transferred; the WASM renderer consumes explicit per-side attributes and does
not implement the public fallback rules.

**Independent sides and corners.** Each enabled side renders as a straight edge
(`─` for horizontal sides, `│` for vertical sides). A corner glyph MUST be
rendered only when both adjacent sides for that corner are enabled; when either
adjacent side is absent, the present side continues straight through the
endpoint with no corner glyph. A left-only border is therefore a plain vertical
line, and a top-plus-bottom border is two plain horizontal rules.

**Corner styling approximation.** A terminal cell carries a single glyph,
foreground, and background, so CSS-style diagonally split corners cannot be
represented. When corners are rendered: top corners (`┌`, `┐`, and their rounded
variants) MUST use the resolved attributes of the `top` side, and bottom corners
(`└`, `┘`, and their rounded variants) MUST use the resolved attributes of the
`bottom` side. Left and right side attributes apply to vertical edge cells
excluding joined corner cells. Per-side attributes affect only the styling of
corner cells; corner glyph shape selection (including rounded corners via
`cornerRadius`) is unchanged.

**Border width and layout interaction.** In the underlying layout engine (Clay),
border configuration does not affect layout computation. This is Clay's intended
Expand Down
48 changes: 31 additions & 17 deletions src/clayterm.c
Original file line number Diff line number Diff line change
Expand Up @@ -303,44 +303,55 @@ static void render_text(struct Clayterm *ct, int x0, int y0,
static void render_border(struct Clayterm *ct, int x0, int y0, int x1, int y1,
Clay_RenderCommand *cmd) {
Clay_BorderRenderData *b = &cmd->renderData.border;
uint32_t fg = color(b->color);
/* userData is currently exclusively the packed border-bg word. */
uint32_t bg = (uint32_t)(uintptr_t)cmd->userData;
/* userData points at eight words in the command buffer carrying resolved
* per-side attributes as fg/bg pairs in top, right, bottom, left order.
* Fallback resolution (shared color/bg vs side overrides) happens on the
* TypeScript side; this renderer consumes explicit values only. The
* command buffer outlives the render pass within reduce(). */
const uint32_t *s = (const uint32_t *)cmd->userData;
uint32_t deffg = color(b->color);
uint32_t top_fg = s ? s[0] : deffg, top_bg = s ? s[1] : ATTR_DEFAULT;
uint32_t right_fg = s ? s[2] : deffg, right_bg = s ? s[3] : ATTR_DEFAULT;
uint32_t bot_fg = s ? s[4] : deffg, bot_bg = s ? s[5] : ATTR_DEFAULT;
uint32_t left_fg = s ? s[6] : deffg, left_bg = s ? s[7] : ATTR_DEFAULT;
Comment on lines +306 to +316

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so if i'm understanding this correctly, deffg can't resolve to the shared color anymore, reduce() no longer sets decl.border.color, so b->color is always {0} and this fallback yields explicit black, not the shared fg, since the s===NULL branch is only reachable on a truncated buffer that validate would reject anyway, maybe just a fall back after to ATTR_DEFAULT here like the bg pairs do? as written it reads like the shared color still exists on the C side but it only lives in the ts resolution now 👀

int top = b->width.top > 0;
int bot = b->width.bottom > 0;
int left = b->width.left > 0;
int right = b->width.right > 0;

/* corners — rounded when corner radius > 0 */
/* corners — rounded when corner radius > 0. Drawn only when both adjacent
* sides are enabled; a terminal cell holds a single fg/bg, so top corners
* take the top side attributes and bottom corners take the bottom side
* attributes (deterministic approximation of CSS split corners). */
uint32_t tl = b->cornerRadius.topLeft > 0 ? 0x256d : 0x250c;
uint32_t tr = b->cornerRadius.topRight > 0 ? 0x256e : 0x2510;
uint32_t bl = b->cornerRadius.bottomLeft > 0 ? 0x2570 : 0x2514;
uint32_t br = b->cornerRadius.bottomRight > 0 ? 0x256f : 0x2518;

if (top && left)
setcell(ct, x0, y0, tl, fg, bg);
setcell(ct, x0, y0, tl, top_fg, top_bg);
if (top && right)
setcell(ct, x1 - 1, y0, tr, fg, bg);
setcell(ct, x1 - 1, y0, tr, top_fg, top_bg);
if (bot && left)
setcell(ct, x0, y1 - 1, bl, fg, bg);
setcell(ct, x0, y1 - 1, bl, bot_fg, bot_bg);
if (bot && right)
setcell(ct, x1 - 1, y1 - 1, br, fg, bg);
setcell(ct, x1 - 1, y1 - 1, br, bot_fg, bot_bg);

/* horizontal edges */
if (top)
for (int x = x0 + left; x < x1 - right; x++)
setcell(ct, x, y0, 0x2500, fg, bg);
setcell(ct, x, y0, 0x2500, top_fg, top_bg);
if (bot)
for (int x = x0 + left; x < x1 - right; x++)
setcell(ct, x, y1 - 1, 0x2500, fg, bg);
setcell(ct, x, y1 - 1, 0x2500, bot_fg, bot_bg);

/* vertical edges */
/* vertical edges — excluding joined corner cells owned by top/bottom */
if (left)
for (int y = y0 + top; y < y1 - bot; y++)
setcell(ct, x0, y, 0x2502, fg, bg);
setcell(ct, x0, y, 0x2502, left_fg, left_bg);
if (right)
for (int y = y0 + top; y < y1 - bot; y++)
setcell(ct, x1 - 1, y, 0x2502, fg, bg);
setcell(ct, x1 - 1, y, 0x2502, right_fg, right_bg);
}

/* ── Command buffer helpers ───────────────────────────────────────── */
Expand Down Expand Up @@ -533,15 +544,18 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row) {
}

if (mask & PROP_BORDER) {
decl.border.color = unpack_color(rd(buf, len, &i));

decl.userData = (void *)(uintptr_t)rd(buf, len, &i);

uint32_t bw = rd(buf, len, &i);
decl.border.width.left = bw & 0xff;
decl.border.width.right = (bw >> 8) & 0xff;
decl.border.width.top = (bw >> 16) & 0xff;
decl.border.width.bottom = (bw >> 24) & 0xff;

/* Resolved per-side fg/bg attribute words (top, right, bottom,
* left). Routed to render_border via userData; the command buffer
* remains valid for the whole render pass. */
if (i + 8 <= len)
decl.userData = (void *)&buf[i];
i += 8;
}

if (mask & PROP_CLIP) {
Expand Down
Loading
Loading