Skip to content

Commit 220901e

Browse files
committed
Animate Toggler
1 parent 81ca3d2 commit 220901e

File tree

4 files changed

+114
-22
lines changed

4 files changed

+114
-22
lines changed

core/src/animation.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,17 @@ where
101101
self.raw.transition(new_state, Instant::now());
102102
}
103103

104+
/// Instantaneously transitions the [`Animation`] from its current state to the given new state.
105+
pub fn force(mut self, new_state: T) -> Self {
106+
self.force_mut(new_state);
107+
self
108+
}
109+
110+
/// Instantaneously transitions the [`Animation`] from its current state to the given new state, by reference.
111+
pub fn force_mut(&mut self, new_state: T) {
112+
self.raw.transition_instantaneous(new_state, Instant::now());
113+
}
114+
104115
/// Returns true if the [`Animation`] is currently in progress.
105116
///
106117
/// An [`Animation`] is in progress when it is transitioning to a different state.

core/src/theme/palette.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -623,15 +623,17 @@ fn lighten(color: Color, amount: f32) -> Color {
623623
from_hsl(hsl)
624624
}
625625

626-
fn deviate(color: Color, amount: f32) -> Color {
626+
/// Lighten dark colors and darken light ones by the specefied amount.
627+
pub fn deviate(color: Color, amount: f32) -> Color {
627628
if is_dark(color) {
628629
lighten(color, amount)
629630
} else {
630631
darken(color, amount)
631632
}
632633
}
633634

634-
fn mix(a: Color, b: Color, factor: f32) -> Color {
635+
/// Mix with another color with the given ratio (from 0 to 1)
636+
pub fn mix(a: Color, b: Color, factor: f32) -> Color {
635637
let a_lin = Rgb::from(a).into_linear();
636638
let b_lin = Rgb::from(b).into_linear();
637639

widget/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ wgpu = ["iced_renderer/wgpu"]
2727
markdown = ["dep:pulldown-cmark", "dep:url"]
2828
highlighter = ["dep:iced_highlighter"]
2929
advanced = []
30+
animations = []
3031

3132
[dependencies]
3233
iced_renderer.workspace = true

widget/src/toggler.rs

Lines changed: 98 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,18 @@
3131
//! }
3232
//! ```
3333
use crate::core::alignment;
34+
use crate::core::animation::Easing;
3435
use crate::core::layout;
3536
use crate::core::mouse;
3637
use crate::core::renderer;
3738
use crate::core::text;
39+
use crate::core::theme::palette::mix;
40+
use crate::core::time::Instant;
3841
use crate::core::touch;
3942
use crate::core::widget;
4043
use crate::core::widget::tree::{self, Tree};
4144
use crate::core::window;
45+
use crate::core::Animation;
4246
use crate::core::{
4347
Border, Clipboard, Color, Element, Event, Layout, Length, Pixels,
4448
Rectangle, Shell, Size, Theme, Widget,
@@ -102,6 +106,28 @@ pub struct Toggler<
102106
last_status: Option<Status>,
103107
}
104108

109+
/// The state of the [`Toggler`]
110+
#[derive(Debug)]
111+
pub struct State<Paragraph>
112+
where
113+
Paragraph: text::Paragraph,
114+
{
115+
now: Instant,
116+
transition: Animation<bool>,
117+
text_state: widget::text::State<Paragraph>,
118+
}
119+
120+
impl<Paragraph> State<Paragraph>
121+
where
122+
Paragraph: text::Paragraph,
123+
{
124+
/// This check is meant to fix cases when we get a tainted state from another
125+
/// ['Toggler'] widget by finding impossible cases.
126+
fn is_animation_state_tainted(&self, is_toggled: bool) -> bool {
127+
is_toggled != self.transition.value()
128+
}
129+
}
130+
105131
impl<'a, Message, Theme, Renderer> Toggler<'a, Message, Theme, Renderer>
106132
where
107133
Theme: Catalog,
@@ -256,7 +282,11 @@ where
256282
}
257283

258284
fn state(&self) -> tree::State {
259-
tree::State::new(widget::text::State::<Renderer::Paragraph>::default())
285+
tree::State::new(State {
286+
now: Instant::now(),
287+
transition: Animation::new(self.is_toggled).easing(Easing::EaseOut),
288+
text_state: widget::text::State::<Renderer::Paragraph>::default(),
289+
})
260290
}
261291

262292
fn size(&self) -> Size<Length> {
@@ -280,12 +310,11 @@ where
280310
|_| layout::Node::new(Size::new(2.0 * self.size, self.size)),
281311
|limits| {
282312
if let Some(label) = self.label.as_deref() {
283-
let state = tree
284-
.state
285-
.downcast_mut::<widget::text::State<Renderer::Paragraph>>();
313+
let state =
314+
tree.state.downcast_mut::<State<Renderer::Paragraph>>();
286315

287316
widget::text::layout(
288-
state,
317+
&mut state.text_state,
289318
renderer,
290319
limits,
291320
self.width,
@@ -308,7 +337,7 @@ where
308337

309338
fn update(
310339
&mut self,
311-
_state: &mut Tree,
340+
tree: &mut Tree,
312341
event: &Event,
313342
layout: Layout<'_>,
314343
cursor: mouse::Cursor,
@@ -327,26 +356,50 @@ where
327356
let mouse_over = cursor.is_over(layout.bounds());
328357

329358
if mouse_over {
359+
let state =
360+
tree.state.downcast_mut::<State<Renderer::Paragraph>>();
361+
if cfg!(feature = "animations") {
362+
state.transition.go_mut(!self.is_toggled);
363+
} else {
364+
state.transition.force_mut(!self.is_toggled);
365+
}
366+
shell.request_redraw();
330367
shell.publish(on_toggle(!self.is_toggled));
331368
shell.capture_event();
332369
}
333370
}
334371
_ => {}
335372
}
336373

374+
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
375+
376+
let animation_progress =
377+
state.transition.interpolate(0.0, 1.0, Instant::now());
337378
let current_status = if self.on_toggle.is_none() {
338379
Status::Disabled
339380
} else if cursor.is_over(layout.bounds()) {
340381
Status::Hovered {
341382
is_toggled: self.is_toggled,
383+
animation_progress,
342384
}
343385
} else {
344386
Status::Active {
345387
is_toggled: self.is_toggled,
388+
animation_progress,
346389
}
347390
};
348391

349-
if let Event::Window(window::Event::RedrawRequested(_now)) = event {
392+
if let Event::Window(window::Event::RedrawRequested(now)) = event {
393+
state.now = *now;
394+
395+
// Reset animation on tainted state
396+
if state.is_animation_state_tainted(self.is_toggled) {
397+
state.transition.force_mut(self.is_toggled);
398+
}
399+
400+
if state.transition.is_animating(*now) {
401+
shell.request_redraw();
402+
}
350403
self.last_status = Some(current_status);
351404
} else if self
352405
.last_status
@@ -394,11 +447,14 @@ where
394447

395448
let mut children = layout.children();
396449
let toggler_layout = children.next().unwrap();
450+
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
397451

398452
if self.label.is_some() {
399453
let label_layout = children.next().unwrap();
400-
let state: &widget::text::State<Renderer::Paragraph> =
401-
tree.state.downcast_ref();
454+
let state: &widget::text::State<Renderer::Paragraph> = &tree
455+
.state
456+
.downcast_ref::<State<Renderer::Paragraph>>()
457+
.text_state;
402458

403459
crate::text::draw(
404460
renderer,
@@ -437,13 +493,10 @@ where
437493
style.background,
438494
);
439495

496+
let x_ratio = state.transition.interpolate(0.0, 1.0, state.now);
440497
let toggler_foreground_bounds = Rectangle {
441498
x: bounds.x
442-
+ if self.is_toggled {
443-
bounds.width - 2.0 * space - (bounds.height - (4.0 * space))
444-
} else {
445-
2.0 * space
446-
},
499+
+ (2.0 * space + (x_ratio * (bounds.width - bounds.height))),
447500
y: bounds.y + (2.0 * space),
448501
width: bounds.height - (4.0 * space),
449502
height: bounds.height - (4.0 * space),
@@ -479,17 +532,21 @@ where
479532
}
480533

481534
/// The possible status of a [`Toggler`].
482-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
535+
#[derive(Debug, Clone, Copy, PartialEq)]
483536
pub enum Status {
484537
/// The [`Toggler`] can be interacted with.
485538
Active {
486539
/// Indicates whether the [`Toggler`] is toggled.
487540
is_toggled: bool,
541+
/// Current progress of the transition animation
542+
animation_progress: f32,
488543
},
489544
/// The [`Toggler`] is being hovered.
490545
Hovered {
491546
/// Indicates whether the [`Toggler`] is toggled.
492547
is_toggled: bool,
548+
/// Current progress of the transition animation
549+
animation_progress: f32,
493550
},
494551
/// The [`Toggler`] is disabled.
495552
Disabled,
@@ -546,25 +603,46 @@ pub fn default(theme: &Theme, status: Status) -> Style {
546603
let palette = theme.extended_palette();
547604

548605
let background = match status {
549-
Status::Active { is_toggled } | Status::Hovered { is_toggled } => {
606+
Status::Active {
607+
is_toggled,
608+
animation_progress,
609+
}
610+
| Status::Hovered {
611+
is_toggled,
612+
animation_progress,
613+
} => {
550614
if is_toggled {
551-
palette.primary.strong.color
615+
mix(
616+
palette.primary.strong.color,
617+
palette.background.strong.color,
618+
1.0 - animation_progress,
619+
)
552620
} else {
553-
palette.background.strong.color
621+
mix(
622+
palette.background.strong.color,
623+
palette.primary.strong.color,
624+
animation_progress,
625+
)
554626
}
555627
}
556628
Status::Disabled => palette.background.weak.color,
557629
};
558630

559631
let foreground = match status {
560-
Status::Active { is_toggled } => {
632+
Status::Active {
633+
is_toggled,
634+
animation_progress: _,
635+
} => {
561636
if is_toggled {
562637
palette.primary.strong.text
563638
} else {
564639
palette.background.base.color
565640
}
566641
}
567-
Status::Hovered { is_toggled } => {
642+
Status::Hovered {
643+
is_toggled,
644+
animation_progress: _,
645+
} => {
568646
if is_toggled {
569647
Color {
570648
a: 0.5,

0 commit comments

Comments
 (0)