From 7d05ed89e194ab0f038a00ab6c82f4d41c1d0e4d Mon Sep 17 00:00:00 2001 From: NoahR02 Date: Mon, 7 Jul 2025 06:54:31 -0400 Subject: [PATCH 01/18] It's a start --- crates/craft_core/src/animation/animation.rs | 139 ++++++++++++++++++ crates/craft_core/src/animation/mod.rs | 1 + crates/craft_core/src/app.rs | 17 +++ crates/craft_core/src/elements/element.rs | 30 ++++ .../craft_core/src/elements/element_states.rs | 1 + .../craft_core/src/elements/element_styles.rs | 6 + crates/craft_core/src/lib.rs | 8 +- crates/craft_core/src/style/styles.rs | 11 ++ 8 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 crates/craft_core/src/animation/animation.rs create mode 100644 crates/craft_core/src/animation/mod.rs diff --git a/crates/craft_core/src/animation/animation.rs b/crates/craft_core/src/animation/animation.rs new file mode 100644 index 00000000..435cbc07 --- /dev/null +++ b/crates/craft_core/src/animation/animation.rs @@ -0,0 +1,139 @@ +use std::cmp::Ordering; +use std::collections::HashMap; +use std::time::Duration; +use peniko::color::HueDirection; +use rustc_hash::FxHashMap; +use smallvec::SmallVec; +use crate::components::ComponentId; +use crate::elements::ElementState; +use crate::style::{Style, StyleProperty}; + +#[derive(Clone, Debug)] +pub struct KeyFrame { + pub offset_percentage: f32, + //pub properties: SmallVec<[StyleProperty; 5]>, + pub properties: Vec, +} + +impl KeyFrame { + +} + +#[derive(Clone, Debug)] +#[derive(PartialEq)] +pub enum AnimationStatus { + Paused, + Playing, + Scheduled, +} + +#[derive(Clone, Debug)] +pub struct Animation { + pub key_frames: Vec, + pub duration: Duration, +} + +pub struct ActiveAnimation { + current: Duration, + status: AnimationStatus, + element_state: ElementState +} + +pub struct AnimationController { + pub(crate) animations: FxHashMap, +} + +impl AnimationController { + pub fn remove(&mut self, component: ComponentId) { + self.animations.remove(&component); + } + + pub fn tick(&mut self, animation: &Animation, state: ElementState, component: ComponentId, delta: Duration) { + let active_animation = if let Some(active_animation) = self.animations.get_mut(&component) { + active_animation + } else { + self.animations.insert(component, ActiveAnimation { + current: Duration::ZERO, + status: AnimationStatus::Playing, + element_state: state, + }); + self.animations.get_mut(&component).unwrap() + }; + + if active_animation.element_state != state { + active_animation.current = Duration::ZERO; + active_animation.status = AnimationStatus::Playing; + active_animation.element_state = state; + } + + if active_animation.status == AnimationStatus::Playing && active_animation.element_state == state { + active_animation.current += delta; + + if active_animation.current >= animation.duration { + active_animation.current = Duration::ZERO; + active_animation.status = AnimationStatus::Paused; + } + } + } + + pub fn compute_style(&mut self, element_style: &Style, animation: &Animation, state: ElementState, component: ComponentId) -> Style { + let active_animation = if let Some(active_animation) = self.animations.get_mut(&component) { + active_animation + } else { + return element_style.clone(); + }; + + if active_animation.status != AnimationStatus::Playing || active_animation.element_state != state { + return element_style.clone(); + } + + let pos = Duration::div_duration_f32(active_animation.current, animation.duration); + fn find_keyframe_pair(pos: f32, animation: &Animation) -> (&KeyFrame, &KeyFrame) { + let mut sorted = animation.key_frames.iter().collect::>(); + sorted.sort_by(|a, b| a.offset_percentage.total_cmp(&b.offset_percentage)); + for window in sorted.windows(2) { + let [start, end] = window else { continue }; + if pos >= (start.offset_percentage / 100.0) && pos <= (end.offset_percentage / 100.0) { + return (start, end); + } + } + + panic!("No keyframes available."); + } + + let (keyframe_start, keyframe_end) = find_keyframe_pair(pos, animation); + println!("{:?}", (keyframe_start, keyframe_end)); + + let mut style = Style::default(); + let mut start_map = HashMap::new(); + let mut end_map = HashMap::new(); + + for prop in &keyframe_start.properties { + start_map.insert(std::mem::discriminant(prop), prop); + } + + for prop in &keyframe_end.properties { + end_map.insert(std::mem::discriminant(prop), prop); + } + + for key in start_map.keys().chain(end_map.keys()).collect::>() { + let start_prop = start_map.get(key); + let end_prop = end_map.get(key); + + match (start_prop, end_prop) { + (Some(StyleProperty::Background(start)), Some(StyleProperty::Background(end))) => { + let start_percentage = keyframe_start.offset_percentage / 100.0; + let end_percentage = keyframe_end.offset_percentage / 100.0; + let local_t = (pos - start_percentage) / (end_percentage - start_percentage); + let new_color = start.lerp_rect(*end, local_t.clamp(0.0, 1.0)); + style.set_background(new_color); + } + _ => {} + } + } + + + + style + } +} \ No newline at end of file diff --git a/crates/craft_core/src/animation/mod.rs b/crates/craft_core/src/animation/mod.rs new file mode 100644 index 00000000..ec640c02 --- /dev/null +++ b/crates/craft_core/src/animation/mod.rs @@ -0,0 +1 @@ +pub mod animation; \ No newline at end of file diff --git a/crates/craft_core/src/app.rs b/crates/craft_core/src/app.rs index a8b1fe3e..bc90ee42 100644 --- a/crates/craft_core/src/app.rs +++ b/crates/craft_core/src/app.rs @@ -35,6 +35,7 @@ use kurbo::{Affine, Point}; use peniko::Color; use std::collections::HashMap; use std::sync::Arc; +use std::time::{Duration, Instant}; use taffy::{AvailableSpace, NodeId, TaffyTree}; use craft_runtime::Sender; use ui_events::keyboard::{KeyState, KeyboardEvent, Modifiers, NamedKey}; @@ -48,6 +49,7 @@ use winit::window::Window; use craft_renderer::RenderList; use craft_resource_manager::resource_event::ResourceEvent; use craft_resource_manager::resource_type::ResourceType; +use crate::animation::animation::AnimationController; use crate::events::update_queue_entry::UpdateQueueEntry; macro_rules! get_tree { @@ -103,6 +105,8 @@ pub struct App { pub(crate) accesskit_adapter: Option, pub(crate) runtime: CraftRuntimeHandle, pub(crate) modifiers: Modifiers, + pub(crate) animation_controller: AnimationController, + pub(crate) last_frame_time: std::time::Instant, } impl App { @@ -283,6 +287,11 @@ impl App { if self.window.is_none() { return; } + + let now = Instant::now(); + let delta_time = now - self.last_frame_time; + self.last_frame_time = now; + let surface_size = self.window_context.window_size(); @@ -319,6 +328,12 @@ impl App { self.window_context.mouse_position, ); + let reactive_tree = get_tree!(self, false); + let root_element = reactive_tree.element_tree.as_mut().unwrap(); + + root_element.on_animation_frame(&mut reactive_tree.element_state, &mut self.animation_controller, delta_time); + self.window.clone().unwrap().request_redraw(); + if self.renderer.is_some() { self.draw_reactive_tree(false, self.window_context.mouse_position, self.window.clone()); } @@ -345,6 +360,8 @@ impl App { self.window_context.mouse_position, ); + + if self.renderer.is_some() { self.draw_reactive_tree(true, self.window_context.mouse_position, self.window.clone()); } diff --git a/crates/craft_core/src/elements/element.rs b/crates/craft_core/src/elements/element.rs index b107d937..d52f3ca0 100644 --- a/crates/craft_core/src/elements/element.rs +++ b/crates/craft_core/src/elements/element.rs @@ -19,8 +19,10 @@ use peniko::Color; use std::any::Any; use std::mem; use std::sync::Arc; +use std::time::Duration; use taffy::{NodeId, Overflow, TaffyTree}; use winit::window::Window; +use crate::animation::animation::AnimationController; #[derive(Clone)] pub struct ElementBoxed { @@ -313,6 +315,34 @@ pub trait Element: Any + StandardElementClone + Send + Sync { fn get_base_state_mut<'a>(&self, element_state: &'a mut ElementStateStore) -> &'a mut ElementStateStoreItem { element_state.storage.get_mut(&self.element_data().component_id).unwrap() } + + fn on_animation_frame(&mut self, element_state: &mut ElementStateStore, animation_controller: &mut AnimationController, delta_time: Duration) { + let element_id = self.component_id().clone(); + let base_state = self.get_base_state(element_state); + let current_style = base_state.base.current_style_mut(self.element_data_mut()); + + let current_state: ElementState = { + if base_state.base.hovered { + ElementState::Hovered + } else if base_state.base.focused { + ElementState::Focused + } else { + ElementState::Normal + } + }; + + if let Some(animation) = ¤t_style.animation { + animation_controller.tick(animation, current_state, element_id, delta_time); + let new_style = animation_controller.compute_style(¤t_style, animation, current_state, element_id); + *current_style = new_style; + } else { + animation_controller.remove(element_id); + } + + for child in self.children_mut() { + child.internal.on_animation_frame(element_state, animation_controller, delta_time); + } + } #[cfg(feature = "accesskit")] fn compute_accessibility_tree( diff --git a/crates/craft_core/src/elements/element_states.rs b/crates/craft_core/src/elements/element_states.rs index cc923697..fe56d7bd 100644 --- a/crates/craft_core/src/elements/element_states.rs +++ b/crates/craft_core/src/elements/element_states.rs @@ -1,4 +1,5 @@ #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Hash)] pub enum ElementState { #[default] Normal, diff --git a/crates/craft_core/src/elements/element_styles.rs b/crates/craft_core/src/elements/element_styles.rs index 087dd7d7..63385073 100644 --- a/crates/craft_core/src/elements/element_styles.rs +++ b/crates/craft_core/src/elements/element_styles.rs @@ -3,6 +3,7 @@ use craft_primitives::geometry::TrblRectangle; use craft_primitives::Color; use crate::style::{AlignItems, Display, FlexDirection, FontStyle, JustifyContent, Overflow, Style, Underline, Unit, Weight, Wrap}; use taffy::Position; +use crate::animation::animation::Animation; pub trait ElementStyles where @@ -262,6 +263,11 @@ where self.styles_mut().set_visible(visible); self } + + fn animation(mut self, animation: Animation) -> Self { + self.styles_mut().set_animation(animation); + self + } } impl From<&str> for Unit { diff --git a/crates/craft_core/src/lib.rs b/crates/craft_core/src/lib.rs index f492df45..c389a279 100644 --- a/crates/craft_core/src/lib.rs +++ b/crates/craft_core/src/lib.rs @@ -24,6 +24,7 @@ pub mod markdown; mod utils; #[cfg(target_arch = "wasm32")] pub mod wasm_queue; +pub mod animation; pub use options::CraftOptions; pub use craft_primitives::palette; @@ -52,7 +53,7 @@ use std::collections::VecDeque; use std::future::Future; use std::pin::Pin; use std::sync::Arc; - +use std::time::Instant; use crate::reactive::reactive_tree::ReactiveTree; use crate::reactive::state_store::{StateStore, StateStoreItem}; #[cfg(target_arch = "wasm32")] @@ -66,6 +67,7 @@ use craft_logging::info; use {winit::event_loop::EventLoopBuilder, winit::platform::android::EventLoopBuilderExtAndroid}; use app::App; +use crate::animation::animation::AnimationController; use crate::craft_winit_state::CraftWinitState; use crate::utils::cloneable_any::CloneableAny; @@ -261,6 +263,10 @@ pub fn setup_craft( }, runtime: runtime_copy, modifiers: Default::default(), + animation_controller: AnimationController { + animations: Default::default(), + }, + last_frame_time: Instant::now(), }); CraftState::new(runtime, winit_receiver, app_sender, craft_options, craft_app) diff --git a/crates/craft_core/src/style/styles.rs b/crates/craft_core/src/style/styles.rs index c3c48f9a..10436513 100644 --- a/crates/craft_core/src/style/styles.rs +++ b/crates/craft_core/src/style/styles.rs @@ -11,6 +11,7 @@ use craft_primitives::ColorBrush; use std::fmt; use std::fmt::Debug; use smallvec::SmallVec; +use crate::animation::animation::Animation; #[derive(Clone, Copy, Debug)] pub enum Unit { @@ -333,6 +334,7 @@ impl Default for FontFamily { pub struct Style { properties: SmallVec<[StyleProperty; 5]>, pub dirty_flags: StyleFlags, + pub animation: Option> } impl Default for Style { @@ -340,6 +342,7 @@ impl Default for Style { Style { properties: SmallVec::new(), dirty_flags: StyleFlags::empty(), + animation: None, } } } @@ -421,6 +424,14 @@ style_property!(selection_color, set_selection_color, SelectionColor, Color, SEL style_property!(cursor_color, set_cursor_color, CursorColor, Option, CURSOR_COLOR, None); impl Style { + pub fn animation(&self) -> &Option> { + &self.animation + } + + pub fn set_animation(&mut self, animation: Animation) { + self.animation = Some(Box::new(animation)); + } + fn remove_property(&mut self, f: impl Fn(&StyleProperty) -> bool) { if let Some(pos) = self.properties.iter().position(f) { self.properties.remove(pos); From 159556c4273e17814d9b53a399db93c6d20501f0 Mon Sep 17 00:00:00 2001 From: NoahR02 Date: Mon, 7 Jul 2025 07:55:19 -0400 Subject: [PATCH 02/18] Add Animation Timing Functions --- crates/craft_core/src/animation/animation.rs | 75 ++++++++++++++++++-- 1 file changed, 68 insertions(+), 7 deletions(-) diff --git a/crates/craft_core/src/animation/animation.rs b/crates/craft_core/src/animation/animation.rs index 435cbc07..df152a6b 100644 --- a/crates/craft_core/src/animation/animation.rs +++ b/crates/craft_core/src/animation/animation.rs @@ -1,6 +1,8 @@ use std::cmp::Ordering; use std::collections::HashMap; use std::time::Duration; +use kurbo::{CubicBez, ParamCurve, ParamCurveCurvature, Point}; +use kurbo::offset::CubicOffset; use peniko::color::HueDirection; use rustc_hash::FxHashMap; use smallvec::SmallVec; @@ -27,10 +29,41 @@ pub enum AnimationStatus { Scheduled, } +#[derive(Clone, Debug)] +pub struct CubicBezier { + cubic_bez: CubicBez, +} + +impl CubicBezier { + pub fn new(x1: f32, y1: f32, x2: f32, y2: f32) -> Self { + Self { + cubic_bez: CubicBez::new( + Point::new(0.0, 0.0), + Point::new(x1 as f64, y1 as f64), + Point::new(x2 as f64, y2 as f64), + Point::new(1.0, 1.0), + ) + } + } +} + + +#[derive(Default, Clone, Debug)] +pub enum TimingFunction { + #[default] + Linear, + EaseIn, + EaseOut, + BezierCurve(CubicBezier), + EaseInOut, + Ease, +} + #[derive(Clone, Debug)] pub struct Animation { pub key_frames: Vec, pub duration: Duration, + pub timing_function: TimingFunction, } pub struct ActiveAnimation { @@ -47,7 +80,7 @@ impl AnimationController { pub fn remove(&mut self, component: ComponentId) { self.animations.remove(&component); } - + pub fn tick(&mut self, animation: &Animation, state: ElementState, component: ComponentId, delta: Duration) { let active_animation = if let Some(active_animation) = self.animations.get_mut(&component) { active_animation @@ -59,7 +92,7 @@ impl AnimationController { }); self.animations.get_mut(&component).unwrap() }; - + if active_animation.element_state != state { active_animation.current = Duration::ZERO; active_animation.status = AnimationStatus::Playing; @@ -82,7 +115,7 @@ impl AnimationController { } else { return element_style.clone(); }; - + if active_animation.status != AnimationStatus::Playing || active_animation.element_state != state { return element_style.clone(); } @@ -120,16 +153,44 @@ impl AnimationController { let start_prop = start_map.get(key); let end_prop = end_map.get(key); + let start_percentage = keyframe_start.offset_percentage / 100.0; + let end_percentage = keyframe_end.offset_percentage / 100.0; + let local_t = (pos - start_percentage) / (end_percentage - start_percentage); + + let t = match &animation.timing_function { + TimingFunction::Linear => { + let linear = CubicBezier::new(0.0, 0.0, 1.0, 1.0); + linear.cubic_bez.eval(local_t as f64).y + } + TimingFunction::Ease => { + let ease = CubicBezier::new(0.25, 0.1, 0.25, 1.0); + ease.cubic_bez.eval(local_t as f64).y + } + TimingFunction::EaseIn => { + let ease_in = CubicBezier::new(0.42, 0.0, 1.0, 1.0); + ease_in.cubic_bez.eval(local_t as f64).y + } + TimingFunction::EaseOut => { + let ease_out = CubicBezier::new(0.0, 0.0, 0.58, 1.0); + ease_out.cubic_bez.eval(local_t as f64).y + } + TimingFunction::EaseInOut => { + let ease_in_out = CubicBezier::new(0.42, 0.0, 0.58, 1.0); + ease_in_out.cubic_bez.eval(local_t as f64).y + } + TimingFunction::BezierCurve(cubic_bezier) => { + cubic_bezier.cubic_bez.eval(local_t as f64).y + } + }; + match (start_prop, end_prop) { (Some(StyleProperty::Background(start)), Some(StyleProperty::Background(end))) => { - let start_percentage = keyframe_start.offset_percentage / 100.0; - let end_percentage = keyframe_end.offset_percentage / 100.0; - let local_t = (pos - start_percentage) / (end_percentage - start_percentage); - let new_color = start.lerp_rect(*end, local_t.clamp(0.0, 1.0)); + let new_color = start.lerp_rect(*end, t as f32); style.set_background(new_color); } _ => {} } + } From 512dbc5215786d00c1acd4b3698dd86714f6537f Mon Sep 17 00:00:00 2001 From: NoahR02 Date: Mon, 7 Jul 2025 10:45:41 -0400 Subject: [PATCH 03/18] Some more progress --- crates/craft_core/src/animation/animation.rs | 59 +++++++++++++++---- crates/craft_core/src/app.rs | 18 +++++- .../src/elements/base_element_state.rs | 15 +++++ crates/craft_core/src/elements/element.rs | 33 +++++++---- 4 files changed, 101 insertions(+), 24 deletions(-) diff --git a/crates/craft_core/src/animation/animation.rs b/crates/craft_core/src/animation/animation.rs index df152a6b..7d939ef8 100644 --- a/crates/craft_core/src/animation/animation.rs +++ b/crates/craft_core/src/animation/animation.rs @@ -1,14 +1,10 @@ -use std::cmp::Ordering; -use std::collections::HashMap; -use std::time::Duration; -use kurbo::{CubicBez, ParamCurve, ParamCurveCurvature, Point}; -use kurbo::offset::CubicOffset; -use peniko::color::HueDirection; -use rustc_hash::FxHashMap; -use smallvec::SmallVec; use crate::components::ComponentId; use crate::elements::ElementState; -use crate::style::{Style, StyleProperty}; +use crate::style::{Style, StyleProperty, Unit}; +use kurbo::{CubicBez, ParamCurve, Point}; +use rustc_hash::FxHashMap; +use std::collections::HashMap; +use std::time::Duration; #[derive(Clone, Debug)] pub struct KeyFrame { @@ -72,6 +68,21 @@ pub struct ActiveAnimation { element_state: ElementState } +#[derive(Clone, Debug, Default)] +pub struct AnimationFlags { + needs_relayout: bool, +} + +impl AnimationFlags { + pub fn set_needs_relayout(&mut self, needs_relayout: bool) { + self.needs_relayout = self.needs_relayout | needs_relayout; + } + + pub fn needs_relayout(&self) -> bool { + self.needs_relayout + } +} + pub struct AnimationController { pub(crate) animations: FxHashMap, } @@ -92,7 +103,7 @@ impl AnimationController { }); self.animations.get_mut(&component).unwrap() }; - + if active_animation.element_state != state { active_animation.current = Duration::ZERO; active_animation.status = AnimationStatus::Playing; @@ -109,7 +120,7 @@ impl AnimationController { } } - pub fn compute_style(&mut self, element_style: &Style, animation: &Animation, state: ElementState, component: ComponentId) -> Style { + pub fn compute_style(&mut self, element_style: &Style, animation: &Animation, state: ElementState, component: ComponentId, animation_flags: &mut AnimationFlags) -> Style { let active_animation = if let Some(active_animation) = self.animations.get_mut(&component) { active_animation } else { @@ -183,14 +194,38 @@ impl AnimationController { } }; + fn lerp(a: f32, b: f32, t: f32) -> f32 { + a + (b - a) * t + } + match (start_prop, end_prop) { (Some(StyleProperty::Background(start)), Some(StyleProperty::Background(end))) => { let new_color = start.lerp_rect(*end, t as f32); style.set_background(new_color); } + (Some(StyleProperty::Width(start)), Some(StyleProperty::Width(end))) => { + + if std::mem::discriminant(start) != std::mem::discriminant(end) { + panic!("Width must be the same Unit type."); + } + + fn resolve_unit(unit: &Unit) -> f32 { + match unit { + Unit::Px(px) => *px, + Unit::Percentage(percent) => *percent, + Unit::Auto => panic!("Unit must not be auto.") + } + } + + let resolved_start = resolve_unit(start); + let resolved_end = resolve_unit(end); + let new = lerp(resolved_start, resolved_end, t as f32); + style.set_width(Unit::Px(new)); + animation_flags.set_needs_relayout(true); + } _ => {} } - + } diff --git a/crates/craft_core/src/app.rs b/crates/craft_core/src/app.rs index bc90ee42..e3f3c1c7 100644 --- a/crates/craft_core/src/app.rs +++ b/crates/craft_core/src/app.rs @@ -49,7 +49,7 @@ use winit::window::Window; use craft_renderer::RenderList; use craft_resource_manager::resource_event::ResourceEvent; use craft_resource_manager::resource_type::ResourceType; -use crate::animation::animation::AnimationController; +use crate::animation::animation::{AnimationController, AnimationFlags}; use crate::events::update_queue_entry::UpdateQueueEntry; macro_rules! get_tree { @@ -331,7 +331,21 @@ impl App { let reactive_tree = get_tree!(self, false); let root_element = reactive_tree.element_tree.as_mut().unwrap(); - root_element.on_animation_frame(&mut reactive_tree.element_state, &mut self.animation_controller, delta_time); + let mut animation_flags = AnimationFlags::default(); + root_element.on_animation_frame(&mut animation_flags, &mut reactive_tree.element_state, &mut self.animation_controller, delta_time); + + if animation_flags.needs_relayout() { + root_element.reset_layout_item(); + + self.layout_tree( + false, + root_size, + Point::new(0.0, 0.0), + self.window_context.effective_scale_factor(), + self.window_context.mouse_position, + ); + } + self.window.clone().unwrap().request_redraw(); if self.renderer.is_some() { diff --git a/crates/craft_core/src/elements/base_element_state.rs b/crates/craft_core/src/elements/base_element_state.rs index 27ce1dd3..d6a3f78d 100644 --- a/crates/craft_core/src/elements/base_element_state.rs +++ b/crates/craft_core/src/elements/base_element_state.rs @@ -43,6 +43,21 @@ impl<'a> BaseElementState { } &mut element_data.style } + pub fn current_style_mut_no_fallback(&self, element_data: &'a mut ElementData) -> Option<&'a mut Style> { + if self.active { + if let Some(pressed_style) = &mut element_data.pressed_style { + return Some(pressed_style); + } + } + if self.hovered { + if let Some(hover_style) = &mut element_data.hover_style { + return Some(hover_style); + } + } + + None + } + } // HACK: Remove this and all usages when pointer capture per device works. diff --git a/crates/craft_core/src/elements/element.rs b/crates/craft_core/src/elements/element.rs index d52f3ca0..06d64f92 100644 --- a/crates/craft_core/src/elements/element.rs +++ b/crates/craft_core/src/elements/element.rs @@ -22,7 +22,7 @@ use std::sync::Arc; use std::time::Duration; use taffy::{NodeId, Overflow, TaffyTree}; use winit::window::Window; -use crate::animation::animation::AnimationController; +use crate::animation::animation::{AnimationController, AnimationFlags}; #[derive(Clone)] pub struct ElementBoxed { @@ -200,6 +200,14 @@ pub trait Element: Any + StandardElementClone + Send + Sync { position, ); } + + fn reset_layout_item(&mut self) { + *self.layout_item_mut() = LayoutItem::default(); + + for child in self.element_data_mut().children.iter_mut() { + child.internal.reset_layout_item(); + } + } fn draw_children( &mut self, @@ -316,31 +324,36 @@ pub trait Element: Any + StandardElementClone + Send + Sync { element_state.storage.get_mut(&self.element_data().component_id).unwrap() } - fn on_animation_frame(&mut self, element_state: &mut ElementStateStore, animation_controller: &mut AnimationController, delta_time: Duration) { + fn on_animation_frame(&mut self, animation_flags: &mut AnimationFlags, element_state: &mut ElementStateStore, animation_controller: &mut AnimationController, delta_time: Duration) { let element_id = self.component_id().clone(); let base_state = self.get_base_state(element_state); - let current_style = base_state.base.current_style_mut(self.element_data_mut()); - - let current_state: ElementState = { - if base_state.base.hovered { + let mut current_state: ElementState = { + if base_state.base.hovered { ElementState::Hovered - } else if base_state.base.focused { + } else if base_state.base.focused { ElementState::Focused } else { ElementState::Normal } }; + let current_style = if let Some(current_style) = base_state.base.current_style_mut_no_fallback(self.element_data_mut()) { + current_style + } else { + current_state = ElementState::Normal; + base_state.base.current_style_mut(self.element_data_mut()) + }; + if let Some(animation) = ¤t_style.animation { animation_controller.tick(animation, current_state, element_id, delta_time); - let new_style = animation_controller.compute_style(¤t_style, animation, current_state, element_id); - *current_style = new_style; + let new_style = animation_controller.compute_style(¤t_style, animation, current_state, element_id, animation_flags); + *current_style = Style::merge(current_style, &new_style); } else { animation_controller.remove(element_id); } for child in self.children_mut() { - child.internal.on_animation_frame(element_state, animation_controller, delta_time); + child.internal.on_animation_frame(animation_flags, element_state, animation_controller, delta_time); } } From ae4630c0e25b259e74e5d601d81111ae15690c77 Mon Sep 17 00:00:00 2001 From: NoahR02 Date: Tue, 8 Jul 2025 07:51:47 -0400 Subject: [PATCH 04/18] Only layout as needed --- crates/craft_core/src/animation/animation.rs | 47 +++++++++--- crates/craft_core/src/app.rs | 78 +++++++++++++------- crates/craft_core/src/craft_winit_state.rs | 8 +- crates/craft_core/src/elements/element.rs | 2 +- crates/craft_core/src/lib.rs | 2 + 5 files changed, 96 insertions(+), 41 deletions(-) diff --git a/crates/craft_core/src/animation/animation.rs b/crates/craft_core/src/animation/animation.rs index 7d939ef8..d391b8ef 100644 --- a/crates/craft_core/src/animation/animation.rs +++ b/crates/craft_core/src/animation/animation.rs @@ -91,8 +91,18 @@ impl AnimationController { pub fn remove(&mut self, component: ComponentId) { self.animations.remove(&component); } + + pub fn has_active_animation(&self) -> bool { + for animation in self.animations.values() { + if animation.status == AnimationStatus::Playing { + return true; + } + } + + false + } - pub fn tick(&mut self, animation: &Animation, state: ElementState, component: ComponentId, delta: Duration) { + pub fn tick(&mut self, animation_flags: &mut AnimationFlags, animation: &Animation, state: ElementState, component: ComponentId, delta: Duration) { let active_animation = if let Some(active_animation) = self.animations.get_mut(&component) { active_animation } else { @@ -109,13 +119,14 @@ impl AnimationController { active_animation.status = AnimationStatus::Playing; active_animation.element_state = state; } - + if active_animation.status == AnimationStatus::Playing && active_animation.element_state == state { active_animation.current += delta; if active_animation.current >= animation.duration { active_animation.current = Duration::ZERO; active_animation.status = AnimationStatus::Paused; + animation_flags.set_needs_relayout(true); } } } @@ -197,32 +208,44 @@ impl AnimationController { fn lerp(a: f32, b: f32, t: f32) -> f32 { a + (b - a) * t } + + fn resolve_unit(unit: &Unit) -> f32 { + match unit { + Unit::Px(px) => *px, + Unit::Percentage(percent) => *percent, + Unit::Auto => panic!("Unit must not be auto.") + } + } match (start_prop, end_prop) { (Some(StyleProperty::Background(start)), Some(StyleProperty::Background(end))) => { let new_color = start.lerp_rect(*end, t as f32); style.set_background(new_color); } - (Some(StyleProperty::Width(start)), Some(StyleProperty::Width(end))) => { - + (Some(StyleProperty::Width(start)), Some(StyleProperty::Width(end))) + => { if std::mem::discriminant(start) != std::mem::discriminant(end) { panic!("Width must be the same Unit type."); } - fn resolve_unit(unit: &Unit) -> f32 { - match unit { - Unit::Px(px) => *px, - Unit::Percentage(percent) => *percent, - Unit::Auto => panic!("Unit must not be auto.") - } - } - let resolved_start = resolve_unit(start); let resolved_end = resolve_unit(end); let new = lerp(resolved_start, resolved_end, t as f32); style.set_width(Unit::Px(new)); animation_flags.set_needs_relayout(true); } + (Some(StyleProperty::Height(start)), Some(StyleProperty::Height(end))) + => { + if std::mem::discriminant(start) != std::mem::discriminant(end) { + panic!("Width must be the same Unit type."); + } + + let resolved_start = resolve_unit(start); + let resolved_end = resolve_unit(end); + let new = lerp(resolved_start, resolved_end, t as f32); + style.set_height(Unit::Px(new)); + animation_flags.set_needs_relayout(true); + } _ => {} } diff --git a/crates/craft_core/src/app.rs b/crates/craft_core/src/app.rs index e3f3c1c7..5b35fac3 100644 --- a/crates/craft_core/src/app.rs +++ b/crates/craft_core/src/app.rs @@ -44,7 +44,7 @@ use ui_events::ScrollDelta; use ui_events::ScrollDelta::PixelDelta; use winit::dpi::{LogicalSize, PhysicalSize}; use winit::event::Ime; -use winit::event_loop::ActiveEventLoop; +use winit::event_loop::{ActiveEventLoop}; use winit::window::Window; use craft_renderer::RenderList; use craft_resource_manager::resource_event::ResourceEvent; @@ -107,6 +107,24 @@ pub struct App { pub(crate) modifiers: Modifiers, pub(crate) animation_controller: AnimationController, pub(crate) last_frame_time: std::time::Instant, + pub redraw_flags: RedrawFlags, +} + +#[derive(Debug)] +pub struct RedrawFlags { + rebuild_layout: bool, +} + +impl RedrawFlags { + pub fn new(rebuild_layout: bool) -> Self { + Self { + rebuild_layout, + } + } + + pub fn should_rebuild_layout(&self) -> bool { + self.rebuild_layout + } } impl App { @@ -292,11 +310,9 @@ impl App { let delta_time = now - self.last_frame_time; self.last_frame_time = now; - let surface_size = self.window_context.window_size(); self.setup_text_context(); - self.update_view(); cfg_if! { @@ -319,22 +335,26 @@ impl App { } } + let old_has_active_animation = self.animation_controller.has_active_animation(); + { - self.layout_tree( - false, - root_size, - Point::new(0.0, 0.0), - self.window_context.effective_scale_factor(), - self.window_context.mouse_position, - ); + if self.redraw_flags.should_rebuild_layout() { + self.layout_tree( + false, + root_size, + Point::new(0.0, 0.0), + self.window_context.effective_scale_factor(), + self.window_context.mouse_position, + ); + } let reactive_tree = get_tree!(self, false); let root_element = reactive_tree.element_tree.as_mut().unwrap(); let mut animation_flags = AnimationFlags::default(); root_element.on_animation_frame(&mut animation_flags, &mut reactive_tree.element_state, &mut self.animation_controller, delta_time); - - if animation_flags.needs_relayout() { + + if animation_flags.needs_relayout() || old_has_active_animation { root_element.reset_layout_item(); self.layout_tree( @@ -345,8 +365,15 @@ impl App { self.window_context.mouse_position, ); } - - self.window.clone().unwrap().request_redraw(); + + { + // Request a redraw if there is at least one animation playing. + // ControlFlow::Poll is set in `about_to_wait`. + if self.animation_controller.has_active_animation() || old_has_active_animation { + // Winit does not guarantee when a redraw event will happen, but that should be fine, at worst we redraw an extra time. + self.request_redraw(RedrawFlags::new(old_has_active_animation)); + } + } if self.renderer.is_some() { self.draw_reactive_tree(false, self.window_context.mouse_position, self.window.clone()); @@ -373,8 +400,6 @@ impl App { self.window_context.effective_scale_factor(), self.window_context.mouse_position, ); - - if self.renderer.is_some() { self.draw_reactive_tree(true, self.window_context.mouse_position, self.window.clone()); @@ -417,7 +442,7 @@ impl App { } else { self.window_context.zoom_in(); } - self.request_redraw(); + self.request_redraw(RedrawFlags::new(true)); return; } @@ -425,7 +450,7 @@ impl App { let message = Message::CraftMessage(event); self.dispatch_event(&message, EventDispatchType::Bubbling, false); - self.request_redraw(); + self.request_redraw(RedrawFlags::new(true)); } pub fn on_pointer_button( @@ -455,7 +480,7 @@ impl App { self.dispatch_event(&message, EventDispatchType::Bubbling, true); } - self.request_redraw(); + self.request_redraw(RedrawFlags::new(true)); } pub fn on_pointer_moved(&mut self, mouse_moved: PointerUpdate) { @@ -470,7 +495,7 @@ impl App { self.dispatch_event(&message, EventDispatchType::Bubbling, true); - self.request_redraw(); + self.request_redraw(RedrawFlags::new(true)); } pub fn on_ime(&mut self, ime: Ime) { @@ -479,7 +504,7 @@ impl App { self.dispatch_event(&message, EventDispatchType::Bubbling, false); - self.request_redraw(); + self.request_redraw(RedrawFlags::new(true)); } /// Dispatch messages to the reactive tree. @@ -518,11 +543,11 @@ impl App { if keyboard_input.modifiers.ctrl() { if keyboard_input.key == ui_events::keyboard::Key::Character("=".to_string()) { self.window_context.zoom_in(); - self.request_redraw(); + self.request_redraw(RedrawFlags::new(true)); return; } else if keyboard_input.key == ui_events::keyboard::Key::Character("-".to_string()) { self.window_context.zoom_out(); - self.request_redraw(); + self.request_redraw(RedrawFlags::new(true)); return; } } @@ -542,7 +567,7 @@ impl App { } } - self.request_redraw(); + self.request_redraw(RedrawFlags::new(true)); } /// Processes async messages sent from the user. @@ -578,7 +603,7 @@ impl App { )); } - self.request_redraw(); + self.request_redraw(RedrawFlags::new(true)); } pub fn on_resource_event(&mut self, resource_event: ResourceEvent) { @@ -606,7 +631,8 @@ impl App { ); } - fn request_redraw(&self) { + fn request_redraw(&mut self, redraw_flags: RedrawFlags) { + self.redraw_flags = redraw_flags; if let Some(window) = &self.window { window.request_redraw(); } diff --git a/crates/craft_core/src/craft_winit_state.rs b/crates/craft_core/src/craft_winit_state.rs index debcaac1..3db953a5 100644 --- a/crates/craft_core/src/craft_winit_state.rs +++ b/crates/craft_core/src/craft_winit_state.rs @@ -250,10 +250,14 @@ impl ApplicationHandler for CraftWinitState { event_loop.exit(); return; } - - if !craft_state.wait_cancelled { + + // Switch to Poll mode if we are running animations. + if craft_state.craft_app.animation_controller.has_active_animation() { + event_loop.set_control_flow(ControlFlow::Poll); + } else { event_loop.set_control_flow(ControlFlow::WaitUntil(time::Instant::now() + WAIT_TIME)); } + } } diff --git a/crates/craft_core/src/elements/element.rs b/crates/craft_core/src/elements/element.rs index 06d64f92..a5b934f3 100644 --- a/crates/craft_core/src/elements/element.rs +++ b/crates/craft_core/src/elements/element.rs @@ -345,7 +345,7 @@ pub trait Element: Any + StandardElementClone + Send + Sync { }; if let Some(animation) = ¤t_style.animation { - animation_controller.tick(animation, current_state, element_id, delta_time); + animation_controller.tick(animation_flags, animation, current_state, element_id, delta_time); let new_style = animation_controller.compute_style(¤t_style, animation, current_state, element_id, animation_flags); *current_style = Style::merge(current_style, &new_style); } else { diff --git a/crates/craft_core/src/lib.rs b/crates/craft_core/src/lib.rs index c389a279..1d38f2c4 100644 --- a/crates/craft_core/src/lib.rs +++ b/crates/craft_core/src/lib.rs @@ -68,6 +68,7 @@ use {winit::event_loop::EventLoopBuilder, winit::platform::android::EventLoopBui use app::App; use crate::animation::animation::AnimationController; +use crate::app::RedrawFlags; use crate::craft_winit_state::CraftWinitState; use crate::utils::cloneable_any::CloneableAny; @@ -267,6 +268,7 @@ pub fn setup_craft( animations: Default::default(), }, last_frame_time: Instant::now(), + redraw_flags: RedrawFlags::new(true), }); CraftState::new(runtime, winit_receiver, app_sender, craft_options, craft_app) From 004ef1c13d8ef858865ad73b327d2fe8f42e3909 Mon Sep 17 00:00:00 2001 From: NoahR02 Date: Tue, 8 Jul 2025 09:06:04 -0400 Subject: [PATCH 05/18] Add docs for the animation feature --- crates/craft_core/src/animation/animation.rs | 88 ++++++++++++++++---- crates/craft_core/src/app.rs | 79 ++++++++++-------- crates/craft_core/src/elements/element.rs | 10 ++- 3 files changed, 127 insertions(+), 50 deletions(-) diff --git a/crates/craft_core/src/animation/animation.rs b/crates/craft_core/src/animation/animation.rs index d391b8ef..31b8d83c 100644 --- a/crates/craft_core/src/animation/animation.rs +++ b/crates/craft_core/src/animation/animation.rs @@ -5,16 +5,30 @@ use kurbo::{CubicBez, ParamCurve, Point}; use rustc_hash::FxHashMap; use std::collections::HashMap; use std::time::Duration; +use smallvec::SmallVec; #[derive(Clone, Debug)] pub struct KeyFrame { - pub offset_percentage: f32, - //pub properties: SmallVec<[StyleProperty; 5]>, - pub properties: Vec, + /// The action / styles interpolated at `offset_percentage`. + /// Range [0.0, 100.0] + offset_percentage: f32, + + /// The list of styles interpolated to an element at this keyframe. + properties: SmallVec<[StyleProperty; 3]>, } impl KeyFrame { + pub fn new(offset_percentage: f32) -> Self { + KeyFrame { + offset_percentage, + properties: SmallVec::new(), + } + } + pub fn push(mut self, property: StyleProperty) -> Self { + self.properties.push(property); + self + } } #[derive(Clone, Debug)] @@ -25,12 +39,14 @@ pub enum AnimationStatus { Scheduled, } +/// A cubic bézier curve where P0 and P3 are stuck at (0,0) and (1,1). #[derive(Clone, Debug)] -pub struct CubicBezier { +pub struct FixedCubicBezier { cubic_bez: CubicBez, } -impl CubicBezier { +impl FixedCubicBezier { + /// Sets P1 and P2 of a fixed cubic bézier curve. pub fn new(x1: f32, y1: f32, x2: f32, y2: f32) -> Self { Self { cubic_bez: CubicBez::new( @@ -44,54 +60,88 @@ impl CubicBezier { } +/// The motion of an animation modeled with a mathematical function. #[derive(Default, Clone, Debug)] pub enum TimingFunction { + /// https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function#linear #[default] Linear, + /// https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function#ease + Ease, + /// https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function#ease-in EaseIn, + /// https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function#ease-out EaseOut, - BezierCurve(CubicBezier), + /// https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function#ease-in-out EaseInOut, - Ease, + /// https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function#cubic-beziernumber_01_number_number_01_number + BezierCurve(FixedCubicBezier), } #[derive(Clone, Debug)] pub struct Animation { - pub key_frames: Vec, + pub key_frames: SmallVec<[KeyFrame; 2]>, pub duration: Duration, pub timing_function: TimingFunction, } +impl Animation { + pub fn new(duration: Duration, timing_function: TimingFunction) -> Self { + Self { + key_frames: SmallVec::new(), + duration, + timing_function, + } + } + + pub fn push(mut self, key_frame: KeyFrame) -> Self { + self.key_frames.push(key_frame); + self + } +} + pub struct ActiveAnimation { + /// How far into an animation we are. current: Duration, + /// Tracks the status of an animation, if it is playing, scheduled, or paused. status: AnimationStatus, + /// Stores the element state of the animation, so that we can track if an animation needs to be removed if an element is in a new state. element_state: ElementState } +/// For damage tracking across recursive calls to `on_animation_frame`. #[derive(Clone, Debug, Default)] pub struct AnimationFlags { needs_relayout: bool, } impl AnimationFlags { + /// OR'd with the provided boolean and the previously stored boolean, to track if an animiatable property effects layout. + /// This is used after `on_animation_frame` to optionally recompute the layout. pub fn set_needs_relayout(&mut self, needs_relayout: bool) { self.needs_relayout = self.needs_relayout | needs_relayout; } + /// Returns whether we need to perform a relayout or not. pub fn needs_relayout(&self) -> bool { self.needs_relayout } } pub struct AnimationController { + /// Maps an element id to a record of an animation's playback state. pub(crate) animations: FxHashMap, } impl AnimationController { + + /// Removes the playback state (ActiveAnimation). pub fn remove(&mut self, component: ComponentId) { self.animations.remove(&component); } + + /// Determines if any animation is currently playing/running. pub fn has_active_animation(&self) -> bool { for animation in self.animations.values() { if animation.status == AnimationStatus::Playing { @@ -101,7 +151,8 @@ impl AnimationController { false } - + + /// Advances an active animation, and it is also responsible for tracking the status and element_state. pub fn tick(&mut self, animation_flags: &mut AnimationFlags, animation: &Animation, state: ElementState, component: ComponentId, delta: Duration) { let active_animation = if let Some(active_animation) = self.animations.get_mut(&component) { active_animation @@ -131,6 +182,8 @@ impl AnimationController { } } + /// Called after `tick`, and is responsible for using the current animation time and + /// computing an interpolated style from a provided `Animation`. pub fn compute_style(&mut self, element_style: &Style, animation: &Animation, state: ElementState, component: ComponentId, animation_flags: &mut AnimationFlags) -> Style { let active_animation = if let Some(active_animation) = self.animations.get_mut(&component) { active_animation @@ -157,7 +210,6 @@ impl AnimationController { } let (keyframe_start, keyframe_end) = find_keyframe_pair(pos, animation); - println!("{:?}", (keyframe_start, keyframe_end)); let mut style = Style::default(); let mut start_map = HashMap::new(); @@ -181,25 +233,31 @@ impl AnimationController { let t = match &animation.timing_function { TimingFunction::Linear => { - let linear = CubicBezier::new(0.0, 0.0, 1.0, 1.0); + // https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function#linear + let linear = FixedCubicBezier::new(0.0, 0.0, 1.0, 1.0); linear.cubic_bez.eval(local_t as f64).y } TimingFunction::Ease => { - let ease = CubicBezier::new(0.25, 0.1, 0.25, 1.0); + // https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function#ease + let ease = FixedCubicBezier::new(0.25, 0.1, 0.25, 1.0); ease.cubic_bez.eval(local_t as f64).y } TimingFunction::EaseIn => { - let ease_in = CubicBezier::new(0.42, 0.0, 1.0, 1.0); + // https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function#ease-in + let ease_in = FixedCubicBezier::new(0.42, 0.0, 1.0, 1.0); ease_in.cubic_bez.eval(local_t as f64).y } TimingFunction::EaseOut => { - let ease_out = CubicBezier::new(0.0, 0.0, 0.58, 1.0); + // https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function#ease-out + let ease_out = FixedCubicBezier::new(0.0, 0.0, 0.58, 1.0); ease_out.cubic_bez.eval(local_t as f64).y } TimingFunction::EaseInOut => { - let ease_in_out = CubicBezier::new(0.42, 0.0, 0.58, 1.0); + // https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function#ease-in-out + let ease_in_out = FixedCubicBezier::new(0.42, 0.0, 0.58, 1.0); ease_in_out.cubic_bez.eval(local_t as f64).y } + // https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function#cubic-beziernumber_01_number_number_01_number TimingFunction::BezierCurve(cubic_bezier) => { cubic_bezier.cubic_bez.eval(local_t as f64).y } diff --git a/crates/craft_core/src/app.rs b/crates/craft_core/src/app.rs index 5b35fac3..b65354c2 100644 --- a/crates/craft_core/src/app.rs +++ b/crates/craft_core/src/app.rs @@ -121,7 +121,7 @@ impl RedrawFlags { rebuild_layout, } } - + pub fn should_rebuild_layout(&self) -> bool { self.rebuild_layout } @@ -335,46 +335,21 @@ impl App { } } - let old_has_active_animation = self.animation_controller.has_active_animation(); - + let layout_origin = Point::new(0.0, 0.0); + { if self.redraw_flags.should_rebuild_layout() { self.layout_tree( false, root_size, - Point::new(0.0, 0.0), - self.window_context.effective_scale_factor(), - self.window_context.mouse_position, - ); - } - - let reactive_tree = get_tree!(self, false); - let root_element = reactive_tree.element_tree.as_mut().unwrap(); - - let mut animation_flags = AnimationFlags::default(); - root_element.on_animation_frame(&mut animation_flags, &mut reactive_tree.element_state, &mut self.animation_controller, delta_time); - - if animation_flags.needs_relayout() || old_has_active_animation { - root_element.reset_layout_item(); - - self.layout_tree( - false, - root_size, - Point::new(0.0, 0.0), + layout_origin, self.window_context.effective_scale_factor(), self.window_context.mouse_position, ); } - - { - // Request a redraw if there is at least one animation playing. - // ControlFlow::Poll is set in `about_to_wait`. - if self.animation_controller.has_active_animation() || old_has_active_animation { - // Winit does not guarantee when a redraw event will happen, but that should be fine, at worst we redraw an extra time. - self.request_redraw(RedrawFlags::new(old_has_active_animation)); - } - } - + + self.animate_tree(false, &delta_time, layout_origin, root_size); + if self.renderer.is_some() { self.draw_reactive_tree(false, self.window_context.mouse_position, self.window.clone()); } @@ -382,6 +357,9 @@ impl App { #[cfg(feature = "dev_tools")] { + let viewport_size = LogicalSize::new(surface_size.width - root_size.width, root_size.height); + let dev_tools_layout_origin = Point::new(root_size.width as f64, 0.0); + if self.is_dev_tools_open { update_reactive_tree( dev_tools_view(self.user_tree.element_tree.clone().unwrap()), @@ -395,11 +373,13 @@ impl App { self.layout_tree( true, - LogicalSize::new(surface_size.width - root_size.width, root_size.height), + viewport_size, Point::new(root_size.width as f64, 0.0), self.window_context.effective_scale_factor(), self.window_context.mouse_position, ); + + self.animate_tree(true, &delta_time, dev_tools_layout_origin, viewport_size); if self.renderer.is_some() { self.draw_reactive_tree(true, self.window_context.mouse_position, self.window.clone()); @@ -638,6 +618,39 @@ impl App { } } + /// "Animates" a tree by calling `on_animation_frame` and changing an element's styles. + fn animate_tree(&mut self, is_dev_tree: bool, delta_time: &Duration, layout_origin: Point, viewport_size: LogicalSize) { + let old_has_active_animation = self.animation_controller.has_active_animation(); + let reactive_tree = get_tree!(self, is_dev_tree); + let root_element = reactive_tree.element_tree.as_mut().unwrap(); + + // Damage track across recursive calls to `on_animation_frame`. + let mut animation_flags = AnimationFlags::default(); + root_element.on_animation_frame(&mut animation_flags, &mut reactive_tree.element_state, &mut self.animation_controller, *delta_time); + + // Perform a relayout if an animation used any layout effecting style property. + if animation_flags.needs_relayout() || old_has_active_animation { + root_element.reset_layout_item(); + + self.layout_tree( + is_dev_tree, + viewport_size, + layout_origin, + self.window_context.effective_scale_factor(), + self.window_context.mouse_position, + ); + } + + { + // Request a redraw if there is at least one animation playing. + // ControlFlow::Poll is set in `about_to_wait`. + if self.animation_controller.has_active_animation() || old_has_active_animation { + // Winit does not guarantee when a redraw event will happen, but that should be fine, at worst we redraw an extra time. + self.request_redraw(RedrawFlags::new(old_has_active_animation)); + } + } + } + #[allow(clippy::too_many_arguments)] fn layout_tree( &mut self, diff --git a/crates/craft_core/src/elements/element.rs b/crates/craft_core/src/elements/element.rs index a5b934f3..2aae92ac 100644 --- a/crates/craft_core/src/elements/element.rs +++ b/crates/craft_core/src/elements/element.rs @@ -201,6 +201,7 @@ pub trait Element: Any + StandardElementClone + Send + Sync { ); } + /// A bit of a hack to reset the layout item of an element recursively. fn reset_layout_item(&mut self) { *self.layout_item_mut() = LayoutItem::default(); @@ -324,8 +325,9 @@ pub trait Element: Any + StandardElementClone + Send + Sync { element_state.storage.get_mut(&self.element_data().component_id).unwrap() } + /// Called after layout, and is responsible for updating the animation state of an element. fn on_animation_frame(&mut self, animation_flags: &mut AnimationFlags, element_state: &mut ElementStateStore, animation_controller: &mut AnimationController, delta_time: Duration) { - let element_id = self.component_id().clone(); + let element_id = self.component_id(); let base_state = self.get_base_state(element_state); let mut current_state: ElementState = { if base_state.base.hovered { @@ -337,7 +339,10 @@ pub trait Element: Any + StandardElementClone + Send + Sync { } }; - let current_style = if let Some(current_style) = base_state.base.current_style_mut_no_fallback(self.element_data_mut()) { + // A bit hacky, but we either get the current style with no fallback or fallback to a style and change the current element state to Normal. + // This is to allow for retaining an animation state on a normal style even if you hover over it (assuming the hover has no animation). + // Basically this is to hack in a basic inherited animation. + let current_style = if let Some(current_style) = base_state.base.current_style_mut_no_fallback(self.element_data_mut()) && current_style.animation.is_some() { current_style } else { current_state = ElementState::Normal; @@ -349,6 +354,7 @@ pub trait Element: Any + StandardElementClone + Send + Sync { let new_style = animation_controller.compute_style(¤t_style, animation, current_state, element_id, animation_flags); *current_style = Style::merge(current_style, &new_style); } else { + // If the element style or the fallback doesn't have an animation, then remove any animation state. animation_controller.remove(element_id); } From 1358a3785fe602507ad5be6a29bd92a116bc8905 Mon Sep 17 00:00:00 2001 From: NoahR02 Date: Tue, 8 Jul 2025 09:12:58 -0400 Subject: [PATCH 06/18] Add folder for the animation example --- Cargo.lock | 8 +++++ Cargo.toml | 1 + examples/animations/Cargo.toml | 17 ++++++++++ examples/animations/main.rs | 60 ++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+) create mode 100644 examples/animations/Cargo.toml create mode 100644 examples/animations/main.rs diff --git a/Cargo.lock b/Cargo.lock index fc811f72..6190484e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -185,6 +185,14 @@ dependencies = [ "libc", ] +[[package]] +name = "animations" +version = "0.1.0" +dependencies = [ + "craft_gui", + "util", +] + [[package]] name = "arrayref" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 6233d8f9..e2db5769 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "examples/text", "examples/tour", "examples/events", + "examples/animations", "examples/overlay", "examples/custom_event_loop", "website", diff --git a/examples/animations/Cargo.toml b/examples/animations/Cargo.toml new file mode 100644 index 00000000..8d9a85e9 --- /dev/null +++ b/examples/animations/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "animations" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "animations" +path = "main.rs" + +[dependencies] +util = { path = "../util" } + +[dependencies.craft] +path = "../../crates/craft" +default-features = false +features = ["vello_renderer", "devtools", "accesskit"] +package = "craft_gui" diff --git a/examples/animations/main.rs b/examples/animations/main.rs new file mode 100644 index 00000000..5caf9d40 --- /dev/null +++ b/examples/animations/main.rs @@ -0,0 +1,60 @@ +use craft::animation::animation::{Animation, KeyFrame, TimingFunction}; +use craft::components::Context; +use craft::style::{StyleProperty, Unit}; +use craft::{components::{Component, ComponentSpecification}, elements::{Container, ElementStyles}, palette, style::{Display, FlexDirection}}; +use std::time::Duration; + +#[derive(Default)] +pub struct AnimationsExample { +} + +impl Component for AnimationsExample { + type GlobalState = (); + type Props = (); + type Message = (); + + fn view(context: &mut Context) -> ComponentSpecification { + let animation_examples = vec![ + Container::new() + .background(palette::css::GRAY) + .width("100px") + .height("40px") + .animation( + Animation::new(Duration::from_secs(5), TimingFunction::EaseOut) + .push( + KeyFrame::new(0.0) + .push(StyleProperty::Background(palette::css::BLACK)) + .push(StyleProperty::Width(Unit::Px(20.0))), + ) + .push( + KeyFrame::new(100.0) + .push(StyleProperty::Background(palette::css::RED)) + .push(StyleProperty::Width(Unit::Px(200.0))) + ) + ), + ]; + + + let mut container = Container::new() + .display(Display::Flex) + .flex_direction(FlexDirection::Column) + .width("100%") + .height("100%") + .gap(20) + ; + + for ani in animation_examples { + container = container.push(ani) + } + + container.component() + } +} + +#[allow(unused)] +#[cfg(not(target_os = "android"))] +fn main() { + use craft::CraftOptions; + util::setup_logging(); + craft::craft_main(AnimationsExample::component(), (), CraftOptions::basic("Counter")); +} From bc6a0c38a18cf4fada3a19fa40c6f52008101ca1 Mon Sep 17 00:00:00 2001 From: NoahR02 Date: Tue, 8 Jul 2025 10:30:27 -0400 Subject: [PATCH 07/18] Animate more StyleProperties --- crates/craft_core/src/animation/animation.rs | 87 +++++++++++++++---- examples/animations/main.rs | 90 ++++++++++++++++---- 2 files changed, 146 insertions(+), 31 deletions(-) diff --git a/crates/craft_core/src/animation/animation.rs b/crates/craft_core/src/animation/animation.rs index 31b8d83c..d5ab277c 100644 --- a/crates/craft_core/src/animation/animation.rs +++ b/crates/craft_core/src/animation/animation.rs @@ -4,8 +4,10 @@ use crate::style::{Style, StyleProperty, Unit}; use kurbo::{CubicBez, ParamCurve, Point}; use rustc_hash::FxHashMap; use std::collections::HashMap; +use std::iter::zip; use std::time::Duration; use smallvec::SmallVec; +use craft_primitives::geometry::TrblRectangle; #[derive(Clone, Debug)] pub struct KeyFrame { @@ -83,6 +85,13 @@ pub struct Animation { pub key_frames: SmallVec<[KeyFrame; 2]>, pub duration: Duration, pub timing_function: TimingFunction, + pub loop_amount: LoopAmount, +} + +#[derive(Clone, Debug)] +pub enum LoopAmount { + Infinite, + Fixed(u32) } impl Animation { @@ -91,6 +100,7 @@ impl Animation { key_frames: SmallVec::new(), duration, timing_function, + loop_amount: LoopAmount::Fixed(1), } } @@ -98,6 +108,11 @@ impl Animation { self.key_frames.push(key_frame); self } + + pub fn loop_amount(mut self, loop_amount: LoopAmount) -> Self { + self.loop_amount = loop_amount; + self + } } pub struct ActiveAnimation { @@ -106,7 +121,8 @@ pub struct ActiveAnimation { /// Tracks the status of an animation, if it is playing, scheduled, or paused. status: AnimationStatus, /// Stores the element state of the animation, so that we can track if an animation needs to be removed if an element is in a new state. - element_state: ElementState + element_state: ElementState, + loop_amount: LoopAmount, } /// For damage tracking across recursive calls to `on_animation_frame`. @@ -161,6 +177,7 @@ impl AnimationController { current: Duration::ZERO, status: AnimationStatus::Playing, element_state: state, + loop_amount: animation.loop_amount.clone(), }); self.animations.get_mut(&component).unwrap() }; @@ -174,11 +191,29 @@ impl AnimationController { if active_animation.status == AnimationStatus::Playing && active_animation.element_state == state { active_animation.current += delta; - if active_animation.current >= animation.duration { - active_animation.current = Duration::ZERO; - active_animation.status = AnimationStatus::Paused; - animation_flags.set_needs_relayout(true); + let is_completed = active_animation.current >= animation.duration; + + match &mut active_animation.loop_amount { + LoopAmount::Infinite => { + if is_completed { + active_animation.current = Duration::ZERO; + } + } + LoopAmount::Fixed(amount) => { + if is_completed { + *amount -= 1; + + if *amount == 0 { + active_animation.current = Duration::ZERO; + active_animation.status = AnimationStatus::Paused; + animation_flags.set_needs_relayout(true); + } else { + active_animation.current = Duration::ZERO; + } + } + } } + } } @@ -280,30 +315,52 @@ impl AnimationController { let new_color = start.lerp_rect(*end, t as f32); style.set_background(new_color); } + (Some(StyleProperty::Color(start)), Some(StyleProperty::Color(end))) => { + let new_color = start.lerp_rect(*end, t as f32); + style.set_color(new_color); + animation_flags.set_needs_relayout(true); + } + (Some(StyleProperty::FontSize(start)), Some(StyleProperty::FontSize(end))) => { + let new = lerp(*start, *end, t as f32); + style.set_font_size(new); + animation_flags.set_needs_relayout(true); + } (Some(StyleProperty::Width(start)), Some(StyleProperty::Width(end))) => { - if std::mem::discriminant(start) != std::mem::discriminant(end) { - panic!("Width must be the same Unit type."); - } - let resolved_start = resolve_unit(start); let resolved_end = resolve_unit(end); let new = lerp(resolved_start, resolved_end, t as f32); style.set_width(Unit::Px(new)); animation_flags.set_needs_relayout(true); } - (Some(StyleProperty::Height(start)), Some(StyleProperty::Height(end))) - => { - if std::mem::discriminant(start) != std::mem::discriminant(end) { - panic!("Width must be the same Unit type."); - } - + (Some(StyleProperty::Height(start)), Some(StyleProperty::Height(end))) => { let resolved_start = resolve_unit(start); let resolved_end = resolve_unit(end); let new = lerp(resolved_start, resolved_end, t as f32); style.set_height(Unit::Px(new)); animation_flags.set_needs_relayout(true); } + + (Some(StyleProperty::Inset(start)), Some(StyleProperty::Inset(end))) => { + let trlb = zip(start.to_array(), end.to_array()).map(|(start, end)| { + let resolved_start = resolve_unit(&start); + let resolved_end = resolve_unit(&end); + let new = lerp(resolved_start, resolved_end, t as f32); + + new + }).collect::>(); + + let inset = TrblRectangle::new( + Unit::Px(trlb[0]), + Unit::Px(trlb[1]), + Unit::Px(trlb[2]), + Unit::Px(trlb[3]), + ); + + style.set_inset(inset); + animation_flags.set_needs_relayout(true); + } + _ => {} } diff --git a/examples/animations/main.rs b/examples/animations/main.rs index 5caf9d40..f91dc930 100644 --- a/examples/animations/main.rs +++ b/examples/animations/main.rs @@ -1,6 +1,8 @@ -use craft::animation::animation::{Animation, KeyFrame, TimingFunction}; +use craft::animation::animation::{Animation, KeyFrame, LoopAmount, TimingFunction}; use craft::components::Context; -use craft::style::{StyleProperty, Unit}; +use craft::elements::Text; +use craft::geometry::TrblRectangle; +use craft::style::{Position, StyleProperty, Unit}; use craft::{components::{Component, ComponentSpecification}, elements::{Container, ElementStyles}, palette, style::{Display, FlexDirection}}; use std::time::Duration; @@ -14,24 +16,80 @@ impl Component for AnimationsExample { type Message = (); fn view(context: &mut Context) -> ComponentSpecification { - let animation_examples = vec![ + + let growing_animation = Animation::new(Duration::from_secs(5), TimingFunction::EaseOut) + .push( + KeyFrame::new(0.0) + .push(StyleProperty::Background(palette::css::GREEN)) + .push(StyleProperty::Width(Unit::Px(20.0))) + .push(StyleProperty::Height(Unit::Px(40.0))), + ) + .push( + KeyFrame::new(100.0) + .push(StyleProperty::Background(palette::css::RED)) + .push(StyleProperty::Width(Unit::Px(400.0))) + .push(StyleProperty::Height(Unit::Px(100.0))) + ) + .loop_amount(LoopAmount::Fixed(3)) + ; + + let moving_animation = Animation::new(Duration::from_secs(5), TimingFunction::Ease) + .push( + KeyFrame::new(0.0) + .push(StyleProperty::Background(palette::css::BLUE)) + .push(StyleProperty::Inset(TrblRectangle::new(Unit::Px(100.0), Unit::Px(0.0), Unit::Px(0.0), Unit::Px(0.0)))) + ) + .push( + KeyFrame::new(50.0) + .push(StyleProperty::Background(palette::css::MAGENTA)) + .push(StyleProperty::Inset(TrblRectangle::new(Unit::Px(150.0), Unit::Px(0.0), Unit::Px(0.0), Unit::Px(250.0)))) + ) + .push( + KeyFrame::new(100.0) + .push(StyleProperty::Background(palette::css::YELLOW)) + .push(StyleProperty::Inset(TrblRectangle::new(100.into(), 0.into(), 0.into(), 0.into()))) + ) + .loop_amount(LoopAmount::Infinite) + ; + + let text_animation = Animation::new(Duration::from_secs(5), TimingFunction::Ease) + .push( + KeyFrame::new(0.0) + .push(StyleProperty::Background(palette::css::RED)) + .push(StyleProperty::Color(palette::css::BLUE)) + .push(StyleProperty::FontSize(20.0)) + ) + .push( + KeyFrame::new(100.0) + .push(StyleProperty::Background(palette::css::YELLOW)) + .push(StyleProperty::Color(palette::css::BLUE_VIOLET)) + .push(StyleProperty::FontSize(40.0)) + ) + .loop_amount(LoopAmount::Infinite) + ; + + let animation_examples: Vec = vec![ Container::new() .background(palette::css::GRAY) .width("100px") .height("40px") - .animation( - Animation::new(Duration::from_secs(5), TimingFunction::EaseOut) - .push( - KeyFrame::new(0.0) - .push(StyleProperty::Background(palette::css::BLACK)) - .push(StyleProperty::Width(Unit::Px(20.0))), - ) - .push( - KeyFrame::new(100.0) - .push(StyleProperty::Background(palette::css::RED)) - .push(StyleProperty::Width(Unit::Px(200.0))) - ) - ), + .animation(growing_animation) + .component(), + + Container::new() + .push( + Container::new() + .inset(Unit::Px(100.0), Unit::Px(0.0), Unit::Px(0.0), Unit::Px(0.0)) + .background(palette::css::BLUE) + .position(Position::Absolute) + .width("40px") + .height("40px") + .animation(moving_animation) + ).component(), + + Text::new("Why, Hello!") + .animation(text_animation) + .component() ]; From d59420dd9e3aecfb88f5b9a8d3ef436b411369d3 Mon Sep 17 00:00:00 2001 From: NoahR02 Date: Tue, 8 Jul 2025 10:33:18 -0400 Subject: [PATCH 08/18] Update the Animations window title --- examples/animations/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/animations/main.rs b/examples/animations/main.rs index f91dc930..78c8d08b 100644 --- a/examples/animations/main.rs +++ b/examples/animations/main.rs @@ -114,5 +114,5 @@ impl Component for AnimationsExample { fn main() { use craft::CraftOptions; util::setup_logging(); - craft::craft_main(AnimationsExample::component(), (), CraftOptions::basic("Counter")); + craft::craft_main(AnimationsExample::component(), (), CraftOptions::basic("Animations")); } From da9979f92b08c883f2569b22f297fcf95f5d55ce Mon Sep 17 00:00:00 2001 From: NoahR02 Date: Tue, 8 Jul 2025 10:42:34 -0400 Subject: [PATCH 09/18] Use web_time on wasm --- crates/craft_core/src/app.rs | 15 +++++++++++---- crates/craft_core/src/lib.rs | 7 +++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/crates/craft_core/src/app.rs b/crates/craft_core/src/app.rs index b65354c2..1504df6b 100644 --- a/crates/craft_core/src/app.rs +++ b/crates/craft_core/src/app.rs @@ -35,7 +35,14 @@ use kurbo::{Affine, Point}; use peniko::Color; use std::collections::HashMap; use std::sync::Arc; -use std::time::{Duration, Instant}; + +#[cfg(not(target_arch = "wasm32"))] +use std::time; +#[cfg(target_arch = "wasm32")] +use web_time as time; + +use std::time::{Duration}; + use taffy::{AvailableSpace, NodeId, TaffyTree}; use craft_runtime::Sender; use ui_events::keyboard::{KeyState, KeyboardEvent, Modifiers, NamedKey}; @@ -106,7 +113,7 @@ pub struct App { pub(crate) runtime: CraftRuntimeHandle, pub(crate) modifiers: Modifiers, pub(crate) animation_controller: AnimationController, - pub(crate) last_frame_time: std::time::Instant, + pub(crate) last_frame_time: time::Instant, pub redraw_flags: RedrawFlags, } @@ -306,9 +313,9 @@ impl App { return; } - let now = Instant::now(); + let now = time::Instant::now(); let delta_time = now - self.last_frame_time; - self.last_frame_time = now; + self.last_frame_time = now.into(); let surface_size = self.window_context.window_size(); diff --git a/crates/craft_core/src/lib.rs b/crates/craft_core/src/lib.rs index 1d38f2c4..6b725028 100644 --- a/crates/craft_core/src/lib.rs +++ b/crates/craft_core/src/lib.rs @@ -53,7 +53,10 @@ use std::collections::VecDeque; use std::future::Future; use std::pin::Pin; use std::sync::Arc; -use std::time::Instant; +#[cfg(not(target_arch = "wasm32"))] +use std::time; +#[cfg(target_arch = "wasm32")] +use web_time as time; use crate::reactive::reactive_tree::ReactiveTree; use crate::reactive::state_store::{StateStore, StateStoreItem}; #[cfg(target_arch = "wasm32")] @@ -267,7 +270,7 @@ pub fn setup_craft( animation_controller: AnimationController { animations: Default::default(), }, - last_frame_time: Instant::now(), + last_frame_time: time::Instant::now(), redraw_flags: RedrawFlags::new(true), }); From 8cb92f1a7cd011511a6cfb13945e8bb76750522b Mon Sep 17 00:00:00 2001 From: NoahR02 Date: Tue, 8 Jul 2025 10:50:57 -0400 Subject: [PATCH 10/18] Add the animation example to the website --- examples/animations/main.rs | 6 +++--- website/src/examples.rs | 9 ++++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/examples/animations/main.rs b/examples/animations/main.rs index 78c8d08b..b78401bb 100644 --- a/examples/animations/main.rs +++ b/examples/animations/main.rs @@ -15,7 +15,7 @@ impl Component for AnimationsExample { type Props = (); type Message = (); - fn view(context: &mut Context) -> ComponentSpecification { + fn view(_context: &mut Context) -> ComponentSpecification { let growing_animation = Animation::new(Duration::from_secs(5), TimingFunction::EaseOut) .push( @@ -75,7 +75,7 @@ impl Component for AnimationsExample { .height("40px") .animation(growing_animation) .component(), - + Container::new() .push( Container::new() @@ -86,7 +86,7 @@ impl Component for AnimationsExample { .height("40px") .animation(moving_animation) ).component(), - + Text::new("Why, Hello!") .animation(text_animation) .component() diff --git a/website/src/examples.rs b/website/src/examples.rs index c2fda091..5090eed9 100644 --- a/website/src/examples.rs +++ b/website/src/examples.rs @@ -10,10 +10,14 @@ mod request; #[path = "../../examples/tour/main.rs"] mod tour; +#[path = "../../examples/animations/main.rs"] +mod animations; + use crate::examples::counter::Counter; use crate::examples::request::AniList; use crate::examples::text::TextState; use crate::examples::tour::Tour; +use crate::examples::animations::AnimationsExample; use crate::navbar::NAVBAR_HEIGHT; use crate::theme::{wrapper, ACTIVE_LINK_COLOR, DEFAULT_LINK_COLOR, MOBILE_MEDIA_QUERY_WIDTH, WRAPPER_PADDING_LEFT, WRAPPER_PADDING_RIGHT}; use crate::WebsiteGlobalState; @@ -30,6 +34,7 @@ const COUNTER_EXAMPLE_LINK: &str = "/examples/counter"; const TOUR_EXAMPLE_LINK: &str = "/examples/tour"; const REQUEST_EXAMPLE_LINK: &str = "/examples/request"; const TEXT_EXAMPLE_LINK: &str = "/examples/text"; +const ANIMATIONS_EXAMPLE_LINK: &str = "/examples/animations"; #[derive(Default)] pub(crate) struct Examples { @@ -60,7 +65,8 @@ fn examples_sidebar(example_to_show: &String, window: &WindowContext) -> Compone create_examples_link("Counter", COUNTER_EXAMPLE_LINK, example_to_show), create_examples_link("Tour", TOUR_EXAMPLE_LINK, example_to_show), create_examples_link("Request", REQUEST_EXAMPLE_LINK, example_to_show), - create_examples_link("Text", TEXT_EXAMPLE_LINK, example_to_show) + create_examples_link("Text", TEXT_EXAMPLE_LINK, example_to_show), + create_examples_link("Animations", ANIMATIONS_EXAMPLE_LINK, example_to_show) ]; if window.window_width() <= MOBILE_MEDIA_QUERY_WIDTH { @@ -128,6 +134,7 @@ impl Component for Examples { TEXT_EXAMPLE_LINK => TextState::component().key(TEXT_EXAMPLE_LINK), TOUR_EXAMPLE_LINK => Tour::component().key(TOUR_EXAMPLE_LINK).props(Props::new(example_props)), REQUEST_EXAMPLE_LINK => AniList::component().key(REQUEST_EXAMPLE_LINK).props(Props::new(example_props)), + ANIMATIONS_EXAMPLE_LINK => AnimationsExample::component().key(ANIMATIONS_EXAMPLE_LINK).props(Props::new(example_props)), _ => Counter::component().key(COUNTER_EXAMPLE_LINK), }; From 9d25832be268936ea9bbd27112eba24f055c2e1c Mon Sep 17 00:00:00 2001 From: NoahR02 Date: Tue, 8 Jul 2025 11:33:48 -0400 Subject: [PATCH 11/18] Clean up deleted animated elements --- crates/craft_core/src/app.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/craft_core/src/app.rs b/crates/craft_core/src/app.rs index 1504df6b..bdffbbc3 100644 --- a/crates/craft_core/src/app.rs +++ b/crates/craft_core/src/app.rs @@ -4,7 +4,7 @@ use { crate::accessibility::activation_handler::CraftActivationHandler, crate::accessibility::deactivation_handler::CraftDeactivationHandler, }; -use crate::components::{ComponentSpecification, Event}; +use crate::components::{ComponentId, ComponentSpecification, Event}; use craft_runtime::CraftRuntimeHandle; #[cfg(feature = "dev_tools")] use crate::devtools::dev_tools_component::dev_tools_view; @@ -33,7 +33,7 @@ use cfg_if::cfg_if; use craft_logging::{info, span, Level}; use kurbo::{Affine, Point}; use peniko::Color; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; #[cfg(not(target_arch = "wasm32"))] @@ -631,6 +631,12 @@ impl App { let reactive_tree = get_tree!(self, is_dev_tree); let root_element = reactive_tree.element_tree.as_mut().unwrap(); + // Clean up deleted elements by looking into the reactive tree. + let element_animation_ids: HashSet = HashSet::from_iter(self.animation_controller.animations.keys().cloned()); + element_animation_ids.difference(&reactive_tree.element_ids).for_each(|element_id| { + self.animation_controller.remove(*element_id); + }); + // Damage track across recursive calls to `on_animation_frame`. let mut animation_flags = AnimationFlags::default(); root_element.on_animation_frame(&mut animation_flags, &mut reactive_tree.element_state, &mut self.animation_controller, *delta_time); From 6d3e10349aa221a2d42b06c77efd6fad4d2441f4 Mon Sep 17 00:00:00 2001 From: NoahR02 Date: Tue, 8 Jul 2025 11:39:17 -0400 Subject: [PATCH 12/18] Track timings with span for the animate_tree fn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It also shows how long a forced layout will take 2025-07-08T15:38:04.517153Z INFO animate_tree:layout: craft_core::app: close time.busy=80.1µs time.idle=2.10µs 2025-07-08T15:38:04.517246Z INFO animate_tree: craft_core::app: close time.busy=221µs time.idle=2.60µs --- crates/craft_core/src/app.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/craft_core/src/app.rs b/crates/craft_core/src/app.rs index bdffbbc3..dde28763 100644 --- a/crates/craft_core/src/app.rs +++ b/crates/craft_core/src/app.rs @@ -627,6 +627,9 @@ impl App { /// "Animates" a tree by calling `on_animation_frame` and changing an element's styles. fn animate_tree(&mut self, is_dev_tree: bool, delta_time: &Duration, layout_origin: Point, viewport_size: LogicalSize) { + let span = span!(Level::INFO, "animate_tree"); + let _enter = span.enter(); + let old_has_active_animation = self.animation_controller.has_active_animation(); let reactive_tree = get_tree!(self, is_dev_tree); let root_element = reactive_tree.element_tree.as_mut().unwrap(); From 98cfb1c03a3555d7323efb89e6047daad3c2ccfd Mon Sep 17 00:00:00 2001 From: NoahR02 Date: Wed, 9 Jul 2025 08:47:44 -0400 Subject: [PATCH 13/18] Put ActiveAnimation state on the base element state + support multiple animations per element and style --- crates/craft_core/src/animation/animation.rs | 101 +++++++----------- crates/craft_core/src/app.rs | 29 ++--- crates/craft_core/src/craft_winit_state.rs | 8 +- .../src/elements/base_element_state.rs | 3 + crates/craft_core/src/elements/element.rs | 70 +++++++++--- .../craft_core/src/elements/element_styles.rs | 2 +- crates/craft_core/src/lib.rs | 6 +- .../craft_core/src/reactive/reactive_tree.rs | 2 + crates/craft_core/src/style/styles.rs | 24 ++++- examples/animations/main.rs | 12 +-- 10 files changed, 142 insertions(+), 115 deletions(-) diff --git a/crates/craft_core/src/animation/animation.rs b/crates/craft_core/src/animation/animation.rs index d5ab277c..c75068bf 100644 --- a/crates/craft_core/src/animation/animation.rs +++ b/crates/craft_core/src/animation/animation.rs @@ -1,13 +1,11 @@ -use crate::components::ComponentId; use crate::elements::ElementState; use crate::style::{Style, StyleProperty, Unit}; +use craft_primitives::geometry::TrblRectangle; use kurbo::{CubicBez, ParamCurve, Point}; -use rustc_hash::FxHashMap; +use smallvec::SmallVec; use std::collections::HashMap; use std::iter::zip; use std::time::Duration; -use smallvec::SmallVec; -use craft_primitives::geometry::TrblRectangle; #[derive(Clone, Debug)] pub struct KeyFrame { @@ -82,6 +80,7 @@ pub enum TimingFunction { #[derive(Clone, Debug)] pub struct Animation { + pub name: String, pub key_frames: SmallVec<[KeyFrame; 2]>, pub duration: Duration, pub timing_function: TimingFunction, @@ -95,8 +94,9 @@ pub enum LoopAmount { } impl Animation { - pub fn new(duration: Duration, timing_function: TimingFunction) -> Self { + pub fn new(name: String, duration: Duration, timing_function: TimingFunction) -> Self { Self { + name, key_frames: SmallVec::new(), duration, timing_function, @@ -115,24 +115,26 @@ impl Animation { } } +#[derive(Clone, Debug)] pub struct ActiveAnimation { /// How far into an animation we are. - current: Duration, + pub(crate) current: Duration, /// Tracks the status of an animation, if it is playing, scheduled, or paused. - status: AnimationStatus, + pub(crate) status: AnimationStatus, /// Stores the element state of the animation, so that we can track if an animation needs to be removed if an element is in a new state. - element_state: ElementState, - loop_amount: LoopAmount, + pub(crate) element_state: ElementState, + pub(crate) loop_amount: LoopAmount, } /// For damage tracking across recursive calls to `on_animation_frame`. #[derive(Clone, Debug, Default)] pub struct AnimationFlags { needs_relayout: bool, + has_active_animation: bool, } impl AnimationFlags { - /// OR'd with the provided boolean and the previously stored boolean, to track if an animiatable property effects layout. + /// OR'd with the provided boolean and the previously stored boolean, to track if an animatable property effects layout. /// This is used after `on_animation_frame` to optionally recompute the layout. pub fn set_needs_relayout(&mut self, needs_relayout: bool) { self.needs_relayout = self.needs_relayout | needs_relayout; @@ -142,61 +144,38 @@ impl AnimationFlags { pub fn needs_relayout(&self) -> bool { self.needs_relayout } -} - -pub struct AnimationController { - /// Maps an element id to a record of an animation's playback state. - pub(crate) animations: FxHashMap, -} -impl AnimationController { - - /// Removes the playback state (ActiveAnimation). - pub fn remove(&mut self, component: ComponentId) { - self.animations.remove(&component); + /// OR'd with the provided boolean and the previously stored boolean, to track if any animation is active. + pub fn set_has_active_animation(&mut self, has_active_animation: bool) { + self.has_active_animation = self.has_active_animation | has_active_animation; } - - - /// Determines if any animation is currently playing/running. + + /// Returns true if any animation is in the Playing state. pub fn has_active_animation(&self) -> bool { - for animation in self.animations.values() { - if animation.status == AnimationStatus::Playing { - return true; - } - } - - false + self.has_active_animation } +} + +impl ActiveAnimation { /// Advances an active animation, and it is also responsible for tracking the status and element_state. - pub fn tick(&mut self, animation_flags: &mut AnimationFlags, animation: &Animation, state: ElementState, component: ComponentId, delta: Duration) { - let active_animation = if let Some(active_animation) = self.animations.get_mut(&component) { - active_animation - } else { - self.animations.insert(component, ActiveAnimation { - current: Duration::ZERO, - status: AnimationStatus::Playing, - element_state: state, - loop_amount: animation.loop_amount.clone(), - }); - self.animations.get_mut(&component).unwrap() - }; + pub fn tick(&mut self, animation_flags: &mut AnimationFlags, animation: &Animation, state: ElementState, delta: Duration) { - if active_animation.element_state != state { - active_animation.current = Duration::ZERO; - active_animation.status = AnimationStatus::Playing; - active_animation.element_state = state; + if self.element_state != state { + self.current = Duration::ZERO; + self.status = AnimationStatus::Playing; + self.element_state = state; } - if active_animation.status == AnimationStatus::Playing && active_animation.element_state == state { - active_animation.current += delta; + if self.status == AnimationStatus::Playing && self.element_state == state { + self.current += delta; - let is_completed = active_animation.current >= animation.duration; + let is_completed = self.current >= animation.duration; - match &mut active_animation.loop_amount { + match &mut self.loop_amount { LoopAmount::Infinite => { if is_completed { - active_animation.current = Duration::ZERO; + self.current = Duration::ZERO; } } LoopAmount::Fixed(amount) => { @@ -204,11 +183,11 @@ impl AnimationController { *amount -= 1; if *amount == 0 { - active_animation.current = Duration::ZERO; - active_animation.status = AnimationStatus::Paused; + self.current = Duration::ZERO; + self.status = AnimationStatus::Paused; animation_flags.set_needs_relayout(true); } else { - active_animation.current = Duration::ZERO; + self.current = Duration::ZERO; } } } @@ -219,18 +198,12 @@ impl AnimationController { /// Called after `tick`, and is responsible for using the current animation time and /// computing an interpolated style from a provided `Animation`. - pub fn compute_style(&mut self, element_style: &Style, animation: &Animation, state: ElementState, component: ComponentId, animation_flags: &mut AnimationFlags) -> Style { - let active_animation = if let Some(active_animation) = self.animations.get_mut(&component) { - active_animation - } else { - return element_style.clone(); - }; - - if active_animation.status != AnimationStatus::Playing || active_animation.element_state != state { + pub fn compute_style(&mut self, element_style: &Style, animation: &Animation, state: ElementState, animation_flags: &mut AnimationFlags) -> Style { + if self.status != AnimationStatus::Playing || self.element_state != state { return element_style.clone(); } - let pos = Duration::div_duration_f32(active_animation.current, animation.duration); + let pos = Duration::div_duration_f32(self.current, animation.duration); fn find_keyframe_pair(pos: f32, animation: &Animation) -> (&KeyFrame, &KeyFrame) { let mut sorted = animation.key_frames.iter().collect::>(); sorted.sort_by(|a, b| a.offset_percentage.total_cmp(&b.offset_percentage)); diff --git a/crates/craft_core/src/app.rs b/crates/craft_core/src/app.rs index dde28763..521abc81 100644 --- a/crates/craft_core/src/app.rs +++ b/crates/craft_core/src/app.rs @@ -56,7 +56,7 @@ use winit::window::Window; use craft_renderer::RenderList; use craft_resource_manager::resource_event::ResourceEvent; use craft_resource_manager::resource_type::ResourceType; -use crate::animation::animation::{AnimationController, AnimationFlags}; +use crate::animation::animation::{AnimationFlags}; use crate::events::update_queue_entry::UpdateQueueEntry; macro_rules! get_tree { @@ -112,7 +112,6 @@ pub struct App { pub(crate) accesskit_adapter: Option, pub(crate) runtime: CraftRuntimeHandle, pub(crate) modifiers: Modifiers, - pub(crate) animation_controller: AnimationController, pub(crate) last_frame_time: time::Instant, pub redraw_flags: RedrawFlags, } @@ -627,23 +626,19 @@ impl App { /// "Animates" a tree by calling `on_animation_frame` and changing an element's styles. fn animate_tree(&mut self, is_dev_tree: bool, delta_time: &Duration, layout_origin: Point, viewport_size: LogicalSize) { + let span = span!(Level::INFO, "animate_tree"); let _enter = span.enter(); - let old_has_active_animation = self.animation_controller.has_active_animation(); let reactive_tree = get_tree!(self, is_dev_tree); + let old_has_active_animation = reactive_tree.previous_animation_flags.has_active_animation(); let root_element = reactive_tree.element_tree.as_mut().unwrap(); - // Clean up deleted elements by looking into the reactive tree. - let element_animation_ids: HashSet = HashSet::from_iter(self.animation_controller.animations.keys().cloned()); - element_animation_ids.difference(&reactive_tree.element_ids).for_each(|element_id| { - self.animation_controller.remove(*element_id); - }); - // Damage track across recursive calls to `on_animation_frame`. let mut animation_flags = AnimationFlags::default(); - root_element.on_animation_frame(&mut animation_flags, &mut reactive_tree.element_state, &mut self.animation_controller, *delta_time); - + root_element.on_animation_frame(&mut animation_flags, &mut reactive_tree.element_state, *delta_time); + reactive_tree.previous_animation_flags = animation_flags.clone(); + // Perform a relayout if an animation used any layout effecting style property. if animation_flags.needs_relayout() || old_has_active_animation { root_element.reset_layout_item(); @@ -657,13 +652,11 @@ impl App { ); } - { - // Request a redraw if there is at least one animation playing. - // ControlFlow::Poll is set in `about_to_wait`. - if self.animation_controller.has_active_animation() || old_has_active_animation { - // Winit does not guarantee when a redraw event will happen, but that should be fine, at worst we redraw an extra time. - self.request_redraw(RedrawFlags::new(old_has_active_animation)); - } + // Request a redraw if there is at least one animation playing. + // ControlFlow::Poll is set in `about_to_wait`. + if animation_flags.has_active_animation() || old_has_active_animation { + // Winit does not guarantee when a redraw event will happen, but that should be fine, at worst we redraw an extra time. + self.request_redraw(RedrawFlags::new(old_has_active_animation)); } } diff --git a/crates/craft_core/src/craft_winit_state.rs b/crates/craft_core/src/craft_winit_state.rs index 3db953a5..1303ff4d 100644 --- a/crates/craft_core/src/craft_winit_state.rs +++ b/crates/craft_core/src/craft_winit_state.rs @@ -250,9 +250,13 @@ impl ApplicationHandler for CraftWinitState { event_loop.exit(); return; } - + // Switch to Poll mode if we are running animations. - if craft_state.craft_app.animation_controller.has_active_animation() { + let has_active_animation = + craft_state.craft_app.user_tree.previous_animation_flags.has_active_animation() + || craft_state.craft_app.dev_tree.previous_animation_flags.has_active_animation() + ; + if has_active_animation { event_loop.set_control_flow(ControlFlow::Poll); } else { event_loop.set_control_flow(ControlFlow::WaitUntil(time::Instant::now() + WAIT_TIME)); diff --git a/crates/craft_core/src/elements/base_element_state.rs b/crates/craft_core/src/elements/base_element_state.rs index d6a3f78d..50ce5348 100644 --- a/crates/craft_core/src/elements/base_element_state.rs +++ b/crates/craft_core/src/elements/base_element_state.rs @@ -2,6 +2,8 @@ use crate::elements::element_data::ElementData; use crate::elements::element_states::ElementState; use crate::style::Style; use std::collections::HashMap; +use rustc_hash::FxHashMap; +use crate::animation::animation::ActiveAnimation; #[derive(Debug, Default, Clone)] pub struct BaseElementState { @@ -13,6 +15,7 @@ pub struct BaseElementState { /// Useful for scroll thumbs. pub(crate) pointer_capture: HashMap, pub(crate) focused: bool, + pub(crate) animations: Option>, } impl<'a> BaseElementState { diff --git a/crates/craft_core/src/elements/element.rs b/crates/craft_core/src/elements/element.rs index 2aae92ac..98ca2d20 100644 --- a/crates/craft_core/src/elements/element.rs +++ b/crates/craft_core/src/elements/element.rs @@ -1,28 +1,29 @@ +use crate::animation::animation::{ActiveAnimation, AnimationFlags, AnimationStatus}; use crate::components::component::{ComponentOrElement, ComponentSpecification}; use crate::components::{ComponentId, Event, FocusAction}; use crate::elements::element_data::ElementData; use crate::elements::element_states::ElementState; use crate::elements::scroll_state::ScrollState; use crate::events::CraftMessage; -use craft_primitives::geometry::borders::{BorderSpec, ComputedBorderSpec}; -use craft_primitives::geometry::{ElementBox, Point, Rectangle, TrblRectangle}; use crate::layout::layout_context::LayoutContext; use crate::layout::layout_item::{draw_borders_generic, LayoutItem}; use crate::reactive::element_state_store::{ElementStateStore, ElementStateStoreItem}; -use craft_renderer::renderer::RenderList; use crate::style::Style; use crate::text::text_context::TextContext; #[cfg(feature = "accesskit")] use accesskit::{Action, Role}; +use craft_primitives::geometry::borders::{BorderSpec, ComputedBorderSpec}; +use craft_primitives::geometry::{ElementBox, Point, Rectangle, TrblRectangle}; +use craft_renderer::renderer::RenderList; use kurbo::Affine; use peniko::Color; use std::any::Any; use std::mem; use std::sync::Arc; use std::time::Duration; +use rustc_hash::FxHashMap; use taffy::{NodeId, Overflow, TaffyTree}; use winit::window::Window; -use crate::animation::animation::{AnimationController, AnimationFlags}; #[derive(Clone)] pub struct ElementBoxed { @@ -326,9 +327,8 @@ pub trait Element: Any + StandardElementClone + Send + Sync { } /// Called after layout, and is responsible for updating the animation state of an element. - fn on_animation_frame(&mut self, animation_flags: &mut AnimationFlags, element_state: &mut ElementStateStore, animation_controller: &mut AnimationController, delta_time: Duration) { - let element_id = self.component_id(); - let base_state = self.get_base_state(element_state); + fn on_animation_frame(&mut self, animation_flags: &mut AnimationFlags, element_state: &mut ElementStateStore, delta_time: Duration) { + let base_state = self.get_base_state_mut(element_state); let mut current_state: ElementState = { if base_state.base.hovered { ElementState::Hovered @@ -342,24 +342,64 @@ pub trait Element: Any + StandardElementClone + Send + Sync { // A bit hacky, but we either get the current style with no fallback or fallback to a style and change the current element state to Normal. // This is to allow for retaining an animation state on a normal style even if you hover over it (assuming the hover has no animation). // Basically this is to hack in a basic inherited animation. - let current_style = if let Some(current_style) = base_state.base.current_style_mut_no_fallback(self.element_data_mut()) && current_style.animation.is_some() { + let current_style = + if let Some(current_style) = base_state.base.current_style_mut_no_fallback(self.element_data_mut()) && + let Some(current_style_animations) = ¤t_style.animations && !current_style_animations.is_empty() { current_style } else { current_state = ElementState::Normal; base_state.base.current_style_mut(self.element_data_mut()) }; - if let Some(animation) = ¤t_style.animation { - animation_controller.tick(animation_flags, animation, current_state, element_id, delta_time); - let new_style = animation_controller.compute_style(¤t_style, animation, current_state, element_id, animation_flags); - *current_style = Style::merge(current_style, &new_style); + // This is pretty hacky, but we can avoid allocating a hashmap for every element. + let active_animations = if current_style.animations.is_some() { + if base_state.base.animations.is_none() { + base_state.base.animations = Some(FxHashMap::default()); + } + + base_state.base.animations.as_mut().unwrap() } else { - // If the element style or the fallback doesn't have an animation, then remove any animation state. - animation_controller.remove(element_id); + for child in self.children_mut() { + child.internal.on_animation_frame(animation_flags, element_state, delta_time); + } + return; + }; + + if let Some(current_style_animations) = &mut current_style.animations { + for ani in current_style_animations { + if !active_animations.contains_key(&ani.name) { + active_animations.insert(ani.name.clone(), ActiveAnimation { + current: Duration::ZERO, + status: AnimationStatus::Playing, + element_state: current_state, + loop_amount: ani.loop_amount.clone(), + }); + } + } + } + + let mut to_remove = Vec::new(); + for (anim_name, active_animation) in active_animations.iter_mut() { + if active_animation.status == AnimationStatus::Playing { + animation_flags.set_has_active_animation(true); + } + + if let Some(animation) = ¤t_style.animation(anim_name.to_string()) { + active_animation.tick(animation_flags, animation, current_state, delta_time); + let new_style = active_animation.compute_style(¤t_style, animation, current_state, animation_flags); + *current_style = Style::merge(current_style, &new_style); + } else { + // If the element style or the fallback doesn't have an animation, then remove any animation state. + to_remove.push(anim_name.clone()); + } } + for anim_name in &to_remove { + active_animations.remove(anim_name); + } + for child in self.children_mut() { - child.internal.on_animation_frame(animation_flags, element_state, animation_controller, delta_time); + child.internal.on_animation_frame(animation_flags, element_state, delta_time); } } diff --git a/crates/craft_core/src/elements/element_styles.rs b/crates/craft_core/src/elements/element_styles.rs index 63385073..4739796d 100644 --- a/crates/craft_core/src/elements/element_styles.rs +++ b/crates/craft_core/src/elements/element_styles.rs @@ -264,7 +264,7 @@ where self } - fn animation(mut self, animation: Animation) -> Self { + fn push_animation(mut self, animation: Animation) -> Self { self.styles_mut().set_animation(animation); self } diff --git a/crates/craft_core/src/lib.rs b/crates/craft_core/src/lib.rs index 6b725028..95ff608f 100644 --- a/crates/craft_core/src/lib.rs +++ b/crates/craft_core/src/lib.rs @@ -70,7 +70,6 @@ use craft_logging::info; use {winit::event_loop::EventLoopBuilder, winit::platform::android::EventLoopBuilderExtAndroid}; use app::App; -use crate::animation::animation::AnimationController; use crate::app::RedrawFlags; use crate::craft_winit_state::CraftWinitState; use crate::utils::cloneable_any::CloneableAny; @@ -248,6 +247,7 @@ pub fn setup_craft( user_state, element_state: Default::default(), focus: None, + previous_animation_flags: Default::default(), }, #[cfg(feature = "dev_tools")] @@ -264,12 +264,10 @@ pub fn setup_craft( component_ids: Default::default(), pointer_captures: Default::default(), focus: None, + previous_animation_flags: Default::default(), }, runtime: runtime_copy, modifiers: Default::default(), - animation_controller: AnimationController { - animations: Default::default(), - }, last_frame_time: time::Instant::now(), redraw_flags: RedrawFlags::new(true), }); diff --git a/crates/craft_core/src/reactive/reactive_tree.rs b/crates/craft_core/src/reactive/reactive_tree.rs index 7bed41c5..dea78b81 100644 --- a/crates/craft_core/src/reactive/reactive_tree.rs +++ b/crates/craft_core/src/reactive/reactive_tree.rs @@ -5,6 +5,7 @@ use crate::reactive::element_state_store::ElementStateStore; use crate::reactive::state_store::StateStore; use crate::reactive::tree::ComponentTreeNode; use std::collections::{HashMap, HashSet, VecDeque}; +use crate::animation::animation::AnimationFlags; #[derive(Default)] pub struct ReactiveTree { @@ -18,6 +19,7 @@ pub struct ReactiveTree { pub(crate) user_state: StateStore, pub(crate) element_state: ElementStateStore, pub(crate) focus: Option, + pub(crate) previous_animation_flags: AnimationFlags, } impl ReactiveTree { diff --git a/crates/craft_core/src/style/styles.rs b/crates/craft_core/src/style/styles.rs index 10436513..9f545c88 100644 --- a/crates/craft_core/src/style/styles.rs +++ b/crates/craft_core/src/style/styles.rs @@ -334,7 +334,7 @@ impl Default for FontFamily { pub struct Style { properties: SmallVec<[StyleProperty; 5]>, pub dirty_flags: StyleFlags, - pub animation: Option> + pub animations: Option> } impl Default for Style { @@ -342,7 +342,7 @@ impl Default for Style { Style { properties: SmallVec::new(), dirty_flags: StyleFlags::empty(), - animation: None, + animations: None, } } } @@ -424,12 +424,26 @@ style_property!(selection_color, set_selection_color, SelectionColor, Color, SEL style_property!(cursor_color, set_cursor_color, CursorColor, Option, CURSOR_COLOR, None); impl Style { - pub fn animation(&self) -> &Option> { - &self.animation + pub fn animation(&self, animation: String) -> Option<&Animation> { + if let Some(animations) = &self.animations { + for ani in animations { + if ani.name == animation { + return Some(ani); + } + } + } + + None } pub fn set_animation(&mut self, animation: Animation) { - self.animation = Some(Box::new(animation)); + if let Some(animations) = &mut self.animations { + animations.push(animation); + } else { + let mut ani_vec = SmallVec::new(); + ani_vec.push(animation); + self.animations = Some(ani_vec); + } } fn remove_property(&mut self, f: impl Fn(&StyleProperty) -> bool) { diff --git a/examples/animations/main.rs b/examples/animations/main.rs index b78401bb..15acde20 100644 --- a/examples/animations/main.rs +++ b/examples/animations/main.rs @@ -17,7 +17,7 @@ impl Component for AnimationsExample { fn view(_context: &mut Context) -> ComponentSpecification { - let growing_animation = Animation::new(Duration::from_secs(5), TimingFunction::EaseOut) + let growing_animation = Animation::new("growing_animation".to_string(), Duration::from_secs(5), TimingFunction::EaseOut) .push( KeyFrame::new(0.0) .push(StyleProperty::Background(palette::css::GREEN)) @@ -33,7 +33,7 @@ impl Component for AnimationsExample { .loop_amount(LoopAmount::Fixed(3)) ; - let moving_animation = Animation::new(Duration::from_secs(5), TimingFunction::Ease) + let moving_animation = Animation::new("moving_animation".to_string(), Duration::from_secs(5), TimingFunction::Ease) .push( KeyFrame::new(0.0) .push(StyleProperty::Background(palette::css::BLUE)) @@ -52,7 +52,7 @@ impl Component for AnimationsExample { .loop_amount(LoopAmount::Infinite) ; - let text_animation = Animation::new(Duration::from_secs(5), TimingFunction::Ease) + let text_animation = Animation::new("text_animation".to_string(), Duration::from_secs(5), TimingFunction::Ease) .push( KeyFrame::new(0.0) .push(StyleProperty::Background(palette::css::RED)) @@ -73,7 +73,7 @@ impl Component for AnimationsExample { .background(palette::css::GRAY) .width("100px") .height("40px") - .animation(growing_animation) + .push_animation(growing_animation) .component(), Container::new() @@ -84,11 +84,11 @@ impl Component for AnimationsExample { .position(Position::Absolute) .width("40px") .height("40px") - .animation(moving_animation) + .push_animation(moving_animation) ).component(), Text::new("Why, Hello!") - .animation(text_animation) + .push_animation(text_animation) .component() ]; From 5c8d60f3ff4cd1d56c1bdde24bc94360b4660d93 Mon Sep 17 00:00:00 2001 From: NoahR02 Date: Wed, 9 Jul 2025 08:59:45 -0400 Subject: [PATCH 14/18] Make sure dev tools is featured gated when accessing its properties --- crates/craft_core/src/craft_winit_state.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/craft_core/src/craft_winit_state.rs b/crates/craft_core/src/craft_winit_state.rs index 1303ff4d..605ac249 100644 --- a/crates/craft_core/src/craft_winit_state.rs +++ b/crates/craft_core/src/craft_winit_state.rs @@ -252,10 +252,16 @@ impl ApplicationHandler for CraftWinitState { } // Switch to Poll mode if we are running animations. - let has_active_animation = - craft_state.craft_app.user_tree.previous_animation_flags.has_active_animation() - || craft_state.craft_app.dev_tree.previous_animation_flags.has_active_animation() - ; + + + + let mut has_active_animation = craft_state.craft_app.user_tree.previous_animation_flags.has_active_animation(); + + #[cfg(feature = "dev_tools")] + { + has_active_animation = has_active_animation || craft_state.craft_app.dev_tree.previous_animation_flags.has_active_animation(); + } + if has_active_animation { event_loop.set_control_flow(ControlFlow::Poll); } else { From 34e1b81ab85c836ad6aa97e4cff15d118389cb0d Mon Sep 17 00:00:00 2001 From: NoahR02 Date: Wed, 9 Jul 2025 09:21:48 -0400 Subject: [PATCH 15/18] Naively resolve percentage units if start and end both are the same unit types --- crates/craft_core/src/animation/animation.rs | 57 ++++++++++++-------- examples/animations/main.rs | 4 +- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/crates/craft_core/src/animation/animation.rs b/crates/craft_core/src/animation/animation.rs index c75068bf..aef7b8b1 100644 --- a/crates/craft_core/src/animation/animation.rs +++ b/crates/craft_core/src/animation/animation.rs @@ -275,12 +275,29 @@ impl ActiveAnimation { a + (b - a) * t } - fn resolve_unit(unit: &Unit) -> f32 { - match unit { + #[inline(always)] + fn resolve_unit(start: &Unit, end: &Unit, t: f64, set_prop: &mut dyn FnMut(Unit)) { + let resolved_start = match start { Unit::Px(px) => *px, Unit::Percentage(percent) => *percent, Unit::Auto => panic!("Unit must not be auto.") - } + }; + + let resolved_end = match end { + Unit::Px(px) => *px, + Unit::Percentage(percent) => *percent, + Unit::Auto => panic!("Unit must not be auto.") + }; + let new = lerp(resolved_start, resolved_end, t as f32); + + // Naively asserts that start and end must be the same Unit type. + let new = match start { + Unit::Px(_) => Unit::Px(new), + Unit::Percentage(_) => Unit::Percentage(new), + _ => unreachable!() + }; + + set_prop(new); } match (start_prop, end_prop) { @@ -300,35 +317,29 @@ impl ActiveAnimation { } (Some(StyleProperty::Width(start)), Some(StyleProperty::Width(end))) => { - let resolved_start = resolve_unit(start); - let resolved_end = resolve_unit(end); - let new = lerp(resolved_start, resolved_end, t as f32); - style.set_width(Unit::Px(new)); + resolve_unit(start, end, t, &mut |new| { + style.set_width(new); + }); animation_flags.set_needs_relayout(true); } (Some(StyleProperty::Height(start)), Some(StyleProperty::Height(end))) => { - let resolved_start = resolve_unit(start); - let resolved_end = resolve_unit(end); - let new = lerp(resolved_start, resolved_end, t as f32); - style.set_height(Unit::Px(new)); + resolve_unit(start, end, t, &mut |new| { + style.set_height(new); + }); animation_flags.set_needs_relayout(true); } (Some(StyleProperty::Inset(start)), Some(StyleProperty::Inset(end))) => { let trlb = zip(start.to_array(), end.to_array()).map(|(start, end)| { - let resolved_start = resolve_unit(&start); - let resolved_end = resolve_unit(&end); - let new = lerp(resolved_start, resolved_end, t as f32); + let mut inset_unit = Unit::Auto; + resolve_unit(&start, &end, t, &mut |new| { + inset_unit = new; + }); - new - }).collect::>(); - - let inset = TrblRectangle::new( - Unit::Px(trlb[0]), - Unit::Px(trlb[1]), - Unit::Px(trlb[2]), - Unit::Px(trlb[3]), - ); + inset_unit + }).collect::>(); + + let inset = TrblRectangle::new(trlb[0], trlb[1], trlb[2], trlb[3]); style.set_inset(inset); animation_flags.set_needs_relayout(true); diff --git a/examples/animations/main.rs b/examples/animations/main.rs index 15acde20..5c7ee383 100644 --- a/examples/animations/main.rs +++ b/examples/animations/main.rs @@ -21,13 +21,13 @@ impl Component for AnimationsExample { .push( KeyFrame::new(0.0) .push(StyleProperty::Background(palette::css::GREEN)) - .push(StyleProperty::Width(Unit::Px(20.0))) + .push(StyleProperty::Width(Unit::Percentage(10.0))) .push(StyleProperty::Height(Unit::Px(40.0))), ) .push( KeyFrame::new(100.0) .push(StyleProperty::Background(palette::css::RED)) - .push(StyleProperty::Width(Unit::Px(400.0))) + .push(StyleProperty::Width(Unit::Percentage(80.0))) .push(StyleProperty::Height(Unit::Px(100.0))) ) .loop_amount(LoopAmount::Fixed(3)) From 50e152f8f024d10e900b08da2a94177a3ac2cb3b Mon Sep 17 00:00:00 2001 From: NoahR02 Date: Wed, 9 Jul 2025 10:56:21 -0400 Subject: [PATCH 16/18] Remove enum element_state from ActiveAnimation and simplify the code --- crates/craft_core/src/animation/animation.rs | 13 ++--------- crates/craft_core/src/elements/element.rs | 23 ++++++++++---------- crates/craft_core/src/style/styles.rs | 1 + 3 files changed, 14 insertions(+), 23 deletions(-) diff --git a/crates/craft_core/src/animation/animation.rs b/crates/craft_core/src/animation/animation.rs index aef7b8b1..4de75f31 100644 --- a/crates/craft_core/src/animation/animation.rs +++ b/crates/craft_core/src/animation/animation.rs @@ -121,8 +121,6 @@ pub struct ActiveAnimation { pub(crate) current: Duration, /// Tracks the status of an animation, if it is playing, scheduled, or paused. pub(crate) status: AnimationStatus, - /// Stores the element state of the animation, so that we can track if an animation needs to be removed if an element is in a new state. - pub(crate) element_state: ElementState, pub(crate) loop_amount: LoopAmount, } @@ -160,14 +158,7 @@ impl ActiveAnimation { /// Advances an active animation, and it is also responsible for tracking the status and element_state. pub fn tick(&mut self, animation_flags: &mut AnimationFlags, animation: &Animation, state: ElementState, delta: Duration) { - - if self.element_state != state { - self.current = Duration::ZERO; - self.status = AnimationStatus::Playing; - self.element_state = state; - } - - if self.status == AnimationStatus::Playing && self.element_state == state { + if self.status == AnimationStatus::Playing { self.current += delta; let is_completed = self.current >= animation.duration; @@ -199,7 +190,7 @@ impl ActiveAnimation { /// Called after `tick`, and is responsible for using the current animation time and /// computing an interpolated style from a provided `Animation`. pub fn compute_style(&mut self, element_style: &Style, animation: &Animation, state: ElementState, animation_flags: &mut AnimationFlags) -> Style { - if self.status != AnimationStatus::Playing || self.element_state != state { + if self.status != AnimationStatus::Playing { return element_style.clone(); } diff --git a/crates/craft_core/src/elements/element.rs b/crates/craft_core/src/elements/element.rs index 98ca2d20..cf8b23bd 100644 --- a/crates/craft_core/src/elements/element.rs +++ b/crates/craft_core/src/elements/element.rs @@ -329,7 +329,7 @@ pub trait Element: Any + StandardElementClone + Send + Sync { /// Called after layout, and is responsible for updating the animation state of an element. fn on_animation_frame(&mut self, animation_flags: &mut AnimationFlags, element_state: &mut ElementStateStore, delta_time: Duration) { let base_state = self.get_base_state_mut(element_state); - let mut current_state: ElementState = { + let current_state: ElementState = { if base_state.base.hovered { ElementState::Hovered } else if base_state.base.focused { @@ -339,16 +339,12 @@ pub trait Element: Any + StandardElementClone + Send + Sync { } }; - // A bit hacky, but we either get the current style with no fallback or fallback to a style and change the current element state to Normal. - // This is to allow for retaining an animation state on a normal style even if you hover over it (assuming the hover has no animation). - // Basically this is to hack in a basic inherited animation. + // If we don't have an animation in the current style then try to fall back to the normal style. let current_style = - if let Some(current_style) = base_state.base.current_style_mut_no_fallback(self.element_data_mut()) && - let Some(current_style_animations) = ¤t_style.animations && !current_style_animations.is_empty() { + if let Some(current_style) = base_state.base.current_style_mut_no_fallback(self.element_data_mut()) && current_style.animations.is_some() { current_style } else { - current_state = ElementState::Normal; - base_state.base.current_style_mut(self.element_data_mut()) + &mut self.element_data_mut().style }; // This is pretty hacky, but we can avoid allocating a hashmap for every element. @@ -366,18 +362,21 @@ pub trait Element: Any + StandardElementClone + Send + Sync { }; if let Some(current_style_animations) = &mut current_style.animations { - for ani in current_style_animations { + for ani in &mut *current_style_animations { if !active_animations.contains_key(&ani.name) { active_animations.insert(ani.name.clone(), ActiveAnimation { current: Duration::ZERO, status: AnimationStatus::Playing, - element_state: current_state, loop_amount: ani.loop_amount.clone(), }); } - } + } + + active_animations.retain(|key, _| { + current_style_animations.iter().any(|ani| &ani.name == key) + }); } - + let mut to_remove = Vec::new(); for (anim_name, active_animation) in active_animations.iter_mut() { if active_animation.status == AnimationStatus::Playing { diff --git a/crates/craft_core/src/style/styles.rs b/crates/craft_core/src/style/styles.rs index 9f545c88..2b0e0d5a 100644 --- a/crates/craft_core/src/style/styles.rs +++ b/crates/craft_core/src/style/styles.rs @@ -470,6 +470,7 @@ impl Style { } let mut merged = old.clone(); + merged.animations = None; for prop in &new.properties { let flag = match prop { From 6f2473557b7811717722686da50a1be4486a88cb Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Wed, 9 Jul 2025 19:21:42 -0400 Subject: [PATCH 17/18] Minor cleanup --- crates/craft_core/src/animation/mod.rs | 1 - .../{animation => animations}/animation.rs | 40 ++--- crates/craft_core/src/animations/mod.rs | 6 + crates/craft_core/src/app.rs | 10 +- .../src/elements/base_element_state.rs | 2 +- crates/craft_core/src/elements/element.rs | 29 ++-- .../craft_core/src/elements/element_styles.rs | 2 +- crates/craft_core/src/lib.rs | 2 +- .../craft_core/src/reactive/reactive_tree.rs | 2 +- crates/craft_core/src/style/styles.rs | 150 +++++++++++------- examples/animations/main.rs | 90 +++++------ 11 files changed, 183 insertions(+), 151 deletions(-) delete mode 100644 crates/craft_core/src/animation/mod.rs rename crates/craft_core/src/{animation => animations}/animation.rs (91%) create mode 100644 crates/craft_core/src/animations/mod.rs diff --git a/crates/craft_core/src/animation/mod.rs b/crates/craft_core/src/animation/mod.rs deleted file mode 100644 index ec640c02..00000000 --- a/crates/craft_core/src/animation/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod animation; \ No newline at end of file diff --git a/crates/craft_core/src/animation/animation.rs b/crates/craft_core/src/animations/animation.rs similarity index 91% rename from crates/craft_core/src/animation/animation.rs rename to crates/craft_core/src/animations/animation.rs index 4de75f31..fcfa2875 100644 --- a/crates/craft_core/src/animation/animation.rs +++ b/crates/craft_core/src/animations/animation.rs @@ -31,7 +31,7 @@ impl KeyFrame { } } -#[derive(Clone, Debug)] +#[derive(Copy, Clone, Debug)] #[derive(PartialEq)] pub enum AnimationStatus { Paused, @@ -40,7 +40,7 @@ pub enum AnimationStatus { } /// A cubic bézier curve where P0 and P3 are stuck at (0,0) and (1,1). -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] pub struct FixedCubicBezier { cubic_bez: CubicBez, } @@ -61,7 +61,7 @@ impl FixedCubicBezier { /// The motion of an animation modeled with a mathematical function. -#[derive(Default, Clone, Debug)] +#[derive(Default, Copy, Clone, Debug)] pub enum TimingFunction { /// https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function#linear #[default] @@ -87,16 +87,16 @@ pub struct Animation { pub loop_amount: LoopAmount, } -#[derive(Clone, Debug)] +#[derive(Copy, Clone, Debug)] pub enum LoopAmount { Infinite, Fixed(u32) } impl Animation { - pub fn new(name: String, duration: Duration, timing_function: TimingFunction) -> Self { + pub fn new(name: &str, duration: Duration, timing_function: TimingFunction) -> Self { Self { - name, + name: name.to_string(), key_frames: SmallVec::new(), duration, timing_function, @@ -115,7 +115,7 @@ impl Animation { } } -#[derive(Clone, Debug)] +#[derive(Copy, Clone, Debug)] pub struct ActiveAnimation { /// How far into an animation we are. pub(crate) current: Duration, @@ -125,7 +125,7 @@ pub struct ActiveAnimation { } /// For damage tracking across recursive calls to `on_animation_frame`. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Copy, Debug, Default)] pub struct AnimationFlags { needs_relayout: bool, has_active_animation: bool, @@ -135,7 +135,7 @@ impl AnimationFlags { /// OR'd with the provided boolean and the previously stored boolean, to track if an animatable property effects layout. /// This is used after `on_animation_frame` to optionally recompute the layout. pub fn set_needs_relayout(&mut self, needs_relayout: bool) { - self.needs_relayout = self.needs_relayout | needs_relayout; + self.needs_relayout |= needs_relayout; } /// Returns whether we need to perform a relayout or not. @@ -145,7 +145,7 @@ impl AnimationFlags { /// OR'd with the provided boolean and the previously stored boolean, to track if any animation is active. pub fn set_has_active_animation(&mut self, has_active_animation: bool) { - self.has_active_animation = self.has_active_animation | has_active_animation; + self.has_active_animation |= has_active_animation; } /// Returns true if any animation is in the Playing state. @@ -157,7 +157,7 @@ impl AnimationFlags { impl ActiveAnimation { /// Advances an active animation, and it is also responsible for tracking the status and element_state. - pub fn tick(&mut self, animation_flags: &mut AnimationFlags, animation: &Animation, state: ElementState, delta: Duration) { + pub fn tick(&mut self, animation_flags: &mut AnimationFlags, animation: &Animation, _state: ElementState, delta: Duration) { if self.status == AnimationStatus::Playing { self.current += delta; @@ -189,7 +189,7 @@ impl ActiveAnimation { /// Called after `tick`, and is responsible for using the current animation time and /// computing an interpolated style from a provided `Animation`. - pub fn compute_style(&mut self, element_style: &Style, animation: &Animation, state: ElementState, animation_flags: &mut AnimationFlags) -> Style { + pub fn compute_style(&mut self, element_style: &Style, animation: &Animation, _state: ElementState, animation_flags: &mut AnimationFlags) -> Style { if self.status != AnimationStatus::Playing { return element_style.clone(); } @@ -267,16 +267,16 @@ impl ActiveAnimation { } #[inline(always)] - fn resolve_unit(start: &Unit, end: &Unit, t: f64, set_prop: &mut dyn FnMut(Unit)) { + fn resolve_unit(start: Unit, end: Unit, t: f64, set_prop: &mut dyn FnMut(Unit)) { let resolved_start = match start { - Unit::Px(px) => *px, - Unit::Percentage(percent) => *percent, + Unit::Px(px) => px, + Unit::Percentage(percent) => percent, Unit::Auto => panic!("Unit must not be auto.") }; let resolved_end = match end { - Unit::Px(px) => *px, - Unit::Percentage(percent) => *percent, + Unit::Px(px) => px, + Unit::Percentage(percent) => percent, Unit::Auto => panic!("Unit must not be auto.") }; let new = lerp(resolved_start, resolved_end, t as f32); @@ -308,13 +308,13 @@ impl ActiveAnimation { } (Some(StyleProperty::Width(start)), Some(StyleProperty::Width(end))) => { - resolve_unit(start, end, t, &mut |new| { + resolve_unit(*start, *end, t, &mut |new| { style.set_width(new); }); animation_flags.set_needs_relayout(true); } (Some(StyleProperty::Height(start)), Some(StyleProperty::Height(end))) => { - resolve_unit(start, end, t, &mut |new| { + resolve_unit(*start, *end, t, &mut |new| { style.set_height(new); }); animation_flags.set_needs_relayout(true); @@ -323,7 +323,7 @@ impl ActiveAnimation { (Some(StyleProperty::Inset(start)), Some(StyleProperty::Inset(end))) => { let trlb = zip(start.to_array(), end.to_array()).map(|(start, end)| { let mut inset_unit = Unit::Auto; - resolve_unit(&start, &end, t, &mut |new| { + resolve_unit(start, end, t, &mut |new| { inset_unit = new; }); diff --git a/crates/craft_core/src/animations/mod.rs b/crates/craft_core/src/animations/mod.rs new file mode 100644 index 00000000..8e10d763 --- /dev/null +++ b/crates/craft_core/src/animations/mod.rs @@ -0,0 +1,6 @@ +pub mod animation; + +pub use animation::Animation; +pub use animation::KeyFrame; +pub use animation::LoopAmount; +pub use animation::TimingFunction; \ No newline at end of file diff --git a/crates/craft_core/src/app.rs b/crates/craft_core/src/app.rs index 521abc81..85d54919 100644 --- a/crates/craft_core/src/app.rs +++ b/crates/craft_core/src/app.rs @@ -4,7 +4,7 @@ use { crate::accessibility::activation_handler::CraftActivationHandler, crate::accessibility::deactivation_handler::CraftDeactivationHandler, }; -use crate::components::{ComponentId, ComponentSpecification, Event}; +use crate::components::{ComponentSpecification, Event}; use craft_runtime::CraftRuntimeHandle; #[cfg(feature = "dev_tools")] use crate::devtools::dev_tools_component::dev_tools_view; @@ -33,7 +33,7 @@ use cfg_if::cfg_if; use craft_logging::{info, span, Level}; use kurbo::{Affine, Point}; use peniko::Color; -use std::collections::{HashMap, HashSet}; +use std::collections::{HashMap}; use std::sync::Arc; #[cfg(not(target_arch = "wasm32"))] @@ -56,7 +56,7 @@ use winit::window::Window; use craft_renderer::RenderList; use craft_resource_manager::resource_event::ResourceEvent; use craft_resource_manager::resource_type::ResourceType; -use crate::animation::animation::{AnimationFlags}; +use crate::animations::animation::{AnimationFlags}; use crate::events::update_queue_entry::UpdateQueueEntry; macro_rules! get_tree { @@ -314,7 +314,7 @@ impl App { let now = time::Instant::now(); let delta_time = now - self.last_frame_time; - self.last_frame_time = now.into(); + self.last_frame_time = now; let surface_size = self.window_context.window_size(); @@ -637,7 +637,7 @@ impl App { // Damage track across recursive calls to `on_animation_frame`. let mut animation_flags = AnimationFlags::default(); root_element.on_animation_frame(&mut animation_flags, &mut reactive_tree.element_state, *delta_time); - reactive_tree.previous_animation_flags = animation_flags.clone(); + reactive_tree.previous_animation_flags = animation_flags; // Perform a relayout if an animation used any layout effecting style property. if animation_flags.needs_relayout() || old_has_active_animation { diff --git a/crates/craft_core/src/elements/base_element_state.rs b/crates/craft_core/src/elements/base_element_state.rs index 50ce5348..05cebf81 100644 --- a/crates/craft_core/src/elements/base_element_state.rs +++ b/crates/craft_core/src/elements/base_element_state.rs @@ -3,7 +3,7 @@ use crate::elements::element_states::ElementState; use crate::style::Style; use std::collections::HashMap; use rustc_hash::FxHashMap; -use crate::animation::animation::ActiveAnimation; +use crate::animations::animation::ActiveAnimation; #[derive(Debug, Default, Clone)] pub struct BaseElementState { diff --git a/crates/craft_core/src/elements/element.rs b/crates/craft_core/src/elements/element.rs index cf8b23bd..288baabd 100644 --- a/crates/craft_core/src/elements/element.rs +++ b/crates/craft_core/src/elements/element.rs @@ -1,4 +1,4 @@ -use crate::animation::animation::{ActiveAnimation, AnimationFlags, AnimationStatus}; +use crate::animations::animation::{ActiveAnimation, AnimationFlags, AnimationStatus}; use crate::components::component::{ComponentOrElement, ComponentSpecification}; use crate::components::{ComponentId, Event, FocusAction}; use crate::elements::element_data::ElementData; @@ -367,7 +367,7 @@ pub trait Element: Any + StandardElementClone + Send + Sync { active_animations.insert(ani.name.clone(), ActiveAnimation { current: Duration::ZERO, status: AnimationStatus::Playing, - loop_amount: ani.loop_amount.clone(), + loop_amount: ani.loop_amount, }); } } @@ -377,25 +377,20 @@ pub trait Element: Any + StandardElementClone + Send + Sync { }); } - let mut to_remove = Vec::new(); - for (anim_name, active_animation) in active_animations.iter_mut() { + active_animations.retain(|anim_name, active_animation| { if active_animation.status == AnimationStatus::Playing { animation_flags.set_has_active_animation(true); } - if let Some(animation) = ¤t_style.animation(anim_name.to_string()) { + if let Some(animation) = current_style.animation(anim_name) { active_animation.tick(animation_flags, animation, current_state, delta_time); - let new_style = active_animation.compute_style(¤t_style, animation, current_state, animation_flags); + let new_style = active_animation.compute_style(current_style, animation, current_state, animation_flags); *current_style = Style::merge(current_style, &new_style); + true } else { - // If the element style or the fallback doesn't have an animation, then remove any animation state. - to_remove.push(anim_name.clone()); + false } - } - - for anim_name in &to_remove { - active_animations.remove(anim_name); - } + }); for child in self.children_mut() { child.internal.on_animation_frame(animation_flags, element_state, delta_time); @@ -678,6 +673,14 @@ macro_rules! generate_component_methods { self } + #[allow(dead_code)] + pub fn extend_children_in_place(&mut self, children: Vec) + where + T: Into, + { + self.element_data.child_specs.extend(children.into_iter().map(|x| x.into())); + } + #[allow(dead_code)] pub fn push_in_place(&mut self, component_specification: ComponentSpecification) { self.element_data.child_specs.push(component_specification); diff --git a/crates/craft_core/src/elements/element_styles.rs b/crates/craft_core/src/elements/element_styles.rs index 4739796d..2833c5a4 100644 --- a/crates/craft_core/src/elements/element_styles.rs +++ b/crates/craft_core/src/elements/element_styles.rs @@ -3,7 +3,7 @@ use craft_primitives::geometry::TrblRectangle; use craft_primitives::Color; use crate::style::{AlignItems, Display, FlexDirection, FontStyle, JustifyContent, Overflow, Style, Underline, Unit, Weight, Wrap}; use taffy::Position; -use crate::animation::animation::Animation; +use crate::animations::animation::Animation; pub trait ElementStyles where diff --git a/crates/craft_core/src/lib.rs b/crates/craft_core/src/lib.rs index 95ff608f..95fecd85 100644 --- a/crates/craft_core/src/lib.rs +++ b/crates/craft_core/src/lib.rs @@ -24,7 +24,7 @@ pub mod markdown; mod utils; #[cfg(target_arch = "wasm32")] pub mod wasm_queue; -pub mod animation; +pub mod animations; pub use options::CraftOptions; pub use craft_primitives::palette; diff --git a/crates/craft_core/src/reactive/reactive_tree.rs b/crates/craft_core/src/reactive/reactive_tree.rs index dea78b81..a8c8a703 100644 --- a/crates/craft_core/src/reactive/reactive_tree.rs +++ b/crates/craft_core/src/reactive/reactive_tree.rs @@ -5,7 +5,7 @@ use crate::reactive::element_state_store::ElementStateStore; use crate::reactive::state_store::StateStore; use crate::reactive::tree::ComponentTreeNode; use std::collections::{HashMap, HashSet, VecDeque}; -use crate::animation::animation::AnimationFlags; +use crate::animations::animation::AnimationFlags; #[derive(Default)] pub struct ReactiveTree { diff --git a/crates/craft_core/src/style/styles.rs b/crates/craft_core/src/style/styles.rs index 2b0e0d5a..a4beeed5 100644 --- a/crates/craft_core/src/style/styles.rs +++ b/crates/craft_core/src/style/styles.rs @@ -6,12 +6,12 @@ pub use taffy::BoxSizing; pub use taffy::Overflow; pub use taffy::Position; +use crate::animations::animation::Animation; use craft_primitives::geometry::TrblRectangle; use craft_primitives::ColorBrush; +use smallvec::SmallVec; use std::fmt; use std::fmt::Debug; -use smallvec::SmallVec; -use crate::animation::animation::Animation; #[derive(Clone, Copy, Debug)] pub enum Unit { @@ -178,24 +178,19 @@ impl TextStyleProperty { pub(crate) fn to_parley_style_property(&self) -> Option> { match self { TextStyleProperty::FontFamily(font_family) => { - let font_stack_cow_list = - Cow::Owned(vec![ - parley::FontFamily::Named(Cow::Owned(font_family.to_string())), - parley::FontFamily::Generic(parley::GenericFamily::SystemUi), - ]); + let font_stack_cow_list = Cow::Owned(vec![ + parley::FontFamily::Named(Cow::Owned(font_family.to_string())), + parley::FontFamily::Generic(parley::GenericFamily::SystemUi), + ]); let font_stack = parley::FontStack::List(font_stack_cow_list); Some(parley::StyleProperty::FontStack(font_stack)) } - TextStyleProperty::FontSize(font_size) => { - Some(parley::StyleProperty::FontSize(*font_size)) - } + TextStyleProperty::FontSize(font_size) => Some(parley::StyleProperty::FontSize(*font_size)), TextStyleProperty::Color(color) => { - let brush = ColorBrush { - color: *color, - }; + let brush = ColorBrush { color: *color }; Some(parley::StyleProperty::Brush(brush)) } @@ -214,25 +209,17 @@ impl TextStyleProperty { TextStyleProperty::FontWeight(font_weight) => { Some(parley::StyleProperty::FontWeight(parley::FontWeight::new(font_weight.0 as f32))) } - TextStyleProperty::Underline(underline) => { - Some(parley::StyleProperty::Underline(*underline)) - } - TextStyleProperty::UnderlineOffset(offset) => { - Some(parley::StyleProperty::UnderlineOffset(Some(*offset))) - } + TextStyleProperty::Underline(underline) => Some(parley::StyleProperty::Underline(*underline)), + TextStyleProperty::UnderlineOffset(offset) => Some(parley::StyleProperty::UnderlineOffset(Some(*offset))), - TextStyleProperty::UnderlineSize(size) => { - Some(parley::StyleProperty::UnderlineSize(Some(*size))) - } + TextStyleProperty::UnderlineSize(size) => Some(parley::StyleProperty::UnderlineSize(Some(*size))), TextStyleProperty::UnderlineBrush(color) => { - let brush = ColorBrush { - color: *color, - }; + let brush = ColorBrush { color: *color }; Some(parley::StyleProperty::UnderlineBrush(Some(brush))) } - TextStyleProperty::Link(_) | TextStyleProperty::BackgroundColor(_) => { None } + TextStyleProperty::Link(_) | TextStyleProperty::BackgroundColor(_) => None, } } } @@ -261,7 +248,7 @@ pub enum StyleProperty { FlexGrow(f32), FlexShrink(f32), FlexBasis(Unit), - + Color(Color), Background(Color), /// Defaults to the text color, if it is None. @@ -287,8 +274,7 @@ pub enum StyleProperty { Visible(bool), } -#[derive(Clone, Debug, Copy)] -#[derive(PartialEq)] +#[derive(Clone, Debug, Copy, PartialEq)] pub struct FontFamily { font_family_length: u8, font_family_name: [u8; 64], @@ -300,7 +286,7 @@ impl FontFamily { font_family_length: 0, font_family_name: [0; 64], }; - + let chars = font_family.chars().collect::>(); font_family_res.font_family_length = chars.len() as u8; font_family_res.font_family_name[..font_family.len()].copy_from_slice(font_family.as_bytes()); @@ -334,7 +320,7 @@ impl Default for FontFamily { pub struct Style { properties: SmallVec<[StyleProperty; 5]>, pub dirty_flags: StyleFlags, - pub animations: Option> + pub animations: Option>, } impl Default for Style { @@ -353,15 +339,12 @@ macro_rules! style_property { ) => { impl Style { pub fn $get(&self) -> $inner { - self.properties.iter().find_map(|p| { - if let StyleProperty::$variant(val) = p { - Some(*val) - } else { - None - } - }).unwrap_or($default) + self.properties + .iter() + .find_map(|p| if let StyleProperty::$variant(val) = p { Some(*val) } else { None }) + .unwrap_or($default) } - + pub fn $set(&mut self, val: $inner) { if self.dirty_flags.contains(StyleFlags::$flag) { self.remove_property(|p| matches!(p, StyleProperty::$variant(_))); @@ -409,43 +392,91 @@ style_property!(font_style, set_font_style, FontStyle, FontStyle, FONT_STYLE, Fo style_property!(underline, set_underline, Underline, Option, UNDERLINE, None); style_property!(overflow, set_overflow, Overflow, [Overflow; 2], OVERFLOW, [Overflow::default(); 2]); -style_property!(border_color, set_border_color, BorderColor, TrblRectangle, BORDER_COLOR, TrblRectangle::new_all(Color::BLACK)); -style_property!(border_width, set_border_width, BorderWidth, TrblRectangle, BORDER_WIDTH, TrblRectangle::new_all(Unit::Px(0.0))); +style_property!( + border_color, + set_border_color, + BorderColor, + TrblRectangle, + BORDER_COLOR, + TrblRectangle::new_all(Color::BLACK) +); +style_property!( + border_width, + set_border_width, + BorderWidth, + TrblRectangle, + BORDER_WIDTH, + TrblRectangle::new_all(Unit::Px(0.0)) +); style_property!(border_radius, set_border_radius, BorderRadius, [(f32, f32); 4], BORDER_RADIUS, [(0.0, 0.0); 4]); -style_property!(scrollbar_color, set_scrollbar_color, ScrollbarColor, ScrollbarColor, SCROLLBAR_COLOR, ScrollbarColor {thumb_color: Color::from_rgb8(150, 150, 152), track_color: Color::TRANSPARENT}); -const SCROLLBAR_THUMB_MARGIN: TrblRectangle = if cfg!(any(target_os = "android", target_os = "ios")) { TrblRectangle::new_all(0.0) } else { TrblRectangle::new(1.0, 2.0, 1.0, 2.0) }; -style_property!(scrollbar_thumb_margin, set_scrollbar_thumb_margin, ScrollbarThumbMargin, TrblRectangle, SCROLLBAR_THUMB_MARGIN, SCROLLBAR_THUMB_MARGIN); -style_property!(scrollbar_thumb_radius, set_scrollbar_thumb_radius, ScrollbarRadius, [(f32, f32); 4], SCROLLBAR_RADIUS, [(10.0, 10.0); 4]); -style_property!(scrollbar_width, set_scrollbar_width, ScrollbarWidth, f32, SCROLLBAR_WIDTH, if cfg!(any(target_os = "android", target_os = "ios")) { 0.0 } else { 10.0 }); +style_property!( + scrollbar_color, + set_scrollbar_color, + ScrollbarColor, + ScrollbarColor, + SCROLLBAR_COLOR, + ScrollbarColor { + thumb_color: Color::from_rgb8(150, 150, 152), + track_color: Color::TRANSPARENT + } +); +const SCROLLBAR_THUMB_MARGIN: TrblRectangle = if cfg!(any(target_os = "android", target_os = "ios")) { + TrblRectangle::new_all(0.0) +} else { + TrblRectangle::new(1.0, 2.0, 1.0, 2.0) +}; +style_property!( + scrollbar_thumb_margin, + set_scrollbar_thumb_margin, + ScrollbarThumbMargin, + TrblRectangle, + SCROLLBAR_THUMB_MARGIN, + SCROLLBAR_THUMB_MARGIN +); +style_property!( + scrollbar_thumb_radius, + set_scrollbar_thumb_radius, + ScrollbarRadius, + [(f32, f32); 4], + SCROLLBAR_RADIUS, + [(10.0, 10.0); 4] +); +style_property!( + scrollbar_width, + set_scrollbar_width, + ScrollbarWidth, + f32, + SCROLLBAR_WIDTH, + if cfg!(any(target_os = "android", target_os = "ios")) { 0.0 } else { 10.0 } +); style_property!(visible, set_visible, Visible, bool, VISIBLE, true); -style_property!(selection_color, set_selection_color, SelectionColor, Color, SELECTION_COLOR, Color::from_rgb8(0, 120, 215)); +style_property!( + selection_color, + set_selection_color, + SelectionColor, + Color, + SELECTION_COLOR, + Color::from_rgb8(0, 120, 215) +); style_property!(cursor_color, set_cursor_color, CursorColor, Option, CURSOR_COLOR, None); impl Style { - pub fn animation(&self, animation: String) -> Option<&Animation> { - if let Some(animations) = &self.animations { - for ani in animations { - if ani.name == animation { - return Some(ani); - } - } - } - - None + pub fn animation(&self, animation: &str) -> Option<&Animation> { + self.animations.as_ref().map(|ani| ani.iter().find(|ani| ani.name == animation)).unwrap_or_default() } pub fn set_animation(&mut self, animation: Animation) { if let Some(animations) = &mut self.animations { - animations.push(animation); + animations.push(animation); } else { let mut ani_vec = SmallVec::new(); ani_vec.push(animation); self.animations = Some(ani_vec); } } - + fn remove_property(&mut self, f: impl Fn(&StyleProperty) -> bool) { if let Some(pos) = self.properties.iter().position(f) { self.properties.remove(pos); @@ -647,5 +678,4 @@ impl Style { style_set.insert(parley::StyleProperty::UnderlineOffset(underline_offset)); style_set.insert(parley::StyleProperty::UnderlineSize(underline_size)); } - } diff --git a/examples/animations/main.rs b/examples/animations/main.rs index 5c7ee383..b40b240b 100644 --- a/examples/animations/main.rs +++ b/examples/animations/main.rs @@ -1,14 +1,18 @@ -use craft::animation::animation::{Animation, KeyFrame, LoopAmount, TimingFunction}; +use craft::animations::{Animation, KeyFrame, LoopAmount, TimingFunction}; use craft::components::Context; use craft::elements::Text; use craft::geometry::TrblRectangle; use craft::style::{Position, StyleProperty, Unit}; -use craft::{components::{Component, ComponentSpecification}, elements::{Container, ElementStyles}, palette, style::{Display, FlexDirection}}; +use craft::{ + components::{Component, ComponentSpecification}, + elements::{Container, ElementStyles}, + palette, + style::{Display, FlexDirection}, +}; use std::time::Duration; #[derive(Default)] -pub struct AnimationsExample { -} +pub struct AnimationsExample {} impl Component for AnimationsExample { type GlobalState = (); @@ -16,8 +20,7 @@ impl Component for AnimationsExample { type Message = (); fn view(_context: &mut Context) -> ComponentSpecification { - - let growing_animation = Animation::new("growing_animation".to_string(), Duration::from_secs(5), TimingFunction::EaseOut) + let growing_animation = Animation::new("growing_animation", Duration::from_secs(5), TimingFunction::EaseOut) .push( KeyFrame::new(0.0) .push(StyleProperty::Background(palette::css::GREEN)) @@ -28,45 +31,43 @@ impl Component for AnimationsExample { KeyFrame::new(100.0) .push(StyleProperty::Background(palette::css::RED)) .push(StyleProperty::Width(Unit::Percentage(80.0))) - .push(StyleProperty::Height(Unit::Px(100.0))) + .push(StyleProperty::Height(Unit::Px(100.0))), ) - .loop_amount(LoopAmount::Fixed(3)) - ; + .loop_amount(LoopAmount::Fixed(3)); - let moving_animation = Animation::new("moving_animation".to_string(), Duration::from_secs(5), TimingFunction::Ease) - .push( - KeyFrame::new(0.0) - .push(StyleProperty::Background(palette::css::BLUE)) - .push(StyleProperty::Inset(TrblRectangle::new(Unit::Px(100.0), Unit::Px(0.0), Unit::Px(0.0), Unit::Px(0.0)))) - ) - .push( - KeyFrame::new(50.0) - .push(StyleProperty::Background(palette::css::MAGENTA)) - .push(StyleProperty::Inset(TrblRectangle::new(Unit::Px(150.0), Unit::Px(0.0), Unit::Px(0.0), Unit::Px(250.0)))) - ) + let moving_animation = Animation::new("moving_animation", Duration::from_secs(5), TimingFunction::Ease) + .push(KeyFrame::new(0.0).push(StyleProperty::Background(palette::css::BLUE)).push(StyleProperty::Inset( + TrblRectangle::new(Unit::Px(100.0), Unit::Px(0.0), Unit::Px(0.0), Unit::Px(0.0)), + ))) + .push(KeyFrame::new(50.0).push(StyleProperty::Background(palette::css::MAGENTA)).push( + StyleProperty::Inset(TrblRectangle::new( + Unit::Px(150.0), + Unit::Px(0.0), + Unit::Px(0.0), + Unit::Px(250.0), + )), + )) .push( KeyFrame::new(100.0) .push(StyleProperty::Background(palette::css::YELLOW)) - .push(StyleProperty::Inset(TrblRectangle::new(100.into(), 0.into(), 0.into(), 0.into()))) + .push(StyleProperty::Inset(TrblRectangle::new(100.into(), 0.into(), 0.into(), 0.into()))), ) - .loop_amount(LoopAmount::Infinite) - ; + .loop_amount(LoopAmount::Infinite); - let text_animation = Animation::new("text_animation".to_string(), Duration::from_secs(5), TimingFunction::Ease) + let text_animation = Animation::new("text_animation", Duration::from_secs(5), TimingFunction::Ease) .push( KeyFrame::new(0.0) .push(StyleProperty::Background(palette::css::RED)) .push(StyleProperty::Color(palette::css::BLUE)) - .push(StyleProperty::FontSize(20.0)) + .push(StyleProperty::FontSize(20.0)), ) .push( KeyFrame::new(100.0) .push(StyleProperty::Background(palette::css::YELLOW)) .push(StyleProperty::Color(palette::css::BLUE_VIOLET)) - .push(StyleProperty::FontSize(40.0)) + .push(StyleProperty::FontSize(40.0)), ) - .loop_amount(LoopAmount::Infinite) - ; + .loop_amount(LoopAmount::Infinite); let animation_examples: Vec = vec![ Container::new() @@ -75,35 +76,28 @@ impl Component for AnimationsExample { .height("40px") .push_animation(growing_animation) .component(), - Container::new() - .push( - Container::new() - .inset(Unit::Px(100.0), Unit::Px(0.0), Unit::Px(0.0), Unit::Px(0.0)) - .background(palette::css::BLUE) - .position(Position::Absolute) - .width("40px") - .height("40px") - .push_animation(moving_animation) - ).component(), - - Text::new("Why, Hello!") - .push_animation(text_animation) - .component() + .push( + Container::new() + .inset(Unit::Px(100.0), Unit::Px(0.0), Unit::Px(0.0), Unit::Px(0.0)) + .background(palette::css::BLUE) + .position(Position::Absolute) + .width("40px") + .height("40px") + .push_animation(moving_animation), + ) + .component(), + Text::new("Why, Hello!").push_animation(text_animation).component(), ]; - let mut container = Container::new() .display(Display::Flex) .flex_direction(FlexDirection::Column) .width("100%") .height("100%") - .gap(20) - ; + .gap(20); - for ani in animation_examples { - container = container.push(ani) - } + container.extend_children_in_place(animation_examples); container.component() } From e8a258c1bd0533cf21785ef62bced35ca687c130 Mon Sep 17 00:00:00 2001 From: "Austin M. Reppert" Date: Wed, 9 Jul 2025 19:25:17 -0400 Subject: [PATCH 18/18] Update crates/craft_core/src/elements/element_states.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- crates/craft_core/src/elements/element_states.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/craft_core/src/elements/element_states.rs b/crates/craft_core/src/elements/element_states.rs index fe56d7bd..8e9e4cb1 100644 --- a/crates/craft_core/src/elements/element_states.rs +++ b/crates/craft_core/src/elements/element_states.rs @@ -1,5 +1,4 @@ -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] -#[derive(Hash)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ElementState { #[default] Normal,