mirror of
https://github.com/danbulant/cushy
synced 2026-07-05 11:10:34 +00:00
parent
a526dc000b
commit
c4151d649c
6 changed files with 281 additions and 52 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -810,6 +810,7 @@ dependencies = [
|
||||||
"interner",
|
"interner",
|
||||||
"kempt",
|
"kempt",
|
||||||
"kludgine",
|
"kludgine",
|
||||||
|
"lyon_geom",
|
||||||
"palette",
|
"palette",
|
||||||
"pollster",
|
"pollster",
|
||||||
"rand",
|
"rand",
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue