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

feat: animations for default widgets #2483

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
1 change: 1 addition & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ web-time.workspace = true

dark-light.workspace = true
dark-light.optional = true
lilt = "0.6.0"

[dev-dependencies]
approx = "0.5"
50 changes: 50 additions & 0 deletions core/src/animations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//! Helpers for internal animations for a [`Widget`], mainly for style transitions.

/// The duration and enabled state of an animation.
#[derive(Debug, Clone)]
pub struct AnimationDuration {
/// Duration, in milliseconds, for the animation.
duration_ms: f32,
/// If disabled, transition is instantaneous.
enabled: bool,
}

impl AnimationDuration {
/// Create a [`AnimationDuration`] with the given duration in milliseconds.
pub fn new(duration_ms: f32) -> Self {
Self {
duration_ms,
enabled: true,
}
}

/// Get the duration, in milliseconds, of the animation.
/// If `enabled` is set to `false`, duration is 0.0.
pub fn get(&self) -> f32 {
if self.enabled {
self.duration_ms
} else {
0.
}
}

/// Set the duration, in milliseconds, of the animation.
pub fn set(&mut self, duration_ms: f32) {
self.duration_ms = duration_ms;
}

/// Disable the animation, making it instantaneous.
pub fn disable(&mut self) {
self.enabled = false;
}

/// Enable the animation.
pub fn enable(&mut self) {
self.enabled = true;
}

/// Is the animation enabled
pub fn enabled(&self) -> bool {
self.enabled
}
}
1 change: 1 addition & 0 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
)]
pub mod alignment;
pub mod animations;
pub mod border;
pub mod clipboard;
pub mod event;
Expand Down
6 changes: 4 additions & 2 deletions core/src/theme/palette.rs
Original file line number Diff line number Diff line change
Expand Up @@ -594,15 +594,17 @@ fn lighten(color: Color, amount: f32) -> Color {
from_hsl(hsl)
}

fn deviate(color: Color, amount: f32) -> Color {
/// Lighten dark colors and darken light ones by the specefied amount.
pub fn deviate(color: Color, amount: f32) -> Color {
if is_dark(color) {
lighten(color, amount)
} else {
darken(color, amount)
}
}

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

Expand Down
19 changes: 17 additions & 2 deletions core/src/widget/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub trait Operation<T>: Send {
_state: &mut dyn Scrollable,
_id: Option<&Id>,
_bounds: Rectangle,
_content_bounds: Rectangle,
_translation: Vector,
) {
}
Expand Down Expand Up @@ -127,9 +128,16 @@ where
state: &mut dyn Scrollable,
id: Option<&Id>,
bounds: Rectangle,
content_bounds: Rectangle,
translation: Vector,
) {
self.operation.scrollable(state, id, bounds, translation);
self.operation.scrollable(
state,
id,
bounds,
content_bounds,
translation,
);
}

fn focusable(
Expand Down Expand Up @@ -170,9 +178,16 @@ where
state: &mut dyn Scrollable,
id: Option<&Id>,
bounds: Rectangle,
content_bounds: Rectangle,
translation: Vector,
) {
self.operation.scrollable(state, id, bounds, translation);
self.operation.scrollable(
state,
id,
bounds,
content_bounds,
translation,
);
}

fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
Expand Down
13 changes: 10 additions & 3 deletions core/src/widget/operation/scrollable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ pub trait Scrollable {
fn snap_to(&mut self, offset: RelativeOffset);

/// Scroll the widget to the given [`AbsoluteOffset`] along the horizontal & vertical axis.
fn scroll_to(&mut self, offset: AbsoluteOffset);
fn scroll_to(
&mut self,
offset: AbsoluteOffset,
bounds: Rectangle,
content_bounds: Rectangle,
);
}

