use crate::core::{Color, HorizontalAlignment, Point, Rect, Size};
use crate::event::{Event, EventHandler};
use crate::impl_widget_property_hooks;
use crate::property_names_of;
use crate::render::{PaintBackend, RenderContext, SoftwarePaintBackend};
use crate::signal::GenericSignal;
use crate::widget::capability::coercion::expect_f32;
use crate::widget::capability::properties_trait::{base_property_get, base_property_set};
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
use crate::widget::capability::WidgetProperties;
use crate::widget::metrics::effective_font;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
use super::pixels::nearest_scale;
struct EndpointSnapshot {
width: u32,
height: u32,
data: Vec<u8>,
}
struct ScaledSnapshot {
width: u32,
height: u32,
data: Vec<u8>,
}
pub struct HeroAnimation {
base: BaseWidget,
source_widget: Option<Box<dyn Widget>>,
target_widget: Option<Box<dyn Widget>>,
animation_progress: f32,
is_animating: bool,
duration_ms: u64,
elapsed_ms: u64,
easing: Option<crate::style::EasingFunction>,
source_rect: Option<Rect>,
target_rect: Option<Rect>,
pub animation_completed: GenericSignal,
source_tint: Color,
target_tint: Color,
progress_label: String,
source_snapshot: Option<EndpointSnapshot>,
target_snapshot: Option<EndpointSnapshot>,
scaled_source: Option<ScaledSnapshot>,
scaled_target: Option<ScaledSnapshot>,
playing_reverse: bool,
reverse_start: f32,
}
impl HeroAnimation {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::HeroAnimation, geometry, "HeroAnimation"),
source_widget: None,
target_widget: None,
animation_progress: 0.0,
is_animating: false,
duration_ms: 300,
elapsed_ms: 0,
easing: None,
source_rect: None,
target_rect: None,
animation_completed: GenericSignal::new(),
source_tint: Color::rgb(33, 118, 210),
target_tint: Color::rgb(76, 175, 80),
progress_label: "Progress: 0%".to_string(),
source_snapshot: None,
target_snapshot: None,
scaled_source: None,
scaled_target: None,
playing_reverse: false,
reverse_start: 0.0,
}
}
pub fn start_animation(&mut self) {
if self.source_widget.is_none() || self.target_widget.is_none() {
return;
}
self.animation_progress = 0.0;
self.elapsed_ms = 0;
self.is_animating = true;
self.playing_reverse = false;
self.progress_label = "Progress: 0%".to_string();
self.refresh_snapshots();
self.base.request_redraw();
}
pub fn play_reverse(&mut self) {
if self.source_widget.is_none() || self.target_widget.is_none() {
return;
}
if self.animation_progress <= 0.0 {
return;
}
self.reverse_start = self.animation_progress;
self.elapsed_ms = 0;
self.playing_reverse = true;
self.is_animating = true;
self.base.request_redraw();
}
pub fn cancel(&mut self) {
self.playing_reverse = false;
self.stop_animation();
}
pub fn reset(&mut self) {
self.is_animating = false;
self.playing_reverse = false;
self.elapsed_ms = 0;
self.animation_progress = 0.0;
self.progress_label = "Progress: 0%".to_string();
self.base.request_redraw();
}
pub fn refresh_snapshots(&mut self) {
self.source_snapshot = self.source_widget.as_mut().and_then(capture_endpoint);
self.target_snapshot = self.target_widget.as_mut().and_then(capture_endpoint);
self.scaled_source = None;
self.scaled_target = None;
}
pub fn source_tint(&self) -> Color {
self.source_tint
}
pub fn target_tint(&self) -> Color {
self.target_tint
}
pub fn set_tints(&mut self, source: Color, target: Color) {
if self.source_tint == source && self.target_tint == target {
return;
}
self.source_tint = source;
self.target_tint = target;
self.base.request_redraw();
}
pub fn stop_animation(&mut self) {
self.is_animating = false;
self.base.request_redraw();
}
pub fn set_progress(&mut self, progress: f32) {
let clamped = progress.clamp(0.0, 1.0);
if (self.animation_progress - clamped).abs() > 0.001 {
self.animation_progress = clamped;
self.progress_label = format!("Progress: {:.0}%", clamped * 100.0);
if (clamped - 1.0).abs() < 0.001 {
self.is_animating = false;
self.animation_completed.emit();
}
self.base.request_redraw();
}
}
pub fn progress(&self) -> f32 {
self.animation_progress
}
pub fn is_animating(&self) -> bool {
self.is_animating
}
pub fn set_source(&mut self, widget: Box<dyn Widget>) {
self.source_widget = Some(widget);
self.source_snapshot = None;
self.scaled_source = None;
self.base.request_redraw();
}
pub fn set_target(&mut self, widget: Box<dyn Widget>) {
self.target_widget = Some(widget);
self.target_snapshot = None;
self.scaled_target = None;
self.base.request_redraw();
}
pub fn source(&self) -> Option<&dyn Widget> {
self.source_widget.as_deref()
}
pub fn target(&self) -> Option<&dyn Widget> {
self.target_widget.as_deref()
}
pub fn set_duration(&mut self, ms: u64) {
self.duration_ms = ms.max(1);
}
pub fn set_easing(&mut self, easing: Option<crate::style::EasingFunction>) {
self.easing = easing;
}
pub fn easing(&self) -> Option<crate::style::EasingFunction> {
self.easing
}
pub fn set_transition_rects(&mut self, source: Option<Rect>, target: Option<Rect>) {
if self.source_rect == source && self.target_rect == target {
return;
}
self.source_rect = source;
self.target_rect = target;
self.source_snapshot = None;
self.target_snapshot = None;
self.scaled_source = None;
self.scaled_target = None;
self.base.request_redraw();
}
pub fn source_rect(&self) -> Option<Rect> {
self.source_rect
}
pub fn target_rect(&self) -> Option<Rect> {
self.target_rect
}
pub fn duration(&self) -> u64 {
self.duration_ms
}
pub fn tick(&mut self, delta_ms: u64) -> bool {
if !self.is_animating {
return false;
}
self.elapsed_ms = self.elapsed_ms.saturating_add(delta_ms);
let fraction = (self.elapsed_ms as f32 / self.duration_ms as f32).min(1.0);
let eased = match self.easing {
Some(curve) => curve.apply(fraction),
None => fraction,
};
if self.playing_reverse {
let progress = self.reverse_start * (1.0 - eased);
self.set_progress(progress);
if fraction >= 1.0 {
self.animation_progress = 0.0;
self.progress_label = "Progress: 0%".to_string();
self.is_animating = false;
self.playing_reverse = false;
self.animation_completed.emit();
self.base.request_redraw();
}
} else {
self.set_progress(eased);
}
self.is_animating
}
fn interpolated_opacity(&self) -> f32 {
let t = self.animation_progress;
if t < 0.5 {
1.0 - t * 2.0 } else {
(t - 0.5) * 2.0 }
}
}
impl Widget for HeroAnimation {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> crate::core::Size {
crate::core::Size::new(200, 200)
}
fn as_draw_mut(&mut self) -> Option<&mut dyn crate::widget::Draw> {
Some(self)
}
impl_widget_property_hooks!();
fn tick(&mut self, delta_ms: u32) -> bool {
HeroAnimation::tick(self, u64::from(delta_ms))
}
fn is_animating(&self) -> bool {
HeroAnimation::is_animating(self)
}
fn event_signal_dyn(&self, name: &str) -> Option<crate::signal::EventSignalRef> {
use crate::signal::EventSignalRef;
match name {
"animation_completed" => {
Some(EventSignalRef::unit("animation_completed", &self.animation_completed))
}
_ => None,
}
}
}
impl WidgetProperties for HeroAnimation {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"animation_progress" => Ok(CapabilityValue::Float(self.progress() as f64)),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"animation_progress" => {
self.set_progress(expect_f32(value)?);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of!["animation_progress", BASE_PROPERTY_NAMES]
}
}
impl Draw for HeroAnimation {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
let is_enabled = self.base.is_enabled();
let style = self.style();
let themed = crate::style::resolved_theme_style_shared("hero_animation");
let themed_bg = themed.as_ref().and_then(|resolved| resolved.background_color);
let themed_border = themed.as_ref().and_then(|resolved| resolved.border_color);
let themed_text = themed.as_ref().and_then(|resolved| resolved.text_color);
let base_bg =
style.background_color.or(themed_bg).unwrap_or(Color::rgba(240, 240, 240, 255));
let window_fill = {
let manager = crate::style::theme_manager();
manager.current_theme().map(|active| active.colors.background).unwrap_or(Color::WHITE)
};
let shell_border = style.border_color.or(themed_border).unwrap_or(Color::rgba(0, 0, 0, 40));
let ink = style
.text_color
.or(themed_text)
.or_else(|| style.background_color.or(themed_bg).map(|bg| bg.contrast_color()))
.unwrap_or(Color::rgba(160, 160, 160, 220));
let bg = if !is_enabled {
self.base.disabled_surface_near(base_bg, window_fill)
} else {
base_bg
};
context.face_with_gradient_corners(
rect,
bg,
style.background_gradient.as_ref(),
style.surface.unwrap_or_default(),
style.resolved_corner_radii().to_array(),
Color::BLACK,
);
let font = effective_font(self.style()).clone();
let font = &font;
let src = self.source_widget.as_ref();
let tgt = self.target_widget.as_ref();
let has_src = src.is_some();
let has_tgt = tgt.is_some();
if !has_src && !has_tgt {
let font = effective_font(style);
let lines = ["HeroAnimation", "Set source & target"];
let metrics = context.measure_text("Hg", font);
let first_y = rect.y + (rect.height as i32 - metrics.height as i32 * 2) / 2;
for (index, line) in lines.iter().enumerate() {
context.draw_text_fitted_cow(
Rect::new(
rect.x,
first_y + index as i32 * metrics.height as i32,
rect.width,
metrics.height,
),
line,
font,
ink,
HorizontalAlignment::Center,
);
}
return;
}
let t = self.animation_progress;
let src_rect = self.source_rect.or_else(|| src.map(|w| w.geometry())).unwrap_or(rect);
let tgt_rect = self.target_rect.or_else(|| tgt.map(|w| w.geometry())).unwrap_or(rect);
let ix = src_rect.x as f32 + (tgt_rect.x as f32 - src_rect.x as f32) * t;
let iy = src_rect.y as f32 + (tgt_rect.y as f32 - src_rect.y as f32) * t;
let iw = src_rect.width as f32 + (tgt_rect.width as f32 - src_rect.width as f32) * t;
let ih = src_rect.height as f32 + (tgt_rect.height as f32 - src_rect.height as f32) * t;
let interp_rect = Rect::new(ix as i32, iy as i32, iw as u32, ih as u32);
let src_color = self.source_tint;
let tgt_color = self.target_tint;
let r = src_color.r as f32 + (tgt_color.r as f32 - src_color.r as f32) * t;
let g = src_color.g as f32 + (tgt_color.g as f32 - src_color.g as f32) * t;
let b = src_color.b as f32 + (tgt_color.b as f32 - src_color.b as f32) * t;
let interp_color = Color::rgb(r as u8, g as u8, b as u8);
let opacity = self.interpolated_opacity();
let fade_color = Color::rgba(
interp_color.r,
interp_color.g,
interp_color.b,
(interp_color.a as f32 * opacity) as u8,
);
context.fill_rounded_rect(interp_rect, 8, fade_color);
context.draw_rounded_rect_stroke(interp_rect, 8, shell_border, 1);
if self.source_snapshot.is_none() && has_src {
self.source_snapshot = self.source_widget.as_mut().and_then(capture_endpoint);
}
if self.target_snapshot.is_none() && has_tgt {
self.target_snapshot = self.target_widget.as_mut().and_then(capture_endpoint);
}
if let Some(snapshot) = self.source_snapshot.as_ref() {
draw_snapshot(context, snapshot, &mut self.scaled_source, interp_rect, 1.0 - t);
}
if let Some(snapshot) = self.target_snapshot.as_ref() {
draw_snapshot(context, snapshot, &mut self.scaled_target, interp_rect, t);
}
let progress_text = &self.progress_label;
let metrics = context.measure_text(progress_text, font);
let text_x = rect.x + (rect.width as i32 - metrics.width as i32) / 2;
let text_y = rect.y + rect.height as i32 - 10;
context.draw_text(
Point::new(text_x, text_y),
progress_text,
font,
ink,
HorizontalAlignment::Left,
);
let label_band = Rect::new(rect.x + 4, rect.y + 4, rect.width.saturating_sub(8), 14);
let label = match (has_src, has_tgt) {
(true, false) => Some(("Source", self.source_tint.with_alpha(200))),
(false, true) => Some(("Target", self.target_tint.with_alpha(200))),
(true, true) if t < 0.5 => {
Some(("Source \u{2192} Target", self.source_tint.with_alpha(200)))
}
(true, true) => Some(("Source \u{2192} Target", self.target_tint.with_alpha(200))),
(false, false) => None,
};
if let Some((text, color)) = label {
context.draw_text_fitted_cow(label_band, text, font, color, HorizontalAlignment::Left);
}
}
}
fn capture_endpoint(endpoint: &mut Box<dyn Widget>) -> Option<EndpointSnapshot> {
let rect = endpoint.geometry();
if rect.width == 0 || rect.height == 0 {
return None;
}
let size = Size::new(rect.width, rect.height);
let mut backend = SoftwarePaintBackend::new(size, 1.0);
backend.begin_frame(Color::rgba(0, 0, 0, 0));
{
let mut context = RenderContext::new(&mut backend);
context.push_offset(-rect.x, -rect.y);
if let Some(drawable) = crate::widget::draw_bridge::draw_of(endpoint.as_mut()) {
drawable.draw(&mut context);
}
context.pop_offset();
}
backend.end_frame();
Some(EndpointSnapshot {
width: rect.width,
height: rect.height,
data: backend.frame_rgba().to_vec(),
})
}
fn draw_snapshot(
context: &mut RenderContext,
snapshot: &EndpointSnapshot,
cache: &mut Option<ScaledSnapshot>,
dest: Rect,
opacity: f32,
) {
if dest.width == 0 || dest.height == 0 || opacity <= 0.0 {
return;
}
let data: &[u8] = if dest.width == snapshot.width && dest.height == snapshot.height {
&snapshot.data
} else {
let reusable = matches!(
cache.as_ref(),
Some(entry) if entry.width == dest.width && entry.height == dest.height
);
if !reusable {
*cache = Some(ScaledSnapshot {
width: dest.width,
height: dest.height,
data: nearest_scale(
&snapshot.data,
snapshot.width,
snapshot.height,
dest.width,
dest.height,
),
});
}
match cache.as_ref() {
Some(entry) => &entry.data,
None => return,
}
};
context.push_layer_group(dest, None, opacity.clamp(0.0, 1.0), None);
context.draw_image(dest.x, dest.y, dest.width, dest.height, data);
context.pop_layer();
}
impl EventHandler for HeroAnimation {
fn handle_event(&mut self, event: &Event) {
if !self.base.is_enabled() {
return;
}
match event {
Event::MouseRelease { pos, button } => {
if *button == 1 && self.geometry().contains_point(*pos) {
if self.is_animating {
self.stop_animation();
} else {
self.start_animation();
}
}
}
_ => {
self.base.handle_event(event);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Point;
use std::sync::{Arc, Mutex};
#[test]
fn hero_animation_creation_defaults() {
let ha = HeroAnimation::new(Rect::new(0, 0, 300, 200));
assert_eq!(ha.progress(), 0.0);
assert!(!ha.is_animating());
assert_eq!(ha.duration(), 300);
assert!(ha.source().is_none());
assert!(ha.target().is_none());
assert_eq!(ha.kind(), WidgetKind::HeroAnimation);
}
#[test]
fn hero_animation_set_source_and_target() {
let mut ha = HeroAnimation::new(Rect::new(0, 0, 300, 200));
let src_rect = Rect::new(0, 0, 50, 50);
let tgt_rect = Rect::new(200, 100, 100, 80);
ha.set_source(Box::new(crate::widget::base_widgets::button::Button::new(
"Src".to_string(),
src_rect,
)));
ha.set_target(Box::new(crate::widget::base_widgets::button::Button::new(
"Tgt".to_string(),
tgt_rect,
)));
assert!(ha.source().is_some());
assert!(ha.target().is_some());
assert_eq!(ha.source().unwrap().geometry(), src_rect);
assert_eq!(ha.target().unwrap().geometry(), tgt_rect);
}
#[test]
fn hero_animation_start_stop() {
let mut ha = HeroAnimation::new(Rect::new(0, 0, 300, 200));
ha.set_source(Box::new(crate::widget::base_widgets::button::Button::new(
"Src".to_string(),
Rect::new(0, 0, 50, 50),
)));
ha.set_target(Box::new(crate::widget::base_widgets::button::Button::new(
"Tgt".to_string(),
Rect::new(200, 100, 100, 80),
)));
assert!(!ha.is_animating());
ha.start_animation();
assert!(ha.is_animating());
ha.stop_animation();
assert!(!ha.is_animating());
}
#[test]
fn hero_animation_start_without_widgets_does_nothing() {
let mut ha = HeroAnimation::new(Rect::new(0, 0, 300, 200));
ha.start_animation();
assert!(!ha.is_animating()); }
#[test]
fn hero_animation_set_progress_clamps() {
let mut ha = HeroAnimation::new(Rect::new(0, 0, 300, 200));
ha.set_progress(0.5);
assert!((ha.progress() - 0.5).abs() < 0.001);
ha.set_progress(1.5);
assert!((ha.progress() - 1.0).abs() < 0.001);
ha.set_progress(-0.5);
assert!((ha.progress() - 0.0).abs() < 0.001);
}
#[test]
fn hero_animation_progress_completes_animation() {
let mut ha = HeroAnimation::new(Rect::new(0, 0, 300, 200));
ha.set_source(Box::new(crate::widget::base_widgets::button::Button::new(
"Src".to_string(),
Rect::new(0, 0, 50, 50),
)));
ha.set_target(Box::new(crate::widget::base_widgets::button::Button::new(
"Tgt".to_string(),
Rect::new(200, 100, 100, 80),
)));
let completed = Arc::new(Mutex::new(false));
ha.animation_completed.connect({
let completed = Arc::clone(&completed);
move || {
*completed.lock().unwrap() = true;
}
});
ha.set_progress(1.0);
assert!(!ha.is_animating());
assert!(*completed.lock().unwrap());
}
#[test]
fn hero_animation_tick() {
let mut ha = HeroAnimation::new(Rect::new(0, 0, 300, 200));
ha.set_source(Box::new(crate::widget::base_widgets::button::Button::new(
"Src".to_string(),
Rect::new(0, 0, 50, 50),
)));
ha.set_target(Box::new(crate::widget::base_widgets::button::Button::new(
"Tgt".to_string(),
Rect::new(200, 100, 100, 80),
)));
ha.start_animation();
let still_running = ha.tick(150);
assert!(still_running);
assert!((ha.progress() - 0.5).abs() < 0.01);
let still_running = ha.tick(150);
assert!(!still_running);
assert!((ha.progress() - 1.0).abs() < 0.01);
}
#[test]
fn hero_animation_tick_not_started_does_nothing() {
let mut ha = HeroAnimation::new(Rect::new(0, 0, 300, 200));
ha.set_source(Box::new(crate::widget::base_widgets::button::Button::new(
"Src".to_string(),
Rect::new(0, 0, 50, 50),
)));
ha.set_target(Box::new(crate::widget::base_widgets::button::Button::new(
"Tgt".to_string(),
Rect::new(200, 100, 100, 80),
)));
let still_running = ha.tick(100);
assert!(!still_running);
assert!((ha.progress() - 0.0).abs() < 0.001);
}
#[test]
fn hero_animation_set_duration() {
let mut ha = HeroAnimation::new(Rect::new(0, 0, 300, 200));
assert_eq!(ha.duration(), 300);
ha.set_duration(500);
assert_eq!(ha.duration(), 500);
ha.set_duration(0); assert_eq!(ha.duration(), 1);
}
#[test]
fn hero_animation_interpolated_opacity() {
let mut ha = HeroAnimation::new(Rect::new(0, 0, 300, 200));
assert!((ha.interpolated_opacity() - 1.0).abs() < 0.001);
ha.set_progress(0.25);
assert!((ha.interpolated_opacity() - 0.5).abs() < 0.01);
ha.set_progress(0.5);
assert!((ha.interpolated_opacity() - 0.0).abs() < 0.01);
ha.set_progress(0.75);
assert!((ha.interpolated_opacity() - 0.5).abs() < 0.01);
ha.set_progress(1.0);
assert!((ha.interpolated_opacity() - 1.0).abs() < 0.001);
}
#[test]
fn hero_animation_disabled_blocks_events() {
let mut ha = HeroAnimation::new(Rect::new(0, 0, 300, 200));
ha.set_source(Box::new(crate::widget::base_widgets::button::Button::new(
"Src".to_string(),
Rect::new(0, 0, 50, 50),
)));
ha.set_target(Box::new(crate::widget::base_widgets::button::Button::new(
"Tgt".to_string(),
Rect::new(200, 100, 100, 80),
)));
ha.set_enabled(false);
ha.handle_event(&Event::MousePress { pos: Point::new(50, 50), button: 1, modifiers: 0 });
assert!(!ha.is_animating());
}
#[test]
fn hero_animation_svg_output() {
let mut ha = HeroAnimation::new(Rect::new(0, 0, 300, 200));
ha.set_source(Box::new(crate::widget::base_widgets::button::Button::new(
"Src".to_string(),
Rect::new(0, 0, 100, 50),
)));
ha.set_target(Box::new(crate::widget::base_widgets::button::Button::new(
"Tgt".to_string(),
Rect::new(200, 100, 200, 100),
)));
ha.set_progress(0.3);
let svg = crate::widget::svg::render_to_svg(&mut ha);
assert!(svg.starts_with("<svg"));
assert!(svg.ends_with("</svg>"));
}
#[test]
fn an_optional_curve_eases_the_movement_without_moving_the_endpoints() {
let mut ha = HeroAnimation::new(Rect::new(0, 0, 300, 200));
ha.set_source(Box::new(crate::widget::base_widgets::button::Button::new(
"Src".to_string(),
Rect::new(0, 0, 50, 50),
)));
ha.set_target(Box::new(crate::widget::base_widgets::button::Button::new(
"Tgt".to_string(),
Rect::new(200, 100, 100, 80),
)));
ha.set_easing(Some(crate::style::EasingFunction::EaseIn));
ha.set_duration(100);
ha.start_animation();
ha.tick(50);
assert!(ha.progress() < 0.5, "ease-in lags the linear fraction");
ha.tick(50);
assert!((ha.progress() - 1.0).abs() < 0.01);
}
#[test]
fn explicit_endpoint_rects_drive_the_transition() {
let mut ha = HeroAnimation::new(Rect::new(0, 0, 300, 200));
ha.set_transition_rects(Some(Rect::new(10, 10, 20, 20)), Some(Rect::new(100, 100, 80, 80)));
assert_eq!(ha.source_rect(), Some(Rect::new(10, 10, 20, 20)));
ha.set_source(Box::new(crate::widget::base_widgets::button::Button::new(
"Src".to_string(),
Rect::new(0, 0, 50, 50),
)));
ha.set_target(Box::new(crate::widget::base_widgets::button::Button::new(
"Tgt".to_string(),
Rect::new(200, 100, 100, 80),
)));
let svg = crate::widget::svg::render_to_svg(&mut ha);
assert!(svg.starts_with("<svg"));
}
#[test]
fn drawing_a_transition_does_not_move_the_endpoints() {
let mut ha = HeroAnimation::new(Rect::new(0, 0, 300, 200));
ha.set_source(Box::new(crate::widget::base_widgets::button::Button::new(
"Src".to_string(),
Rect::new(0, 0, 50, 50),
)));
ha.set_target(Box::new(crate::widget::base_widgets::button::Button::new(
"Tgt".to_string(),
Rect::new(200, 100, 100, 80),
)));
ha.set_progress(0.3);
let source_before = ha.source().expect("source").geometry();
let target_before = ha.target().expect("target").geometry();
let _ = crate::widget::svg::render_to_svg(&mut ha);
assert_eq!(ha.source().expect("source").geometry(), source_before);
assert_eq!(ha.target().expect("target").geometry(), target_before);
}
#[test]
fn endpoint_tints_and_cached_label_are_settings_not_paint_work() {
let mut ha = HeroAnimation::new(Rect::new(0, 0, 300, 200));
let blue = Color::rgb(10, 20, 30);
let red = Color::rgb(200, 10, 10);
ha.set_tints(blue, red);
assert_eq!(ha.source_tint(), blue);
assert_eq!(ha.target_tint(), red);
ha.set_progress(0.5);
assert_eq!(ha.progress_label, "Progress: 50%");
}
}