Skip to content

Decouple style engine#1068

Draft
dzhou121 wants to merge 83 commits intomainfrom
floem-style-extract
Draft

Decouple style engine#1068
dzhou121 wants to merge 83 commits intomainfrom
floem-style-extract

Conversation

@dzhou121
Copy link
Copy Markdown
Contributor

No description provided.

dzhou121 added 30 commits April 16, 2026 20:14
dzhou121 and others added 30 commits April 17, 2026 19:34
Animations now run inside `StyleTree::compute_style` via a new
`StyleSink::apply_animations` hook. The tree applies them on top of
the resolved `combined_style` — after the cache write (so the cache
holds a pre-animation baseline) and before the inherited-context
derivation (so animated inherited props propagate to descendants on
the same pass). The cached `computed_style` the tree stores is now
post-animation, which matches what Phase 7 / Phase 8 / `style_pass`
callbacks expect.

This also lets `style_view` drop its explicit `apply_animations` call
and the companion `schedule_style` for `has_active_animation`; both
now happen inside `compute_one` via the sink.

Also fix a subtle `RefCell` issue in `run_style_cascade`: the
`root_view.state().borrow().style_node` read was holding the borrow
across `tree.compute_style`, which now tries to `borrow_mut` the
root ViewState through `apply_animations`. Scope the borrow.

Fixes the caveat from 1aba279 where animated inherited props (e.g.
animated font-size on a parent) did not propagate to children.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The tree already derives the final `computed_style` per node, so it
can decide whether the node is position:fixed and call
`sink.register_fixed_element` / `unregister_fixed_element` directly.
This removes the last piece of post-cascade work from `style_view`'s
Phase 5 that required reading `computed_style` back from storage for
a simple is_fixed check.

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

Two related slims after fixed-element registration moved into the tree:

1. The old-vs-new interaction diff (hidden flip → dirty children +
   layout; selected/disabled flip → dirty descendants via their
   selectors) now runs inside `compute_one` using the node's stored
   previous `style_interaction_cx` and the accumulated `dirty.selectors`.
   `style_view` stops reading `old_interact_state`, writing
   `style_interaction_cx` back, and emitting the diff-driven side-effects.

2. `post_compute_combined_interaction` is dropped. Since the per-pass
   diff now runs in the tree, `style_interaction_cx` no longer holds
   the "previous pass" value for style_view — both fields always held
   the same full-cascade value, so one of them is enough. Pass 3 of
   `run_style_cascade` populates `style_interaction_cx` from the tree,
   and `style_view` / public queries read from there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The tree already computes which selectors each node's style tree uses,
so it's the natural place to keep the host's window-level
selector-interest registry in sync. Removes one read of
`storage.has_style_selectors` from `style_view`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`StyleTree::compute_style` already emits `sink.inspector_capture_style`
with the same post-animation computed_style, so the call in
`style_view` was a duplicate. Removing it also lets the tiny
`CaptureState::capture_style` helper go — `record_computed_style` is
the only method on the type now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Phase 7 cursor block was doing the same work `run_style_cascade`
could do when it copies `computed_style` from the tree to storage:
compare old vs new cursor, update `style_cursor`, flip
`needs_cursor_resolution`. Pulling it into Pass 3 keeps
`style_view` focused on host-side layout/taffy/box-tree work and
avoids a redundant `computed_style.builtin().cursor()` read.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`StyleCx::new` runs after `run_style_cascade` Pass 3 has already
mirrored the tree's `combined_style`, inherited context, and class
context into `storage`, so it can read from *this* view's storage
directly rather than from the parent's. That collapses Phase 5's
three-field plumbing block into the constructor and drops the last
bit of non-trivial work Phase 5 was doing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
floem's style model follows CSS where layout *is* style — display,
flex-*, grid-*, width, padding resolve through the cascade and feed a
layout solver. The engine already embedded taffy types deeply
(Style::to_taffy_style, builtin_props re-exports, cascade matches
against taffy::Display), but without acknowledging the contract.