/// Produces an [`Operation`] that snaps the widget with the given [`Id`] to
Expand All @@ -34,6 +39,7 @@ pub fn snap_to<T>(target: Id, offset: RelativeOffset) -> impl Operation<T> {
state: &mut dyn Scrollable,
id: Option<&Id>,
_bounds: Rectangle,
_content_bounds: Rectangle,
_translation: Vector,
) {
if Some(&self.target) == id {
Expand Down Expand Up @@ -67,11 +73,12 @@ pub fn scroll_to<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
&mut self,
state: &mut dyn Scrollable,
id: Option<&Id>,
_bounds: Rectangle,
bounds: Rectangle,
content_bounds: Rectangle,
_translation: Vector,
) {
if Some(&self.target) == id {
state.scroll_to(self.offset);
state.scroll_to(self.offset, bounds, content_bounds);
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions examples/animations/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "animations"
version = "0.1.0"
authors = ["LazyTanuki"]
edition = "2021"
publish = false

[dependencies]
iced = { path = "../..", features = ["debug"] }
once_cell.workspace = true
12 changes: 12 additions & 0 deletions examples/animations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Animations

An application to showcase Iced widgets that have default animations.

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

You can run it with `cargo run`:
```
cargo run --package animations
```

[`main`]: src/main.rs
136 changes: 136 additions & 0 deletions examples/animations/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
use iced::{
widget::{
button, checkbox, column, container, row, scrollable, text, text_input,
Toggler,
},
Element, Length, Task, Theme,
};
use once_cell::sync::Lazy;
use std::fmt::Write;

pub fn main() -> iced::Result {
iced::application("Animated widgets", Animations::update, Animations::view)
.theme(Animations::theme)
.run()
}

#[derive(Default)]
struct Animations {
input_text: String,
toggled: bool,
checked: bool,
animations_disabled: bool,
}

#[derive(Debug, Clone)]
enum Message {
ButtonPressed,
TextSubmitted,
TextChanged(String),
Toggle(bool),
Check(bool),
DisableAnimations(bool),
GoToStart,
GoToEnd,
}

static SCROLLABLE_TEXT: Lazy<String> = Lazy::new(|| {
(0..50).fold(String::new(), |mut output, i| {
let _ = writeln!(
output,
"This is a long text string to test the scrollbar: {i}\n"
);
output
})
});
static SCROLLABLE_ID: Lazy<scrollable::Id> = Lazy::new(scrollable::Id::unique);

impl Animations {
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::TextChanged(txt) => {
self.input_text = txt;
Task::none()
}
Message::Toggle(t) => {
self.toggled = t;
Task::none()
}
Message::TextSubmitted | Message::ButtonPressed => Task::none(),
Message::Check(c) => {
self.checked = c;
Task::none()
}
Message::DisableAnimations(b) => {
self.animations_disabled = b;
Task::none()
}
Message::GoToStart => scrollable::snap_to(
SCROLLABLE_ID.clone(),
scrollable::RelativeOffset::START,
),
Message::GoToEnd => scrollable::snap_to(
SCROLLABLE_ID.clone(),
scrollable::RelativeOffset::END,
),
}
}

fn view(&self) -> Element<Message> {
column![
text_input("Insert some text here...", &self.input_text)
.on_submit(Message::TextSubmitted)
.on_input(Message::TextChanged),
row![
button("Primary")
.on_press(Message::ButtonPressed)
.style(button::primary)
.set_animations_enabled(!self.animations_disabled),
button("Secondary")
.on_press(Message::ButtonPressed)
.style(button::secondary)
.set_animations_enabled(!self.animations_disabled),
button("Success")
.on_press(Message::ButtonPressed)
.style(button::success)
.set_animations_enabled(!self.animations_disabled),
button("Danger")
.on_press(Message::ButtonPressed)
.style(button::danger)
.set_animations_enabled(!self.animations_disabled),
container(Toggler::new(
Some("Disable buttons animations".into()),
self.animations_disabled,
Message::DisableAnimations,
))
.padding(5)
],
button("Text")
.on_press(Message::ButtonPressed)
.style(button::text),
checkbox("Check me", self.checked)
.on_toggle(|t| { Message::Check(t) }),
Toggler::new(Some("Toggle me".into()), self.toggled, |t| {
Message::Toggle(t)
}),
scrollable(
container(column![
button("Go to end").on_press(Message::GoToEnd),
text(SCROLLABLE_TEXT.as_str()),
button("Go to start").on_press(Message::GoToStart)
])
.width(Length::Fill)
.padding(5)
)
.id(SCROLLABLE_ID.clone())
]
.spacing(10)
.padding(50)
.max_width(800)
.into()
}

fn theme(&self) -> Theme {
Theme::Light
}
}
2 changes: 2 additions & 0 deletions widget/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,5 @@ iced_highlighter.optional = true

url.workspace = true
url.optional = true

lilt = "0.6.0"
Loading