Added Spinner widget

Closes #80
This commit is contained in:
Jonathan Johnson 2023-12-10 15:05:59 -08:00
parent a526dc000b
commit c4151d649c
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
6 changed files with 281 additions and 52 deletions

1
Cargo.lock generated
View file

@ -810,6 +810,7 @@ dependencies = [
"interner", "interner",
"kempt", "kempt",
"kludgine", "kludgine",
"lyon_geom",
"palette", "palette",
"pollster", "pollster",
"rand", "rand",

View file

@ -29,6 +29,7 @@ gooey-macros = { version = "0.1.0", path = "gooey-macros" }
arboard = "3.2.1" arboard = "3.2.1"
zeroize = "1.6.1" zeroize = "1.6.1"
unicode-segmentation = "1.10.1" unicode-segmentation = "1.10.1"
lyon_geom = "1.0.4"
# [patch."https://github.com/khonsulabs/kludgine"] # [patch."https://github.com/khonsulabs/kludgine"]

View file

@ -15,7 +15,14 @@ fn main() -> gooey::Result {
value value
.clone() .clone()
.slider() .slider()
.and(progress.clone().progress_bar()) .and(
progress
.clone()
.progress_bar()
.expand()
.and(progress.clone().progress_bar().spinner())
.into_columns(),
)
.and("Indeterminant".into_checkbox(indeterminant)) .and("Indeterminant".into_checkbox(indeterminant))
.into_rows() .into_rows()
.fit_horizontally() .fit_horizontally()

View file

@ -308,6 +308,30 @@ pub trait AnimateTarget: Send + Sync {
fn finish(&self); fn finish(&self);
} }
macro_rules! impl_tuple_animate {
($($type:ident $field:tt $var:ident),+) => {
impl<$($type),+> AnimationTarget for ($($type,)+) where $($type: AnimationTarget),+ {
type Running = ($(<$type>::Running,)+);
fn begin(self) -> Self::Running {
($(self.$field.begin(),)+)
}
}
impl<$($type),+> AnimateTarget for ($($type,)+) where $($type: AnimateTarget),+ {
fn update(&self, percent: f32) {
$(self.$field.update(percent);)+
}
fn finish(&self) {
$(self.$field.finish();)+
}
}
}
}
impl_all_tuples!(impl_tuple_animate);
/// A type that can convert into `Box<dyn Animate>`. /// A type that can convert into `Box<dyn Animate>`.
pub trait BoxAnimate { pub trait BoxAnimate {
/// Returns the boxed animation. /// Returns the boxed animation.
@ -353,29 +377,40 @@ pub trait IntoAnimate: Sized + Send + Sync {
} }
} }
macro_rules! impl_tuple_animate { macro_rules! impl_tuple_into_animate {
($($type:ident $field:tt $var:ident),+) => { ($($type:ident $field:tt $var:ident),+) => {
impl<$($type),+> AnimationTarget for ($($type,)+) where $($type: AnimationTarget),+ { impl<$($type),+> IntoAnimate for ($($type,)+) where $($type: IntoAnimate),+ {
type Running = ($(<$type>::Running,)+); type Animate = ($(<$type>::Animate,)+);
fn begin(self) -> Self::Running { fn into_animate(self) -> Self::Animate {
($(self.$field.begin(),)+) ($(self.$field.into_animate(),)+)
} }
} }
impl<$($type),+> Animate for ($($type,)+) where $($type: Animate),+ {
impl<$($type),+> AnimateTarget for ($($type,)+) where $($type: AnimateTarget),+ { fn animate(&mut self, elapsed: Duration) -> ControlFlow<Duration> {
fn update(&self, percent: f32) { let mut min_remaining = Duration::MAX;
$(self.$field.update(percent);)+ let mut completely_done = true;
} $(
match self.$field.animate(elapsed) {
fn finish(&self) { ControlFlow::Break(remaining) => {
$(self.$field.finish();)+ min_remaining = min_remaining.min(remaining);
}
ControlFlow::Continue(()) => {
completely_done = false;
}
}
)+
if completely_done {
ControlFlow::Break(min_remaining)
} else {
ControlFlow::Continue(())
}
} }
} }
} }
} }
impl_all_tuples!(impl_tuple_animate); impl_all_tuples!(impl_tuple_into_animate);
impl<T> BoxAnimate for T impl<T> BoxAnimate for T
where where