This commit makes it explicit:

- Crate-level docs state that floem-style owns the style→layout-input
  bridge for taffy; reactive runtime and view tree remain out of scope.
- `pub use taffy;` at the crate root lets consumers reach taffy types
  through `floem_style::taffy::...` without a separate dependency.
- `tree.rs` and `visibility.rs` use the local `Display` re-export
  instead of `taffy::style::Display` in comparison sites; taffy is
  still taffy, but the cascade code reads as floem-style's own.
- Dropped the stale "Phase 1a/1b" extraction-history comment.

Behavior unchanged; all 1210 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The reactive-scope machinery existed so `ContextValue` closures could
re-enter the reactive effect that produced a Style — if such a closure
happened to read external signals during cascade, its reads would
register as dependencies of the original effect.

An audit of every `.def(...)` and `.defer(...)` call in src/, tests/,
and floem-style/ itself shows no closure actually does this. Every use
is theming (`t.def(|t| t.primary())`) or font-size math
(`fs.def(|fs| fs * 0.8)`) — pure functions of the prop value they
receive. The test `test_signal_outside_with_context_is_tracked`
explicitly asserts the signal read happens in the *enclosing* style
closure, not inside `.def()`. So the reactive hook was insurance
against a use case nothing exercises.

Removed:
- `Style::effect_context` field.
- `Runtime::get_current_effect()` call in `Style::with_capacity`.
- `Runtime::with_effect(...)` wrap in `Style::resolve_context`.
- effect_context propagation arg threaded through `apply_iter` /
  `apply_mut`.
- `floem_reactive` from `floem-style/Cargo.toml`.

Behavior unchanged — all 1210 tests pass. If a future host genuinely
needs signal reads inside cascade-time closures, they can wrap the
closure on their side (capture-and-reenter via their own reactive
runtime); floem-style no longer prescribes one.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
floem_renderer was pulled in for three type names:
- `FontWeight` / `FontStyle` — re-exports of `fontique`, also re-exported by
  `parley` which floem-style already depends on.
- `LineHeightValue` — actually defined in floem_renderer, but semantically a
  CSS-style value (multiplier vs absolute points), not a renderer concept.

Both legs resolve:

- Font types: switch the four imports in floem-style to `parley::{FontStyle,
  FontWeight}`. No new dep; fewer indirections.
- `LineHeightValue`: move the enum + resolver + `From` impls from
  `renderer/src/text/attrs.rs` into `floem-style/src/unit.rs` and re-export
  at the crate root. `floem_renderer` now imports it from floem_style and
  re-exports it under `floem_renderer::text::LineHeightValue` for backward
  compat with downstream users.

This flips the layering edge: `floem_renderer` now depends on
`floem_style` (style primitives flow up into the renderer), not the
other way around. Which matches the conceptual model — style resolves
first, renderer consumes resolved values.

floem-style's runtime deps are now `taffy`, `parley`, `peniko`, and
`understory_box_tree`. No floem-specific crates other than
`understory_box_tree` (which itself is generic).

All 1210 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Expand the doc on `StyleTree::compute_style` to enumerate the
side-effects it emits through the sink (fixed-element registry,
selector-interest, dirty propagation for inherited/class/visibility
flips, layout, animations, inspector capture), and add an `ignore`'d
code example showing a non-floem host driving the cascade end-to-end
(new_node → set_parent → set_direct_style → compute_style →
computed_style). Point at `tests/mock_sink.rs` and
`tests/style_tree_cascade.rs` for complete executable examples.

Also drop two redundant explicit link targets introduced by the
previous crate-level doc rewrite (`[Display](taffy::style::Display)`
→ `[Display]`, etc.) so rustdoc resolves through the `pub use taffy;`
re-export.

