From 8ced9ef76fffe5030b27aecc297a3eb38748b44c Mon Sep 17 00:00:00 2001 From: LazyTanuki <43273245+lazytanuki@users.noreply.github.com> Date: Sun, 9 Apr 2023 00:16:30 +0200 Subject: [PATCH 01/12] wip: button on hover fading animation --- core/src/color.rs | 23 +++++++++++++++++ style/src/button.rs | 6 ++++- style/src/theme.rs | 27 +++++++++++++++++--- widget/src/button.rs | 60 ++++++++++++++++++++++++++++++++++++++------ 4 files changed, 104 insertions(+), 12 deletions(-) diff --git a/core/src/color.rs b/core/src/color.rs index fe0a18569b..6b9cb49f61 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -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 { @@ -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)); } } diff --git a/style/src/button.rs b/style/src/button.rs index a564a2b7ea..6170a97103 100644 --- a/style/src/button.rs +++ b/style/src/button.rs @@ -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, + _hovered_style_ratio: Option, + ) -> Appearance { let active = self.active(style); Appearance { diff --git a/style/src/theme.rs b/style/src/theme.rs index d9893bcfa0..322944131c 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -169,16 +169,20 @@ impl button::StyleSheet for Theme { } } - fn hovered(&self, style: &Self::Style) -> button::Appearance { + fn hovered( + &self, + style: &Self::Style, + hovered_style_ratio: Option, + ) -> button::Appearance { let palette = self.extended_palette(); if let Button::Custom(custom) = style { - return custom.hovered(self); + return custom.hovered(self, hovered_style_ratio); } let active = self.active(style); - let background = match style { + let mut background = match style { Button::Primary => Some(palette.primary.base.color), Button::Secondary => Some(palette.background.strong.color), Button::Positive => Some(palette.success.strong.color), @@ -186,6 +190,23 @@ impl button::StyleSheet for Theme { Button::Text | Button::Custom(_) => None, }; + // Mix the hovered and active styles backgrounds according to the animation state + if let ( + Some(hovered_style_ratio), + Some(active_background), + Some(background_color), + ) = (hovered_style_ratio, active.background, background.as_mut()) + { + match active_background { + Background::Color(active_background_color) => { + background_color.mix( + active_background_color, + 1.0 - hovered_style_ratio, + ); + } + } + } + button::Appearance { background: background.map(Background::from), ..active diff --git a/widget/src/button.rs b/widget/src/button.rs index 7eee69cbae..5ac524a225 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -1,6 +1,8 @@ //! Allow your users to perform actions by pressing a button. //! //! A [`Button`] has some local [`State`]. +use std::time::Instant; + use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -13,6 +15,7 @@ use crate::core::{ Background, Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, Shell, Vector, Widget, }; +use crate::core::window; pub use iced_style::button::{Appearance, StyleSheet}; @@ -62,6 +65,8 @@ where height: Length, padding: Padding, style: ::Style, + /// Animation duration in milliseconds + animation_duration_ms: u16, } impl<'a, Message, Renderer> Button<'a, Message, Renderer> @@ -78,6 +83,7 @@ where height: Length::Shrink, padding: Padding::new(5.0), style: ::Style::default(), + animation_duration_ms: 250, } } @@ -211,6 +217,7 @@ where shell, &self.on_press, || tree.state.downcast_mut::(), + self.animation_duration_ms, ) } @@ -230,7 +237,6 @@ where let styling = draw( renderer, bounds, - cursor_position, self.on_press.is_some(), theme, &self.style, @@ -287,10 +293,17 @@ where } } +#[derive(Debug, Clone, Copy, PartialEq)] +struct Hover { + started_at: Instant, + animation_progress: f32, +} + /// The local state of a [`Button`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct State { is_pressed: bool, + is_hovered: Option, } impl State { @@ -309,6 +322,7 @@ pub fn update<'a, Message: Clone>( shell: &mut Shell<'_, Message>, on_press: &Option, state: impl FnOnce() -> &'a mut State, + animation_duration_ms: u16, ) -> event::Status { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) @@ -348,6 +362,39 @@ pub fn update<'a, Message: Clone>( state.is_pressed = false; } + Event::Window(window::Event::RedrawRequested(now)) => { + let state = state(); + + if let Some(hover) = &mut state.is_hovered { + if animation_duration_ms == 0 || hover.animation_progress >= 1.0 + { + hover.animation_progress = 1.0; + } else { + hover.animation_progress = + ((((now - hover.started_at).as_millis() as f64) + / (animation_duration_ms as f64)) + as f32) + .clamp(0.0, 1.0); + } + shell.request_redraw(window::RedrawRequest::NextFrame); + } + } + Event::Mouse(mouse::Event::CursorMoved { position }) => { + let state = state(); + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(position); + + if is_mouse_over && state.is_hovered.is_none() { + state.is_hovered = Some(Hover { + started_at: std::time::Instant::now(), + animation_progress: 0.0, + }); + shell.request_redraw(window::RedrawRequest::NextFrame); + } else if !is_mouse_over { + state.is_hovered = None; + shell.request_redraw(window::RedrawRequest::NextFrame); + } + } _ => {} } @@ -358,7 +405,6 @@ pub fn update<'a, Message: Clone>( pub fn draw<'a, Renderer: crate::core::Renderer>( renderer: &mut Renderer, bounds: Rectangle, - cursor_position: Point, is_enabled: bool, style_sheet: &dyn StyleSheet< Style = ::Style, @@ -369,17 +415,15 @@ pub fn draw<'a, Renderer: crate::core::Renderer>( where Renderer::Theme: StyleSheet, { - let is_mouse_over = bounds.contains(cursor_position); + let state = state(); let styling = if !is_enabled { style_sheet.disabled(style) - } else if is_mouse_over { - let state = state(); - + } else if let Some(hover) = state.is_hovered { if state.is_pressed { style_sheet.pressed(style) } else { - style_sheet.hovered(style) + style_sheet.hovered(style, Some(hover.animation_progress)) } } else { style_sheet.active(style) From 9b3e0fe22d205812e589fd9d88b65086344866ec Mon Sep 17 00:00:00 2001 From: LazyTanuki <43273245+lazytanuki@users.noreply.github.com> Date: Mon, 10 Apr 2023 18:04:07 +0200 Subject: [PATCH 02/12] wip: button on hover lost fading animation --- style/src/button.rs | 27 ++++++++++++++++- style/src/theme.rs | 14 ++++----- widget/src/button.rs | 70 +++++++++++++++++++++++++++++++------------- 3 files changed, 81 insertions(+), 30 deletions(-) diff --git a/style/src/button.rs b/style/src/button.rs index 6170a97103..608a6af8f3 100644 --- a/style/src/button.rs +++ b/style/src/button.rs @@ -1,4 +1,6 @@ //! Change the apperance of a button. +use std::time::Instant; + use iced_core::{Background, Color, Vector}; /// The appearance of a button. @@ -31,6 +33,29 @@ impl std::default::Default for Appearance { } } +#[derive(Debug, Clone, Copy, PartialEq, Default)] +/// Direction of the animation +pub enum AnimationDirection { + #[default] + /// The animation goes forward + Forward, + /// The animation goes backward + Backward, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +/// Hover animation +pub struct Hover { + /// Animation direction: forward means it goes from non-hovered to hovered state + pub direction: AnimationDirection, + /// The instant the animation was started at + pub started_at: 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, +} + /// A set of rules that dictate the style of a button. pub trait StyleSheet { /// The supported style of the [`StyleSheet`]. @@ -43,7 +68,7 @@ pub trait StyleSheet { fn hovered( &self, style: &Self::Style, - _hovered_style_ratio: Option, + _hover: Option, ) -> Appearance { let active = self.active(style); diff --git a/style/src/theme.rs b/style/src/theme.rs index 322944131c..416960494f 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -6,6 +6,7 @@ pub use self::palette::Palette; use crate::application; use crate::button; +use crate::button::Hover; use crate::checkbox; use crate::container; use crate::core::widget::text; @@ -172,12 +173,12 @@ impl button::StyleSheet for Theme { fn hovered( &self, style: &Self::Style, - hovered_style_ratio: Option, + hover: Option, ) -> button::Appearance { let palette = self.extended_palette(); if let Button::Custom(custom) = style { - return custom.hovered(self, hovered_style_ratio); + return custom.hovered(self, hover); } let active = self.active(style); @@ -191,17 +192,14 @@ impl button::StyleSheet for Theme { }; // Mix the hovered and active styles backgrounds according to the animation state - if let ( - Some(hovered_style_ratio), - Some(active_background), - Some(background_color), - ) = (hovered_style_ratio, active.background, background.as_mut()) + if let (Some(hover), Some(active_background), Some(background_color)) = + (hover, active.background, background.as_mut()) { match active_background { Background::Color(active_background_color) => { background_color.mix( active_background_color, - 1.0 - hovered_style_ratio, + 1.0 - hover.animation_progress, ); } } diff --git a/widget/src/button.rs b/widget/src/button.rs index 5ac524a225..0ee1ae499f 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -17,6 +17,7 @@ use crate::core::{ }; use crate::core::window; +use iced_style::button::{AnimationDirection, Hover}; pub use iced_style::button::{Appearance, StyleSheet}; /// A generic widget that produces a message when pressed. @@ -83,7 +84,7 @@ where height: Length::Shrink, padding: Padding::new(5.0), style: ::Style::default(), - animation_duration_ms: 250, + animation_duration_ms: 150, } } @@ -293,12 +294,6 @@ where } } -#[derive(Debug, Clone, Copy, PartialEq)] -struct Hover { - started_at: Instant, - animation_progress: f32, -} - /// The local state of a [`Button`]. #[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct State { @@ -369,12 +364,30 @@ pub fn update<'a, Message: Clone>( if animation_duration_ms == 0 || hover.animation_progress >= 1.0 { hover.animation_progress = 1.0; + } + if hover.animation_progress == 0.0 + && hover.direction == AnimationDirection::Backward + { + state.is_hovered = None; } else { - hover.animation_progress = - ((((now - hover.started_at).as_millis() as f64) - / (animation_duration_ms as f64)) - as f32) - .clamp(0.0, 1.0); + match hover.direction { + AnimationDirection::Forward => { + hover.animation_progress = (hover.initial_progress + + (((now - hover.started_at).as_millis() + as f64) + / (animation_duration_ms as f64)) + as f32) + .clamp(0.0, 1.0); + } + AnimationDirection::Backward => { + hover.animation_progress = (hover.initial_progress + - (((now - hover.started_at).as_millis() + as f64) + / (animation_duration_ms as f64)) + as f32) + .clamp(0.0, 1.0); + } + } } shell.request_redraw(window::RedrawRequest::NextFrame); } @@ -384,15 +397,30 @@ pub fn update<'a, Message: Clone>( let bounds = layout.bounds(); let is_mouse_over = bounds.contains(position); - if is_mouse_over && state.is_hovered.is_none() { - state.is_hovered = Some(Hover { - started_at: std::time::Instant::now(), - animation_progress: 0.0, - }); - shell.request_redraw(window::RedrawRequest::NextFrame); - } else if !is_mouse_over { - state.is_hovered = None; + if is_mouse_over { + if let Some(hover) = &mut state.is_hovered { + if hover.direction == AnimationDirection::Backward { + hover.initial_progress = hover.animation_progress; + hover.direction = AnimationDirection::Forward; + hover.started_at = std::time::Instant::now(); + } + } + if state.is_hovered.is_none() { + state.is_hovered = Some(Hover { + direction: AnimationDirection::Forward, + started_at: std::time::Instant::now(), + animation_progress: 0.0, + initial_progress: 0.0, + }); + } shell.request_redraw(window::RedrawRequest::NextFrame); + } else if let Some(hover) = &mut state.is_hovered { + if hover.direction == AnimationDirection::Forward { + hover.initial_progress = hover.animation_progress; + hover.direction = AnimationDirection::Backward; + hover.started_at = std::time::Instant::now(); + shell.request_redraw(window::RedrawRequest::NextFrame); + } } } _ => {} @@ -423,7 +451,7 @@ where if state.is_pressed { style_sheet.pressed(style) } else { - style_sheet.hovered(style, Some(hover.animation_progress)) + style_sheet.hovered(style, Some(hover)) } } else { style_sheet.active(style) From da687248091a79b7d6e5415c15c6aba2347e5865 Mon Sep 17 00:00:00 2001 From: LazyTanuki <43273245+lazytanuki@users.noreply.github.com> Date: Mon, 10 Apr 2023 21:49:20 +0200 Subject: [PATCH 03/12] wip: made a `style::animation` module and moved the `Hover` struct to it --- style/src/animation.rs | 24 ++++++++++++++++++++++++ style/src/button.rs | 26 ++------------------------ style/src/lib.rs | 1 + style/src/theme.rs | 4 ++-- widget/src/button.rs | 3 +-- 5 files changed, 30 insertions(+), 28 deletions(-) create mode 100644 style/src/animation.rs diff --git a/style/src/animation.rs b/style/src/animation.rs new file mode 100644 index 0000000000..0be1fbfb62 --- /dev/null +++ b/style/src/animation.rs @@ -0,0 +1,24 @@ +//! Add animations to widgets. + +#[derive(Debug, Clone, Copy, PartialEq, Default)] +/// Direction of the animation +pub enum AnimationDirection { + #[default] + /// The animation goes forward + Forward, + /// The animation goes backward + Backward, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +/// Hover animation +pub struct Hover { + /// Animation direction: forward means it goes from non-hovered to hovered state + pub direction: AnimationDirection, + /// The instant the animation was started at + pub started_at: 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, +} diff --git a/style/src/button.rs b/style/src/button.rs index 608a6af8f3..3eddc1c76f 100644 --- a/style/src/button.rs +++ b/style/src/button.rs @@ -1,8 +1,9 @@ //! Change the apperance of a button. -use std::time::Instant; use iced_core::{Background, Color, Vector}; +use crate::animation::Hover; + /// The appearance of a button. #[derive(Debug, Clone, Copy)] pub struct Appearance { @@ -33,29 +34,6 @@ impl std::default::Default for Appearance { } } -#[derive(Debug, Clone, Copy, PartialEq, Default)] -/// Direction of the animation -pub enum AnimationDirection { - #[default] - /// The animation goes forward - Forward, - /// The animation goes backward - Backward, -} - -#[derive(Debug, Clone, Copy, PartialEq)] -/// Hover animation -pub struct Hover { - /// Animation direction: forward means it goes from non-hovered to hovered state - pub direction: AnimationDirection, - /// The instant the animation was started at - pub started_at: 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, -} - /// A set of rules that dictate the style of a button. pub trait StyleSheet { /// The supported style of the [`StyleSheet`]. diff --git a/style/src/lib.rs b/style/src/lib.rs index 286ff9db22..e18d40d4f7 100644 --- a/style/src/lib.rs +++ b/style/src/lib.rs @@ -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; diff --git a/style/src/theme.rs b/style/src/theme.rs index 416960494f..ca0d10feaa 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -4,9 +4,9 @@ pub mod palette; use self::palette::Extended; pub use self::palette::Palette; +use crate::animation; use crate::application; use crate::button; -use crate::button::Hover; use crate::checkbox; use crate::container; use crate::core::widget::text; @@ -173,7 +173,7 @@ impl button::StyleSheet for Theme { fn hovered( &self, style: &Self::Style, - hover: Option, + hover: Option, ) -> button::Appearance { let palette = self.extended_palette(); diff --git a/widget/src/button.rs b/widget/src/button.rs index 0ee1ae499f..c483c1e43e 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -1,7 +1,6 @@ //! Allow your users to perform actions by pressing a button. //! //! A [`Button`] has some local [`State`]. -use std::time::Instant; use crate::core::event::{self, Event}; use crate::core::layout; @@ -17,7 +16,7 @@ use crate::core::{ }; use crate::core::window; -use iced_style::button::{AnimationDirection, Hover}; +use iced_style::animation::{AnimationDirection, Hover}; pub use iced_style::button::{Appearance, StyleSheet}; /// A generic widget that produces a message when pressed. From 349fd7346f80869cf1c1bb541266c76849d7f686 Mon Sep 17 00:00:00 2001 From: LazyTanuki <43273245+lazytanuki@users.noreply.github.com> Date: Mon, 10 Apr 2023 23:45:39 +0200 Subject: [PATCH 04/12] wip: generalized button animations --- style/src/animation.rs | 141 ++++++++++++++++++++++++++++++++++++++--- style/src/button.rs | 4 +- style/src/theme.rs | 24 +++---- widget/src/button.rs | 118 +++++++++++++++++----------------- 4 files changed, 203 insertions(+), 84 deletions(-) diff --git a/style/src/animation.rs b/style/src/animation.rs index 0be1fbfb62..b48cdcfc77 100644 --- a/style/src/animation.rs +++ b/style/src/animation.rs @@ -1,24 +1,145 @@ //! Add animations to widgets. +/// Hover animation of the widget #[derive(Debug, Clone, Copy, PartialEq, Default)] -/// Direction of the animation -pub enum AnimationDirection { +pub struct HoverAnimation { + /// 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, + /// 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 { + /// The background color of the widget fades into the "hovered" color when hovered #[default] - /// The animation goes forward - Forward, - /// The animation goes backward - Backward, + Fade, } +/// Hover animation of the widget #[derive(Debug, Clone, Copy, PartialEq)] -/// Hover animation -pub struct Hover { +pub enum PressedAnimation { + /// The background color of the widget fades into the "pressed" color when pressed + Fade(Fade), +} + +#[derive(Debug, Clone, Copy, PartialEq, Default)] +/// Fade.. animation +pub struct Fade { /// Animation direction: forward means it goes from non-hovered to hovered state pub direction: AnimationDirection, - /// The instant the animation was started at - pub started_at: std::time::Instant, + /// The instant the animation was started at (`None` if it is not running) + pub started_at: Option, /// 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, } + +#[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 HoverAnimation { + /// Check if the animation is running + pub fn is_running(&self) -> bool { + self.started_at.is_some() + } + + /// 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 if ended + if self.animation_progress == 0.0 + && self.direction == AnimationDirection::Backward + { + self.started_at = None; + } else { + // Evaluate new progress + match &mut self.effect { + AnimationEffect::Fade => match self.direction { + AnimationDirection::Forward => { + let progress_since_start = + ((now - started_at).as_millis() as f64) + / (animation_duration_ms as f64); + self.animation_progress = (self.initial_progress + + progress_since_start as f32) + .clamp(0.0, 1.0); + } + AnimationDirection::Backward => { + let progress_since_start = + ((now - started_at).as_millis() as f64) + / (animation_duration_ms as f64); + self.animation_progress = (self.initial_progress + - progress_since_start as f32) + .clamp(0.0, 1.0); + } + }, + } + } + 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 + } + } +} diff --git a/style/src/button.rs b/style/src/button.rs index 3eddc1c76f..af8dcc3656 100644 --- a/style/src/button.rs +++ b/style/src/button.rs @@ -2,7 +2,7 @@ use iced_core::{Background, Color, Vector}; -use crate::animation::Hover; +use crate::animation::HoverAnimation; /// The appearance of a button. #[derive(Debug, Clone, Copy)] @@ -46,7 +46,7 @@ pub trait StyleSheet { fn hovered( &self, style: &Self::Style, - _hover: Option, + _hover: &HoverAnimation, ) -> Appearance { let active = self.active(style); diff --git a/style/src/theme.rs b/style/src/theme.rs index ca0d10feaa..84f85ff705 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -173,12 +173,12 @@ impl button::StyleSheet for Theme { fn hovered( &self, style: &Self::Style, - hover: Option, + hover_animation: &animation::HoverAnimation, ) -> button::Appearance { let palette = self.extended_palette(); if let Button::Custom(custom) = style { - return custom.hovered(self, hover); + return custom.hovered(self, hover_animation); } let active = self.active(style); @@ -192,16 +192,18 @@ impl button::StyleSheet for Theme { }; // Mix the hovered and active styles backgrounds according to the animation state - if let (Some(hover), Some(active_background), Some(background_color)) = - (hover, active.background, background.as_mut()) + if let (Some(active_background), Some(background_color)) = + (active.background, background.as_mut()) { - match active_background { - Background::Color(active_background_color) => { - background_color.mix( - active_background_color, - 1.0 - hover.animation_progress, - ); - } + match hover_animation.effect { + animation::AnimationEffect::Fade => match active_background { + Background::Color(active_background_color) => { + background_color.mix( + active_background_color, + 1.0 - hover_animation.animation_progress, + ); + } + }, } } diff --git a/widget/src/button.rs b/widget/src/button.rs index c483c1e43e..0a9d60b032 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -16,7 +16,9 @@ use crate::core::{ }; use crate::core::window; -use iced_style::animation::{AnimationDirection, Hover}; +use iced_style::animation::{ + AnimationDirection, AnimationEffect, Fade, HoverAnimation, PressedAnimation, +}; pub use iced_style::button::{Appearance, StyleSheet}; /// A generic widget that produces a message when pressed. @@ -65,8 +67,9 @@ where height: Length, padding: Padding, style: ::Style, - /// Animation duration in milliseconds animation_duration_ms: u16, + hover_animation: HoverAnimation, + pressed_animation: PressedAnimation, } impl<'a, Message, Renderer> Button<'a, Message, Renderer> @@ -84,6 +87,8 @@ where padding: Padding::new(5.0), style: ::Style::default(), animation_duration_ms: 150, + hover_animation: HoverAnimation::default(), + pressed_animation: PressedAnimation::Fade(Fade::default()), } } @@ -121,6 +126,27 @@ where self.style = style; self } + + /// Sets the animation duration (in milliseconds) of the [`Button`]. + pub fn animation_duration(mut self, animation_duration_ms: u16) -> Self { + self.animation_duration_ms = animation_duration_ms; + self + } + + /// Sets the animation when hovering the [`Button`]. + pub fn hover_animation(mut self, hover_animation: HoverAnimation) -> Self { + self.hover_animation = hover_animation; + self + } + + /// Sets the animation when pressing the [`Button`]. + pub fn press_animation( + mut self, + pressed_animation: PressedAnimation, + ) -> Self { + self.pressed_animation = pressed_animation; + self + } } impl<'a, Message, Renderer> Widget @@ -135,7 +161,10 @@ where } fn state(&self) -> tree::State { - tree::State::new(State::new()) + tree::State::new(State::new( + self.hover_animation, + self.pressed_animation, + )) } fn children(&self) -> Vec { @@ -240,6 +269,7 @@ where self.on_press.is_some(), theme, &self.style, + cursor_position, || tree.state.downcast_ref::(), ); @@ -294,16 +324,24 @@ where } /// The local state of a [`Button`]. -#[derive(Debug, Clone, Copy, PartialEq, Default)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct State { is_pressed: bool, - is_hovered: Option, + hovered_animation: HoverAnimation, + pressed_animation: PressedAnimation, } impl State { /// Creates a new [`State`]. - pub fn new() -> State { - State::default() + pub fn new( + hovered_animation: HoverAnimation, + pressed_animation: PressedAnimation, + ) -> State { + State { + is_pressed: false, + hovered_animation, + pressed_animation, + } } } @@ -359,35 +397,10 @@ pub fn update<'a, Message: Clone>( Event::Window(window::Event::RedrawRequested(now)) => { let state = state(); - if let Some(hover) = &mut state.is_hovered { - if animation_duration_ms == 0 || hover.animation_progress >= 1.0 - { - hover.animation_progress = 1.0; - } - if hover.animation_progress == 0.0 - && hover.direction == AnimationDirection::Backward - { - state.is_hovered = None; - } else { - match hover.direction { - AnimationDirection::Forward => { - hover.animation_progress = (hover.initial_progress - + (((now - hover.started_at).as_millis() - as f64) - / (animation_duration_ms as f64)) - as f32) - .clamp(0.0, 1.0); - } - AnimationDirection::Backward => { - hover.animation_progress = (hover.initial_progress - - (((now - hover.started_at).as_millis() - as f64) - / (animation_duration_ms as f64)) - as f32) - .clamp(0.0, 1.0); - } - } - } + if state + .hovered_animation + .on_redraw_request_update(animation_duration_ms, now) + { shell.request_redraw(window::RedrawRequest::NextFrame); } } @@ -396,30 +409,11 @@ pub fn update<'a, Message: Clone>( let bounds = layout.bounds(); let is_mouse_over = bounds.contains(position); - if is_mouse_over { - if let Some(hover) = &mut state.is_hovered { - if hover.direction == AnimationDirection::Backward { - hover.initial_progress = hover.animation_progress; - hover.direction = AnimationDirection::Forward; - hover.started_at = std::time::Instant::now(); - } - } - if state.is_hovered.is_none() { - state.is_hovered = Some(Hover { - direction: AnimationDirection::Forward, - started_at: std::time::Instant::now(), - animation_progress: 0.0, - initial_progress: 0.0, - }); - } + if state + .hovered_animation + .on_cursor_moved_update(is_mouse_over) + { shell.request_redraw(window::RedrawRequest::NextFrame); - } else if let Some(hover) = &mut state.is_hovered { - if hover.direction == AnimationDirection::Forward { - hover.initial_progress = hover.animation_progress; - hover.direction = AnimationDirection::Backward; - hover.started_at = std::time::Instant::now(); - shell.request_redraw(window::RedrawRequest::NextFrame); - } } } _ => {} @@ -437,20 +431,22 @@ pub fn draw<'a, Renderer: crate::core::Renderer>( Style = ::Style, >, style: &::Style, + cursor_position: Point, state: impl FnOnce() -> &'a State, ) -> Appearance where Renderer::Theme: StyleSheet, { let state = state(); + let is_hovered = bounds.contains(cursor_position); let styling = if !is_enabled { style_sheet.disabled(style) - } else if let Some(hover) = state.is_hovered { + } else if is_hovered || state.hovered_animation.is_running() { if state.is_pressed { style_sheet.pressed(style) } else { - style_sheet.hovered(style, Some(hover)) + style_sheet.hovered(style, &state.hovered_animation) } } else { style_sheet.active(style) From c7931f3c62bfbb09d52ccd8923a6081012e1722c Mon Sep 17 00:00:00 2001 From: LazyTanuki <43273245+lazytanuki@users.noreply.github.com> Date: Tue, 11 Apr 2023 22:40:03 +0200 Subject: [PATCH 05/12] wip: reducing diff --- style/src/button.rs | 5 +---- widget/src/button.rs | 8 +++----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/style/src/button.rs b/style/src/button.rs index af8dcc3656..03824519e9 100644 --- a/style/src/button.rs +++ b/style/src/button.rs @@ -1,9 +1,6 @@ //! Change the apperance of a button. - use iced_core::{Background, Color, Vector}; -use crate::animation::HoverAnimation; - /// The appearance of a button. #[derive(Debug, Clone, Copy)] pub struct Appearance { @@ -46,7 +43,7 @@ pub trait StyleSheet { fn hovered( &self, style: &Self::Style, - _hover: &HoverAnimation, + _hover: &crate::animation::HoverAnimation, ) -> Appearance { let active = self.active(style); diff --git a/widget/src/button.rs b/widget/src/button.rs index 0a9d60b032..f3cc062b73 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -16,9 +16,7 @@ use crate::core::{ }; use crate::core::window; -use iced_style::animation::{ - AnimationDirection, AnimationEffect, Fade, HoverAnimation, PressedAnimation, -}; +use iced_style::animation::{Fade, HoverAnimation, PressedAnimation}; pub use iced_style::button::{Appearance, StyleSheet}; /// A generic widget that produces a message when pressed. @@ -266,10 +264,10 @@ where let styling = draw( renderer, bounds, + cursor_position, self.on_press.is_some(), theme, &self.style, - cursor_position, || tree.state.downcast_ref::(), ); @@ -426,12 +424,12 @@ pub fn update<'a, Message: Clone>( pub fn draw<'a, Renderer: crate::core::Renderer>( renderer: &mut Renderer, bounds: Rectangle, + cursor_position: Point, is_enabled: bool, style_sheet: &dyn StyleSheet< Style = ::Style, >, style: &::Style, - cursor_position: Point, state: impl FnOnce() -> &'a State, ) -> Appearance where From 10efb7fe03e9a9fe9b2c5ca81aa492b47b56b82a Mon Sep 17 00:00:00 2001 From: LazyTanuki <43273245+lazytanuki@users.noreply.github.com> Date: Tue, 11 Apr 2023 23:12:43 +0200 Subject: [PATCH 06/12] wip: merged `HoverAnimation` and `PressedAnimation` into `HoverPressedAnimation` --- style/src/animation.rs | 37 ++++++++++++++----------------------- style/src/button.rs | 2 +- style/src/theme.rs | 3 ++- widget/src/button.rs | 39 +++++++++++++++++++++------------------ 4 files changed, 38 insertions(+), 43 deletions(-) diff --git a/style/src/animation.rs b/style/src/animation.rs index b48cdcfc77..30eafa471e 100644 --- a/style/src/animation.rs +++ b/style/src/animation.rs @@ -2,7 +2,7 @@ /// Hover animation of the widget #[derive(Debug, Clone, Copy, PartialEq, Default)] -pub struct HoverAnimation { +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) @@ -18,29 +18,11 @@ pub struct HoverAnimation { /// The type of effect for the animation #[derive(Debug, Clone, Copy, PartialEq, Default)] pub enum AnimationEffect { - /// The background color of the widget fades into the "hovered" color when hovered + /// The background color of the widget fades into the other color when hovered or pressed #[default] Fade, -} - -/// Hover animation of the widget -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum PressedAnimation { - /// The background color of the widget fades into the "pressed" color when pressed - Fade(Fade), -} - -#[derive(Debug, Clone, Copy, PartialEq, Default)] -/// Fade.. animation -pub struct Fade { - /// 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, - /// 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 background color of the widget instantly changes into the other color when hovered or pressed + None, } #[derive(Debug, Clone, Copy, PartialEq, Default)] @@ -53,7 +35,15 @@ pub enum AnimationDirection { Backward, } -impl HoverAnimation { +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() @@ -97,6 +87,7 @@ impl HoverAnimation { .clamp(0.0, 1.0); } }, + AnimationEffect::None => {} } } return true; diff --git a/style/src/button.rs b/style/src/button.rs index 03824519e9..ef0908ee90 100644 --- a/style/src/button.rs +++ b/style/src/button.rs @@ -43,7 +43,7 @@ pub trait StyleSheet { fn hovered( &self, style: &Self::Style, - _hover: &crate::animation::HoverAnimation, + _hover: &crate::animation::HoverPressedAnimation, ) -> Appearance { let active = self.active(style); diff --git a/style/src/theme.rs b/style/src/theme.rs index 84f85ff705..0a1b4c8c6a 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -173,7 +173,7 @@ impl button::StyleSheet for Theme { fn hovered( &self, style: &Self::Style, - hover_animation: &animation::HoverAnimation, + hover_animation: &animation::HoverPressedAnimation, ) -> button::Appearance { let palette = self.extended_palette(); @@ -204,6 +204,7 @@ impl button::StyleSheet for Theme { ); } }, + animation::AnimationEffect::None => {} } } diff --git a/widget/src/button.rs b/widget/src/button.rs index f3cc062b73..11a0731062 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -16,7 +16,7 @@ use crate::core::{ }; use crate::core::window; -use iced_style::animation::{Fade, HoverAnimation, PressedAnimation}; +use iced_style::animation::{AnimationEffect, HoverPressedAnimation}; pub use iced_style::button::{Appearance, StyleSheet}; /// A generic widget that produces a message when pressed. @@ -66,8 +66,8 @@ where padding: Padding, style: ::Style, animation_duration_ms: u16, - hover_animation: HoverAnimation, - pressed_animation: PressedAnimation, + hover_animation_effect: AnimationEffect, + pressed_animation_effect: AnimationEffect, } impl<'a, Message, Renderer> Button<'a, Message, Renderer> @@ -85,8 +85,8 @@ where padding: Padding::new(5.0), style: ::Style::default(), animation_duration_ms: 150, - hover_animation: HoverAnimation::default(), - pressed_animation: PressedAnimation::Fade(Fade::default()), + hover_animation_effect: AnimationEffect::Fade, + pressed_animation_effect: AnimationEffect::Fade, } } @@ -131,18 +131,21 @@ where self } - /// Sets the animation when hovering the [`Button`]. - pub fn hover_animation(mut self, hover_animation: HoverAnimation) -> Self { - self.hover_animation = hover_animation; + /// Sets the animation effect when hovering the [`Button`]. + pub fn hover_animation_effect( + mut self, + hover_animation_effect: AnimationEffect, + ) -> Self { + self.hover_animation_effect = hover_animation_effect; self } - /// Sets the animation when pressing the [`Button`]. - pub fn press_animation( + /// Sets the animation effect when pressing the [`Button`]. + pub fn pressed_animation_effect( mut self, - pressed_animation: PressedAnimation, + pressed_animation_effect: AnimationEffect, ) -> Self { - self.pressed_animation = pressed_animation; + self.pressed_animation_effect = pressed_animation_effect; self } } @@ -160,8 +163,8 @@ where fn state(&self) -> tree::State { tree::State::new(State::new( - self.hover_animation, - self.pressed_animation, + HoverPressedAnimation::new(self.hover_animation_effect), + HoverPressedAnimation::new(self.hover_animation_effect), )) } @@ -325,15 +328,15 @@ where #[derive(Debug, Clone, Copy, PartialEq)] pub struct State { is_pressed: bool, - hovered_animation: HoverAnimation, - pressed_animation: PressedAnimation, + hovered_animation: HoverPressedAnimation, + pressed_animation: HoverPressedAnimation, } impl State { /// Creates a new [`State`]. pub fn new( - hovered_animation: HoverAnimation, - pressed_animation: PressedAnimation, + hovered_animation: HoverPressedAnimation, + pressed_animation: HoverPressedAnimation, ) -> State { State { is_pressed: false, From 149f9196f369ee7a60df79ab55b2873160d0a656 Mon Sep 17 00:00:00 2001 From: LazyTanuki <43273245+lazytanuki@users.noreply.github.com> Date: Tue, 11 Apr 2023 23:51:00 +0200 Subject: [PATCH 07/12] wip: add button pressed animation --- style/src/animation.rs | 17 ++++++++++++++++- style/src/button.rs | 8 ++++++-- style/src/theme.rs | 37 +++++++++++++++++++++++++++++++++++-- widget/src/button.rs | 34 +++++++++++++++++++++++----------- 4 files changed, 80 insertions(+), 16 deletions(-) diff --git a/style/src/animation.rs b/style/src/animation.rs index 30eafa471e..a818a6dc5c 100644 --- a/style/src/animation.rs +++ b/style/src/animation.rs @@ -61,7 +61,7 @@ impl HoverPressedAnimation { self.animation_progress = 1.0; } - // Reset if ended + // Reset the animation once it has gone forward and now fully backward if self.animation_progress == 0.0 && self.direction == AnimationDirection::Backward { @@ -133,4 +133,19 @@ impl HoverPressedAnimation { 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; + } } diff --git a/style/src/button.rs b/style/src/button.rs index ef0908ee90..c11f51e382 100644 --- a/style/src/button.rs +++ b/style/src/button.rs @@ -43,7 +43,7 @@ pub trait StyleSheet { fn hovered( &self, style: &Self::Style, - _hover: &crate::animation::HoverPressedAnimation, + _hover_animation: &crate::animation::HoverPressedAnimation, ) -> Appearance { let active = self.active(style); @@ -54,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) diff --git a/style/src/theme.rs b/style/src/theme.rs index 0a1b4c8c6a..48f89164ea 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -214,13 +214,46 @@ impl button::StyleSheet for Theme { } } - fn pressed(&self, style: &Self::Style) -> button::Appearance { + fn pressed( + &self, + style: &Self::Style, + pressed_animation: &animation::HoverPressedAnimation, + ) -> button::Appearance { if let Button::Custom(custom) = style { - return custom.pressed(self); + return custom.pressed(self, pressed_animation); + } + let palette = self.extended_palette(); + + let active = self.active(style); + + let mut background = match style { + Button::Primary => Some(palette.primary.base.color), + Button::Secondary => Some(palette.background.strong.color), + Button::Positive => Some(palette.success.strong.color), + Button::Destructive => Some(palette.danger.strong.color), + Button::Text | Button::Custom(_) => None, + }; + + // Mix the hovered and active styles backgrounds according to the animation state + if let (Some(active_background), Some(background_color)) = + (active.background, background.as_mut()) + { + match pressed_animation.effect { + animation::AnimationEffect::Fade => match active_background { + Background::Color(active_background_color) => { + background_color.mix( + active_background_color, + pressed_animation.animation_progress, + ); + } + }, + animation::AnimationEffect::None => {} + } } button::Appearance { shadow_offset: Vector::default(), + background: background.map(Background::from), ..self.active(style) } } diff --git a/widget/src/button.rs b/widget/src/button.rs index 11a0731062..20bacd5ca1 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -84,9 +84,9 @@ where height: Length::Shrink, padding: Padding::new(5.0), style: ::Style::default(), - animation_duration_ms: 150, - hover_animation_effect: AnimationEffect::Fade, - pressed_animation_effect: AnimationEffect::Fade, + animation_duration_ms: 250, + hover_animation_effect: AnimationEffect::EaseOut, + pressed_animation_effect: AnimationEffect::EaseOut, } } @@ -367,6 +367,8 @@ pub fn update<'a, Message: Clone>( let state = state(); state.is_pressed = true; + state.pressed_animation.on_press(); + shell.request_redraw(window::RedrawRequest::NextFrame); return event::Status::Captured; } @@ -379,6 +381,8 @@ pub fn update<'a, Message: Clone>( if state.is_pressed { state.is_pressed = false; + state.pressed_animation.on_released(); + shell.request_redraw(window::RedrawRequest::NextFrame); let bounds = layout.bounds(); @@ -398,21 +402,29 @@ pub fn update<'a, Message: Clone>( Event::Window(window::Event::RedrawRequested(now)) => { let state = state(); - if state + if state.is_pressed || state.pressed_animation.is_running() { + if state + .pressed_animation + .on_redraw_request_update(animation_duration_ms, now) + { + shell.request_redraw(window::RedrawRequest::NextFrame); + } + } else if state .hovered_animation .on_redraw_request_update(animation_duration_ms, now) { shell.request_redraw(window::RedrawRequest::NextFrame); } } - Event::Mouse(mouse::Event::CursorMoved { position }) => { + Event::Mouse(mouse::Event::CursorMoved { position: _ }) => { let state = state(); let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(position); + let is_mouse_over = bounds.contains(cursor_position); - if state - .hovered_animation - .on_cursor_moved_update(is_mouse_over) + if !state.is_pressed + && state + .hovered_animation + .on_cursor_moved_update(is_mouse_over) { shell.request_redraw(window::RedrawRequest::NextFrame); } @@ -444,8 +456,8 @@ where let styling = if !is_enabled { style_sheet.disabled(style) } else if is_hovered || state.hovered_animation.is_running() { - if state.is_pressed { - style_sheet.pressed(style) + if state.is_pressed || state.pressed_animation.is_running() { + style_sheet.pressed(style, &state.pressed_animation) } else { style_sheet.hovered(style, &state.hovered_animation) } From b917870f0cc23e7a64c466de4fd7db4ae2bcafdb Mon Sep 17 00:00:00 2001 From: LazyTanuki <43273245+lazytanuki@users.noreply.github.com> Date: Wed, 12 Apr 2023 23:24:06 +0200 Subject: [PATCH 08/12] wip: add radio button hover and pressed animation --- style/src/animation.rs | 20 +++++++- style/src/radio.rs | 16 ++++++- style/src/theme.rs | 44 +++++++++++++++-- widget/src/button.rs | 10 ++-- widget/src/radio.rs | 106 +++++++++++++++++++++++++++++++++++++---- 5 files changed, 174 insertions(+), 22 deletions(-) diff --git a/style/src/animation.rs b/style/src/animation.rs index a818a6dc5c..fb5788ddce 100644 --- a/style/src/animation.rs +++ b/style/src/animation.rs @@ -49,6 +49,14 @@ impl HoverPressedAnimation { 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, @@ -134,7 +142,7 @@ impl HoverPressedAnimation { } } - /// Start the animation when pressed + /// Start the animation when pressed. pub fn on_press(&mut self) { self.started_at = Some(std::time::Instant::now()); self.direction = AnimationDirection::Forward; @@ -142,10 +150,18 @@ impl HoverPressedAnimation { self.initial_progress = 0.0; } - /// End the animation when released + /// 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; + } } diff --git a/style/src/radio.rs b/style/src/radio.rs index 06c49029a6..f00c626b6b 100644 --- a/style/src/radio.rs +++ b/style/src/radio.rs @@ -1,6 +1,8 @@ //! Change the appearance of radio buttons. use iced_core::{Background, Color}; +use crate::animation; + /// The appearance of a radio button. #[derive(Debug, Clone, Copy)] pub struct Appearance { @@ -22,8 +24,18 @@ pub trait StyleSheet { type Style: Default; /// Produces the active [`Appearance`] of a radio button. - fn active(&self, style: &Self::Style, is_selected: bool) -> Appearance; + fn active( + &self, + style: &Self::Style, + is_selected: bool, + pressed_animation: &animation::HoverPressedAnimation, + ) -> Appearance; /// Produces the hovered [`Appearance`] of a radio button. - fn hovered(&self, style: &Self::Style, is_selected: bool) -> Appearance; + fn hovered( + &self, + style: &Self::Style, + is_selected: bool, + hover_animation: &animation::HoverPressedAnimation, + ) -> Appearance; } diff --git a/style/src/theme.rs b/style/src/theme.rs index 48f89164ea..85c36125d7 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -640,20 +640,36 @@ impl radio::StyleSheet for Theme { &self, style: &Self::Style, is_selected: bool, + pressed_animation: &animation::HoverPressedAnimation, ) -> radio::Appearance { match style { Radio::Default => { let palette = self.extended_palette(); + let mut dot_color = palette.primary.strong.color; + + if is_selected { + match pressed_animation.effect { + animation::AnimationEffect::Fade => { + dot_color.mix( + Color::TRANSPARENT, + pressed_animation.animation_progress, + ); + } + animation::AnimationEffect::None => {} + } + } radio::Appearance { background: Color::TRANSPARENT.into(), - dot_color: palette.primary.strong.color, + dot_color, border_width: 1.0, border_color: palette.primary.strong.color, text_color: None, } } - Radio::Custom(custom) => custom.active(self, is_selected), + Radio::Custom(custom) => { + custom.active(self, is_selected, pressed_animation) + } } } @@ -661,19 +677,37 @@ impl radio::StyleSheet for Theme { &self, style: &Self::Style, is_selected: bool, + hover_animation: &animation::HoverPressedAnimation, ) -> radio::Appearance { match style { Radio::Default => { - let active = self.active(style, is_selected); + let active = self.active(style, is_selected, hover_animation); let palette = self.extended_palette(); + let mut background_color = palette.primary.weak.color; + + // Mix the hovered and active styles backgrounds according to the animation state + match hover_animation.effect { + animation::AnimationEffect::Fade => match active.background + { + Background::Color(active_background_color) => { + background_color.mix( + active_background_color, + 1.0 - hover_animation.animation_progress, + ); + } + }, + animation::AnimationEffect::None => {} + } radio::Appearance { dot_color: palette.primary.strong.color, - background: palette.primary.weak.color.into(), + background: background_color.into(), ..active } } - Radio::Custom(custom) => custom.hovered(self, is_selected), + Radio::Custom(custom) => { + custom.hovered(self, is_selected, hover_animation) + } } } } diff --git a/widget/src/button.rs b/widget/src/button.rs index 20bacd5ca1..62ee9398db 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -10,11 +10,11 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::Operation; +use crate::core::window; use crate::core::{ Background, Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, Shell, Vector, Widget, }; -use crate::core::window; use iced_style::animation::{AnimationEffect, HoverPressedAnimation}; pub use iced_style::button::{Appearance, StyleSheet}; @@ -85,8 +85,8 @@ where padding: Padding::new(5.0), style: ::Style::default(), animation_duration_ms: 250, - hover_animation_effect: AnimationEffect::EaseOut, - pressed_animation_effect: AnimationEffect::EaseOut, + hover_animation_effect: AnimationEffect::Fade, + pressed_animation_effect: AnimationEffect::Fade, } } @@ -451,11 +451,11 @@ where Renderer::Theme: StyleSheet, { let state = state(); - let is_hovered = bounds.contains(cursor_position); + let is_mouse_over = bounds.contains(cursor_position); let styling = if !is_enabled { style_sheet.disabled(style) - } else if is_hovered || state.hovered_animation.is_running() { + } else if is_mouse_over || state.hovered_animation.is_running() { if state.is_pressed || state.pressed_animation.is_running() { style_sheet.pressed(style, &state.pressed_animation) } else { diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 9dad1e22a2..9a047796fb 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -6,13 +6,15 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::text; use crate::core::touch; -use crate::core::widget::Tree; +use crate::core::widget::{self, Tree}; +use crate::core::window; use crate::core::{ Alignment, Clipboard, Color, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Widget, }; use crate::{Row, Text}; +use iced_style::animation::{AnimationEffect, HoverPressedAnimation}; pub use iced_style::radio::{Appearance, StyleSheet}; /// A circular button representing a choice. @@ -85,6 +87,8 @@ where text_shaping: text::Shaping, font: Option, style: ::Style, + animation_duration_ms: u16, + hover_animation_effect: AnimationEffect, } impl Radio @@ -129,6 +133,8 @@ where text_shaping: text::Shaping::Basic, font: None, style: Default::default(), + animation_duration_ms: 250, + hover_animation_effect: AnimationEffect::Fade, } } @@ -201,6 +207,13 @@ where Length::Shrink } + fn state(&self) -> widget::tree::State { + widget::tree::State::new(State::new( + HoverPressedAnimation::new(self.hover_animation_effect), + HoverPressedAnimation::new(self.hover_animation_effect), + )) + } + fn layout( &self, renderer: &Renderer, @@ -226,7 +239,7 @@ where fn on_event( &mut self, - _state: &mut Tree, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -238,11 +251,47 @@ where Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { if layout.bounds().contains(cursor_position) { + let state = tree.state.downcast_mut::(); + + state.hovered_animation.reset(); + state.pressed_animation.on_activate(); + shell.request_redraw(window::RedrawRequest::NextFrame); + shell.publish(self.on_click.clone()); return event::Status::Captured; } } + Event::Window(window::Event::RedrawRequested(now)) => { + let state = tree.state.downcast_mut::(); + + if self.is_selected && state.pressed_animation.is_running() { + if state.pressed_animation.on_redraw_request_update( + self.animation_duration_ms, + now, + ) { + shell.request_redraw(window::RedrawRequest::NextFrame); + } + } else if state + .hovered_animation + .on_redraw_request_update(self.animation_duration_ms, now) + { + shell.request_redraw(window::RedrawRequest::NextFrame); + } + } + Event::Mouse(mouse::Event::CursorMoved { position }) => { + let state = tree.state.downcast_mut::(); + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(position); + + if !self.is_selected + && state + .hovered_animation + .on_cursor_moved_update(is_mouse_over) + { + shell.request_redraw(window::RedrawRequest::NextFrame); + } + } _ => {} } @@ -266,7 +315,7 @@ where fn draw( &self, - _state: &Tree, + tree: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, @@ -274,16 +323,37 @@ where cursor_position: Point, _viewport: &Rectangle, ) { + let state = tree.state.downcast_ref::(); + let style_sheet: &dyn StyleSheet< + Style = ::Style, + > = theme; let bounds = layout.bounds(); let is_mouse_over = bounds.contains(cursor_position); let mut children = layout.children(); - let custom_style = if is_mouse_over { - theme.hovered(&self.style, self.is_selected) - } else { - theme.active(&self.style, self.is_selected) - }; + let custom_style = + if is_mouse_over || state.hovered_animation.is_running() { + if self.is_selected || state.pressed_animation.is_running() { + style_sheet.active( + &self.style, + self.is_selected, + &state.pressed_animation, + ) + } else { + style_sheet.hovered( + &self.style, + self.is_selected, + &state.hovered_animation, + ) + } + } else { + theme.active( + &self.style, + self.is_selected, + &state.pressed_animation, + ) + }; { let layout = children.next().unwrap(); @@ -342,6 +412,26 @@ where } } +/// The local state of a [`Radio`]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct State { + hovered_animation: HoverPressedAnimation, + pressed_animation: HoverPressedAnimation, +} + +impl State { + /// Creates a new [`State`]. + pub fn new( + hovered_animation: HoverPressedAnimation, + pressed_animation: HoverPressedAnimation, + ) -> State { + State { + hovered_animation, + pressed_animation, + } + } +} + impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> where From 99c575fc5eba2982bd22e6c5e05e039862563eb9 Mon Sep 17 00:00:00 2001 From: LazyTanuki <43273245+lazytanuki@users.noreply.github.com> Date: Sun, 16 Apr 2023 13:42:51 +0200 Subject: [PATCH 09/12] wip: add toggler animation --- style/src/theme.rs | 26 ++++++++++++--- style/src/toggler.rs | 14 ++++++-- widget/src/toggler.rs | 77 +++++++++++++++++++++++++++++++++++++------ 3 files changed, 100 insertions(+), 17 deletions(-) diff --git a/style/src/theme.rs b/style/src/theme.rs index 85c36125d7..c8ff960607 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -729,6 +729,7 @@ impl toggler::StyleSheet for Theme { &self, style: &Self::Style, is_active: bool, + pressed_animation: &crate::animation::HoverPressedAnimation, ) -> toggler::Appearance { match style { Toggler::Default => { @@ -736,9 +737,19 @@ impl toggler::StyleSheet for Theme { toggler::Appearance { background: if is_active { - palette.primary.strong.color + let mut color = palette.background.strong.color; + color.mix( + palette.primary.strong.color, + pressed_animation.animation_progress, + ); + color } else { - palette.background.strong.color + let mut color = palette.primary.strong.color; + color.mix( + palette.background.strong.color, + 1.0 - pressed_animation.animation_progress, + ); + color }, background_border: None, foreground: if is_active { @@ -749,7 +760,9 @@ impl toggler::StyleSheet for Theme { foreground_border: None, } } - Toggler::Custom(custom) => custom.active(self, is_active), + Toggler::Custom(custom) => { + custom.active(self, is_active, pressed_animation) + } } } @@ -757,6 +770,7 @@ impl toggler::StyleSheet for Theme { &self, style: &Self::Style, is_active: bool, + pressed_animation: &crate::animation::HoverPressedAnimation, ) -> toggler::Appearance { match style { Toggler::Default => { @@ -771,10 +785,12 @@ impl toggler::StyleSheet for Theme { } else { palette.background.weak.color }, - ..self.active(style, is_active) + ..self.active(style, is_active, pressed_animation) } } - Toggler::Custom(custom) => custom.hovered(self, is_active), + Toggler::Custom(custom) => { + custom.hovered(self, is_active, pressed_animation) + } } } } diff --git a/style/src/toggler.rs b/style/src/toggler.rs index abc73f2a25..8a34189d03 100644 --- a/style/src/toggler.rs +++ b/style/src/toggler.rs @@ -22,10 +22,20 @@ pub trait StyleSheet { /// Returns the active [`Appearance`] of the toggler for the provided [`Style`]. /// /// [`Style`]: Self::Style - fn active(&self, style: &Self::Style, is_active: bool) -> Appearance; + fn active( + &self, + style: &Self::Style, + is_active: bool, + _pressed_animation: &crate::animation::HoverPressedAnimation, + ) -> Appearance; /// Returns the hovered [`Appearance`] of the toggler for the provided [`Style`]. /// /// [`Style`]: Self::Style - fn hovered(&self, style: &Self::Style, is_active: bool) -> Appearance; + fn hovered( + &self, + style: &Self::Style, + is_active: bool, + _pressed_animation: &crate::animation::HoverPressedAnimation, + ) -> Appearance; } diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index b1ba65c95b..ec6b7ba5d3 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -5,13 +5,16 @@ use crate::core::layout; use crate::core::mouse; use crate::core::renderer; use crate::core::text; -use crate::core::widget::Tree; +use crate::core::widget::{self, Tree}; +use crate::core::window; use crate::core::{ Alignment, Clipboard, Element, Event, Layout, Length, Pixels, Point, Rectangle, Shell, Widget, }; use crate::{Row, Text}; +use crate::style::animation::AnimationEffect; +use crate::style::animation::HoverPressedAnimation; pub use crate::style::toggler::{Appearance, StyleSheet}; /// A toggler widget. @@ -48,6 +51,8 @@ where spacing: f32, font: Option, style: ::Style, + animation_duration: u16, + pressed_animation_effect: AnimationEffect, } impl<'a, Message, Renderer> Toggler<'a, Message, Renderer> @@ -87,6 +92,8 @@ where spacing: 0.0, font: None, style: Default::default(), + animation_duration: 250, + pressed_animation_effect: AnimationEffect::Fade, } } @@ -167,6 +174,19 @@ where Length::Shrink } + fn tag(&self) -> widget::tree::Tag { + widget::tree::Tag::of::() + } + + fn state(&self) -> widget::tree::State { + let mut pressed_animation = + HoverPressedAnimation::new(self.pressed_animation_effect); + if self.is_toggled { + pressed_animation.animation_progress = 1.0; + } + widget::tree::State::new(State::new(pressed_animation)) + } + fn layout( &self, renderer: &Renderer, @@ -199,7 +219,7 @@ where fn on_event( &mut self, - _state: &mut Tree, + tree: &mut Tree, event: Event, layout: Layout<'_>, cursor_position: Point, @@ -212,6 +232,14 @@ where let mouse_over = layout.bounds().contains(cursor_position); if mouse_over { + let state = tree.state.downcast_mut::(); + + if !self.is_toggled { + state.pressed_animation.on_press(); + } else { + state.pressed_animation.on_released(); + } + shell.request_redraw(window::RedrawRequest::NextFrame); shell.publish((self.on_toggle)(!self.is_toggled)); event::Status::Captured @@ -219,6 +247,18 @@ where event::Status::Ignored } } + Event::Window(window::Event::RedrawRequested(now)) => { + let state = tree.state.downcast_mut::(); + + if state.pressed_animation.is_running() + && state + .pressed_animation + .on_redraw_request_update(self.animation_duration, now) + { + shell.request_redraw(window::RedrawRequest::NextFrame); + } + event::Status::Captured + } _ => event::Status::Ignored, } } @@ -240,7 +280,7 @@ where fn draw( &self, - _state: &Tree, + tree: &Tree, renderer: &mut Renderer, theme: &Renderer::Theme, style: &renderer::Style, @@ -248,6 +288,8 @@ where cursor_position: Point, _viewport: &Rectangle, ) { + let state = tree.state.downcast_ref::(); + /// Makes sure that the border radius of the toggler looks good at every size. const BORDER_RADIUS_RATIO: f32 = 32.0 / 13.0; @@ -281,9 +323,13 @@ where let is_mouse_over = bounds.contains(cursor_position); let style = if is_mouse_over { - theme.hovered(&self.style, self.is_toggled) + theme.hovered( + &self.style, + self.is_toggled, + &state.pressed_animation, + ) } else { - theme.active(&self.style, self.is_toggled) + theme.active(&self.style, self.is_toggled, &state.pressed_animation) }; let border_radius = bounds.height / BORDER_RADIUS_RATIO; @@ -310,11 +356,9 @@ where let toggler_foreground_bounds = Rectangle { x: bounds.x - + if self.is_toggled { - bounds.width - 2.0 * space - (bounds.height - (4.0 * space)) - } else { - 2.0 * space - }, + + (2.0 * space + + (state.pressed_animation.animation_progress + * (bounds.width - bounds.height))), y: bounds.y + (2.0 * space), width: bounds.height - (4.0 * space), height: bounds.height - (4.0 * space), @@ -347,3 +391,16 @@ where Element::new(toggler) } } + +/// The local state of a [`Toggler`]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct State { + pressed_animation: HoverPressedAnimation, +} + +impl State { + /// Creates a new [`State`]. + pub fn new(pressed_animation: HoverPressedAnimation) -> State { + State { pressed_animation } + } +} From 62d14c0c942d1d7e3c36fa1a25564711732d84cb Mon Sep 17 00:00:00 2001 From: LazyTanuki <43273245+lazytanuki@users.noreply.github.com> Date: Sun, 16 Apr 2023 16:34:09 +0200 Subject: [PATCH 10/12] wip: rename `Fade` to `Linear` and add `EaseOut` cubic transition effect --- style/src/animation.rs | 64 ++++++++++++++++++++++++++++++------------ style/src/theme.rs | 55 +++++++++++++++++++++--------------- widget/src/button.rs | 4 +-- widget/src/radio.rs | 2 +- widget/src/toggler.rs | 2 +- 5 files changed, 82 insertions(+), 45 deletions(-) diff --git a/style/src/animation.rs b/style/src/animation.rs index fb5788ddce..d04a2d2bea 100644 --- a/style/src/animation.rs +++ b/style/src/animation.rs @@ -18,10 +18,12 @@ pub struct HoverPressedAnimation { /// The type of effect for the animation #[derive(Debug, Clone, Copy, PartialEq, Default)] pub enum AnimationEffect { - /// The background color of the widget fades into the other color when hovered or pressed + /// Transition is linear. #[default] - Fade, - /// The background color of the widget instantly changes into the other color when hovered or pressed + Linear, + /// Transition is a cubic ease out. + EaseOut, + /// Transistion is instantaneous. None, } @@ -77,24 +79,44 @@ impl HoverPressedAnimation { } else { // Evaluate new progress match &mut self.effect { - AnimationEffect::Fade => match self.direction { - AnimationDirection::Forward => { - let progress_since_start = - ((now - started_at).as_millis() as f64) - / (animation_duration_ms as f64); - self.animation_progress = (self.initial_progress - + progress_since_start as f32) - .clamp(0.0, 1.0); + 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); + } } - AnimationDirection::Backward => { - let progress_since_start = - ((now - started_at).as_millis() as f64) - / (animation_duration_ms as f64); - self.animation_progress = (self.initial_progress - - progress_since_start as f32) + } + 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 => {} } } @@ -165,3 +187,9 @@ impl HoverPressedAnimation { 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 +} diff --git a/style/src/theme.rs b/style/src/theme.rs index c8ff960607..5f37650490 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -196,14 +196,17 @@ impl button::StyleSheet for Theme { (active.background, background.as_mut()) { match hover_animation.effect { - animation::AnimationEffect::Fade => match active_background { - Background::Color(active_background_color) => { - background_color.mix( - active_background_color, - 1.0 - hover_animation.animation_progress, - ); + animation::AnimationEffect::Linear + | animation::AnimationEffect::EaseOut => { + match active_background { + Background::Color(active_background_color) => { + background_color.mix( + active_background_color, + 1.0 - hover_animation.animation_progress, + ); + } } - }, + } animation::AnimationEffect::None => {} } } @@ -239,14 +242,17 @@ impl button::StyleSheet for Theme { (active.background, background.as_mut()) { match pressed_animation.effect { - animation::AnimationEffect::Fade => match active_background { - Background::Color(active_background_color) => { - background_color.mix( - active_background_color, - pressed_animation.animation_progress, - ); + animation::AnimationEffect::Linear + | animation::AnimationEffect::EaseOut => { + match active_background { + Background::Color(active_background_color) => { + background_color.mix( + active_background_color, + pressed_animation.animation_progress, + ); + } } - }, + } animation::AnimationEffect::None => {} } } @@ -649,7 +655,8 @@ impl radio::StyleSheet for Theme { if is_selected { match pressed_animation.effect { - animation::AnimationEffect::Fade => { + animation::AnimationEffect::Linear + | animation::AnimationEffect::EaseOut => { dot_color.mix( Color::TRANSPARENT, pressed_animation.animation_progress, @@ -687,15 +694,17 @@ impl radio::StyleSheet for Theme { // Mix the hovered and active styles backgrounds according to the animation state match hover_animation.effect { - animation::AnimationEffect::Fade => match active.background - { - Background::Color(active_background_color) => { - background_color.mix( - active_background_color, - 1.0 - hover_animation.animation_progress, - ); + animation::AnimationEffect::Linear + | animation::AnimationEffect::EaseOut => { + match active.background { + Background::Color(active_background_color) => { + background_color.mix( + active_background_color, + 1.0 - hover_animation.animation_progress, + ); + } } - }, + } animation::AnimationEffect::None => {} } diff --git a/widget/src/button.rs b/widget/src/button.rs index 62ee9398db..ca029bab8f 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -85,8 +85,8 @@ where padding: Padding::new(5.0), style: ::Style::default(), animation_duration_ms: 250, - hover_animation_effect: AnimationEffect::Fade, - pressed_animation_effect: AnimationEffect::Fade, + hover_animation_effect: AnimationEffect::EaseOut, + pressed_animation_effect: AnimationEffect::EaseOut, } } diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 9a047796fb..95330427e5 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -134,7 +134,7 @@ where font: None, style: Default::default(), animation_duration_ms: 250, - hover_animation_effect: AnimationEffect::Fade, + hover_animation_effect: AnimationEffect::EaseOut, } } diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index ec6b7ba5d3..b463351608 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -93,7 +93,7 @@ where font: None, style: Default::default(), animation_duration: 250, - pressed_animation_effect: AnimationEffect::Fade, + pressed_animation_effect: AnimationEffect::EaseOut, } } From 300872d50eee0b0362e5189e53668d7c0ca13dee Mon Sep 17 00:00:00 2001 From: LazyTanuki <43273245+lazytanuki@users.noreply.github.com> Date: Fri, 12 May 2023 22:42:50 +0200 Subject: [PATCH 11/12] wip: add cursor animation --- widget/src/text_input.rs | 178 ++++++++++++++++++++++++++------ widget/src/text_input/cursor.rs | 10 ++ 2 files changed, 159 insertions(+), 29 deletions(-) diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index bbc07dac07..65b16ec364 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -7,6 +7,7 @@ mod value; pub mod cursor; pub use cursor::Cursor; +use iced_style::animation::AnimationDirection; pub use value::Value; use editor::Editor; @@ -32,6 +33,8 @@ use crate::runtime::Command; pub use iced_style::text_input::{Appearance, StyleSheet}; +use self::cursor::CursorAnimation; + /// A field that can be filled with text. /// /// # Example @@ -74,6 +77,7 @@ where on_submit: Option, icon: Option>, style: ::Style, + cursor_animation_style: CursorAnimation, } impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> @@ -103,6 +107,7 @@ where on_submit: None, icon: None, style: Default::default(), + cursor_animation_style: Default::default(), } } @@ -197,6 +202,15 @@ where self } + /// Sets the animation style of the [`Cursor`]. + pub fn set_cursor_animation_style( + mut self, + cursor_animation_style: CursorAnimation, + ) -> Self { + self.cursor_animation_style = cursor_animation_style; + self + } + /// Draws the [`TextInput`] with the given [`Renderer`], overriding its /// [`Value`] if provided. /// @@ -225,6 +239,7 @@ where self.is_secure, self.icon.as_ref(), &self.style, + self.cursor_animation_style, ) } } @@ -319,6 +334,7 @@ where self.on_paste.as_deref(), &self.on_submit, || tree.state.downcast_mut::(), + self.cursor_animation_style, ) } @@ -347,6 +363,7 @@ where self.is_secure, self.icon.as_ref(), &self.style, + self.cursor_animation_style, ) } @@ -541,6 +558,7 @@ pub fn update<'a, Message, Renderer>( on_paste: Option<&dyn Fn(String) -> Message>, on_submit: &Option, state: impl FnOnce() -> &'a mut State, + cursor_animation_style: CursorAnimation, ) -> event::Status where Message: Clone, @@ -699,6 +717,8 @@ where focus.updated_at = Instant::now(); + reset_cursor(cursor_animation_style, state); + return event::Status::Captured; } } @@ -712,6 +732,8 @@ where let modifiers = state.keyboard_modifiers; focus.updated_at = Instant::now(); + reset_cursor(cursor_animation_style, state); + match key_code { keyboard::KeyCode::Enter | keyboard::KeyCode::NumpadEnter => { @@ -921,13 +943,73 @@ where if let Some(focus) = &mut state.is_focused { focus.now = now; - let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS - - (now - focus.updated_at).as_millis() - % CURSOR_BLINK_INTERVAL_MILLIS; - - shell.request_redraw(window::RedrawRequest::At( - now + Duration::from_millis(millis_until_redraw as u64), - )); + match cursor_animation_style { + CursorAnimation::Blink => { + let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS + - (now - focus.updated_at).as_millis() + % CURSOR_BLINK_INTERVAL_MILLIS; + + shell.request_redraw(window::RedrawRequest::At( + now + Duration::from_millis( + millis_until_redraw as u64, + ), + )); + } + CursorAnimation::Fade => { + if let Some(last_direction_change) = + &mut state.cursor_animation_last_direction_change + { + match state.cursor_animation_direction { + AnimationDirection::Forward => { + let interval_ms = (now + - *last_direction_change) + .as_millis() + as f32; + let opacity_diff = interval_ms + / (CURSOR_FADE_INTERVAL_MILLIS as f32); + state.cursor_opacity = opacity_diff; + if state.cursor_opacity >= 1.0 { + state.cursor_animation_direction = + AnimationDirection::Backward; + state.cursor_opacity = 1.0; + *last_direction_change = now; + } + } + AnimationDirection::Backward => { + let interval_ms = (now + - *last_direction_change) + .as_millis() + as f32; + + // Make a pause at full opacity + if interval_ms + > CURSOR_FADE_INTERVAL_MILLIS_PAUSE + { + let opacity_diff = (interval_ms + - CURSOR_FADE_INTERVAL_MILLIS_PAUSE) + / (CURSOR_FADE_INTERVAL_MILLIS + as f32); + state.cursor_opacity = + 1.0 - opacity_diff; + } + if state.cursor_opacity <= 0.0 { + state.cursor_animation_direction = + AnimationDirection::Forward; + state.cursor_opacity = 0.0; + *last_direction_change = now; + } + } + } + } else { + state.cursor_animation_last_direction_change = + Some(now); + state.cursor_opacity = 1.0; + state.cursor_animation_direction = + AnimationDirection::Backward; + } + shell.request_redraw(window::RedrawRequest::NextFrame); + } + } } } _ => {} @@ -936,6 +1018,14 @@ where event::Status::Ignored } +fn reset_cursor(cursor_animation_style: CursorAnimation, state: &mut State) { + if let CursorAnimation::Fade = cursor_animation_style { + state.cursor_animation_direction = AnimationDirection::Backward; + state.cursor_opacity = 1.0; + state.cursor_animation_last_direction_change = None; + } +} + /// Draws the [`TextInput`] with the given [`Renderer`], overriding its /// [`Value`] if provided. /// @@ -955,6 +1045,7 @@ pub fn draw( is_secure: bool, icon: Option<&Icon>, style: &::Style, + cursor_animation_style: CursorAnimation, ) where Renderer: text::Renderer, Renderer::Theme: StyleSheet, @@ -1025,29 +1116,50 @@ pub fn draw( font, ); - let is_cursor_visible = ((focus.now - focus.updated_at) - .as_millis() - / CURSOR_BLINK_INTERVAL_MILLIS) - % 2 - == 0; - - let cursor = if is_cursor_visible { - Some(( - renderer::Quad { - bounds: Rectangle { - x: text_bounds.x + text_value_width, - y: text_bounds.y, - width: 1.0, - height: text_bounds.height, + let cursor = match cursor_animation_style { + CursorAnimation::Blink => { + // Check if cursor is visible + if ((focus.now - focus.updated_at).as_millis() + / CURSOR_BLINK_INTERVAL_MILLIS) + % 2 + == 0 + { + Some(( + renderer::Quad { + bounds: Rectangle { + x: text_bounds.x + text_value_width, + y: text_bounds.y, + width: 1.0, + height: text_bounds.height, + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + theme.value_color(style), + )) + } else { + None + } + } + CursorAnimation::Fade => { + let mut color = theme.value_color(style); + color.a = state.cursor_opacity; + Some(( + renderer::Quad { + bounds: Rectangle { + x: text_bounds.x + text_value_width, + y: text_bounds.y, + width: 1.0, + height: text_bounds.height, + }, + border_radius: 0.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, }, - border_radius: 0.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - theme.value_color(style), - )) - } else { - None + color, + )) + } }; (cursor, offset) @@ -1177,6 +1289,9 @@ pub struct State { last_click: Option, cursor: Cursor, keyboard_modifiers: keyboard::Modifiers, + cursor_opacity: f32, + cursor_animation_direction: AnimationDirection, + cursor_animation_last_direction_change: Option, // TODO: Add stateful horizontal scrolling offset } @@ -1201,6 +1316,9 @@ impl State { last_click: None, cursor: Cursor::default(), keyboard_modifiers: keyboard::Modifiers::default(), + cursor_opacity: 1.0, + cursor_animation_direction: AnimationDirection::Backward, + cursor_animation_last_direction_change: None, } } @@ -1399,3 +1517,5 @@ where } const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500; +const CURSOR_FADE_INTERVAL_MILLIS: u128 = 600; +const CURSOR_FADE_INTERVAL_MILLIS_PAUSE: f32 = 400.0; diff --git a/widget/src/text_input/cursor.rs b/widget/src/text_input/cursor.rs index 9680dfd746..782f94bd8d 100644 --- a/widget/src/text_input/cursor.rs +++ b/widget/src/text_input/cursor.rs @@ -7,6 +7,16 @@ pub struct Cursor { state: State, } +/// The animation styles for the [`Cursor`]. +#[derive(Debug, Clone, Copy, Default)] +pub enum CursorAnimation { + /// Blink the cursor (visible then not visible in a loop) + Blink, + /// Fade from visible to invisible in a loop + #[default] + Fade, +} + /// The state of a [`Cursor`]. #[derive(Debug, Copy, Clone)] pub enum State { From 96af0305b9c76d1bf153fe904736bb0a8c6e6083 Mon Sep 17 00:00:00 2001 From: LazyTanuki <43273245+lazytanuki@users.noreply.github.com> Date: Fri, 26 May 2023 19:38:23 +0200 Subject: [PATCH 12/12] wip: adding example --- examples/animations/Cargo.toml | 11 +++++ examples/animations/README.md | 8 +++ examples/animations/src/main.rs | 88 +++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+) create mode 100644 examples/animations/Cargo.toml create mode 100644 examples/animations/README.md create mode 100644 examples/animations/src/main.rs diff --git a/examples/animations/Cargo.toml b/examples/animations/Cargo.toml new file mode 100644 index 0000000000..54785cf917 --- /dev/null +++ b/examples/animations/Cargo.toml @@ -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"] } diff --git a/examples/animations/README.md b/examples/animations/README.md new file mode 100644 index 0000000000..2a4e12b3b9 --- /dev/null +++ b/examples/animations/README.md @@ -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/ diff --git a/examples/animations/src/main.rs b/examples/animations/src/main.rs new file mode 100644 index 0000000000..5bf5c6f434 --- /dev/null +++ b/examples/animations/src/main.rs @@ -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, + radio2: Option, + 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 { + 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() + } +}