feat(shell-core/canvas): paint-time \$ref substitution active

`paint_fill_then_stroke` now takes the resolved fill explicitly
rather than reading `node.fill` directly. Callers in `paint_node`
pre-resolve via `node_fill(node, var_table)` which checks
`var_table.fill_for(node.id)` first, falling through to `node.fill`
otherwise. Result: a Frame / Rect whose canonical loader registered
a `$ref` for its fill paints the current themed value at runtime;
flipping `active_theme` repaints with the new colour.

  - `paint_fill_then_stroke` signature: adds `fill: Option<Color>`
    as the last arg (after world_rect + zoom).
  - Both `NodeKind::Frame` + `NodeKind::Rect` branches in
    `paint_node` now compute `node_fill(node, var_table)` before
    calling the helper.

#5 Variables/Themes: types + storage + canonical loader +
fill_refs map + resolve + paint-time substitution all working.
Variables panel UI (active-theme picker + variable list with
edit) is the remaining piece — that's a widget, not a model
change.

Tests total: 236 shell-core (no new assertions in this commit;
the existing 8 variable tests cover the resolution chain that
paint now consumes). Wasm32 build clean.
This commit is contained in:
Kayshen-X 2026-05-14 15:43:46 +08:00
parent 0b964ff84d
commit b68dfd70e5
2 changed files with 13 additions and 9 deletions

View file

@ -412,7 +412,7 @@ fn paint_node(
match &node.kind {
NodeKind::Frame => {
paint_fill_then_stroke(cx, node, world_rect, zoom);
paint_fill_then_stroke(cx, node, world_rect, zoom, node_fill(node, var_table));
for child in &node.children {
paint_node(cx, child, viewport_origin, zoom, selected, edit_caret, cull, var_table);
}
@ -426,7 +426,7 @@ fn paint_node(
}
}
NodeKind::Rect => {
paint_fill_then_stroke(cx, node, world_rect, zoom);
paint_fill_then_stroke(cx, node, world_rect, zoom, node_fill(node, var_table));
}
NodeKind::Ellipse => {
if let Some(fill) = node.fill {

View file

@ -138,15 +138,19 @@ pub fn paint_selection_overlay(
/// Paint a node's fill rect followed by its stroke rect. Stroke
/// width is scaled by `zoom` so it stays visually constant under
/// canvas zoom.
pub fn paint_fill_then_stroke(cx: &mut PaintCx<'_>, node: &Node, world_rect: Rect, zoom: f32) {
// Scale doc-space radius into world-space alongside the rect.
// 0.5px is below the visible threshold for most renders; collapse
// to a square fill so the round-rect path doesn't accidentally
// soften 0-radius corners due to sub-pixel rounding.
/// canvas zoom. Effective `fill` is passed explicitly so callers
/// can substitute via `var_table.fill_for(node.id)` before
/// rendering (paint-time `$ref` resolution — #5).
pub fn paint_fill_then_stroke(
cx: &mut PaintCx<'_>,
node: &Node,
world_rect: Rect,
zoom: f32,
fill: Option<crate::Color>,
) {
let r = node.corner_radius * zoom;
let use_round = r > 0.5;
if let Some(fill) = node.fill {
if let Some(fill) = fill {
if use_round {
cx.backend.fill_round_rect(world_rect, r, fill);
} else {