Skip to content

Phase 4: layout writing modes#32

Merged
intendednull merged 12 commits into
mainfrom
claude/v01-layout-writing-modes
May 10, 2026
Merged

Phase 4: layout writing modes#32
intendednull merged 12 commits into
mainfrom
claude/v01-layout-writing-modes

Conversation

@intendednull
Copy link
Copy Markdown
Owner

Summary

  • WritingMode + WritingModeResolved ship the writing-mode surface; sideways-* warn-once and fall back to vertical-rl/lr.
  • New BuiyLayoutStep::WritingModeInherit pipeline step (memoized multi-level ancestor walk) populates the resolved cache before SyncStyles. Layout pipeline goes from 8 to 9 steps.
  • LogicalBoxModel + LogicalInset ergonomic builders translate logical → physical at construct time; LogicalEdges is the underlying value type.
  • Direction::Rtl wires through taffy::Style.direction, mirroring flex children under RTL.
  • 7 fluent setters on Style: .writing_mode(_), .writing_mode_kind(_), .direction(_), .ltr(), .rtl(), .text_orientation(_), .unicode_bidi(_).

Test plan

  • cargo test --workspace green
  • Pipeline order test asserts 9 steps
  • Phase 1+2+3 invariants intact (scroll-offset-doesn't-invalidate, grid integration, overflow tests)
  • CI: Lint / Doc / Deny / Test on ubuntu/macos/windows

References

  • Plan: docs/plans/2026-05-10-buiy-layout-writing-modes.md
  • Spec: docs/specs/2026-05-08-buiy-layout-design/container-queries-and-writing-modes.md § 2

Notes from final branch review

  • Late-discovered fix in Task 8: inherit_writing_mode's ancestor walk treats WritingMode::default() as "unset" (CSS initial-like). Without this, every Style-spawned entity's bundled WritingMode::default() would short-circuit the walk so descendants would resolve to default instead of inheriting. Trade-off: an author cannot explicitly pin WritingMode::default() as an override against a non-default ancestor (observationally identical to inheriting the root default). Documented in CHANGELOG and inline in systems.rs::resolve_writing_mode.
  • Spec deferrals (all documented in CHANGELOG and plan):
    • ContainingBlock cache — Phase 6 (anchor positioning) where it has a real consumer.
    • Dynamic writing-mode switches — v1.x (LogicalBoxModel/LogicalInset translate at construct time only; runtime flips require re-spawn).
    • Vertical-mode Taffy axis-swap — Taffy 0.10 has no writing-mode awareness.
    • Sideways glyph rotation — buiy-text-rendering-design.
    • BiDi resolution — buiy-i18n-design.
  • Per-frame HashMap allocation in inherit_writing_mode is acceptable for v1 (O(N) memoization is essential); a Local<HashMap> with .clear() is a possible micro-optimization follow-up.

🤖 Generated with Claude Code

intendednull and others added 12 commits May 9, 2026 23:21
Phase 4 ships WritingMode + WritingModeResolved with a new pre-SyncStyles
inheritance pipeline step. LogicalBoxModel + LogicalInset are
non-component author-ergonomic builders translating logical -> physical
at construct time. Direction::Rtl wires through taffy::Style.direction;
sideways-* fall back to vertical-rl/lr with warn-once. ContainingBlock
cache deferred to Phase 6 (anchor positioning) where it has a consumer.
Plan tagged [active]; flips to [landed] post-merge.
Three reviewer agents surfaced 5 real BLOCKERs; this revision addresses:

1. Single-level inheritance — replaced with memoized ancestor walk that
   honors spec § 2.2 'nearest ancestor's' across arbitrary depth.
2. Changed<WritingModeResolved> cascade — system now reads existing
   resolved value and only inserts when changed, preserving Phase 1's
   O(0) steady-state contract.
3. Pipeline order test rewrite — explicit 9-tracker / 9-label diff
   instead of glossing over the test's actual structure.
4. Task 4 signature mismatch — LogicalEdges::to_edges takes
   (WritingModeKind, Direction) tuple form. Test snippets aligned.
5. Task 2 dead_code — added #[allow(dead_code)] on
   WritingModeResolved::from_writing_mode pending Task 3 consumer.

Plus: Decision item #13 documents the spec § 4.2 'dynamic re-translation
pass' deferral to v1.x; coverage-map row added to call out the deferral;
pipeline.rs '8-step' doc-comment widening called out in Task 3.
Adds WritingModeKind, Direction, TextOrientation, UnicodeBidi. Sideways
variants reach a warn-once gate in Phase 4 Task 5; TextOrientation and
UnicodeBidi are stored only (consumed by buiy-text-rendering-design and
buiy-i18n-design respectively, not layout).
WritingMode (author-set, will join Style Bundle in Task 6) carries the
full CSS writing-mode + direction + text-orientation + unicode-bidi
surface. WritingModeResolved (private cache, synced by Task 3's
inheritance pass) carries the inherited effective value used by
translate.rs in Task 5.

#[allow(dead_code)] on WritingModeResolved::from_writing_mode pending
Task 3's consumer; clippy -D warnings would otherwise reject the
unused pub(crate) helper.
New BuiyLayoutStep::WritingModeInherit set runs between RemovedNodesGc
and SyncStyles, populating WritingModeResolved by walking ancestors via
ChildOf. The walk is memoized in a per-frame HashMap so each entity's
effective WritingMode is resolved at most once even when many
descendants share an ancestor (O(N) total, not O(N x depth)).

The inherit_writing_mode system reads the entity's current
WritingModeResolved and only commands.insert(...) when the new value
differs. This idempotence is load-bearing: Task 5 widens sync_styles's
trigger filter to depend on Changed<WritingModeResolved>, and an
unconditional re-insert every frame would void Phase 1's O(0)
steady-state contract.

Drops the #[allow(dead_code)] on WritingModeResolved::from_writing_mode
now that the inheritance system consumes it.

Pipeline test widens from 8 to 9 expected steps; trackers re-labeled by
enum order (gc, wmi, sync, cq_activate, taffy, cq_flip, cq_rerun,
post_taffy, write) so the assertion is visually unambiguous.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ilders

Non-component, non-Bundle author-ergonomic structs. Each carries logical
(writing-mode-aware) edges or dimensions and emits the corresponding
physical type via .to_edges / .to_box_model / .to_inset. The 6-row
mapping table from the spec is exercised by the new unit tests;
vertical-rl / vertical-lr swap inline <-> block onto height <-> width.

LogicalEdges::to_edges takes (mode, direction) directly to keep the
types.rs -> components.rs dependency one-way; style.rs callers
(LogicalBoxModel, LogicalInset) hold a &WritingMode and forward
wm.mode, wm.direction. LogicalInset duplicates the 6-row mapping
because Inset uses Sizing (not Length), so it can't reuse
LogicalEdges::to_edges.

Sideways modes are silently normalized to their non-sideways vertical
equivalents (the warn-once gate lands in Task 5). LogicalEdges +
LogicalInset take self by value to satisfy clippy::wrong_self_convention
(both derive Copy); LogicalBoxModel takes &self because it's
intentionally non-Copy per the plan. The three structs and their
to_* methods carry #[allow(dead_code)] for the lib build until
Task 6 wires Style fluent setters and Task 7 re-exports them.
Atomic: extends StyleView with writing_mode_resolved; populates
taffy::Style.direction from WritingModeResolved.direction; sideways-*
modes hit a warn-once gate naming buiy-text-rendering-design as the
future owner.

sync_styles' Or filter widens with Changed<WritingMode> and
Changed<WritingModeResolved>. Phase 2 invariant intact:
Changed<ScrollOffset> / Changed<ScrollSnapItem> remain excluded.

Note: this is one commit because StyleView is the bridge between
translate.rs and systems.rs - splitting would break the lib build
between commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7 new setters: .writing_mode, .writing_mode_kind, .direction, .ltr,
.rtl, .text_orientation, .unicode_bidi. WritingModeResolved stays out
of the Bundle (private cache, populated by inherit_writing_mode).
WritingMode + WritingModeResolved registered for reflection; new value
types (WritingModeKind, Direction, TextOrientation, UnicodeBidi,
LogicalEdges) and builders (LogicalBoxModel, LogicalInset) re-exported
from buiy_core and the buiy facade alongside Phases 1-3 surface. Drops
the dead_code allows on LogicalBoxModel / LogicalInset / LogicalEdges
and their methods now that the public re-export wires them in.
Integration tests pin: rtl flips flex-row child order; vertical-rl
swaps inline/block dimensions via LogicalBoxModel; inheritance pass
propagates parent's WritingMode to descendants' WritingModeResolved;
sideways-rl falls back to vertical-rl layout (warn-once fires
observably but is not asserted).

Root-cause fix in inherit_writing_mode: Task 6 added WritingMode to
Style's Bundle, which made every Style-spawned entity a "set"
short-circuit in the Task 3 inheritance walk — descendants would
never inherit because their own default-valued WritingMode component
beat the ancestor lookup. Fix is CSS-faithful "default = inherit":
WritingMode::default() on an entity is treated as unset for
inheritance purposes, so the walk continues to the nearest non-
default ancestor. Spec § 2.2 wording ("its own WritingMode if set,
else the nearest ancestor's") now holds end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@intendednull intendednull merged commit e2c0b5a into main May 10, 2026
6 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