mirror of
https://github.com/danbulant/cushy
synced 2026-06-18 14:01:10 +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",
|
||||
"kempt",
|
||||
"kludgine",
|
||||
"lyon_geom",
|
||||
"palette",
|
||||
"pollster",
|
||||
"rand",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ gooey-macros = { version = "0.1.0", path = "gooey-macros" }
|
|||
arboard = "3.2.1"
|
||||
zeroize = "1.6.1"
|
||||
unicode-segmentation = "1.10.1"
|
||||
lyon_geom = "1.0.4"
|
||||
|
||||
|
||||
# [patch."https://github.com/khonsulabs/kludgine"]
|
||||
|
|
|
|||
|
|
@ -15,7 +15,14 @@ fn main() -> gooey::Result {
|
|||
value
|
||||
.clone()
|
||||
.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))
|
||||
.into_rows()
|
||||
.fit_horizontally()
|
||||
|
|
|
|||
|
|
@ -308,6 +308,30 @@ pub trait AnimateTarget: Send + Sync {
|
|||
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>`.
|
||||
pub trait BoxAnimate {
|
||||
/// 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),+) => {
|
||||
impl<$($type),+> AnimationTarget for ($($type,)+) where $($type: AnimationTarget),+ {
|
||||
type Running = ($(<$type>::Running,)+);
|
||||
impl<$($type),+> IntoAnimate for ($($type,)+) where $($type: IntoAnimate),+ {
|
||||
type Animate = ($(<$type>::Animate,)+);
|
||||
|
||||
fn begin(self) -> Self::Running {
|
||||
($(self.$field.begin(),)+)
|
||||
fn into_animate(self) -> Self::Animate {
|
||||
($(self.$field.into_animate(),)+)
|
||||
}
|
||||
}
|
||||
|
||||
impl<$($type),+> AnimateTarget for ($($type,)+) where $($type: AnimateTarget),+ {
|
||||
fn update(&self, percent: f32) {
|
||||
$(self.$field.update(percent);)+
|
||||
}
|
||||
|
||||
fn finish(&self) {
|
||||
$(self.$field.finish();)+
|
||||
impl<$($type),+> Animate for ($($type,)+) where $($type: Animate),+ {
|
||||
fn animate(&mut self, elapsed: Duration) -> ControlFlow<Duration> {
|
||||
let mut min_remaining = Duration::MAX;
|
||||
let mut completely_done = true;
|
||||
$(
|
||||
match self.$field.animate(elapsed) {
|
||||
ControlFlow::Break(remaining) => {
|
||||
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
|
||||
where
|
||||
|
|
|
|||
|
|
@ -19,7 +19,10 @@ pub struct Data<T> {
|
|||
child: WidgetRef,
|
||||
}
|
||||
|
||||
impl<T> Data<T> {
|
||||
impl<T> Data<T>
|
||||
where
|
||||
T: Debug,
|
||||
{
|
||||
/// Returns an empty widget with the contained value.
|
||||
pub fn new(value: T) -> Self {
|
||||
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 {
|
||||
Self::new(value)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,21 +3,26 @@
|
|||
use std::ops::RangeInclusive;
|
||||
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::{
|
||||
AnimationHandle, AnimationTarget, IntoAnimate, PercentBetween, Spawn, ZeroToOne,
|
||||
};
|
||||
use crate::value::{Dynamic, IntoDynamic, IntoValue, MapEach, Value};
|
||||
use crate::widget::{MakeWidget, MakeWidgetWithId, WidgetInstance};
|
||||
use crate::widgets::slider::Slidable;
|
||||
use crate::widget::{MakeWidget, MakeWidgetWithId, Widget, WidgetInstance};
|
||||
use crate::widgets::slider::{InactiveTrackColor, Slidable, TrackColor, TrackSize};
|
||||
use crate::widgets::Data;
|
||||
|
||||
/// A bar-shaped progress indicator.
|
||||
#[derive(Debug)]
|
||||
pub struct ProgressBar {
|
||||
progress: Value<Progress>,
|
||||
spinner: bool,
|
||||
}
|
||||
|
||||
impl ProgressBar {
|
||||
|
|
@ -26,6 +31,7 @@ impl ProgressBar {
|
|||
pub const fn indeterminant() -> Self {
|
||||
Self {
|
||||
progress: Value::Constant(Progress::Indeterminant),
|
||||
spinner: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -34,8 +40,16 @@ impl ProgressBar {
|
|||
pub fn new(progress: impl IntoDynamic<Progress>) -> Self {
|
||||
Self {
|
||||
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`].
|
||||
|
|
@ -54,18 +68,43 @@ impl MakeWidgetWithId for ProgressBar {
|
|||
let value = (&start, &end).map_each(|(start, end)| *start..=*end);
|
||||
|
||||
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(
|
||||
self.progress.get(),
|
||||
&mut indeterminant_animation,
|
||||
&start,
|
||||
&end,
|
||||
degree_offset.as_ref(),
|
||||
);
|
||||
|
||||
let slider = value.slider().knobless().non_interactive().make_with_id(id);
|
||||
match self.progress {
|
||||
Value::Dynamic(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()
|
||||
}
|
||||
|
|
@ -74,50 +113,75 @@ impl MakeWidgetWithId for ProgressBar {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct IndeterminantAnimations {
|
||||
_primary: AnimationHandle,
|
||||
_degree_offset: Option<AnimationHandle>,
|
||||
}
|
||||
|
||||
fn update_progress_bar(
|
||||
progress: Progress,
|
||||
indeterminant_animation: &mut Option<AnimationHandle>,
|
||||
indeterminant_animation: &mut Option<IndeterminantAnimations>,
|
||||
start: &Dynamic<ZeroToOne>,
|
||||
end: &Dynamic<ZeroToOne>,
|
||||
degree_offset: Option<&Dynamic<f32>>,
|
||||
) {
|
||||
match progress {
|
||||
Progress::Indeterminant => {
|
||||
if indeterminant_animation.is_none() {
|
||||
*indeterminant_animation = Some(
|
||||
(
|
||||
start.transition_to(ZeroToOne::ZERO),
|
||||
end.transition_to(ZeroToOne::ZERO),
|
||||
*indeterminant_animation = Some(IndeterminantAnimations {
|
||||
_primary: (
|
||||
start
|
||||
.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()
|
||||
.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) => {
|
||||
let _stopped_animation = indeterminant_animation.take();
|
||||
if let Some(degree_offset) = degree_offset {
|
||||
degree_offset.set(0.);
|
||||
}
|
||||
start.set(ZeroToOne::ZERO);
|
||||
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