Closes the last item of the engine-audit priority list. All 1210
tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `prop_extractor!` macro's expansion referenced floem's `StyleCx`
in four places (now, direct_style, current_view().get_element_id(),
request_transition_for). That coupling was the sole reason the macro
lived in floem rather than floem-style — so every `prop_extractor!`
call site transitively required floem's view context type to be in
scope.

Introduce `floem_style::PropExtractorCx` as the narrow contract the
macro actually needs:

    fn now(&self) -> Instant;
    fn direct_style(&self) -> &Style;
    fn current_element(&self) -> ElementId;
    fn request_transition_for(&mut self, target: ElementId);

Move the macro into `floem-style/src/style_macros.rs`, expanding
against `dyn PropExtractorCx` instead of a concrete type. Implement
the trait on floem's `StyleCx` (4 trivial lines). Re-export
`prop_extractor` from floem so `use crate::{prop, prop_extractor, …}`
keeps working.

No behavior change — all 1210 tests pass. Second hosts now get the
full `prop_extractor!` macro by implementing a 4-method trait, no
floem import needed. Unblocks moving engine extractors
(LayoutProps, ViewStyleProps, TransformProps) into floem-style next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
With the `prop_extractor!` macro relocated (previous commit) and the
`PropExtractorCx` trait decoupling it from floem's `StyleCx`, the
three engine-facing extractors no longer have any reason to live in
floem. They read properties that floem-style defines (sizes,
padding, flex metrics, overflow, border radii, outlines, shadows —
all built-in props), and their impls (`apply_to_taffy_style`,
`affine`, `clip_rect`, `border_radius`, `border`, `font_size_cx`,
`border_color`) reach for peniko/taffy/FontSizeCx types that
floem-style already owns.

New file `floem-style/src/extractors.rs` holds all three structs and
their inherent impls; floem re-exports them at their previous paths
(`floem::style::{LayoutProps, TransformProps}`,
`floem::view::state::ViewStyleProps`) so no caller changes are
needed. `ViewStyleProps`'s visibility loosens from `pub(crate)` to
`pub` (required to live in another crate) but it only surfaces
through the same re-export paths.

Net effect: floem-style owns the full style→prop-extraction side of
the cascade. A second host picks up layout / transform / view-style
extraction for free — the tree's computed_style → prop-extraction
path no longer imports anything from floem.

All 1210 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Integration test that exercises the style engine the way a second
host (floem-native, a headless renderer, whatever) would consume it.
Uses only `floem_style::*` plus generic support crates (peniko,
taffy, understory_box_tree) — no floem imports anywhere. If this
compiles and passes, a downstream host can drive the engine
through the documented public surface.

Seven scenarios:

- `tree_cascade_produces_computed_style_and_inheritance` — build a
  tree, push direct styles, run `compute_style`, verify inheritance
  works through the engine edges.
- `layout_extractor_fills_taffy_style` — drive `LayoutProps` via
  `read_explicit`, confirm `apply_to_taffy_style` populates the
  taffy style the host would hand to a taffy layout solver.
- `transform_extractor_through_prop_extractor_cx` — exercise the
  `PropExtractorCx` trait path by calling `TransformProps::read_style`
  with the mock host as `&mut dyn PropExtractorCx`. Verifies `affine`
  and `border_radius` helpers produce sensible outputs.
- `view_style_extractor_reads_visual_props` — `ViewStyleProps` against
  a visual style (background, outline), checks the background
  aggregator.
- `class_context_propagates_through_tree` — parent declares a class,
  child applies it, cascade picks it up.
- `hover_selector_switches_between_passes` — flip `is_hovered` on the
  sink, re-cascade, verify `:hover` branch activates.
- `sink_apply_animations_hook_is_invoked` — override the default
  `StyleSink::apply_animations` impl, confirm the tree invokes it.

The mock host is a plain struct implementing both `StyleSink` (engine
callbacks) and `PropExtractorCx` (extractor context). No view tree,
no reactive runtime, no window state. It's the blueprint for what a
second host's style-engine glue layer looks like.

