Skip to content

Two-pen immediate-mode layout: stack directions, hug, fill, indent, centering, origin-relative scrolling - #58

Merged
treeform merged 4 commits into
masterfrom
silky-layout
Sep 4, 2026
Merged

treeform merged 4 commits into
masterfrom
silky-layout

Conversation

@treeform

Copy link
Copy Markdown
Owner

What

A single-pass immediate-mode layout system built on an explicit theory: a layout scope is two pens — the pen P (sk.at, where the next element draws) and the stretch pen S (sk.stretchAt, the farthest corner reached) — plus a sign vector σ pointing from the scope's origin corner into the region. The axioms (A1–A8) and the theorems derived from them (T1–T8) live in docs/layout_theory.md; docs/layout_plan.md maps each one to the implementation. Everything resolves on the first frame: no layout tree, no measure pass, no caches.

Features

  • All four stack directions (T1): TopToBottom, BottomToTop, LeftToRight, RightToLeft are one rule — flipping σ — instead of per-direction case blocks. All direction math lives in the new src/silky/layout.nim (placedPos / advancePen / contentBox); the old parallel stacks (atStack/posStack/sizeStack/directionStack) and global stretchAt are replaced by a scope stack in both the GPU and testing Silky variants, with sk.at/sk.stretchAt kept as accessors so existing code is unchanged.
  • Per-axis sizing modes (A8): width/height/size (fixed), fillWidth/fillHeight (remaining known space, T3), hug() (children extent, T2). Hug works in one pass because the vertex buffer is patchable: the parent's chrome is emitted at scope close and rotated behind its children; reverse-direction hug translates the finished span into place (T6).
  • indent n: blocks (T4) — cross-axis pen nudge, nests naturally.
  • center() (T5) — legal when both parent and child sizes are known; doesn't advance the pen.
  • Origin-relative scrolling (T7): FrameState.scrollPos is now the distance t from the origin corner, clamped to overflow max(|S−O| − R, 0); content translates by −σ·t. A BottomToTop frame rests scrolled to the bottom with its thumb resting at the bottom of the track — chat-log behavior with no special case. One shared frameScrollbars proc replaces the duplicated widget/DSL scrollbar code.
  • Base widgets (button, text, checkbox, radio, scrubber, dropdown, listbox, …) place via placedAt() before drawing, so they flow correctly in every direction.

Try it

nim r tests/manual_layout.nim

Interactive playground: radios for the four directions and for fixed/hug/fill/scroll parent modes, scrubbers for padding/spacing/box count, toggles for indent, centering, and live widgets in the flow.

Testing

  • nim r -d:silkyTesting tests/test_layout.nim — 14 headless tests asserting exact rects for every feature through the semantic layer, including reverse-direction hug and scroll rest/translate/clamp positions.
  • All 86 existing example tests (basicwindow, calculator, gameplayer, layouts, menu, panels, the7gui, todomvc) pass unchanged — forward-direction behavior is preserved.
  • Fixed a latent newTestHarness bug: sk.window was never assigned, so any interactive widget segfaulted under the harness.

Known limitations (documented in the plan)

  • Interaction rects inside a reverse-direction hug are provisional for that frame (event handlers should follow children in the body).
  • Semantic child rects inside translated spans are not re-stamped.
  • Weighted multi-fill (flex-grow) and justify/space-between are deliberately out: they require seeing all siblings before placing the first — a second pass, which the theory forbids (A8).

🤖 Generated with Claude Code

@treeform treeform mentioned this pull request Jul 11, 2026
treeform and others added 4 commits September 4, 2026 14:06
…ter, origin-relative scrolling

Implements the layout theory in docs/layout_theory.md: a layout scope is
two pens (pen P, stretch pen S) plus a sign vector sigma pointing from
the origin corner into the region. All direction math lives in
placedPos/advancePen in the new src/silky/layout.nim; the old
per-direction case blocks and parallel stacks are gone.

- All four stack directions work through one rule (flip sigma).
- Per-axis sizing modes: fixed, fill (remaining known space), and hug
  (children extent measured by the stretch pen; chrome emitted at close
  and rotated behind the children in the vertex buffer).
- Reverse-direction hug via vertex-span translation (position is
  relative until close).
- indent blocks (cross-axis pen nudge), center() when both sizes known.
- Scrolling is origin-relative: scrollPos is the distance t from the
  origin corner, clamped to overflow; content translates by -sigma*t.
  A bottom-origin frame rests scrolled to the bottom with its thumb
  resting at the bottom of the track. One shared frameScrollbars proc
  replaces the duplicated widget/DSL scrollbar code.
- tests/manual_layout.nim: interactive playground (directions, sizing
  modes, indent, centering, scroll frames).
- tests/test_layout.nim: 14 headless tests asserting exact rects for
  every feature via the semantic layer.
- newTestHarness now wires sk.window (interactive widgets segfaulted).

All 86 existing example tests pass unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The T7 demo frame now composes fill (T3) with scrolling: fillWidth/
fillHeight instead of a fixed 340x300 box, so it uses the whole demo
area. Box count cap raised to 30 (palette wraps) so horizontal
directions can overflow and show their scrollbars too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Scrolling is a ramification of clipping, not a sizing mode — so it's
now scrollable() on any known-size node, orthogonal to fixed/fill/hug.
Internally scrollable nodes route through the same clip + origin-
relative-scroll machinery as frames (finishFrameScrollbars now keys
off frameState instead of the node kind).

Playground: "Scroll frame" radio replaced by a "Scrollable (T7)"
checkbox that makes the demo area itself scrollable, composing with
all three sizing modes. New test: scrollable() rectangle rests on its
bottom edge in BottomToTop and translates by -sigma*t.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@treeform
treeform merged commit 9b18340 into master Sep 4, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant