Phase 4: layout writing modes#32
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
WritingMode+WritingModeResolvedship the writing-mode surface; sideways-* warn-once and fall back to vertical-rl/lr.BuiyLayoutStep::WritingModeInheritpipeline step (memoized multi-level ancestor walk) populates the resolved cache beforeSyncStyles. Layout pipeline goes from 8 to 9 steps.LogicalBoxModel+LogicalInsetergonomic builders translate logical → physical at construct time;LogicalEdgesis the underlying value type.Direction::Rtlwires throughtaffy::Style.direction, mirroring flex children under RTL.Style:.writing_mode(_),.writing_mode_kind(_),.direction(_),.ltr(),.rtl(),.text_orientation(_),.unicode_bidi(_).Test plan
cargo test --workspacegreenReferences
docs/plans/2026-05-10-buiy-layout-writing-modes.mddocs/specs/2026-05-08-buiy-layout-design/container-queries-and-writing-modes.md§ 2Notes from final branch review
inherit_writing_mode's ancestor walk treatsWritingMode::default()as "unset" (CSSinitial-like). Without this, everyStyle-spawned entity's bundledWritingMode::default()would short-circuit the walk so descendants would resolve to default instead of inheriting. Trade-off: an author cannot explicitly pinWritingMode::default()as an override against a non-default ancestor (observationally identical to inheriting the root default). Documented in CHANGELOG and inline insystems.rs::resolve_writing_mode.ContainingBlockcache — Phase 6 (anchor positioning) where it has a real consumer.LogicalBoxModel/LogicalInsettranslate at construct time only; runtime flips require re-spawn).buiy-text-rendering-design.buiy-i18n-design.HashMapallocation ininherit_writing_modeis acceptable for v1 (O(N) memoization is essential); aLocal<HashMap>with.clear()is a possible micro-optimization follow-up.🤖 Generated with Claude Code