Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions crates/rustmotion-components/src/box_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1135,18 +1135,21 @@ fn apply_glow_effect(css: &mut CssStyle, effects: &[rustmotion_core::schema::Ani
/// `cascaded_css` is this node's own `CssStyle` after `cascade::inherit_from`
/// has already merged it against the parent, plus every subsequent overlay
/// (timeline states, animation) — the exact same value `LegacyPaintDispatcher`
/// receives at paint time. `Text` uses it so the reserved box always matches
/// what `Text::paint_content` (which reads the same cascade through
/// `with_cascaded_style`) actually draws.
/// receives at paint time. `Component::with_cascaded_style` folds it into
/// whichever component variants read inherited typography off their own
/// style before this function's match ever sees them, so the reserved box
/// always matches what those components' painters (which fold the same
/// cascade in at paint time) actually draw.
fn component_intrinsic(
component: &Component,
cascaded_css: &CssStyle,
) -> Option<Arc<dyn rustmotion_core::engine::box_tree::IntrinsicMeasure>> {
let cascaded_component = component.with_cascaded_style(cascaded_css);
let component = cascaded_component.as_ref().unwrap_or(component);

use Component::*;
match component {
Text(t) => Some(Arc::new(crate::intrinsic::TextIntrinsic::from_text(
&t.with_cascaded_style(cascaded_css),
))),
Text(t) => Some(Arc::new(crate::intrinsic::TextIntrinsic::from_text(t))),
GradientText(t) => Some(Arc::new(
crate::intrinsic::GradientTextIntrinsic::from_gradient_text(t),
)),
Expand Down Expand Up @@ -2147,7 +2150,7 @@ pub fn component_kind(c: &Component) -> &'static str {
Particle(_) => "particle",
PillNav(_) => "pill_nav",
Progress(_) => "progress",
QrCode(_) => "qrcode",
QrCode(_) => "qr_code",
NumberWheel(_) => "number_wheel",
SuccessCheck(_) => "success_check",
Pointer(_) => "pointer",
Expand All @@ -2169,7 +2172,11 @@ pub fn component_kind(c: &Component) -> &'static str {
Flex(_) => "flex",
Grid(_) => "grid",
Card(_) => "card",
Container(_) => "container",
// The schema tag is `div` (`#[serde(rename = "div", alias =
// "container")]` on the enum in `lib.rs`) — `container` only
// survives as a deserialize alias, so naming it that way here told
// an author to look for a tag their scenario cannot contain.
Container(_) => "div",
AudioSpectrum(_) => "audio_spectrum",
Waveform(_) => "waveform",
}
Expand Down
25 changes: 11 additions & 14 deletions crates/rustmotion-components/src/legacy_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//! taffy-laid-out `BoxNode` tree back to component-typed `Painter` impls.
//!
//! Naming kept as "legacy" for now to avoid churn in callers; this is the
//! sole dispatcher in use since all 51 components implement `Painter`.
//! sole dispatcher in use since every component implements `Painter`.
//!
//! Containers (Card / Flex / Grid / Container / Positioned) are intentionally
//! skipped: paint_pass already paints their box decorations and recurses into
Expand All @@ -12,18 +12,18 @@
//! `dispatch` receives the node's cascaded `CssStyle` (`css` below) from
//! `paint_pass`, but `Painter::paint_content` has no `CssStyle` parameter —
//! its signature is frozen — and every painter reads typography off its own
//! `self.style` instead. `Text` is the one component wired to see the
//! cascade anyway: `Text::with_cascaded_style` rebuilds it with `css`
//! folded into its own `style` field before painting, the same technique
//! `box_builder::component_intrinsic` uses for `TextIntrinsic` so measure
//! and paint agree.
//! `self.style` instead. `Component::with_cascaded_style` rebuilds the
//! subset of components that read inherited typography off their own style
//! with `css` folded in before painting, the same call
//! `box_builder::component_intrinsic` makes for the intrinsic measurers so
//! measure and paint agree.

use rustmotion_core::css::CssStyle;
use rustmotion_core::engine::animator::{resolve_props_for_effects, AnimatedProperties};
use rustmotion_core::engine::box_tree::NodeId;
use rustmotion_core::engine::layout_pass::BoxLayout;
use rustmotion_core::engine::paint_pass::{PaintDispatcher, PaintFrame};
use rustmotion_core::traits::{PaintCtx, Painter};
use rustmotion_core::traits::PaintCtx;
use skia_safe::Canvas;

use crate::{ChildComponent, Component};
Expand Down Expand Up @@ -125,7 +125,9 @@ impl<'a> PaintDispatcher for LegacyPaintDispatcher<'a> {
return;
}

let Some(painter) = child.component.as_painter() else {
let cascaded_component = child.component.with_cascaded_style(css);
let effective_component = cascaded_component.as_ref().unwrap_or(&child.component);
let Some(painter) = effective_component.as_painter() else {
return;
};

Expand Down Expand Up @@ -175,12 +177,7 @@ impl<'a> PaintDispatcher for LegacyPaintDispatcher<'a> {
video_height: frame.video_height,
stagger_offset: stagger_delay,
};
if let Component::Text(t) = &child.component {
t.with_cascaded_style(css)
.paint_content(canvas, &local, &props, &paint_ctx);
} else {
painter.paint_content(canvas, &local, &props, &paint_ctx);
}
painter.paint_content(canvas, &local, &props, &paint_ctx);

canvas.restore();
}
Expand Down
140 changes: 137 additions & 3 deletions crates/rustmotion-components/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ pub mod world_bitmap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use rustmotion_core::traits::{Animatable, Painter, Styled, Timed};
use rustmotion_core::css::CssStyle;
use rustmotion_core::traits::{Animatable, Painter, Styled, StyledMut, Timed};

pub use arrow::Arrow;
pub use audio_spectrum::AudioSpectrum;
Expand Down Expand Up @@ -615,8 +616,11 @@ impl Component {
}
}

/// Returns the Painter trait. All 51 components are migrated to the new
/// pipeline; the dispatcher always uses Painter::paint_content.
/// Returns the Painter trait. Every `Component` variant is migrated to
/// the new pipeline; the dispatcher always uses Painter::paint_content.
/// `tests/audit_ws_i.rs::cascaded_components_match_the_verified_set`
/// exercises every variant — not a count here, which would just go
/// stale again the next time a component is added.
pub fn as_painter(&self) -> Option<&dyn Painter> {
match self {
Component::AudioSpectrum(c) => Some(c),
Expand Down Expand Up @@ -681,4 +685,134 @@ impl Component {
Component::Connector(c) => Some(c),
}
}

/// `Some(clone)` for the components whose `Painter`/intrinsic measurer
/// read inherited typography (`color`, `font-*`, `text-align`,
/// `white-space`, ...) off their own `style` field with `resolved`'s
/// twelve `cascade::inherit_from` properties folded in; `None` for every
/// other component, since the cascade cannot affect anything they draw.
/// `resolved` is the caller's own `CssStyle` post-cascade (`box_builder`
/// and `LegacyPaintDispatcher` both already have it on hand).
///
/// The `true` set below was built by reading every component's own
/// `paint`/`paint_content` (and, where one exists, its `*Intrinsic`
/// measurer) for a direct `self.style.color`/`font_family`/`font_size`/
/// `font_weight`/`font_style` read with no cascade in between — not by
/// guessing from the component's name. Several read only `font-size`/
/// `font-family` and keep their own dedicated field for text colour
/// (`Kbd::text_color`, `PillNav::text_color`, `Terminal`'s theme) —
/// still members, since those two properties alone are enough for the
/// same defect: a `font-size` set on a card never reaching the child.
///
/// Exhaustive on purpose, no wildcard arm: adding a new `Component`
/// variant is a compile error here until this match says whether it
/// belongs to that set — the same completeness `as_painter`/
/// `as_animatable`/`as_timed`/`as_styled` above already enforce for
/// their own questions, extended to this one.
/// `tests/audit_ws_i.rs::cascaded_components_match_the_verified_set`
/// pins the current membership directly, so a variant silently
/// reclassified here still fails a test even though the compiler has
/// nothing to object to.
///
/// Builds the clone via a `serde_json` round-trip rather than `Clone`:
/// `Component`'s inner types are already `Serialize + Deserialize` (the
/// whole scenario tree is built that way), but not every one of them is
/// `Clone` — `Caption`'s `CaptionWord`/`CaptionStyle` (`rustmotion-core`)
/// are not, and adding it there is outside this crate. The round trip
/// costs more than a field-wise clone would, but only for the
/// components in the `Some` arm, and only once per box-tree build /
/// paint call — the same per-frame cost class `box_builder` already
/// pays elsewhere.
pub fn with_cascaded_style(&self, resolved: &CssStyle) -> Option<Component> {
let is_typographic = match self {
Component::Text(_)
| Component::GradientText(_)
| Component::Caption(_)
| Component::RichText(_)
| Component::Badge(_)
| Component::Callout(_)
| Component::Counter(_)
| Component::Divider(_)
| Component::Icon(_)
| Component::Kbd(_)
| Component::List(_)
| Component::Marquee(_)
| Component::Notification(_)
| Component::NumberWheel(_)
| Component::PillNav(_)
| Component::Table(_)
| Component::Terminal(_)
| Component::Tooltip(_) => true,
Component::AudioSpectrum(_)
| Component::Shape(_)
| Component::Image(_)
| Component::Svg(_)
| Component::Video(_)
| Component::Gif(_)
| Component::Cursor(_)
| Component::Codeblock(_)
| Component::Connector(_)
| Component::Avatar(_)
| Component::AvatarGroup(_)
| Component::Arrow(_)
| Component::Chart(_)
| Component::Comparison(_)
| Component::Countdown(_)
| Component::DotMap(_)
| Component::Gauge(_)
| Component::Heatmap(_)
| Component::Line(_)
| Component::Lottie(_)
| Component::Mockup(_)
| Component::Particle(_)
| Component::Progress(_)
| Component::QrCode(_)
| Component::SuccessCheck(_)
| Component::Pointer(_)
| Component::Rating(_)
| Component::Skeleton(_)
| Component::Slider(_)
| Component::Sparkline(_)
| Component::Stat(_)
| Component::Stepper(_)
| Component::Switch(_)
| Component::TagCloud(_)
| Component::Timeline(_)
| Component::Treemap(_)
| Component::Positioned(_)
| Component::Flex(_)
| Component::Grid(_)
| Component::Card(_)
| Component::Container(_)
| Component::Waveform(_) => false,
};
if !is_typographic {
return None;
}
let value = serde_json::to_value(self).ok()?;
let mut clone: Component = serde_json::from_value(value).ok()?;
let style = match &mut clone {
Component::Text(c) => c.style_config_mut(),
Component::GradientText(c) => c.style_config_mut(),
Component::Caption(c) => c.style_config_mut(),
Component::RichText(c) => c.style_config_mut(),
Component::Badge(c) => c.style_config_mut(),
Component::Callout(c) => c.style_config_mut(),
Component::Counter(c) => c.style_config_mut(),
Component::Divider(c) => c.style_config_mut(),
Component::Icon(c) => c.style_config_mut(),
Component::Kbd(c) => c.style_config_mut(),
Component::List(c) => c.style_config_mut(),
Component::Marquee(c) => c.style_config_mut(),
Component::Notification(c) => c.style_config_mut(),
Component::NumberWheel(c) => c.style_config_mut(),
Component::PillNav(c) => c.style_config_mut(),
Component::Table(c) => c.style_config_mut(),
Component::Terminal(c) => c.style_config_mut(),
Component::Tooltip(c) => c.style_config_mut(),
_ => unreachable!("classified as typographic by the match above"),
};
rustmotion_core::css::cascade::inherit_from(resolved, style);
Some(clone)
}
}
20 changes: 1 addition & 19 deletions crates/rustmotion-components/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use rustmotion_core::schema::{
};
use rustmotion_core::traits::{PaintCtx, Painter, TimingConfig};

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct Text {
pub content: String,
#[serde(default)]
Expand Down Expand Up @@ -59,24 +59,6 @@ rustmotion_core::impl_traits!(Text {
Styled => style,
});

impl Text {
/// `Painter::paint_content` and `TextIntrinsic::from_text` both read
/// `self.style` directly and have no way to receive the box tree's
/// resolved `CssStyle` — the `Painter` trait's signature is frozen, and
/// `self` is bound to this exact struct. `resolved` is the box-tree
/// node's own `CssStyle` after `cascade::inherit_from` already merged
/// it against the parent at `box_builder` build time, so — mirroring
/// `inherit_from`'s own "child overrides parent" rule — filling this
/// clone's still-unset inheritable fields from it reproduces the same
/// outcome the box tree already computed, just where the painter and
/// the intrinsic measurer can see it.
pub fn with_cascaded_style(&self, resolved: &CssStyle) -> Self {
let mut clone = self.clone();
rustmotion_core::css::cascade::inherit_from(resolved, &mut clone.style);
clone
}
}

/// Eased progress (0..1) of unit `idx` at `time`, honouring the config's
/// deterministic jitter.
fn unit_progress(cfg: &ResolvedCharAnimation, idx: usize, time: f64) -> f32 {
Expand Down
Loading
Loading