All 1217 tests pass (1210 existing + 7 new).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Last of the engine-shaped extractors living on the floem side.
`FontProps` bundles four built-in props (FontSize, FontFamily,
FontWeight, FontStyle), all of which already live in floem-style;
nothing in its declaration references a floem-side type. Sits
naturally alongside the other three extractors in
`floem-style/src/extractors.rs`.

Floem re-exports it at its previous path (`floem::style::FontProps`),
so `views/label.rs` and `views/text_input.rs` keep compiling unchanged.

All 1217 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both traits had a single impl and served no real extensibility. Collapsing
them removes two public traits from floem-style's API surface without
affecting behavior:

- StyleSelectorKey had one impl for StyleSelector, an enum closed inside
  the crate. The doc claim of host-extensibility was never realizable.
  Replaced with an inherent `to_key` method on StyleSelector.

- StylePropReader had a single blanket impl over StyleProp, so every
  ExtractorField was parameterised over P::Type anyway. Inlined the
  read/get/new logic directly into ExtractorField<P: StyleProp> and
  switched prop_extractor! to take `$prop:ty` so the getter resolves via
  `<$prop as StyleProp>::Type`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The style cache is pure cascade bookkeeping — validating parent-inherited
state on lookup and storing combined_style / selectors / post-interact
flags. Nothing about it is host-specific. Routing access through
`StyleSink::style_cache_mut()` was a round-trip through the host for data
the engine already owned.

Moves the cache field into `StyleTree` and exposes two narrow hooks for
hosts that need cache-level side-effects:
- `StyleTree::clear_cache()` — floem calls this on theme flip and
  responsive-breakpoint changes.
- `StyleTree::cache_stats()` — read-only for tests/debug.

Drops `style_cache_mut` from `StyleSink`, shrinking the trait by one
method and eliminating a cache-reachability test that only existed to
prove the sink pathway worked.

First of several internalizations that aim to keep the engine owning its
own state, so `StyleSink` is left covering only genuine host facts and
policy hooks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before: floem's `WindowState` owned three `FxHashMap<ViewId, ()>`
registries (responsive / disabled / selected) plus ancestry-filtered
descendant-dirty walks; `StyleSink` carried the trait plumbing so the
cascade could keep them in sync.

After: `StyleTree` owns the interest registries as `FxHashSet<StyleNodeId>`
fields. The cascade updates them inline via a private
`update_selector_interest`. `mark_descendants_with_selector_dirty` and its
responsive counterpart live on `StyleTree`, walk the engine's own
parent-edge graph, and return `(ElementId, StyleReason)` pairs for the
host to funnel into its per-frame scheduling.

Changes:
- `update_selector_interest`, `mark_descendants_with_selector_dirty`, and
  `mark_descendants_with_responsive_selector_dirty` deleted from
  `StyleSink`.
- `WindowState` drops its three interest maps and the inherent walk
  bodies; the two remaining inherent entry points (called from
  `src/style/cx.rs` during dirty-reason propagation) delegate to the
  tree.
- `StyleCx::window_state` changes from `&mut dyn StyleSink` to
  `&mut WindowState`. `StyleCx` is floem-specific and already needed
  the concrete type for every other cx in the crate; the trait object
  was vestigial.
- `request_paint` also leaves `StyleSink` — never called by the engine,
  and keeping both inherent (`impl Into<ElementId>`) and trait
  (`ElementId`) overloads on the concrete type triggered `.into()`
  inference ambiguities at every view-crate caller.
- Call-site churn: drop `self.id.into()` → `self.id` at the ~8 view
  impls that were paying the now-redundant conversion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two sink methods were pure pass-through: engine computed "animation
still in flight" or "transition still interpolating," called
`schedule_style` / `schedule_style_with_target` on the sink, and the host
routed them into its frame queue. The engine already owned the state
that triggered the schedule.