View file

@ -19,7 +19,10 @@ pub struct Data<T> {
child: WidgetRef, child: WidgetRef,
} }
impl<T> Data<T> { impl<T> Data<T>
where
T: Debug,
{
/// Returns an empty widget with the contained value. /// Returns an empty widget with the contained value.
pub fn new(value: T) -> Self { pub fn new(value: T) -> Self {
Self::new_wrapping(value, Space::clear()) Self::new_wrapping(value, Space::clear())
@ -34,7 +37,10 @@ impl<T> Data<T> {
} }
} }
impl<T> From<T> for Data<T> { impl<T> From<T> for Data<T>
where
T: Debug,
{
fn from(value: T) -> Self { fn from(value: T) -> Self {
Self::new(value) Self::new(value)
} }

View file

@ -3,21 +3,26 @@
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
use std::time::Duration; use std::time::Duration;
use kludgine::figures::{Ranged, Zero}; use kludgine::figures::units::Px;
use kludgine::figures::{FloatConversion, Point, Ranged, ScreenScale, Zero};
use kludgine::shapes::{Path, PathEvent, StrokeOptions};
use kludgine::Color;
use lyon_geom::Arc;
use crate::animation::easings::{EaseInOutQuadradic, EaseOutQuadradic}; use crate::animation::easings::{EaseInQuadradic, EaseOutQuadradic};
use crate::animation::{ use crate::animation::{
AnimationHandle, AnimationTarget, IntoAnimate, PercentBetween, Spawn, ZeroToOne, AnimationHandle, AnimationTarget, IntoAnimate, PercentBetween, Spawn, ZeroToOne,
}; };
use crate::value::{Dynamic, IntoDynamic, IntoValue, MapEach, Value}; use crate::value::{Dynamic, IntoDynamic, IntoValue, MapEach, Value};
use crate::widget::{MakeWidget, MakeWidgetWithId, WidgetInstance}; use crate::widget::{MakeWidget, MakeWidgetWithId, Widget, WidgetInstance};
use crate::widgets::slider::Slidable; use crate::widgets::slider::{InactiveTrackColor, Slidable, TrackColor, TrackSize};
use crate::widgets::Data; use crate::widgets::Data;
/// A bar-shaped progress indicator. /// A bar-shaped progress indicator.
#[derive(Debug)] #[derive(Debug)]
pub struct ProgressBar { pub struct ProgressBar {
progress: Value<Progress>, progress: Value<Progress>,
spinner: bool,
} }
impl ProgressBar { impl ProgressBar {
@ -26,6 +31,7 @@ impl ProgressBar {
pub const fn indeterminant() -> Self { pub const fn indeterminant() -> Self {
Self { Self {
progress: Value::Constant(Progress::Indeterminant), progress: Value::Constant(Progress::Indeterminant),
spinner: false,
} }
} }
@ -34,8 +40,16 @@ impl ProgressBar {
pub fn new(progress: impl IntoDynamic<Progress>) -> Self { pub fn new(progress: impl IntoDynamic<Progress>) -> Self {
Self { Self {
progress: Value::Dynamic(progress.into_dynamic()), progress: Value::Dynamic(progress.into_dynamic()),
spinner: false,
} }
} }
/// Returns a new progress bar that displays `progress`.
#[must_use]
pub fn spinner(mut self) -> Self {
self.spinner = true;
self
}
} }
/// A measurement of progress for an indicator widget like [`ProgressBar`]. /// A measurement of progress for an indicator widget like [`ProgressBar`].
@ -54,18 +68,43 @@ impl MakeWidgetWithId for ProgressBar {
let value = (&start, &end).map_each(|(start, end)| *start..=*end); let value = (&start, &end).map_each(|(start, end)| *start..=*end);
let mut indeterminant_animation = None; let mut indeterminant_animation = None;
let (slider, degree_offset) = if self.spinner {
let degree_offset = Dynamic::new(0.);
(
Spinner {
start: start.clone(),
end: end.clone(),
degree_offset: degree_offset.clone(),
}
.make_with_id(id),
Some(degree_offset),
)
} else {
(
value.slider().knobless().non_interactive().make_with_id(id),
None,
)
};
update_progress_bar( update_progress_bar(
self.progress.get(), self.progress.get(),
&mut indeterminant_animation, &mut indeterminant_animation,
&start, &start,
&end, &end,
degree_offset.as_ref(),
); );
let slider = value.slider().knobless().non_interactive().make_with_id(id);
match self.progress { match self.progress {
Value::Dynamic(progress) => { Value::Dynamic(progress) => {
let callback = progress.for_each(move |progress| { let callback = progress.for_each(move |progress| {
update_progress_bar(*progress, &mut indeterminant_animation, &start, &end); update_progress_bar(
*progress,
&mut indeterminant_animation,
&start,
&end,
degree_offset.as_ref(),
);
}); });
Data::new_wrapping((callback, progress), slider).make_widget() Data::new_wrapping((callback, progress), slider).make_widget()
} }
@ -74,50 +113,75 @@ impl MakeWidgetWithId for ProgressBar {
} }
} }
#[derive(Debug)]
struct IndeterminantAnimations {
_primary: AnimationHandle,
_degree_offset: Option<AnimationHandle>,
}
fn update_progress_bar( fn update_progress_bar(
progress: Progress, progress: Progress,
indeterminant_animation: &mut Option<AnimationHandle>, indeterminant_animation: &mut Option<IndeterminantAnimations>,
start: &Dynamic<ZeroToOne>, start: &Dynamic<ZeroToOne>,
end: &Dynamic<ZeroToOne>, end: &Dynamic<ZeroToOne>,
degree_offset: Option<&Dynamic<f32>>,
) { ) {
match progress { match progress {
Progress::Indeterminant => { Progress::Indeterminant => {
if indeterminant_animation.is_none() { if indeterminant_animation.is_none() {
*indeterminant_animation = Some( *indeterminant_animation = Some(IndeterminantAnimations {
( _primary: (
start.transition_to(ZeroToOne::ZERO), start
end.transition_to(ZeroToOne::ZERO), .transition_to(ZeroToOne::ZERO)
.immediately()
.and_then(Duration::from_millis(250))
.and_then(
start
.transition_to(ZeroToOne::new(0.33))
.over(Duration::from_millis(500))
.with_easing(EaseInQuadradic),
)
.and_then(
start
.transition_to(ZeroToOne::new(1.0))
.over(Duration::from_millis(500))
.with_easing(EaseOutQuadradic),
),
end.transition_to(ZeroToOne::ZERO)
.immediately()
.and_then(
end.transition_to(ZeroToOne::new(0.75))
.over(Duration::from_millis(500))
.with_easing(EaseInQuadradic),
)
.and_then(
end.transition_to(ZeroToOne::ONE)
.over(Duration::from_millis(250))
.with_easing(EaseOutQuadradic),
),
) )
.immediately()
.and_then(
end.transition_to(ZeroToOne::new(0.75))
.over(Duration::from_millis(500))
.with_easing(EaseOutQuadradic),
)
.and_then(
start
.transition_to(ZeroToOne::new(0.25))
.over(Duration::from_millis(500))
.with_easing(EaseOutQuadradic),
)
.and_then(
end.transition_to(ZeroToOne::ONE)
.over(Duration::from_millis(250))
.with_easing(EaseOutQuadradic),
)
.and_then(
start
.transition_to(ZeroToOne::ONE)
.over(Duration::from_millis(250))
.with_easing(EaseInOutQuadradic),
)
.cycle() .cycle()
.spawn(), .spawn(),
); _degree_offset: degree_offset.map(|degree_offset| {
degree_offset
.transition_to(0.)
.immediately()
.and_then(
degree_offset
.transition_to(359.9)
.over(Duration::from_secs_f32(1.66)),
)
.cycle()
.spawn()
}),
});
} }
} }
Progress::Percent(value) => { Progress::Percent(value) => {
let _stopped_animation = indeterminant_animation.take(); let _stopped_animation = indeterminant_animation.take();
if let Some(degree_offset) = degree_offset {
degree_offset.set(0.);
}
start.set(ZeroToOne::ZERO); start.set(ZeroToOne::ZERO);
end.set(value); end.set(value);
} }
@ -233,3 +297,118 @@ where
} }
} }
} }
/// A circular progress widget.
#[derive(Debug)]
pub struct Spinner {
start: Dynamic<ZeroToOne>,
end: Dynamic<ZeroToOne>,
degree_offset: Dynamic<f32>,
}
impl Spinner {
fn draw_arc(
track_size: Px,
radius: f32,
degree_offset: f32,
start: ZeroToOne,
sweep: ZeroToOne,
color: Color,
context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>,
) {
let mut events = Vec::<PathEvent<Px>>::new();
Arc {
center: lyon_geom::point(
radius + track_size.into_float() / 2.,
radius + track_size.into_float() / 2.,
),
radii: lyon_geom::vector(radius, radius),
start_angle: lyon_geom::Angle::degrees(*start * 360. - 90. + degree_offset),
sweep_angle: lyon_geom::Angle::degrees(*sweep * 360.),
x_rotation: lyon_geom::Angle::zero(),
}
.for_each_cubic_bezier(&mut |segment| {
if events.is_empty() {
events.push(PathEvent::Begin {
at: Point::new(segment.from.x, segment.from.y).cast(),
texture: Point::ZERO,
});
}
events.push(PathEvent::Cubic {
ctrl1: Point::new(segment.ctrl1.x, segment.ctrl1.y).cast(),
ctrl2: Point::new(segment.ctrl2.x, segment.ctrl2.y).cast(),
to: Point::new(segment.to.x, segment.to.y).cast(),
texture: Point::ZERO,
});
});
let full = sweep == ZeroToOne::ONE;
if !events.is_empty() {
events.push(PathEvent::End { close: full });
context.gfx.draw_shape(
&events
.into_iter()
.collect::<Path<Px, false>>()
.stroke(StrokeOptions::px_wide(track_size).colored(color)),
);
}
}
}
impl Widget for Spinner {
fn redraw(&mut self, context: &mut crate::context::GraphicsContext<'_, '_, '_, '_, '_>) {
let track_size = context.get(&TrackSize).into_px(context.gfx.scale());
let start = self.start.get_tracking_refresh(context);
let end = self.end.get_tracking_refresh(context);
let size = context.gfx.region().size;
let render_size = size.width.min(size.height).into_float();
let radius = render_size / 2. - track_size.into_float();
let degree_offset = self.degree_offset.get();
if start > ZeroToOne::ZERO {
Self::draw_arc(
track_size,
radius,
degree_offset,
ZeroToOne::ZERO,
start,
context.get(&InactiveTrackColor),
context,
);
}
if start != end {
Self::draw_arc(
track_size,
radius,
degree_offset,
start,
ZeroToOne::new(*end - *start),
context.get(&TrackColor),
context,
);
}
if end < ZeroToOne::ONE {
Self::draw_arc(
track_size,
radius,
degree_offset,
end,
end.one_minus(),
context.get(&InactiveTrackColor),
context,
);
}
}
fn layout(
&mut self,
available_space: kludgine::figures::Size<crate::ConstraintLimit>,
context: &mut crate::context::LayoutContext<'_, '_, '_, '_, '_>,
) -> kludgine::figures::Size<kludgine::figures::units::UPx> {
let track_size = context.get(&TrackSize).into_px(context.gfx.scale());
let minimum_size = track_size * 4;
available_space.map(|constraint| constraint.fit_measured(minimum_size, context.gfx.scale()))
}
}