Skip to content

design: upstream OctosCode's measured-design lowering (flows, fill/fit, markdown, right anchors, in-slot) - #77

Merged
ymote merged 10 commits into
mainfrom
feat/octoscode-design-lowering
Oct 6, 2026
Merged

ymote merged 10 commits into
mainfrom
feat/octoscode-design-lowering

Conversation

@ymote

@ymote ymote commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

Upstreams OctosCode's Octoscript-Makepad patch series, so OctosCode can drop its fork and build against this repository.

Source. OctosCode (octos-org/octoscode-app) keeps a fork of this repository built from patches/octoscript-makepad/0001–0007 on its main, applied on top of 6881fb6 by tools/prepare-octoscript-makepad-fork.sh. This PR is stacked on #75 (feat/mail-card-runtime, 27e9c1b, the commit OctoSense pins today), so its diff shows only these commits. design.rs and beauty.rs are identical at 6881fb6 and 27e9c1b, and the series applies there without conflicts.

Commits

One commit per behaviour. Each one builds and passes the portable tests on its own.

Commit Behaviour From
design: lay out row and col stacks as flows A stack with variant: row|col lowers to flow: Right|Down with pad/padx/pady and spacing; the flow places its children (no abs_pos, no margin). A filled one is a DesignSurface, so radius rounds it. 0001
design: size measured nodes with fill and fit flags fillw/fillh emit Fill and fitw/fith emit Fit in place of the measured extent. Unmeasured text wraps; an unmeasured image must state image_width/image_height; an empty font_src fails like a missing one. 0001
design: end a filled single-line label in an ellipsis fillw single lines get max_lines: 1 and TextOverflow.Ellipsis. 0001
kit-host: clear the beauty-host window to white Components without a fill preview on white rather than the theme's grey. 0001
design: lower markdown text to a Markdown flow with inline-code chips variant: markdown text lowers to Markdown. Inline code is drawn as chips filled with the node's bg. Wrapped lines advance at most 1.4× the font size; the source line_height becomes paragraph_spacing. 0002, 0004
design: anchor alignx 1 nodes to their parent's right edge A non-text node with alignx: 1 keeps its inset from the parent's right edge and follows the parent's width; its children move with it. 0003
design: place fill-width text and stacks with a wrapper A fillw stack or left-aligned text outside a flow sits in a wrapper that keeps its insets on both sides, so Fill resolves to a finite width. 0003, 0004, 0006
node, render, design: add padright, an inset from the parent's right edge New Attrs::padright overrides that right inset for both wrappers. 0006
design: add to_makepad_ui_in_slot for trees mounted in a slot Places the root by a margin from its slot, so every node is parent-relative instead of using window-coordinate abs_pos. 0007
design: cap the line spacing of wrapping text only when asked to_makepad_ui_with(tree, Options). cap_wrapped_line_spacing applies the 1.4× cap to wrapping text, and in_slot does what to_makepad_ui_in_slot does. The defaults lower exactly as before. 0005

Changes relative to the original patches

Requested cleanups:

  • Generic monospace font. 0002 named OctosCode's own self:resources/ux/LiberationMono-Regular.ttf. Inline code now uses mod.theme.font_code, the theme's code style, which makepad's own Markdown uses. Both makepad font sets map that role to the same LiberationMono face, now from makepad_widgets. The member metrics are now the theme's (asc/desc 0 instead of 0.04).
  • Opt-in line-spacing cap. 0005 capped every wrapping Text at 1.4× its font size, which changed every caller's output. It is now Options::cap_wrapped_line_spacing, off by default. Markdown regions are new here and are always capped (0004).
  • Comments and messages describe behaviour instead of OctosCode card numbers, probe names or file references.

Fixes to the patch code (output is unchanged for everything in OctosCode's committed design tree; see Verification):

  • 0002 put inline_code_padding/inline_code_margin inside draw_block. Those are TextFlow widget fields, not DrawFlowBlock fields, so they did nothing. They are dropped; chips keep the theme's padding and margin, as they already did.
  • 0001 emitted draw_bg.radius on a plain View, whose background has no radius. A filled row/col stack is now a DesignSurface, so the radius rounds it, as the patch's comment intended.
  • Wrappers are offset from the parent's source frame. The patch used the node's window position, which is only right when the parent is at the origin. The wrapper also takes the node's own height, so fillh is honoured; the patch used the measured h or 0.
  • For a right-anchored node, padright replaces the measured gap. The patch added it on top of the gap (on the wrapper), so a button stated as 40 from the edge landed 40 plus its gap. A root's gap is 0, so roots behave the same either way.
  • An image with fillw/fillh and no frame returns "design image size required"; the patch panicked on a.w.unwrap().
  • Text with no measured height wraps. The patch read the missing height as 0, which forced it onto one line.
  • A fill flag wins over a fit flag on both axes. The patch let fith beat fillh; no known source sets both.
  • Removed two clippy findings in the patch: unnecessary_unwrap, and an if with two identical abs_pos branches.

Kept out on purpose:

  • Row/Column kinds stay unsupported in this translator. 0001 also accepted the DSL's Row/Column kinds. Its own comment says compiled cards never produce them, and OctosCode's design sources use only stack + variant: row|col. Accepting them would route kit-composed L0 cards into this translator (see Glance tiles).
  • An empty font_src fails closed, for the same reason: the L0 kit's text roles carry font_src: "".

Downstream notes

  • Attrs has a new field. Code that builds Attrs { … } listing every field must add padright. In OctoSense that is AppCard's apps/appcard/app/app/src/app/l0_eval.rs:222. It mirrors this repository's evaluator, so padright: f32_prop(vm, value, id!(padright)) keeps it in step; padright: None also compiles. OctoSense's other Attrs literals end with ..Attrs::default(). OctoSense is not changed here.
  • Reproducing OctosCode's fork output. Call design::to_makepad_ui_with(&tree, Options { cap_wrapped_line_spacing: true, ..Default::default() }) where it now calls to_makepad_ui. Call Options { in_slot: true, cap_wrapped_line_spacing: true } where it calls to_makepad_ui_in_slot. The mono font no longer goes through OctosCode's with_fonts path rewrite.

Glance tiles (OctoSense crates/shell/src/glance_card.rs)

lower_report refuses kit packs (line 139), so a tile's tree is always kit-composed. It tries design::to_makepad_ui first and falls back to to_makepad_l0_ui when that fails (line 154).

  • At the base, every such tree fails here, mostly with unsupported measured design node: Column.

  • With the series as written, real cards would still fall back: their columns and rows lack heights, and their titles and captions lack widths. Minimal cards would not:

    • Surface { TextBody }
    • Surface { TextCaption(width: .fill) }
    • a bare TextBody root

    These would lower through the measured-design path with crate_resource("") as their font, the body forced onto one ellipsized line, and the page's top and bottom padding dropped.

  • With this PR, tiles behave as before. I replayed lower_report up to line 154 for OctoSense's 27 .card files and 8 minimal cards, in both shell modes. Every card gets the same outcome as at the base.

Verification

The repository has no .github/workflows, so the commands follow AGENTS.md, run in a worktree. Setup:

  • ../makepad and ../octoscript are worktrees at the runtime.json revisions (c155f61d, 2e37d9e6).

  • Cargo.lock is gitignored, so I used the lockfile already generated for 27e9c1b and ran every command with --locked.

  • cargo test --release --locked -p octoscript-node -p octoscript-render -p octoscript-makepad: base 86 passed. Head 96 passed, 0 failed (10 new tests in design.rs). Each of the 10 commits also passes on its own.

  • cargo clippy --locked --no-deps -p octoscript-node -p octoscript-render -p octoscript-makepad -p kit-host --all-targets -- -W clippy::erasing_op: passes, with the same warnings as the base and none in the files touched here.

    • Without -W clippy::erasing_op, clippy fails at base and head alike on the existing deny-level lint at crates/octoscript-makepad/src/material.rs:672, which this PR does not touch.
    • Without --no-deps, it also lints makepad-plot, which has existing deny-level approx_constant errors.
  • cargo fmt -p <crate> -- --check: already fails at the base on existing code, so this PR only avoids adding to it. Hunk counts, base → head: octoscript-node 9 → 9, octoscript-render 7 → 7, octoscript-makepad 159 → 155, kit-host 45 → 45.

    • Lines added here are rustfmt-clean, except the new Markdown arm inside the existing, unformatted widget match.
  • Output comparison. A scratch harness, not committed, lowered all 124 page.card files in OctosCode's design/ through l0::prepare:

    • Default options against the base: byte-identical except the 5 Markdown regions, which were Labels before.
    • With cap_wrapped_line_spacing, and with in_slot plus the cap, against the original series: byte-identical except two lines per Markdown region, text_style_fixed and draw_block.
  • Native acceptance: a hidden MAKEPAD_HIDE_WINDOWS=1 BEAUTY_REQUEST=… target/release/beauty-host --remote run (Metal) on a design that exercises every feature. The artboard is 393 wide, the window 480, and the root has fillw.

    • The tree evaluated and mounted, and /log held no warnings or errors.

    /snap rectangles:

    Node Rect Why
    row flow [16,16,361,44] Its children are at [28,24,…] and [64,24,…]: 12/8 padding, 8 spacing.
    filled title [16,76,287,24] 480 − 16 − its measured 177 right gap.
    padright: 16 bar [16,112,448,40] Insets of 16 on both sides.
    right-anchored button [420,168,44,44] 480 − 16 − 44; its child is at [432,180,20,20], moved with it.
    Markdown [16,228,361,150] Its measured frame.

    The /gq frame shows a white clear colour, a rounded row fill, the title ending in "…", and inline code drawn in monospace on light chips. The process exited after /gq.

🤖 Generated with Claude Code

ymote and others added 10 commits October 5, 2026 11:48
A measured design pins every node at its source frame, so a component
keeps the exact geometry of the artboard it was cut from and cannot
follow the size of the slot it is mounted in. Let a container opt into
native layout instead: a `stack` with `variant: row` or `variant: col`
lowers to a makepad `flow: Right` or `flow: Down` with its padding
(`pad`, `padx`, `pady`) and `spacing`, and the flow places its children,
so they emit neither `abs_pos` nor a positioning margin.

Measured sources describe every container as a `stack`, so the flow is
a stack variant. The DSL's Row and Column kinds stay unsupported in this
translator, as before: kit-composed L0 trees are built from them, and
hosts that try this translator first and fall back to the L0 translator
on error (OctoSense's glance tiles) rely on those trees being rejected.

A filled row/col stack lowers to `DesignSurface`, like any other filled
stack, so its `radius` rounds the fill.

From OctosCode's Octoscript-Makepad patch series.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every measured node emitted its source `w` and `h`, so an extracted
component kept its artboard size wherever it was mounted. Honour the
sizing flags the L0 translator already reads: `fillw` and `fillh` emit
`Fill`, `fitw` and `fith` emit `Fit`, in place of the measured extent,
and a fill flag wins over a fit flag. A node without flags keeps its
measured size, so existing designs lower exactly as before, and an axis
with neither a flag nor a measurement still fails.

Because a flag can now stand in for a missing measurement, inputs that
used to fail on their size need rules of their own:

- Text with no measured height takes it from its content, so it wraps
  unless it is marked `single_line`; it used to be read as a box of
  height 0, which forced it onto one line.
- An image with no measured frame must state its raster size
  (`image_width` and `image_height`), since the image shader samples by
  it; deriving it from the missing frame would panic.
- An empty `font_src` fails like a missing one, since it names no font.
  This also keeps kit-composed L0 text, whose `font_src` is empty, out
  of this translator.

From OctosCode's Octoscript-Makepad patch series.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A single measured line is lowered with `flow: Right` so that it never
wraps a whole word, and it clips at its measured box. A line that fills
its width (`fillw`) cannot know how long its text will be, since a
reusable row binds a title of any length, so it now gets `max_lines: 1`
and `TextOverflow.Ellipsis` and ends in an ellipsis when it overflows.
Measured lines are lowered as before.

From OctosCode's Octoscript-Makepad patch series.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The preview window cleared to the theme's app background, so a measured
component with no fill of its own previewed on grey rather than on the
white artboard it was drawn on. Clear the window pass to white.

From OctosCode's Octoscript-Makepad patch series.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A measured text node is a `Label`, so prose that contains inline code
could not draw that code as a chip. A text node with `variant: markdown`
now lowers to makepad's `Markdown`, the widget with both a declarative
body and an inline-code style: `body` carries the raw markdown, inline
code is set in the theme's code style (`mod.theme.font_code`, the
monospace face makepad's own Markdown uses) and drawn on a chip filled
with the node's `bg`, and the prose keeps the node's font, size and
colour.

The source `line_height` of such a region is the distance between its
paragraphs, which can be twice the font size; fed to `line_spacing` it
left a blank line under every wrapped line. It becomes
`paragraph_spacing`, and wrapped lines advance by at most 1.4 times the
font size, scaled against `TextFlow`'s own line advance.

`TextFlow` sizes each run by its own `font_size` and paints it with its
own `font_color`, so the size and ink are set on those fields.

The Label arm now shares the font helpers (resource, line box, emoji
and symbol faces) with the new arm; its output is unchanged.

From OctosCode's Octoscript-Makepad patch series, which named an
app-bundled monospace font and also put the chip padding and margin
inside `draw_block`, where `TextFlow` does not read them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A measured node stays at its source x, so a trailing control (a send
button, a toggle at the end of a row) is left behind when its component
is mounted wider than its artboard, or clipped when mounted narrower.

A non-text node with `alignx: 1` outside a flow is now anchored to its
parent's right edge. makepad ignores `align` on a child of an Overlay,
so the node goes in a full-width wrapper aligned right, and its right
margin keeps its measured gap to the parent's edge. Its children are
placed relative to it, so they move with it. On text, `alignx` keeps
its meaning: the alignment of the run inside the label.

The wrapper is offset from the parent's source frame, which each node
now receives (the root's parent is the mount: at the origin, as wide as
the root), and it is as tall as the node, so a node's extents are now
worked out before its wrapper is written. Nodes without `alignx: 1`
lower as before.

From OctosCode's Octoscript-Makepad patch series, which offset the
wrapper by the node's window position, correct only for children of a
parent at the origin, and gave it the measured height even under
`fillh`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A node pinned with `abs_pos` in an Overlay gets no usable width from a
`Fill` extent: with a parent whose height is `Fit` it resolves to
nothing, and a filled single-line label has no width to end its line
with an ellipsis, so it hard-clips mid-word.

A stack, or a left-aligned text, that fills its width outside a flow
now sits in a wrapper that keeps the node's measured insets on both
sides: its offset from the parent's left edge and its gap to the
parent's right edge. The node fills the wrapper, so it resolves a
finite width that follows the parent's, and its border or radius is
not cut off at the parent's edge. Text with another alignment places
its own run and keeps its position, and a markdown region is left to
the `row`/`col` flow it is meant to sit in. Like the right-anchor
wrapper, this one is offset from the parent's source frame and is as
tall as the node.

From OctosCode's Octoscript-Makepad patch series.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…edge

A node that fills its width, or is anchored to its parent's right edge,
keeps its measured gap to that edge. A source can now state the inset
instead: `padright` is the right margin of a right-anchored node and of
a fill node's wrapper. A root's measured gap is always 0, so a root
uses it to keep a gutter from the edge of its mount, where its border
and radius would otherwise be cut off.

`Attrs` gains a `padright` field, read by the evaluator like the other
insets. Code that builds `Attrs` with a struct literal naming every
field must add `padright: None` (or end with `..Default::default()`).

From OctosCode's Octoscript-Makepad patch series, which inset the
right-anchor wrapper by `padright` on top of the node's measured gap,
so a node stated as 40 from the edge landed 40 plus its gap from it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`to_makepad_ui` places the root, and every node outside a scroll view
or flow, with `abs_pos`, which makepad applies as given rather than
relative to the parent. That suits a tree that is the whole window, as
in beauty-host, but a tree mounted in a slot elsewhere is drawn as if
the slot were at the window origin, and the slot's clip discards it.

`to_makepad_ui_in_slot` lowers the same tree with the root placed by a
margin from its slot's origin, which puts every node below it on the
parent-relative margins a scroll view already uses. The layout inside
the tree is unchanged; only its frame of reference is.

From OctosCode's Octoscript-Makepad patch series.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Some sources give multi-line text a `line_height` that is the distance
between paragraphs, so every wrapped line inherits a paragraph-sized
gap. Capping the advance between wrapped lines at 1.4 times the font
size, as a markdown region does, fixes those sources, but applied to
every caller it would also tighten wrapping text whose `line_height` is
a true line pitch.

So the cap is opt-in. `to_makepad_ui_with` takes `Options`; with
`cap_wrapped_line_spacing` set, a wrapping text node advances its
wrapped lines by at most 1.4 times its font size (single lines and
inputs keep their spacing). `Options::in_slot` does what
`to_makepad_ui_in_slot` does, so the two combine. The default options
lower exactly as `to_makepad_ui` always has, and both convenience
functions stay.

From OctosCode's Octoscript-Makepad patch series, which applied the cap
to all wrapping text unconditionally.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ymote
ymote changed the base branch from feat/mail-card-runtime to main October 6, 2026 23:22
@ymote
ymote merged commit cbe5487 into main Oct 6, 2026
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