`StyleTree` now keeps a `scheduled: FxHashMap<ElementId, StyleReason>`
populated by:
- the cascade itself when `apply_animations` reports in-flight animation,
- floem's `PropExtractorCx::request_transition_for` impl, which writes
  through `self.window_state.style_tree.schedule(...)` instead of the
  removed sink method.

After each `compute_style`, `WindowState` drains
`tree.take_scheduled()` once and funnels entries into its per-frame
update queue. No behavioral change — just a shorter round-trip for
data the engine already owned.

`schedule_style` and `schedule_style_with_target` deleted from
`StyleSink`. `mark_style_dirty_with` stays — it's tangled with floem's
downstream taffy/animation triggers and merits its own focused move.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage A of the animation relocation: the declarative data model and
tick machinery land in `floem_style::animation`. Floem's existing
`Animation` shrinks to a thin reactive wrapper.

In floem-style:
- `KeyFrame`, `KeyFrameStyle`, `KeyFrameProp`, `PropCache`
- `ReverseOnce`, `RepeatMode`, `AnimState{Kind,Command}`
- `Animation` struct with the state machine, keyframe storage, folded
  style, ext-mode props, cache, and debug description.
- All ~30 builder methods (`duration`, `delay`, `keyframe`,
  `keyframe_override`, `auto_reverse`, `reverse_on_exit`, `repeat`,
  `repeat_times`, `max_key_frame`, `run_on_create`, `run_on_remove`,
  `apply_when_finished`, `initial_state`, `debug_name`,
  `view_transition*`, `scale*_effect`, etc.).
- Engine methods: `advance`, `transition`, `animate_into`,
  `apply_folded`, `total_time_percent`, `get_local_percent`,
  `get_current_kf_props`, `state_kind`, `elapsed`, `is_idle`,
  `is_in_progress`, `is_completed`, `is_stopped`, `is_reversing`,
  `is_auto_reverse`, `can_advance`, `should_apply_folded`,
  `runs_on_create`, `runs_on_remove`, `get_duration`,
  `get_repeat_mode`, `debug_description`, `touched_props`.
- `AnimationEvents { started, visual_completed, completed }` is the
  return type of `advance`. Engine stays pure data — no Trigger/
  signal dependency.

In floem, `Animation` becomes `{ engine: floem_style::Animation,
effect_states, on_start, on_visual_complete, on_complete }`:
- Config builders forward to the engine.
- `.state()/.start()/.pause()/.resume()/.reverse()/.stop()`,
  `.on_create()/.on_complete()/.on_visual_complete()` stay here —
  they need `RwSignal`, `UpdaterEffect`, and `ViewId::update_animation_state`.
- `advance()` calls `engine.advance()` and maps the returned
  `AnimationEvents` to `Trigger.notify()` calls.

Floem types re-export from floem-style (`KeyFrame`, `AnimStateCommand`,
`RepeatMode`, `Easing` variants, etc.) so downstream callers keep
compiling unchanged.

Floem-native or other non-reactive hosts can now take a dependency on
floem-style alone and drive animations through `AnimationEvents` +
their platform-native notification mechanism, without pulling in
floem-reactive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each `StyleNode` now carries a `SmallVec<[Animation; 1]>` registry plus a
per-node event buffer. The cascade ticks these animations inline in
`compute_one`, folds their interpolated values into `combined_style`,
queues lifecycle events (`started`, `visual_completed`, `completed`) for
the host to drain, and schedules another cascade pass while anything
is active. This runs alongside the existing `StyleSink::apply_animations`
hook so both paths coexist:

- Hosts with their own per-element animation storage (floem today)
  keep overriding `apply_animations`; the tree-side registry stays
  empty and contributes nothing.
- Standalone consumers (floem-style tests, future floem-native, any
  host that doesn't want to reimplement a Stack<Animation>) push
  engine animations directly via `StyleTree::push_animation`.

Public API added to `StyleTree`:
- `push_animation(node, anim) -> usize` — append, return slot index.
- `set_animation(node, slot, anim)` — replace at slot.
- `update_animation_with(node, slot, f)` — mutate in place.
- `animations(node)` / `animations_mut(node)` — slice accessors.
- `take_animation_events(node)` — drain `(slot, AnimationEvents)`
  pairs from the latest cascade.

A new `tree_stored_animations_tick_during_cascade` integration test
in `host_integration.rs` drives a two-keyframe animation end-to-end
through `compute_style` using only floem-style — no reactive runtime,
no per-view stack — and verifies that `started` fires, events route
to the correct slot, and the node lands on the tree's schedule while
interpolation is in progress.

This is Stage B-data from the animation extraction plan. Stage B-tick
(baking the ticker into the cascade unconditionally) stayed off the
table because it would block the native-offload story: a future
floem-native host needs to *replace* the tick with a CoreAnimation /
ViewPropertyAnimator delegation, not run alongside one.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`set_cursor`, `clear_cursor`, `mark_needs_cursor_resolution`, and
`invalidate_focus_nav_cache` were on the trait but the engine never
called any of them. They're purely host policy: cursor overrides and
focus-nav caches are data only the host consults during input
dispatch and paint. Keeping them on the trait was vestigial — a
leftover from an earlier "make everything host-facing a trait method
just in case" pass.

After this change, every `StyleSink` method corresponds to an actual
`sink.*` call in the cascade. Module docs rewritten to describe what
the trait is for now (reads + engine-detected writes) rather than
the aspirational "second host plumbing" framing that no longer fits.

Mocks get `set_cursor` / `clear_cursor` / `mark_needs_cursor_resolution`
as inherent methods for the tests that exercised them directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`register_fixed_element` / `unregister_fixed_element` were two more
engine-detects-a-fact-host-bookkeeps-it round trips. The cascade
computes `builtin().is_fixed()` per node and the host maintained a
`FxHashSet<ViewId>` it read back for layout positioning and root-size
relayout broadcasts. Nothing external wrote to that set.

`StyleTree` now owns the set authoritatively as
`FxHashSet<ElementId>`. The cascade calls internal
`self.register_fixed_element` / `self.unregister_fixed_element` each
node, which dedupe (only the actual set-membership transitions get
recorded) and push onto per-pass `added` / `removed` small-vecs.
After `compute_style` returns, `WindowState::drain_fixed_element_changes`
reads those transitions and fires floem's post-side-effects — marks
layout dirty on additions, resets box-tree world positions and marks
box-tree commit + layout dirty on removals.

Both sink methods are gone. Floem's inherent
`register_fixed_element` / `unregister_fixed_element` are gone too —
they existed only as the sink destinations, and the drain now does
their work post-cascade. Read-side call sites
(`set_root_size`, `apply_fixed_element_styles`,
`apply_fixed_positioning_transforms`) query `tree.fixed_elements()`
and map `ElementId::owning_id()` where a `ViewId` is needed.

Membership cleans up automatically in `StyleTree::remove_node` — when
a view is destroyed, its companion style node's removal records a
`fixed_elements_removed` entry, so `drain_fixed_element_changes` on
the next cascade also hits floem's box-tree-position-reset path for
removed views. Matches the old `self.fixed_elements.remove(&id)` in
`remove_view` without the extra line.

`StyleSink` is now: 9 reads, `mark_style_dirty_with`,
`mark_needs_layout`, `inspector_capture_style`, and `apply_animations`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three sink methods collapse into tree-owned state the host drains
after `compute_style`:

- **`mark_needs_layout`** → `tree.needs_layout: bool`. The cascade
  flips it on visibility transitions and on fixed-element membership
  changes; `tree.take_needs_layout()` drains it post-cascade. Floem
  plumbs into its existing `needs_layout` field. One shared sentinel
  now covers every engine-detected reason layout should re-run.

- **`mark_style_dirty_with`** → `tree.dirtied_this_pass:
  FxHashMap<ElementId, StyleReason>`. Cascade writes to it wherever
  it used to call `sink.mark_style_dirty_with` — inherited/class-
  context descendants, visibility-flipped descendants, and selector-
  interest walk results. Host drains via
  `tree.take_dirtied_this_pass()` and funnels each entry into its
  own `style_dirty` map so the next frame's traversal still picks
  them up.

- **`inspector_capture_style`** → gone from the trait. Floem's
  inspector capture inlines into Pass 3 of `run_style_cascade`, which
  already iterates every dirty view and reads `tree.computed_style`.
  No extra work, no extra buffer.

Two cascade tests in `style_tree_cascade.rs` that observed
`inspector_capture_style` calls to verify traversal order /
incremental behavior rewrite to check `tree.is_dirty` +
`tree.computed_style` + `tree.take_scheduled` directly. The
`inspector_capture_routes_through_default_impl` test in
`mock_sink.rs` is deleted — with no default impl left to route
through, it had nothing to test.

`StyleSink` is now exactly nine read methods plus
`apply_animations`:

- frame_start, screen_size_bp, keyboard_navigation, root_size_width,
  is_dark_mode, default_theme_classes, default_theme_inherited,
  is_hovered, is_focused, is_focus_within, is_active, is_file_hover
- apply_animations (policy hook for native-offload backends)

Every remaining method corresponds to a read the cascade actually
performs or the one mid-cascade callback the native-offload story
requires keeping. The sink is at its minimum viable shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`ElementId` leaves `StyleTree`. Every engine-level API — sink reads,
scheduling, fixed-element registry, dirtied-this-pass buffer,
animation hooks — keys on [`StyleNodeId`], the slotmap handle the
tree already owns. This matches taffy's model: the engine's node
identity is the engine's own, opaque to the host, and hosts keep a
sidecar mapping to relate it back to their view type.

Concretely:
- `StyleNode` no longer carries `element_id`. Host identity is a
  host concern.
- `StyleTree::new_node()` takes no arguments.
- `tree.fixed_elements() -> &FxHashSet<StyleNodeId>`,
  `tree.take_scheduled()`, `tree.take_dirtied_this_pass()`, and
  `tree.take_fixed_element_changes()` yield `StyleNodeId`s.
- `StyleSink` reads (`is_hovered`, `is_focused`, `is_focus_within`,
  `is_active`, `is_file_hover`) and `apply_animations` all take
  `StyleNodeId`.

Floem side: `WindowState.style_node_to_view: FxHashMap<StyleNodeId,
ViewId>` is the reverse of `ViewState.style_node`, populated on
`ensure_style_node` and cleaned up on `remove_view`. The
`StyleSink` impl translates `StyleNodeId → ViewId → ElementId` at
every read, and every post-cascade drain (schedule, dirtied,
fixed-element transitions, descendant-dirty walks) translates
before feeding floem's view-keyed maps. `StyleCx::get_interact_state`
takes `&WindowState` concretely instead of `&dyn StyleSink` — it
was only called by floem code and needs direct field access now.
`StyleCx::request_transition_for` translates its target `ElementId`
to the owning view's `StyleNodeId` before calling `tree.schedule`.

`ElementId` lives on in the workspace — floem uses it internally
for box-tree lookups and `PropExtractorCx::current_element`
(transition targeting that hosts and extractors both care about).
It just no longer leaks into `StyleTree` as the engine's primary
identity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Port 2 of the cascade-boundary refactor: delete the `StyleSink` trait.
[`StyleTree::compute_style`] now takes `&CascadeInputs`, matching
taffy's shape — reads flow in through plain values plus one closure
for per-node interactions, and the only cascade-time callback is
[`AnimationBackend`], the policy hook a future native host overrides
to delegate animation ticking to its compositor.

`CascadeInputs`:
  - frame_start, screen_size_bp, keyboard_navigation, root_size_width,
    is_dark_mode: `Copy` values
  - default_theme_classes, default_theme_inherited: `&Style`
  - interactions: `&dyn Fn(StyleNodeId) -> PerNodeInteraction` — the
    five pseudo-class bits, read per dirty node
  - animations: `&dyn AnimationBackend` — the policy hook

`AnimationBackend::apply(&self, ...)` takes `&self` rather than
`&mut self`; floem's implementor mutates through `RefCell`-backed
view state, and the `&self` signature lets the closure in the
`interactions` field coexist with the backend reference under the
borrow checker. A `NoAnimationBackend` ZST covers standalone hosts
that don't animate anything.

Floem side:
  - `StyleCx::get_interact_state` already took `&WindowState`
    concretely; its reads now go straight to `WindowState` fields
    (`frame_start`, `screen_size_bp`) instead of the removed sink
    trait methods.
  - `run_style_cascade` builds a `CascadeInputs` inline per pass: the
    interactions closure captures `&WindowState` and translates
    `StyleNodeId → ViewId → ElementId` via the reverse map; the
    animation backend is a small stack struct holding only a borrow
    of the reverse map.
  - `FloemAnimationBackend` (in `src/style/sink.rs`) is the sole
    remaining `AnimationBackend` impl — finds the owning view for a
    `StyleNodeId` and forwards the tick into `ViewState.animations`.

Tests rewritten:
  - `style_tree_cascade.rs`: per-test `cascade()` + `inputs()` helpers
    build the `CascadeInputs` from a `MockHost`.
  - `mock_sink.rs`: no more trait impl; exercises cache + hover-aware
    `resolve_nested_maps` directly, plus a standalone demo of the
    `interactions` closure shape.
  - `host_integration.rs`: `with_inputs(host, |cx| ...)` helper closes
    over the host, and `animation_backend_hook_is_invoked` uses a
    minimal standalone `AnimationBackend` impl.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Folder rename matches the convention used by the other floem_X
crates (renderer, reactive, etc.): short topical folder name,
floem_X package.

ElementId is now defined in floem core (src/box_tree.rs) and no
longer named by floem_style. The engine's sole identity is
StyleNodeId: PropExtractorCx, StyleReason::targets, and the
style_macros all take StyleNodeId. understory_box_tree dropped
from the style crate.

Sub-element ElementIds (scroll handles/tracks, resizable handles)
each lazy-allocate a dedicated StyleNodeId via the new
WindowState::ensure_style_node_for_element. These are orphan
nodes in the style tree — no parent/child edges, never cascaded;
they exist only as identity tokens so StyleReason::targets and
the sub-handler dispatch loops route to the right sub-element.

ElementIdExt goes away — owning_id() is inherent on ElementId now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A `MiniHost` reference driver exercises the extracted style engine
the way a non-floem consumer would: build a tree, run multiple
frames, drain scheduled/dirtied outputs between passes, mutate host
state, and assert across frames. Six scenarios cover steady-state
settling, hover-driven restyle, tree-native animation scheduling,
mid-loop structural changes (add/remove), and responsive
width-breakpoint flips.

Uses only `floem_style::*` plus generic support crates — proof the
engine can be driven by a second host (floem-native, etc.) through
the public surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`PropExtractorCx` goes from four methods to two: only `now` and
`direct_style` remain. `current_element` and `request_transition_for`
are gone — the callback during property extraction is replaced by a
`&mut transitioning: &mut bool` out-param that the generated `read` /
`read_style` methods set when a field is still animating. The caller
schedules the re-cascade explicitly.

Generated methods drop their `_for` variants. Targets were only
needed for the removed transition callback; now the caller already
knows which `StyleNodeId` to schedule against.

Brings transitions in line with animations: both surface through
data (events / flags) at the engine↔host boundary, not through
callbacks fired during the pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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