Merge pull request #1902 from nicksenger/loading-spinners
Example loading spinners
This commit is contained in:
commit
2d2ed4048c
6 changed files with 1019 additions and 0 deletions
11
examples/loading_spinners/Cargo.toml
Normal file
11
examples/loading_spinners/Cargo.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "loading_spinners"
|
||||
version = "0.1.0"
|
||||
authors = ["Nick Senger <dev@nsenger.com>"]
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced = { path = "../..", features = ["advanced", "canvas"] }
|
||||
lyon_algorithms = "1"
|
||||
once_cell = "1"
|
||||
14
examples/loading_spinners/README.md
Normal file
14
examples/loading_spinners/README.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
## Loading Spinners
|
||||
|
||||
Example implementation of animated indeterminate loading spinners.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://gfycat.com/importantdevotedhammerheadbird">
|
||||
<img src="https://thumbs.gfycat.com/ImportantDevotedHammerheadbird-small.gif">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
You can run it with `cargo run`:
|
||||
```
|
||||
cargo run --package loading_spinners
|
||||
```
|
||||
417
examples/loading_spinners/src/circular.rs
Normal file
417
examples/loading_spinners/src/circular.rs
Normal file
|
|
@ -0,0 +1,417 @@
|
|||
//! Show a circular progress indicator.
|
||||
use iced::advanced::layout;
|
||||
use iced::advanced::renderer;
|
||||
use iced::advanced::widget::tree::{self, Tree};
|
||||
use iced::advanced::{Clipboard, Layout, Renderer, Shell, Widget};
|
||||
use iced::event;
|
||||
use iced::time::Instant;
|
||||
use iced::widget::canvas;
|
||||
use iced::window::{self, RedrawRequest};
|
||||
use iced::{Background, Color, Element, Rectangle};
|
||||
use iced::{Event, Length, Point, Size, Vector};
|
||||
|
||||
use super::easing::{self, Easing};
|
||||
|
||||
use std::f32::consts::PI;
|
||||
use std::time::Duration;
|
||||
|
||||
const MIN_RADIANS: f32 = PI / 8.0;
|
||||
const WRAP_RADIANS: f32 = 2.0 * PI - PI / 4.0;
|
||||
const BASE_ROTATION_SPEED: u32 = u32::MAX / 80;
|
||||
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Circular<'a, Theme>
|
||||
where
|
||||
Theme: StyleSheet,
|
||||
{
|
||||
size: f32,
|
||||
bar_height: f32,
|
||||
style: <Theme as StyleSheet>::Style,
|
||||
easing: &'a Easing,
|
||||
cycle_duration: Duration,
|
||||
rotation_duration: Duration,
|
||||
}
|
||||
|
||||
impl<'a, Theme> Circular<'a, Theme>
|
||||
where
|
||||
Theme: StyleSheet,
|
||||
{
|
||||
/// Creates a new [`Circular`] with the given content.
|
||||
pub fn new() -> Self {
|
||||
Circular {
|
||||
size: 40.0,
|
||||
bar_height: 4.0,
|
||||
style: <Theme as StyleSheet>::Style::default(),
|
||||
easing: &easing::STANDARD,
|
||||
cycle_duration: Duration::from_millis(600),
|
||||
rotation_duration: Duration::from_secs(2),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the size of the [`Circular`].
|
||||
pub fn size(mut self, size: f32) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the bar height of the [`Circular`].
|
||||
pub fn bar_height(mut self, bar_height: f32) -> Self {
|
||||
self.bar_height = bar_height;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style variant of this [`Circular`].
|
||||
pub fn style(mut self, style: <Theme as StyleSheet>::Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the easing of this [`Circular`].
|
||||
pub fn easing(mut self, easing: &'a Easing) -> Self {
|
||||
self.easing = easing;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the cycle duration of this [`Circular`].
|
||||
pub fn cycle_duration(mut self, duration: Duration) -> Self {
|
||||
self.cycle_duration = duration / 2;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the base rotation duration of this [`Circular`]. This is the duration that a full
|
||||
/// rotation would take if the cycle rotation were set to 0.0 (no expanding or contracting)
|
||||
pub fn rotation_duration(mut self, duration: Duration) -> Self {
|
||||
self.rotation_duration = duration;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Theme> Default for Circular<'a, Theme>
|
||||
where
|
||||
Theme: StyleSheet,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Animation {
|
||||
Expanding {
|
||||
start: Instant,
|
||||
progress: f32,
|
||||
rotation: u32,
|
||||
last: Instant,
|
||||
},
|
||||
Contracting {
|
||||
start: Instant,
|
||||
progress: f32,
|
||||
rotation: u32,
|
||||
last: Instant,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for Animation {
|
||||
fn default() -> Self {
|
||||
Self::Expanding {
|
||||
start: Instant::now(),
|
||||
progress: 0.0,
|
||||
rotation: 0,
|
||||
last: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Animation {
|
||||
fn next(&self, additional_rotation: u32, now: Instant) -> Self {
|
||||
match self {
|
||||
Self::Expanding { rotation, .. } => Self::Contracting {
|
||||
start: now,
|
||||
progress: 0.0,
|
||||
rotation: rotation.wrapping_add(additional_rotation),
|
||||
last: now,
|
||||
},
|
||||
Self::Contracting { rotation, .. } => Self::Expanding {
|
||||
start: now,
|
||||
progress: 0.0,
|
||||
rotation: rotation.wrapping_add(
|
||||
BASE_ROTATION_SPEED.wrapping_add(
|
||||
((WRAP_RADIANS / (2.0 * PI)) * u32::MAX as f32) as u32,
|
||||
),
|
||||
),
|
||||
last: now,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn start(&self) -> Instant {
|
||||
match self {
|
||||
Self::Expanding { start, .. } | Self::Contracting { start, .. } => {
|
||||
*start
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn last(&self) -> Instant {
|
||||
match self {
|
||||
Self::Expanding { last, .. } | Self::Contracting { last, .. } => {
|
||||
*last
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn timed_transition(
|
||||
&self,
|
||||
cycle_duration: Duration,
|
||||
rotation_duration: Duration,
|
||||
now: Instant,
|
||||
) -> Self {
|
||||
let elapsed = now.duration_since(self.start());
|
||||
let additional_rotation = ((now - self.last()).as_secs_f32()
|
||||
/ rotation_duration.as_secs_f32()
|
||||
* (u32::MAX) as f32) as u32;
|
||||
|
||||
match elapsed {
|
||||
elapsed if elapsed > cycle_duration => {
|
||||
self.next(additional_rotation, now)
|
||||
}
|
||||
_ => self.with_elapsed(
|
||||
cycle_duration,
|
||||
additional_rotation,
|
||||
elapsed,
|
||||
now,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_elapsed(
|
||||
&self,
|
||||
cycle_duration: Duration,
|
||||
additional_rotation: u32,
|
||||
elapsed: Duration,
|
||||
now: Instant,
|
||||
) -> Self {
|
||||
let progress = elapsed.as_secs_f32() / cycle_duration.as_secs_f32();
|
||||
match self {
|
||||
Self::Expanding {
|
||||
start, rotation, ..
|
||||
} => Self::Expanding {
|
||||
start: *start,
|
||||
progress,
|
||||
rotation: rotation.wrapping_add(additional_rotation),
|
||||
last: now,
|
||||
},
|
||||
Self::Contracting {
|
||||
start, rotation, ..
|
||||
} => Self::Contracting {
|
||||
start: *start,
|
||||
progress,
|
||||
rotation: rotation.wrapping_add(additional_rotation),
|
||||
last: now,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn rotation(&self) -> f32 {
|
||||
match self {
|
||||
Self::Expanding { rotation, .. }
|
||||
| Self::Contracting { rotation, .. } => {
|
||||
*rotation as f32 / u32::MAX as f32
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
animation: Animation,
|
||||
cache: canvas::Cache,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme> Widget<Message, iced::Renderer<Theme>>
|
||||
for Circular<'a, Theme>
|
||||
where
|
||||
Message: 'a + Clone,
|
||||
Theme: StyleSheet,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::default())
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
Length::Fixed(self.size)
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
Length::Fixed(self.size)
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_renderer: &iced::Renderer<Theme>,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits.width(self.size).height(self.size);
|
||||
let size = limits.resolve(Size::ZERO);
|
||||
|
||||
layout::Node::new(size)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
_layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
_renderer: &iced::Renderer<Theme>,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
const FRAME_RATE: u64 = 60;
|
||||
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
||||
if let Event::Window(window::Event::RedrawRequested(now)) = event {
|
||||
state.animation = state.animation.timed_transition(
|
||||
self.cycle_duration,
|
||||
self.rotation_duration,
|
||||
now,
|
||||
);
|
||||
|
||||
state.cache.clear();
|
||||
shell.request_redraw(RedrawRequest::At(
|
||||
now + Duration::from_millis(1000 / FRAME_RATE),
|
||||
));
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut iced::Renderer<Theme>,
|
||||
theme: &Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
let bounds = layout.bounds();
|
||||
let custom_style =
|
||||
<Theme as StyleSheet>::appearance(theme, &self.style);
|
||||
|
||||
let geometry = state.cache.draw(renderer, bounds.size(), |frame| {
|
||||
let track_radius = frame.width() / 2.0 - self.bar_height;
|
||||
let track_path = canvas::Path::circle(frame.center(), track_radius);
|
||||
|
||||
frame.stroke(
|
||||
&track_path,
|
||||
canvas::Stroke::default()
|
||||
.with_color(custom_style.track_color)
|
||||
.with_width(self.bar_height),
|
||||
);
|
||||
|
||||
let mut builder = canvas::path::Builder::new();
|
||||
|
||||
let start = state.animation.rotation() * 2.0 * PI;
|
||||
|
||||
match state.animation {
|
||||
Animation::Expanding { progress, .. } => {
|
||||
builder.arc(canvas::path::Arc {
|
||||
center: frame.center(),
|
||||
radius: track_radius,
|
||||
start_angle: start,
|
||||
end_angle: start
|
||||
+ MIN_RADIANS
|
||||
+ WRAP_RADIANS * (self.easing.y_at_x(progress)),
|
||||
});
|
||||
}
|
||||
Animation::Contracting { progress, .. } => {
|
||||
builder.arc(canvas::path::Arc {
|
||||
center: frame.center(),
|
||||
radius: track_radius,
|
||||
start_angle: start
|
||||
+ WRAP_RADIANS * (self.easing.y_at_x(progress)),
|
||||
end_angle: start + MIN_RADIANS + WRAP_RADIANS,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let bar_path = builder.build();
|
||||
|
||||
frame.stroke(
|
||||
&bar_path,
|
||||
canvas::Stroke::default()
|
||||
.with_color(custom_style.bar_color)
|
||||
.with_width(self.bar_height),
|
||||
);
|
||||
});
|
||||
|
||||
renderer.with_translation(
|
||||
Vector::new(bounds.x, bounds.y),
|
||||
|renderer| {
|
||||
renderer.draw_primitive(geometry.0);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme> From<Circular<'a, Theme>>
|
||||
for Element<'a, Message, iced::Renderer<Theme>>
|
||||
where
|
||||
Message: Clone + 'a,
|
||||
Theme: StyleSheet + 'a,
|
||||
{
|
||||
fn from(circular: Circular<'a, Theme>) -> Self {
|
||||
Self::new(circular)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Appearance {
|
||||
/// The [`Background`] of the progress indicator.
|
||||
pub background: Option<Background>,
|
||||
/// The track [`Color`] of the progress indicator.
|
||||
pub track_color: Color,
|
||||
/// The bar [`Color`] of the progress indicator.
|
||||
pub bar_color: Color,
|
||||
}
|
||||
|
||||
impl std::default::Default for Appearance {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
background: None,
|
||||
track_color: Color::TRANSPARENT,
|
||||
bar_color: Color::BLACK,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of rules that dictate the style of an indicator.
|
||||
pub trait StyleSheet {
|
||||
/// The supported style of the [`StyleSheet`].
|
||||
type Style: Default;
|
||||
|
||||
/// Produces the active [`Appearance`] of a indicator.
|
||||
fn appearance(&self, style: &Self::Style) -> Appearance;
|
||||
}
|
||||
|
||||
impl StyleSheet for iced::Theme {
|
||||
type Style = ();
|
||||
|
||||
fn appearance(&self, _style: &Self::Style) -> Appearance {
|
||||
let palette = self.extended_palette();
|
||||
|
||||
Appearance {
|
||||
background: None,
|
||||
track_color: palette.background.weak.color,
|
||||
bar_color: palette.primary.base.color,
|
||||
}
|
||||
}
|
||||
}
|
||||
133
examples/loading_spinners/src/easing.rs
Normal file
133
examples/loading_spinners/src/easing.rs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
use iced::Point;
|
||||
|
||||
use lyon_algorithms::measure::PathMeasurements;
|
||||
use lyon_algorithms::path::{builder::NoAttributes, path::BuilderImpl, Path};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
pub static EMPHASIZED: Lazy<Easing> = Lazy::new(|| {
|
||||
Easing::builder()
|
||||
.cubic_bezier_to([0.05, 0.0], [0.133333, 0.06], [0.166666, 0.4])
|
||||
.cubic_bezier_to([0.208333, 0.82], [0.25, 1.0], [1.0, 1.0])
|
||||
.build()
|
||||
});
|
||||
|
||||
pub static EMPHASIZED_DECELERATE: Lazy<Easing> = Lazy::new(|| {
|
||||
Easing::builder()
|
||||
.cubic_bezier_to([0.05, 0.7], [0.1, 1.0], [1.0, 1.0])
|
||||
.build()
|
||||
});
|
||||
|
||||
pub static EMPHASIZED_ACCELERATE: Lazy<Easing> = Lazy::new(|| {
|
||||
Easing::builder()
|
||||
.cubic_bezier_to([0.3, 0.0], [0.8, 0.15], [1.0, 1.0])
|
||||
.build()
|
||||
});
|
||||
|
||||
pub static STANDARD: Lazy<Easing> = Lazy::new(|| {
|
||||
Easing::builder()
|
||||
.cubic_bezier_to([0.2, 0.0], [0.0, 1.0], [1.0, 1.0])
|
||||
.build()
|
||||
});
|
||||
|
||||
pub static STANDARD_DECELERATE: Lazy<Easing> = Lazy::new(|| {
|
||||
Easing::builder()
|
||||
.cubic_bezier_to([0.0, 0.0], [0.0, 1.0], [1.0, 1.0])
|
||||
.build()
|
||||
});
|
||||
|
||||
pub static STANDARD_ACCELERATE: Lazy<Easing> = Lazy::new(|| {
|
||||
Easing::builder()
|
||||
.cubic_bezier_to([0.3, 0.0], [1.0, 1.0], [1.0, 1.0])
|
||||
.build()
|
||||
});
|
||||
|
||||
pub struct Easing {
|
||||
path: Path,
|
||||
measurements: PathMeasurements,
|
||||
}
|
||||
|
||||
impl Easing {
|
||||
pub fn builder() -> Builder {
|
||||
Builder::new()
|
||||
}
|
||||
|
||||
pub fn y_at_x(&self, x: f32) -> f32 {
|
||||
let mut sampler = self.measurements.create_sampler(
|
||||
&self.path,
|
||||
lyon_algorithms::measure::SampleType::Normalized,
|
||||
);
|
||||
let sample = sampler.sample(x);
|
||||
|
||||
sample.position().y
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Builder(NoAttributes<BuilderImpl>);
|
||||
|
||||
impl Builder {
|
||||
pub fn new() -> Self {
|
||||
let mut builder = Path::builder();
|
||||
builder.begin(lyon_algorithms::geom::point(0.0, 0.0));
|
||||
|
||||
Self(builder)
|
||||
}
|
||||
|
||||
/// Adds a line segment. Points must be between 0,0 and 1,1
|
||||
pub fn line_to(mut self, to: impl Into<Point>) -> Self {
|
||||
self.0.line_to(Self::point(to));
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a quadratic bézier curve. Points must be between 0,0 and 1,1
|
||||
pub fn quadratic_bezier_to(
|
||||
mut self,
|
||||
ctrl: impl Into<Point>,
|
||||
to: impl Into<Point>,
|
||||
) -> Self {
|
||||
self.0
|
||||
.quadratic_bezier_to(Self::point(ctrl), Self::point(to));
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a cubic bézier curve. Points must be between 0,0 and 1,1
|
||||
pub fn cubic_bezier_to(
|
||||
mut self,
|
||||
ctrl1: impl Into<Point>,
|
||||
ctrl2: impl Into<Point>,
|
||||
to: impl Into<Point>,
|
||||
) -> Self {
|
||||
self.0.cubic_bezier_to(
|
||||
Self::point(ctrl1),
|
||||
Self::point(ctrl2),
|
||||
Self::point(to),
|
||||
);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(mut self) -> Easing {
|
||||
self.0.line_to(lyon_algorithms::geom::point(1.0, 1.0));
|
||||
self.0.end(false);
|
||||
|
||||
let path = self.0.build();
|
||||
let measurements = PathMeasurements::from_path(&path, 0.0);
|
||||
|
||||
Easing { path, measurements }
|
||||
}
|
||||
|
||||
fn point(p: impl Into<Point>) -> lyon_algorithms::geom::Point<f32> {
|
||||
let p: Point = p.into();
|
||||
lyon_algorithms::geom::point(
|
||||
p.x.min(1.0).max(0.0),
|
||||
p.y.min(1.0).max(0.0),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Builder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
326
examples/loading_spinners/src/linear.rs
Normal file
326
examples/loading_spinners/src/linear.rs
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
//! Show a linear progress indicator.
|
||||
use iced::advanced::layout;
|
||||
use iced::advanced::renderer::{self, Quad};
|
||||
use iced::advanced::widget::tree::{self, Tree};
|
||||
use iced::advanced::{Clipboard, Layout, Shell, Widget};
|
||||
use iced::event;
|
||||
use iced::time::Instant;
|
||||
use iced::window::{self, RedrawRequest};
|
||||
use iced::{Background, Color, Element, Rectangle};
|
||||
use iced::{Event, Length, Point, Size};
|
||||
|
||||
use super::easing::{self, Easing};
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Linear<'a, Renderer>
|
||||
where
|
||||
Renderer: iced::advanced::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
width: Length,
|
||||
height: Length,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
easing: &'a Easing,
|
||||
cycle_duration: Duration,
|
||||
}
|
||||
|
||||
impl<'a, Renderer> Linear<'a, Renderer>
|
||||
where
|
||||
Renderer: iced::advanced::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
/// Creates a new [`Linear`] with the given content.
|
||||
pub fn new() -> Self {
|
||||
Linear {
|
||||
width: Length::Fixed(100.0),
|
||||
height: Length::Fixed(4.0),
|
||||
style: <Renderer::Theme as StyleSheet>::Style::default(),
|
||||
easing: &easing::STANDARD,
|
||||
cycle_duration: Duration::from_millis(600),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Linear`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Linear`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style variant of this [`Linear`].
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: <Renderer::Theme as StyleSheet>::Style,
|
||||
) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the motion easing of this [`Linear`].
|
||||
pub fn easing(mut self, easing: &'a Easing) -> Self {
|
||||
self.easing = easing;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the cycle duration of this [`Linear`].
|
||||
pub fn cycle_duration(mut self, duration: Duration) -> Self {
|
||||
self.cycle_duration = duration / 2;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Renderer> Default for Linear<'a, Renderer>
|
||||
where
|
||||
Renderer: iced::advanced::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum State {
|
||||
Expanding { start: Instant, progress: f32 },
|
||||
Contracting { start: Instant, progress: f32 },
|
||||
}
|
||||
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
Self::Expanding {
|
||||
start: Instant::now(),
|
||||
progress: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn next(&self, now: Instant) -> Self {
|
||||
match self {
|
||||
Self::Expanding { .. } => Self::Contracting {
|
||||
start: now,
|
||||
progress: 0.0,
|
||||
},
|
||||
Self::Contracting { .. } => Self::Expanding {
|
||||
start: now,
|
||||
progress: 0.0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn start(&self) -> Instant {
|
||||
match self {
|
||||
Self::Expanding { start, .. } | Self::Contracting { start, .. } => {
|
||||
*start
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn timed_transition(&self, cycle_duration: Duration, now: Instant) -> Self {
|
||||
let elapsed = now.duration_since(self.start());
|
||||
|
||||
match elapsed {
|
||||
elapsed if elapsed > cycle_duration => self.next(now),
|
||||
_ => self.with_elapsed(cycle_duration, elapsed),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_elapsed(
|
||||
&self,
|
||||
cycle_duration: Duration,
|
||||
elapsed: Duration,
|
||||
) -> Self {
|
||||
let progress = elapsed.as_secs_f32() / cycle_duration.as_secs_f32();
|
||||
match self {
|
||||
Self::Expanding { start, .. } => Self::Expanding {
|
||||
start: *start,
|
||||
progress,
|
||||
},
|
||||
Self::Contracting { start, .. } => Self::Contracting {
|
||||
start: *start,
|
||||
progress,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Widget<Message, Renderer> for Linear<'a, Renderer>
|
||||
where
|
||||
Message: 'a + Clone,
|
||||
Renderer: 'a + iced::advanced::Renderer,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::default())
|
||||
}
|
||||
|
||||
fn width(&self) -> Length {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> Length {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits.width(self.width).height(self.height);
|
||||
let size = limits.resolve(Size::ZERO);
|
||||
|
||||
layout::Node::new(size)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
_layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
const FRAME_RATE: u64 = 60;
|
||||
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
||||
if let Event::Window(window::Event::RedrawRequested(now)) = event {
|
||||
*state = state.timed_transition(self.cycle_duration, now);
|
||||
|
||||
shell.request_redraw(RedrawRequest::At(
|
||||
now + Duration::from_millis(1000 / FRAME_RATE),
|
||||
));
|
||||
}
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Renderer::Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
_cursor_position: Point,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let bounds = layout.bounds();
|
||||
let custom_style = theme.appearance(&self.style);
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
width: bounds.width,
|
||||
height: bounds.height,
|
||||
},
|
||||
border_radius: 0.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
Background::Color(custom_style.track_color),
|
||||
);
|
||||
|
||||
match state {
|
||||
State::Expanding { progress, .. } => renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: Rectangle {
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
width: self.easing.y_at_x(*progress) * bounds.width,
|
||||
height: bounds.height,
|
||||
},
|
||||
border_radius: 0.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
Background::Color(custom_style.bar_color),
|
||||
),
|
||||
|
||||
State::Contracting { progress, .. } => renderer.fill_quad(
|
||||
Quad {
|
||||
bounds: Rectangle {
|
||||
x: bounds.x
|
||||
+ self.easing.y_at_x(*progress) * bounds.width,
|
||||
y: bounds.y,
|
||||
width: (1.0 - self.easing.y_at_x(*progress))
|
||||
* bounds.width,
|
||||
height: bounds.height,
|
||||
},
|
||||
border_radius: 0.0.into(),
|
||||
border_width: 0.0,
|
||||
border_color: Color::TRANSPARENT,
|
||||
},
|
||||
Background::Color(custom_style.bar_color),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> From<Linear<'a, Renderer>>
|
||||
for Element<'a, Message, Renderer>
|
||||
where
|
||||
Message: Clone + 'a,
|
||||
Renderer: iced::advanced::Renderer + 'a,
|
||||
Renderer::Theme: StyleSheet,
|
||||
{
|
||||
fn from(linear: Linear<'a, Renderer>) -> Self {
|
||||
Self::new(linear)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Appearance {
|
||||
/// The track [`Color`] of the progress indicator.
|
||||
pub track_color: Color,
|
||||
/// The bar [`Color`] of the progress indicator.
|
||||
pub bar_color: Color,
|
||||
}
|
||||
|
||||
impl std::default::Default for Appearance {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
track_color: Color::TRANSPARENT,
|
||||
bar_color: Color::BLACK,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of rules that dictate the style of an indicator.
|
||||
pub trait StyleSheet {
|
||||
/// The supported style of the [`StyleSheet`].
|
||||
type Style: Default;
|
||||
|
||||
/// Produces the active [`Appearance`] of a indicator.
|
||||
fn appearance(&self, style: &Self::Style) -> Appearance;
|
||||
}
|
||||
|
||||
impl StyleSheet for iced::Theme {
|
||||
type Style = ();
|
||||
|
||||
fn appearance(&self, _style: &Self::Style) -> Appearance {
|
||||
let palette = self.extended_palette();
|
||||
|
||||
Appearance {
|
||||
track_color: palette.background.weak.color,
|
||||
bar_color: palette.primary.base.color,
|
||||
}
|
||||
}
|
||||
}
|
||||
118
examples/loading_spinners/src/main.rs
Normal file
118
examples/loading_spinners/src/main.rs
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
use iced::executor;
|
||||
use iced::widget::{column, container, row, slider, text};
|
||||
use iced::{Application, Command, Element, Length, Settings, Theme};
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
mod circular;
|
||||
mod easing;
|
||||
mod linear;
|
||||
|
||||
use circular::Circular;
|
||||
use linear::Linear;
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
LoadingSpinners::run(Settings {
|
||||
antialiasing: true,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
struct LoadingSpinners {
|
||||
cycle_duration: f32,
|
||||
}
|
||||
|
||||
impl Default for LoadingSpinners {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
cycle_duration: 2.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Message {
|
||||
CycleDurationChanged(f32),
|
||||
}
|
||||
|
||||
impl Application for LoadingSpinners {
|
||||
type Message = Message;
|
||||
type Flags = ();
|
||||
type Executor = executor::Default;
|
||||
type Theme = Theme;
|
||||
|
||||
fn new(_flags: Self::Flags) -> (Self, Command<Message>) {
|
||||
(Self::default(), Command::none())
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
String::from("Loading Spinners - Iced")
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Command<Message> {
|
||||
match message {
|
||||
Message::CycleDurationChanged(duration) => {
|
||||
self.cycle_duration = duration;
|
||||
}
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
let column = [
|
||||
&easing::EMPHASIZED,
|
||||
&easing::EMPHASIZED_DECELERATE,
|
||||
&easing::EMPHASIZED_ACCELERATE,
|
||||
&easing::STANDARD,
|
||||
&easing::STANDARD_DECELERATE,
|
||||
&easing::STANDARD_ACCELERATE,
|
||||
]
|
||||
.iter()
|
||||
.zip([
|
||||
"Emphasized:",
|
||||
"Emphasized Decelerate:",
|
||||
"Emphasized Accelerate:",
|
||||
"Standard:",
|
||||
"Standard Decelerate:",
|
||||
"Standard Accelerate:",
|
||||
])
|
||||
.fold(column![], |column, (easing, label)| {
|
||||
column.push(
|
||||
row![
|
||||
text(label).width(250),
|
||||
Linear::new().easing(easing).cycle_duration(
|
||||
Duration::from_secs_f32(self.cycle_duration)
|
||||
),
|
||||
Circular::new().easing(easing).cycle_duration(
|
||||
Duration::from_secs_f32(self.cycle_duration)
|
||||
)
|
||||
]
|
||||
.align_items(iced::Alignment::Center)
|
||||
.spacing(20.0),
|
||||
)
|
||||
})
|
||||
.spacing(20);
|
||||
|
||||
container(
|
||||
column.push(
|
||||
row(vec![
|
||||
text("Cycle duration:").into(),
|
||||
slider(1.0..=1000.0, self.cycle_duration * 100.0, |x| {
|
||||
Message::CycleDurationChanged(x / 100.0)
|
||||
})
|
||||
.width(200.0)
|
||||
.into(),
|
||||
text(format!("{:.2}s", self.cycle_duration)).into(),
|
||||
])
|
||||
.align_items(iced::Alignment::Center)
|
||||
.spacing(20.0),
|
||||
),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.center_x()
|
||||
.center_y()
|
||||
.into()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue