mod frames; pub(crate) use frames::{ContentMeasurement, frame_driven, geometry_driven}; use cosmic::iced::advanced::{ Clipboard, Layout, Renderer as _, Shell, Widget, layout, mouse, renderer, widget, }; use cosmic::iced::{Event, Length, Rectangle, Size, Transformation, Vector}; use cosmic::{Element, Theme}; pub type Regions = std::sync::Arc>>; pub(crate) use ferese_animation::MotionSettings as Settings; use ferese_animation::{AnimatedValue, SpringConfig}; pub(crate) struct PopupMotion { value: AnimatedValue, started: Option, settings: Settings, } impl PopupMotion { pub(crate) fn update_settings(&mut self, settings: Settings) { self.update_settings_at(settings, std::time::Instant::now()); } fn update_settings_at(&mut self, settings: Settings, now: std::time::Instant) { self.value = self.sample(now); // Stored velocities use simulation seconds. Preserve visible velocity // when changing the clock rate during an in-flight transition. self.value.velocity *= self.settings.speed / settings.speed; self.settings = settings; if self.started.is_some() { self.started = Some(now); } } pub(crate) fn new(settings: Settings) -> Self { Self { value: AnimatedValue { current: 0.0, target: 1.0, velocity: 0.0, }, started: None, settings, } } pub(crate) fn begin(&mut self, now: std::time::Instant) { if self.started.is_none() { self.started = Some(now); } } fn sample(&self, now: std::time::Instant) -> AnimatedValue { let mut value = self.value; let Some(started) = self.started else { return value; }; if !self.settings.motion_enabled() { value.snap(); return value; } let spring = self .settings .spring_config() .expect("validated motion settings"); value.advance( now.saturating_duration_since(started) .mul_f64(self.settings.speed), SpringConfig { position_tolerance: 0.00001, velocity_tolerance: 0.00001, ..spring }, ); value } pub(crate) fn progress_at(&self, now: std::time::Instant) -> f32 { self.sample(now).current as f32 } #[cfg(test)] pub(crate) fn progress(&self) -> f32 { self.progress_at(std::time::Instant::now()) } pub(crate) fn closing(&self) -> bool { self.value.target == 0.0 } pub(crate) fn revision(&self) -> Option { self.started } pub(crate) fn frame_active(&self, now: std::time::Instant) -> bool { self.started.is_some() && self.animating_at(now) } pub(crate) fn animating(&self) -> bool { self.animating_at(std::time::Instant::now()) } pub(crate) fn animating_at(&self, now: std::time::Instant) -> bool { let value = self.sample(now); value.current != value.target || value.velocity != 0.0 } pub(crate) fn retarget(&mut self, target: f32, now: std::time::Instant) { self.value = self.sample(now); self.value.set_target(f64::from(target)); self.started = Some(now); } } /// Animate paint only; the original button retains its layout, hit target, /// keyboard handling, pressed state, and accessibility identity. pub(crate) fn button<'a, M: Clone + 'a>( render: crate::rendering::Rendering, button: cosmic::widget::button::Button<'a, M>, foreground: cosmic::iced::Color, selected: bool, opacity: f32, ) -> Element<'a, M> { let paint = move |progress: f32, pressed: bool| { ferese_theme::controls::shell_button( foreground, selected, opacity, render.radius(14.), progress, pressed, ) }; painted_button(render, button, paint) } pub(crate) fn circular_button<'a, M: Clone + 'a>( render: crate::rendering::Rendering, button: cosmic::widget::button::Button<'a, M>, foreground: cosmic::iced::Color, fill: Option, opacity: f32, diameter: f32, ) -> Element<'a, M> { let paint = move |progress: f32, pressed: bool| { let mut style = ferese_theme::controls::shell_button( foreground, false, opacity, diameter / 2.0, progress, pressed, ); style.shape = Some(cosmic::iced::border::Shape::Circular); if let Some(fill) = fill { style.background = Some( cosmic::iced::Color { a: opacity * if pressed { 0.75 } else { 1.0 - 0.1 * progress }, ..fill } .into(), ); } style }; painted_button(render, button, paint) } fn painted_button<'a, M: Clone + 'a>( render: crate::rendering::Rendering, button: cosmic::widget::button::Button<'a, M>, paint: impl Fn(f32, bool) -> cosmic::widget::button::Style + Copy + 'static, ) -> Element<'a, M> { let progress = std::rc::Rc::new(std::cell::Cell::new(0.0)); let active = progress.clone(); let hovered = progress.clone(); let content = button .class(cosmic::theme::Button::Custom { active: Box::new(move |_, _| paint(active.get(), false)), hovered: Box::new(move |_, _| paint(hovered.get(), false)), pressed: Box::new(move |_, _| paint(1.0, true)), disabled: Box::new(move |_| paint(0.0, false)), }) .into(); Element::new(Hover { content, progress, duration: render.motion.duration(120.0), }) } #[derive(Default)] struct HoverState { current: f32, start: f32, target: f32, started: Option, } impl HoverState { fn advance( &mut self, target: f32, now: std::time::Instant, duration: std::time::Duration, ) -> bool { if self.target != target { self.start = self.current; self.target = target; self.started = Some(now); } let t = if duration.is_zero() { 1.0 } else { self.started.map_or(1.0, |start| { (now.saturating_duration_since(start).as_secs_f32() / duration.as_secs_f32()) .min(1.0) }) }; self.current = self.start + (self.target - self.start) * t * t * (3.0 - 2.0 * t); if t == 1.0 { self.current = target; self.started = None; } self.current != target } } struct Hover<'a, M> { content: Element<'a, M>, progress: std::rc::Rc>, duration: std::time::Duration, } impl Widget for Hover<'_, M> { fn a11y_nodes( &self, layout: Layout<'_>, tree: &widget::Tree, cursor: mouse::Cursor, ) -> iced_accessibility::A11yTree { self.content .as_widget() .a11y_nodes(layout, &tree.children[0], cursor) } fn tag(&self) -> widget::tree::Tag { widget::tree::Tag::of::() } fn state(&self) -> widget::tree::State { widget::tree::State::new(HoverState::default()) } fn children(&self) -> Vec { vec![widget::Tree::new(&self.content)] } fn diff(&mut self, tree: &mut widget::Tree) { tree.diff_children(std::slice::from_mut(&mut self.content)); } fn size(&self) -> Size { self.content.as_widget().size() } fn layout( &mut self, tree: &mut widget::Tree, renderer: &cosmic::Renderer, limits: &layout::Limits, ) -> layout::Node { self.content .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) } fn update( &mut self, tree: &mut widget::Tree, event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &cosmic::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, M>, viewport: &Rectangle, ) { let now = match event { Event::Window(cosmic::iced::window::Event::RedrawRequested(at)) => *at, _ => std::time::Instant::now(), }; let hovered = cursor.is_over(layout.bounds()) && cursor.is_over(*viewport); let state = tree.state.downcast_mut::(); if state.advance(if hovered { 1.0 } else { 0.0 }, now, self.duration) { shell.request_redraw(); } self.progress.set(state.current); self.content.as_widget_mut().update( &mut tree.children[0], event, layout, cursor, renderer, clipboard, shell, viewport, ); } fn draw( &self, tree: &widget::Tree, renderer: &mut cosmic::Renderer, theme: &Theme, style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, ) { self.progress .set(tree.state.downcast_ref::().current); self.content.as_widget().draw( &tree.children[0], renderer, theme, style, layout, cursor, viewport, ); } fn mouse_interaction( &self, tree: &widget::Tree, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, renderer: &cosmic::Renderer, ) -> mouse::Interaction { self.content.as_widget().mouse_interaction( &tree.children[0], layout, cursor, viewport, renderer, ) } fn operate( &mut self, tree: &mut widget::Tree, layout: Layout<'_>, renderer: &cosmic::Renderer, operation: &mut dyn widget::Operation, ) { self.content .as_widget_mut() .operate(&mut tree.children[0], layout, renderer, operation); } fn overlay<'a>( &'a mut self, tree: &'a mut widget::Tree, layout: Layout<'a>, renderer: &cosmic::Renderer, viewport: &Rectangle, translation: Vector, ) -> Option> { self.content.as_widget_mut().overlay( &mut tree.children[0], layout, renderer, viewport, translation, ) } } pub fn animated<'a, M: 'a>( content: Element<'a, M>, progress: f32, regions: Regions, radius: f32, ) -> Element<'a, M> { animated_from_edge(content, progress, regions, radius, ferese_panel::Edge::Top) } pub fn animated_from_edge<'a, M: 'a>( content: Element<'a, M>, progress: f32, regions: Regions, radius: f32, edge: ferese_panel::Edge, ) -> Element<'a, M> { Element::new(Motion { content, progress, regions: Some(regions), radius, scale: 1., direction: if edge == ferese_panel::Edge::Top { -1. } else { 1. }, }) } pub fn scaled<'a, M: 'a>(content: Element<'a, M>, scale: f32, regions: Regions) -> Element<'a, M> { Element::new(Motion { content, progress: 1., regions: Some(regions), radius: 28., direction: 0., scale, }) } struct Motion<'a, M> { content: Element<'a, M>, progress: f32, regions: Option, radius: f32, direction: f32, scale: f32, } impl Motion<'_, M> { fn translation(&self) -> Vector { Vector::new(0.0, self.direction * 4.0 * (1.0 - self.progress)) } fn cursor(&self, layout: Layout<'_>, cursor: mouse::Cursor) -> mouse::Cursor { cursor.position().map_or(mouse::Cursor::Unavailable, |p| { mouse::Cursor::Available( layout.bounds().center() + (p - layout.bounds().center() - self.translation()) / self.scale, ) }) } } impl Widget for Motion<'_, M> { fn a11y_nodes( &self, layout: Layout<'_>, tree: &widget::Tree, cursor: mouse::Cursor, ) -> iced_accessibility::A11yTree { let mut nodes = self.content .as_widget() .a11y_nodes(layout, tree, self.cursor(layout, cursor)); let offset = self.translation(); for node in nodes.root_mut() { transform_accessible_node(node, offset, layout.bounds().center(), self.scale); } for node in nodes.children_mut() { transform_accessible_node(node, offset, layout.bounds().center(), self.scale); } nodes } fn tag(&self) -> widget::tree::Tag { self.content.as_widget().tag() } fn state(&self) -> widget::tree::State { self.content.as_widget().state() } fn children(&self) -> Vec { self.content.as_widget().children() } fn diff(&mut self, tree: &mut widget::Tree) { self.content.as_widget_mut().diff(tree); } fn size(&self) -> Size { self.content.as_widget().size() } fn layout( &mut self, tree: &mut widget::Tree, renderer: &cosmic::Renderer, limits: &layout::Limits, ) -> layout::Node { self.content.as_widget_mut().layout(tree, renderer, limits) } fn update( &mut self, tree: &mut widget::Tree, event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &cosmic::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, M>, viewport: &Rectangle, ) { if let Some(regions) = &self.regions { let mut collected = regions.lock().unwrap(); collected.clear(); self.content.as_widget_mut().operate( tree, layout, renderer, &mut ReuseRegions(&mut collected), ); if collected.is_empty() && let Some(content) = layout.children().next() { let bounds = content.bounds(); collected.push([bounds.x, bounds.y, bounds.width, bounds.height, 0.]); } transform_regions( &mut collected, layout.bounds().center(), self.scale, self.translation(), self.radius, ); } let cursor = self.cursor(layout, cursor); self.content.as_widget_mut().update( tree, event, layout, cursor, renderer, clipboard, shell, viewport, ); } fn draw( &self, tree: &widget::Tree, renderer: &mut cosmic::Renderer, theme: &Theme, style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, ) { let v = self.translation(); let center = layout.bounds().center(); let transform = Transformation::translate(center.x + v.x, center.y + v.y) * Transformation::scale(self.scale) * Transformation::translate(-center.x, -center.y); // Layout/region collection still run before the surface starts opening. // No child may paint an opaque control before the presentation begins. if self.progress <= 0.0 { return; } renderer.with_transformation(transform, |renderer| { self.content.as_widget().draw( tree, renderer, theme, style, layout, self.cursor(layout, cursor), viewport, ) }); } fn mouse_interaction( &self, tree: &widget::Tree, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, renderer: &cosmic::Renderer, ) -> mouse::Interaction { self.content.as_widget().mouse_interaction( tree, layout, self.cursor(layout, cursor), viewport, renderer, ) } fn operate( &mut self, tree: &mut widget::Tree, layout: Layout<'_>, renderer: &cosmic::Renderer, operation: &mut dyn widget::Operation, ) { self.content .as_widget_mut() .operate(tree, layout, renderer, operation); } fn overlay<'a>( &'a mut self, tree: &'a mut widget::Tree, layout: Layout<'a>, renderer: &cosmic::Renderer, viewport: &Rectangle, translation: Vector, ) -> Option> { let translation = translation + self.translation(); self.content .as_widget_mut() .overlay(tree, layout, renderer, viewport, translation) } } fn transform_accessible_node( node: &mut iced_accessibility::A11yNode, offset: Vector, center: cosmic::iced::Point, scale: f32, ) { if let Some(mut bounds) = node.node().bounds() { let x = f64::from(center.x); let y = f64::from(center.y); bounds.x0 = x + (bounds.x0 - x) * f64::from(scale) + f64::from(offset.x); bounds.x1 = x + (bounds.x1 - x) * f64::from(scale) + f64::from(offset.x); bounds.y0 = y + (bounds.y0 - y) * f64::from(scale) + f64::from(offset.y); bounds.y1 = y + (bounds.y1 - y) * f64::from(scale) + f64::from(offset.y); node.node_mut().set_bounds(bounds); } } fn transform_regions( regions: &mut [[f32; 5]], center: cosmic::iced::Point, scale: f32, offset: Vector, radius: f32, ) { for region in regions { region[0] = center.x + (region[0] - center.x) * scale + offset.x; region[1] = center.y + (region[1] - center.y) * scale + offset.y; region[2] *= scale; region[3] *= scale; region[4] = radius.max(0.) * scale; } } struct ReuseRegions<'a>(&'a mut Vec<[f32; 5]>); impl widget::Operation for ReuseRegions<'_> { fn traverse(&mut self, children: &mut dyn FnMut(&mut dyn widget::Operation)) { children(self); } fn container(&mut self, id: Option<&widget::Id>, bounds: Rectangle) { if id != Some(&widget::Id::new("ferese-blur-card")) || bounds.width <= 0. || bounds.height <= 0. { return; } let r = [bounds.x, bounds.y, bounds.width, bounds.height, 0.]; let contains = |outer: &[f32; 5], inner: &[f32; 5]| { outer[0] <= inner[0] && outer[1] <= inner[1] && outer[0] + outer[2] >= inner[0] + inner[2] && outer[1] + outer[3] >= inner[1] + inner[3] }; if self.0.iter().any(|outer| contains(outer, &r)) { return; } self.0.retain(|inner| !contains(&r, inner)); if self.0.len() < 32 { self.0.push(r); } } } #[cfg(test)] mod tests { #[test] fn motion_preserves_accessible_controls_and_translates_tooltip_overlays() { use super::*; use cosmic::iced::advanced::clipboard; use cosmic::iced::advanced::renderer::Headless; use cosmic::iced::{Font, Pixels, Point}; let renderer = cosmic::iced::futures::executor::block_on(cosmic::Renderer::new( Font::default(), Pixels(14.), Some("tiny-skia"), )) .unwrap(); let viewport = Rectangle::with_size(Size::new(400., 300.)); for edge in [ferese_panel::Edge::Top, ferese_panel::Edge::Bottom] { let make_content = || { // The toolkit tooltip itself does not expose an accessibility // tree. A sibling button verifies our wrapper's forwarding. cosmic::widget::row::with_capacity(2) .push(cosmic::widget::tooltip( cosmic::widget::Space::new().width(100).height(40), cosmic::widget::text("Tooltip"), cosmic::widget::tooltip::Position::Bottom, )) .push( cosmic::widget::button::custom(cosmic::widget::text("Action")) .name("Accessible action") .on_press(()) .width(100) .height(40), ) }; let mut plain: Element<'_, ()> = make_content().into(); let mut plain_tree = widget::Tree::new(&plain); let plain_node = plain.as_widget_mut().layout( &mut plain_tree, &renderer, &layout::Limits::new(Size::ZERO, viewport.size()), ); let plain_a11y = plain.as_widget().a11y_nodes( Layout::new(&plain_node), &plain_tree, mouse::Cursor::Unavailable, ); assert!(!plain_a11y.root().is_empty()); let mut wrapped = animated_from_edge(make_content().into(), 0.5, Default::default(), 10., edge); let mut tree = widget::Tree::new(&wrapped); let node = wrapped.as_widget_mut().layout( &mut tree, &renderer, &layout::Limits::new(Size::ZERO, viewport.size()), ); let offset = if edge == ferese_panel::Edge::Top { -2. } else { 2. }; let accessible = wrapped.as_widget().a11y_nodes( Layout::new(&node), &tree, mouse::Cursor::Unavailable, ); assert_eq!(accessible.root().len(), plain_a11y.root().len()); for (actual, expected) in accessible .root() .iter() .chain(accessible.children()) .zip(plain_a11y.root().iter().chain(plain_a11y.children())) { assert_eq!(actual.node().label(), expected.node().label()); if let Some(bounds) = expected.node().bounds() { let actual = actual.node().bounds().unwrap(); assert_eq!(actual.x0, bounds.x0); assert_eq!(actual.y0, bounds.y0 + offset); } } let cursor = mouse::Cursor::Available(Point::new(50., 20.)); let event = Event::Mouse(mouse::Event::CursorMoved { position: Point::new(50., 20.), }); let mut messages = Vec::new(); plain.as_widget_mut().update( &mut plain_tree, &event, Layout::new(&plain_node), cursor, &renderer, &mut clipboard::Null, &mut Shell::new(&mut messages), &viewport, ); wrapped.as_widget_mut().update( &mut tree, &event, Layout::new(&node), cursor, &renderer, &mut clipboard::Null, &mut Shell::new(&mut messages), &viewport, ); let mut plain_overlay = plain .as_widget_mut() .overlay( &mut plain_tree, Layout::new(&plain_node), &renderer, &viewport, Vector::new(40., 80.), ) .expect("tooltip opens"); let mut overlay = wrapped .as_widget_mut() .overlay( &mut tree, Layout::new(&node), &renderer, &viewport, Vector::new(40., 80.), ) .expect("motion forwards tooltip"); let plain_layout = plain_overlay .as_overlay_mut() .layout(&renderer, viewport.size()); let overlay_layout = overlay.as_overlay_mut().layout(&renderer, viewport.size()); fn tooltip_bounds(node: &layout::Node) -> Rectangle { // Tooltip and row each return an overlay group; descend past // full-viewport group nodes to the positioned tooltip root. let mut node = node; while let [child] = node.children() { if node.bounds().position() != cosmic::iced::Point::ORIGIN { break; } node = child; } node.bounds() } let plain_bounds = tooltip_bounds(&plain_layout); let bounds = tooltip_bounds(&overlay_layout); assert!(plain_bounds.y > 0.); assert_eq!(bounds.x, plain_bounds.x); assert_eq!(bounds.y, plain_bounds.y + offset as f32); } } #[test] fn duration_bounce_spring_finishes_popup_close() { let settings = super::Settings { spring: ferese_animation::SpringSettings { duration_ms: Some(240.0), bounce: 0.3, overshoot: true, }, ..Default::default() }; settings.validate().unwrap(); let now = std::time::Instant::now(); let mut motion = super::PopupMotion::new(settings); motion.begin(now); let opened = now + std::time::Duration::from_secs(1); assert_eq!(motion.sample(opened).current, 1.0); motion.retarget(0.0, opened); let closed = motion.sample(opened + std::time::Duration::from_secs(1)); assert!(motion.closing()); assert_eq!(closed.current, closed.target); assert_eq!(closed.velocity, 0.0); } use super::*; #[test] fn material_regions_preserve_fractional_layout_and_translation() { let mut regions = vec![[0.25, 2.5, 100.25, 40.5, 0.]]; transform_regions( &mut regions, cosmic::iced::Point::ORIGIN, 1., Vector::new(0.125, -0.25), 13.5, ); assert_eq!(regions, vec![[0.375, 2.25, 100.25, 40.5, 13.5]]); } #[test] fn centered_modal_material_tracks_surface_position_and_animation() { let mut regions = Vec::new(); let mut collector = ReuseRegions(&mut regions); let card = Rectangle::new((380.0, 420.0).into(), (440.0, 220.0).into()); widget::Operation::container( &mut collector, Some(&widget::Id::new("ferese-blur-card")), card, ); transform_regions( &mut regions, cosmic::iced::Point::ORIGIN, 1., Vector::new(0.0, -2.0), 14.0, ); assert_eq!(regions, vec![[380., 418., 440., 220., 14.]]); } #[test] fn popup_waits_for_configuration_and_preserves_reversal_velocity() { let now = std::time::Instant::now(); let mut motion = PopupMotion::new(Settings::default()); assert_eq!( motion.progress_at(now + std::time::Duration::from_secs(30)), 0.0 ); motion.begin(now); let reverse = now + std::time::Duration::from_millis(90); let before = motion.sample(reverse); motion.begin(reverse); assert_eq!(motion.sample(reverse), before); motion.retarget(0.0, reverse); let after = motion.sample(reverse); assert_eq!(after.current, before.current); assert_eq!(after.velocity, before.velocity); assert!(motion.closing()); assert_eq!( motion.progress_at(reverse + std::time::Duration::from_secs(2)), 0.0 ); } #[test] fn live_speed_change_preserves_visible_velocity_and_reduced_motion_snaps() { let now = std::time::Instant::now(); let mut motion = PopupMotion::new(Settings::default()); motion.begin(now); let time = now + std::time::Duration::from_millis(90); let before = motion.sample(time); motion.update_settings_at( Settings { speed: 2.0, ..Settings::default() }, time, ); let after = motion.sample(time); assert_eq!(after.current, before.current); assert_eq!(after.velocity * 2.0, before.velocity); motion.update_settings_at( Settings { reduced_motion: true, ..Settings::default() }, time, ); assert_eq!(motion.progress_at(time), 1.0); } #[test] fn popup_reversal_and_motion_policy_are_consistent() { let now = std::time::Instant::now(); let mut motion = PopupMotion::new(Settings { speed: 0.75, ..Settings::default() }); motion.begin(now); let reverse = now + std::time::Duration::from_millis(120); let before = motion.progress_at(reverse); motion.retarget(0.0, reverse); assert_eq!(motion.progress_at(reverse), before); let reopen = reverse + std::time::Duration::from_millis(60); let before = motion.progress_at(reopen); motion.retarget(1.0, reopen); assert_eq!(motion.progress_at(reopen), before); assert_eq!( motion.progress_at(reopen + std::time::Duration::from_secs(2)), 1.0 ); for settings in [ Settings { reduced_motion: true, ..Settings::default() }, Settings { enabled: false, ..Settings::default() }, ] { let mut motion = PopupMotion::new(settings); assert_eq!(motion.progress_at(now), 0.0); motion.begin(now); assert_eq!(motion.progress_at(now), 1.0); motion.retarget(0.0, now); assert_eq!(motion.progress_at(now), 0.0); } } #[test] fn updating_motion_policy_preserves_surface_readiness() { let mut motion = PopupMotion::new(Settings::default()); motion.update_settings(Settings { reduced_motion: true, ..Settings::default() }); assert_eq!(motion.progress(), 0.0); motion.begin(std::time::Instant::now()); assert_eq!(motion.progress(), 1.0); } #[test] fn hover_fades_settle_and_idle_does_not_schedule_frames() { let now = std::time::Instant::now(); let duration = std::time::Duration::from_millis(120); let mut hover = HoverState::default(); assert!(!hover.advance(0.0, now, duration)); assert!(hover.advance(1.0, now + std::time::Duration::from_secs(10), duration)); assert_eq!(hover.current, 0.0); let start = now + std::time::Duration::from_secs(10); assert!(hover.advance(1.0, start + duration / 2, duration)); assert!((hover.current - 0.5).abs() < 0.00001); assert!(!hover.advance(1.0, start + duration, duration)); assert_eq!(hover.current, 1.0); assert!(!hover.advance(1.0, start + duration * 10, duration)); assert!(hover.advance(0.0, start + duration * 11, duration)); assert!(!hover.advance(0.0, start + duration * 12, duration)); assert_eq!(hover.current, 0.0); } #[test] fn hover_reversals_preserve_current_paint() { let now = std::time::Instant::now(); let duration = std::time::Duration::from_millis(120); let mut hover = HoverState::default(); hover.advance(1.0, now, duration); hover.advance(1.0, now + duration / 2, duration); let before = hover.current; assert!(hover.advance(0.0, now + duration / 2, duration)); assert_eq!(hover.current, before); hover.advance(0.0, now + duration, duration); assert!(hover.current > 0.0 && hover.current < before); } #[test] fn hover_obeys_global_motion_policy() { assert_eq!( Settings { speed: 0.75, ..Settings::default() } .duration(120.0), std::time::Duration::from_millis(160) ); for settings in [ Settings { enabled: false, ..Settings::default() }, Settings { reduced_motion: true, ..Settings::default() }, ] { assert!(settings.duration(120.0).is_zero()); let mut hover = HoverState::default(); assert!(!hover.advance(1.0, std::time::Instant::now(), settings.duration(120.0))); assert_eq!(hover.current, 1.0); } } #[test] fn material_regions_include_cards_but_not_the_parent_or_gaps() { let mut regions = Vec::new(); let mut collector = ReuseRegions(&mut regions); let id = widget::Id::new("ferese-blur-card"); widget::Operation::container( &mut collector, None, Rectangle::new((0.0, 0.0).into(), (320.0, 200.0).into()), ); let first = Rectangle::new((12.0, 12.0).into(), (140.0, 80.0).into()); let second = Rectangle::new((160.0, 12.0).into(), (140.0, 80.0).into()); widget::Operation::container(&mut collector, Some(&id), first); widget::Operation::container(&mut collector, Some(&id), second); assert_eq!( *collector.0, vec![[12., 12., 140., 80., 0.], [160., 12., 140., 80., 0.]] ); widget::Operation::container( &mut collector, Some(&id), Rectangle::new((20.0, 20.0).into(), (40.0, 40.0).into()), ); assert_eq!( *collector.0, vec![[12., 12., 140., 80., 0.], [160., 12., 140., 80., 0.]] ); } }