Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: animations for default widgets #1855

Closed
wants to merge 12 commits into from
23 changes: 23 additions & 0 deletions core/src/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,22 @@ impl Color {
pub fn inverse(self) -> Color {
Color::new(1.0f32 - self.r, 1.0f32 - self.g, 1.0f32 - self.b, self.a)
}

/// Mix with another color with the given ratio (from 0 to 1)
pub fn mix(&mut self, other: Color, ratio: f32) {
if ratio >= 1.0 {
self.r = other.r;
self.g = other.g;
self.b = other.b;
self.a = other.a;
} else if ratio > 0.0 {
let self_ratio = 1.0 - ratio;
self.r = self.r * self_ratio + other.r * ratio;
self.g = self.g * self_ratio + other.g * ratio;
self.b = self.b * self_ratio + other.b * ratio;
self.a = self.a * self_ratio + other.a * ratio;
}
}
}

impl From<[f32; 3]> for Color {
Expand Down Expand Up @@ -252,5 +268,12 @@ mod tests {
a: 1.0
}
);

// Mix two colors
let white = Color::WHITE;
let black = Color::BLACK;
let mut darkgrey = white;
darkgrey.mix(black, 0.75);
assert_eq!(darkgrey, Color::from_rgba(0.25, 0.25, 0.25, 1.0));
}
}
11 changes: 11 additions & 0 deletions examples/animations/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "animations"
version = "0.1.0"
authors = ["LazyTanuki"]
edition = "2021"
publish = false

[dependencies]
iced = { path = "../..", features = ["debug"] }
# iced = "0.9"
iced_aw = { git = "https://github.com/iced-rs/iced_aw", branch = "main", features = ["tabs"] }
8 changes: 8 additions & 0 deletions examples/animations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## Animations

An application to showcase Iced widgets that have default animations.

The __[`main`]__ file contains all the code of the example.

[`main`]: src/main.rs
[TodoMVC]: http://todomvc.com/
88 changes: 88 additions & 0 deletions examples/animations/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
use iced::widget::{button, column, radio, text_input, Toggler};
use iced::{Element, Sandbox, Settings};

pub fn main() -> iced::Result {
Animations::run(Settings::default())
}

struct Animations {
_animation_multiplier: i32,
radio1: Option<usize>,
radio2: Option<usize>,
input_text: String,
toggled: bool,
}

#[derive(Debug, Clone)]
enum Message {
ButtonPressed,
RadioPressed(usize),
TextSubmitted,
TextChanged(String),
Toggle(bool),
}

impl Sandbox for Animations {
type Message = Message;

fn new() -> Self {
Self {
_animation_multiplier: 0,
radio1: None,
radio2: None,
input_text: "".into(),
toggled: false,
}
}

fn theme(&self) -> iced::Theme {
iced::Theme::Dark
}

fn title(&self) -> String {
String::from("Counter - Iced")
}

fn update(&mut self, message: Message) {
match message {
Message::RadioPressed(i) => match i {
1 => {
self.radio1 = Some(1);
self.radio2 = None;
}
2 => {
self.radio2 = Some(2);
self.radio1 = None;
}
_ => {}
},
Message::TextChanged(txt) => {
self.input_text = txt;
}
Message::Toggle(t) => self.toggled = t,
Message::TextSubmitted | Message::ButtonPressed => {}
}
}

fn view(&self) -> Element<Message> {
column![
text_input("Insert some text here...", &self.input_text)
.on_submit(Message::TextSubmitted)
.on_input(|txt| Message::TextChanged(txt)),
button("Press me").on_press(Message::ButtonPressed),
radio("Click me 1", 1, self.radio1, |i| {
Message::RadioPressed(i)
}),
radio("Click me 2", 2, self.radio2, |i| {
Message::RadioPressed(i)
}),
Toggler::new(Some("Toggle me".into()), self.toggled, |t| {
Message::Toggle(t)
})
]
.spacing(10)
.padding(50)
.max_width(300)
.into()
}
}
195 changes: 195 additions & 0 deletions style/src/animation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
//! Add animations to widgets.

/// Hover animation of the widget
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct HoverPressedAnimation {
/// Animation direction: forward means it goes from non-hovered to hovered state
pub direction: AnimationDirection,
/// The instant the animation was started at (`None` if it is not running)
pub started_at: Option<std::time::Instant>,
/// The progress of the animationn, between 0.0 and 1.0
pub animation_progress: f32,
/// The progress the animation has been started at
pub initial_progress: f32,
/// The type of effect for the animation
pub effect: AnimationEffect,
}

/// The type of effect for the animation
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum AnimationEffect {
/// Transition is linear.
#[default]
Linear,
/// Transition is a cubic ease out.
EaseOut,
/// Transistion is instantaneous.
None,
}

#[derive(Debug, Clone, Copy, PartialEq, Default)]
/// Direction of the animation
pub enum AnimationDirection {
#[default]
/// The animation goes forward
Forward,
/// The animation goes backward
Backward,
}

impl HoverPressedAnimation {
/// Create a hover animation with the given transision effect
pub fn new(effect: AnimationEffect) -> Self {
Self {
effect,
..Default::default()
}
}

/// Check if the animation is running
pub fn is_running(&self) -> bool {
self.started_at.is_some()
}

/// Reset the animation
pub fn reset(&mut self) {
self.direction = AnimationDirection::Forward;
self.started_at = None;
self.animation_progress = 0.0;
self.initial_progress = 0.0;
}

/// Update the animation progress, if necessary, and returns the need to request a redraw.
pub fn on_redraw_request_update(
&mut self,
animation_duration_ms: u16,
now: std::time::Instant,
) -> bool {
// Is the animation running ?
if let Some(started_at) = self.started_at {
if self.animation_progress >= 1.0 || animation_duration_ms == 0 {
self.animation_progress = 1.0;
}

// Reset the animation once it has gone forward and now fully backward
if self.animation_progress == 0.0
&& self.direction == AnimationDirection::Backward
{
self.started_at = None;
} else {
// Evaluate new progress
match &mut self.effect {
AnimationEffect::Linear => {
let progress_since_start =
((now - started_at).as_millis() as f64)
/ (animation_duration_ms as f64);
match self.direction {
AnimationDirection::Forward => {
self.animation_progress = (self
.initial_progress
+ progress_since_start as f32)
.clamp(0.0, 1.0);
}
AnimationDirection::Backward => {
self.animation_progress = (self
.initial_progress
- progress_since_start as f32)
.clamp(0.0, 1.0);
}
}
}
AnimationEffect::EaseOut => {
let progress_since_start =
((now - started_at).as_millis() as f32)
/ (animation_duration_ms as f32);
match self.direction {
AnimationDirection::Forward => {
self.animation_progress = (self
.initial_progress
+ ease_out_cubic(progress_since_start))
.clamp(0.0, 1.0);
}
AnimationDirection::Backward => {
self.animation_progress = (self
.initial_progress
- ease_out_cubic(progress_since_start))
.clamp(0.0, 1.0);
}
}
}
AnimationEffect::None => {}
}
}
return true;
}
false
}

/// Update the hovered state and return the need to request a redraw.
pub fn on_cursor_moved_update(&mut self, is_mouse_over: bool) -> bool {
if is_mouse_over {
// Is it already running ?
if self.started_at.is_some() {
// This is when the cursor re-enters the widget's area before the animation finishes
if self.direction == AnimationDirection::Backward {
// Change animation direction
self.direction = AnimationDirection::Forward;
// Start from where the animation was at
self.initial_progress = self.animation_progress;
self.started_at = Some(std::time::Instant::now());
}
} else {
// Start the animation
self.direction = AnimationDirection::Forward;
self.started_at = Some(std::time::Instant::now());
self.animation_progress = 0.0;
self.initial_progress = 0.0;
}
self.animation_progress != 1.0
} else if self.started_at.is_some() {
// This is when the cursor leaves the widget's area
match self.direction {
AnimationDirection::Forward => {
// Change animation direction
self.direction = AnimationDirection::Backward;
// Start from where the animation was at
self.initial_progress = self.animation_progress;
self.started_at = Some(std::time::Instant::now());
true
}
AnimationDirection::Backward => true,
}
} else {
false
}
}

/// Start the animation when pressed.
pub fn on_press(&mut self) {
self.started_at = Some(std::time::Instant::now());
self.direction = AnimationDirection::Forward;
self.animation_progress = 0.0;
self.initial_progress = 0.0;
}

/// End the animation when released.
pub fn on_released(&mut self) {
self.started_at = Some(std::time::Instant::now());
self.direction = AnimationDirection::Backward;
self.initial_progress = self.animation_progress;
}

/// End the animation (go backgwards), skipping the forward phase.
pub fn on_activate(&mut self) {
self.started_at = Some(std::time::Instant::now());
self.direction = AnimationDirection::Backward;
self.initial_progress = 1.0;
self.animation_progress = 1.0;
}
}

/// Based on Robert Penner's infamous easing equations, MIT license.
fn ease_out_cubic(t: f32) -> f32 {
let p = t - 1f32;
p * p * p + 1f32
}
12 changes: 10 additions & 2 deletions style/src/button.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ pub trait StyleSheet {
fn active(&self, style: &Self::Style) -> Appearance;

/// Produces the hovered [`Appearance`] of a button.
fn hovered(&self, style: &Self::Style) -> Appearance {
fn hovered(
&self,
style: &Self::Style,
_hover_animation: &crate::animation::HoverPressedAnimation,
) -> Appearance {
let active = self.active(style);

Appearance {
Expand All @@ -50,7 +54,11 @@ pub trait StyleSheet {
}

/// Produces the pressed [`Appearance`] of a button.
fn pressed(&self, style: &Self::Style) -> Appearance {
fn pressed(
&self,
style: &Self::Style,
_pressed_animation: &crate::animation::HoverPressedAnimation,
) -> Appearance {
Appearance {
shadow_offset: Vector::default(),
..self.active(style)
Expand Down
1 change: 1 addition & 0 deletions style/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#![allow(clippy::inherent_to_string, clippy::type_complexity)]
pub use iced_core as core;

pub mod animation;
pub mod application;
pub mod button;
pub mod checkbox;
Expand Down
Loading