mirror of
https://github.com/danbulant/cushy
synced 2026-07-08 12:40:36 +00:00
parent
2201f2c83b
commit
801337ab7a
7 changed files with 515 additions and 60 deletions
|
|
@ -3,12 +3,13 @@ use std::time::Duration;
|
||||||
use gooey::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn};
|
use gooey::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn};
|
||||||
use gooey::value::Dynamic;
|
use gooey::value::Dynamic;
|
||||||
use gooey::widget::MakeWidget;
|
use gooey::widget::MakeWidget;
|
||||||
|
use gooey::widgets::progress::Progressable;
|
||||||
use gooey::{Run, WithClone};
|
use gooey::{Run, WithClone};
|
||||||
|
use kludgine::figures::units::Lp;
|
||||||
|
|
||||||
fn main() -> gooey::Result {
|
fn main() -> gooey::Result {
|
||||||
let animation = Dynamic::new(AnimationHandle::new());
|
let animation = Dynamic::new(AnimationHandle::new());
|
||||||
let value = Dynamic::new(50);
|
let value = Dynamic::new(50);
|
||||||
let label = value.map_each(|value| value.to_string());
|
|
||||||
|
|
||||||
// Gooey's animation system supports using a `Duration` as a step in
|
// Gooey's animation system supports using a `Duration` as a step in
|
||||||
// animation to create a delay. This can also be used to call a function
|
// animation to create a delay. This can also be used to call a function
|
||||||
|
|
@ -20,7 +21,13 @@ fn main() -> gooey::Result {
|
||||||
"To 0"
|
"To 0"
|
||||||
.into_button()
|
.into_button()
|
||||||
.on_click(animate_to(&animation, &value, 0))
|
.on_click(animate_to(&animation, &value, 0))
|
||||||
.and(label)
|
.and(
|
||||||
|
value
|
||||||
|
.clone()
|
||||||
|
.progress_bar_to(100)
|
||||||
|
.width(Lp::inches(3))
|
||||||
|
.centered(),
|
||||||
|
)
|
||||||
.and(
|
.and(
|
||||||
"To 100"
|
"To 100"
|
||||||
.into_button()
|
.into_button()
|
||||||
|
|
|
||||||
103
src/animation.rs
103
src/animation.rs
|
|
@ -171,6 +171,7 @@ pub trait Animate: Send + Sync {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A pending transition for a [`Dynamic`] to a new value.
|
/// A pending transition for a [`Dynamic`] to a new value.
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct DynamicTransition<T> {
|
pub struct DynamicTransition<T> {
|
||||||
/// The dynamic value to change.
|
/// The dynamic value to change.
|
||||||
pub dynamic: Dynamic<T>,
|
pub dynamic: Dynamic<T>,
|
||||||
|
|
@ -226,6 +227,7 @@ where
|
||||||
/// [`Duration`], using the `Easing` generic parameter to control how the value
|
/// [`Duration`], using the `Easing` generic parameter to control how the value
|
||||||
/// is interpolated.
|
/// is interpolated.
|
||||||
#[must_use = "animations are not performed until they are spawned"]
|
#[must_use = "animations are not performed until they are spawned"]
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct Animation<Target, Easing = Linear>
|
pub struct Animation<Target, Easing = Linear>
|
||||||
where
|
where
|
||||||
Target: AnimationTarget,
|
Target: AnimationTarget,
|
||||||
|
|
@ -321,6 +323,22 @@ pub trait IntoAnimate: Sized + Send + Sync {
|
||||||
Chain::new(self, other)
|
Chain::new(self, other)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns an animation that repeats `self` indefinitely.
|
||||||
|
fn cycle(self) -> Cycle<Self>
|
||||||
|
where
|
||||||
|
Self: Clone,
|
||||||
|
{
|
||||||
|
Cycle::forever(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an animation that repeats a number of times before completing.
|
||||||
|
fn repeat(self, times: usize) -> Cycle<Self>
|
||||||
|
where
|
||||||
|
Self: Clone,
|
||||||
|
{
|
||||||
|
Cycle::n_times(times, self)
|
||||||
|
}
|
||||||
|
|
||||||
/// Invokes `on_complete` after this animation finishes.
|
/// Invokes `on_complete` after this animation finishes.
|
||||||
fn on_complete<F>(self, on_complete: F) -> OnCompleteAnimation<Self>
|
fn on_complete<F>(self, on_complete: F) -> OnCompleteAnimation<Self>
|
||||||
where
|
where
|
||||||
|
|
@ -473,6 +491,7 @@ impl Drop for AnimationHandle {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An animation combinator that runs animation `A`, then animation `B`.
|
/// An animation combinator that runs animation `A`, then animation `B`.
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct Chain<A: IntoAnimate, B: IntoAnimate>(A, B);
|
pub struct Chain<A: IntoAnimate, B: IntoAnimate>(A, B);
|
||||||
|
|
||||||
/// A [`Chain`] that is currently animating.
|
/// A [`Chain`] that is currently animating.
|
||||||
|
|
@ -529,6 +548,90 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An animation that repeats another animation.
|
||||||
|
pub struct Cycle<A>
|
||||||
|
where
|
||||||
|
A: IntoAnimate + Clone,
|
||||||
|
{
|
||||||
|
cycles: Option<usize>,
|
||||||
|
animation: A,
|
||||||
|
running: Option<A::Animate>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A> Cycle<A>
|
||||||
|
where
|
||||||
|
A: IntoAnimate + Clone,
|
||||||
|
{
|
||||||
|
/// Returns a new animation that repeats `animation` an unlimited number of
|
||||||
|
/// times.
|
||||||
|
pub fn forever(animation: A) -> Self {
|
||||||
|
Self {
|
||||||
|
animation,
|
||||||
|
cycles: None,
|
||||||
|
running: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new animation that repeats `animation` a specific number of
|
||||||
|
/// times.
|
||||||
|
///
|
||||||
|
/// Passing 1 as `cycles` is equivalent to executing the animation directly.
|
||||||
|
pub fn n_times(cycles: usize, animation: A) -> Self {
|
||||||
|
Self {
|
||||||
|
animation,
|
||||||
|
cycles: Some(cycles),
|
||||||
|
running: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn keep_cycling(&mut self) -> bool {
|
||||||
|
match &mut self.cycles {
|
||||||
|
Some(0) => false,
|
||||||
|
Some(cycles) => {
|
||||||
|
*cycles -= 1;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
None => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A> IntoAnimate for Cycle<A>
|
||||||
|
where
|
||||||
|
A: IntoAnimate + Clone,
|
||||||
|
{
|
||||||
|
type Animate = Self;
|
||||||
|
|
||||||
|
fn into_animate(self) -> Self::Animate {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A> Animate for Cycle<A>
|
||||||
|
where
|
||||||
|
A: IntoAnimate + Clone,
|
||||||
|
{
|
||||||
|
fn animate(&mut self, mut elapsed: Duration) -> ControlFlow<Duration> {
|
||||||
|
while !elapsed.is_zero() {
|
||||||
|
if let Some(running) = &mut self.running {
|
||||||
|
match running.animate(elapsed) {
|
||||||
|
ControlFlow::Break(remaining) => elapsed = remaining,
|
||||||
|
ControlFlow::Continue(()) => return ControlFlow::Continue(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.keep_cycling() {
|
||||||
|
self.running = Some(self.animation.clone().into_animate());
|
||||||
|
} else {
|
||||||
|
self.running = None;
|
||||||
|
return ControlFlow::Break(elapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ControlFlow::Continue(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// An animation wrapper that invokes a callback upon the animation completing.
|
/// An animation wrapper that invokes a callback upon the animation completing.
|
||||||
///
|
///
|
||||||
/// This type guarantees the callback will only be invoked once per animation
|
/// This type guarantees the callback will only be invoked once per animation
|
||||||
|
|
|
||||||
10
src/value.rs
10
src/value.rs
|
|
@ -1201,6 +1201,16 @@ impl<T> Value<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> IntoDynamic<T> for Value<T> {
|
||||||
|
fn into_dynamic(self) -> Dynamic<T> {
|
||||||
|
match self {
|
||||||
|
Value::Constant(value) => Dynamic::new(value),
|
||||||
|
Value::Dynamic(value) => value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> Clone for Value<T>
|
impl<T> Clone for Value<T>
|
||||||
where
|
where
|
||||||
T: Clone,
|
T: Clone,
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,12 @@ mod canvas;
|
||||||
pub mod checkbox;
|
pub mod checkbox;
|
||||||
pub mod container;
|
pub mod container;
|
||||||
mod custom;
|
mod custom;
|
||||||
|
mod data;
|
||||||
mod expand;
|
mod expand;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod label;
|
pub mod label;
|
||||||
mod mode_switch;
|
mod mode_switch;
|
||||||
|
pub mod progress;
|
||||||
mod resize;
|
mod resize;
|
||||||
pub mod scroll;
|
pub mod scroll;
|
||||||
pub mod slider;
|
pub mod slider;
|
||||||
|
|
@ -26,10 +28,12 @@ pub use canvas::Canvas;
|
||||||
pub use checkbox::Checkbox;
|
pub use checkbox::Checkbox;
|
||||||
pub use container::Container;
|
pub use container::Container;
|
||||||
pub use custom::Custom;
|
pub use custom::Custom;
|
||||||
|
pub use data::Data;
|
||||||
pub use expand::Expand;
|
pub use expand::Expand;
|
||||||
pub use input::Input;
|
pub use input::Input;
|
||||||
pub use label::Label;
|
pub use label::Label;
|
||||||
pub use mode_switch::ThemedMode;
|
pub use mode_switch::ThemedMode;
|
||||||
|
pub use progress::ProgressBar;
|
||||||
pub use resize::Resize;
|
pub use resize::Resize;
|
||||||
pub use scroll::Scroll;
|
pub use scroll::Scroll;
|
||||||
pub use slider::Slider;
|
pub use slider::Slider;
|
||||||
|
|
|
||||||
49
src/widgets/data.rs
Normal file
49
src/widgets/data.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::panic::UnwindSafe;
|
||||||
|
|
||||||
|
use crate::widget::{MakeWidget, WidgetRef, WrapperWidget};
|
||||||
|
use crate::widgets::Space;
|
||||||
|
|
||||||
|
/// A widget that stores arbitrary data in the widget hierachy.
|
||||||
|
///
|
||||||
|
/// This widget is useful if data needs to live as long as a related widget. For
|
||||||
|
/// example, [`ProgressBar`](crate::widgets::ProgressBar) is not a "real" widget
|
||||||
|
/// -- it implements [`MakeWidget`] and returns a customized
|
||||||
|
/// [`Slider`](crate::widgets::Slider). To ensure the indeterminant animation
|
||||||
|
/// lives only as long as the created slider does, `ProgressBar` wraps the
|
||||||
|
/// `Slider` in a `Data` widget to store the animation handle.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Data<T> {
|
||||||
|
_data: T,
|
||||||
|
child: WidgetRef,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Data<T> {
|
||||||
|
/// Returns an empty widget with the contained value.
|
||||||
|
pub fn new(value: T) -> Self {
|
||||||
|
Self::new_wrapping(value, Space::clear())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new instance that wraps `widget` and stores `value`.
|
||||||
|
pub fn new_wrapping(value: T, widget: impl MakeWidget) -> Self {
|
||||||
|
Self {
|
||||||
|
_data: value,
|
||||||
|
child: WidgetRef::new(widget),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<T> for Data<T> {
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> WrapperWidget for Data<T>
|
||||||
|
where
|
||||||
|
T: Debug + Send + UnwindSafe + 'static,
|
||||||
|
{
|
||||||
|
fn child_mut(&mut self) -> &mut WidgetRef {
|
||||||
|
&mut self.child
|
||||||
|
}
|
||||||
|
}
|
||||||
185
src/widgets/progress.rs
Normal file
185
src/widgets/progress.rs
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
//! Widgets for displaying progress indicators.
|
||||||
|
|
||||||
|
use std::ops::RangeInclusive;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use kludgine::figures::Ranged;
|
||||||
|
|
||||||
|
use crate::animation::easings::EaseInOutSine;
|
||||||
|
use crate::animation::{
|
||||||
|
AnimationHandle, AnimationTarget, IntoAnimate, PercentBetween, Spawn, ZeroToOne,
|
||||||
|
};
|
||||||
|
use crate::value::{Dynamic, IntoDynamic, IntoValue, MapEach, Value};
|
||||||
|
use crate::widget::{MakeWidget, WidgetInstance};
|
||||||
|
use crate::widgets::slider::Slidable;
|
||||||
|
use crate::widgets::Data;
|
||||||
|
|
||||||
|
/// A bar-shaped progress indicator.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ProgressBar {
|
||||||
|
progress: Value<Progress>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProgressBar {
|
||||||
|
/// Returns an indeterminant progress bar.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn indeterminant() -> Self {
|
||||||
|
Self {
|
||||||
|
progress: Value::Constant(Progress::Indeterminant),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new progress bar that displays `progress`.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(progress: impl IntoDynamic<Progress>) -> Self {
|
||||||
|
Self {
|
||||||
|
progress: Value::Dynamic(progress.into_dynamic()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A measurement of progress for an indicator widget like [`ProgressBar`].
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum Progress {
|
||||||
|
/// The task has an indeterminant length.
|
||||||
|
Indeterminant,
|
||||||
|
/// The task is a specified percent complete.
|
||||||
|
Percent(ZeroToOne),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MakeWidget for ProgressBar {
|
||||||
|
fn make_widget(self) -> WidgetInstance {
|
||||||
|
let start = Dynamic::new(ZeroToOne::ZERO);
|
||||||
|
let end = Dynamic::new(ZeroToOne::ZERO);
|
||||||
|
let value = (&start, &end).map_each(|(start, end)| *start..=*end);
|
||||||
|
|
||||||
|
let mut indeterminant_animation = None;
|
||||||
|
update_progress_bar(
|
||||||
|
self.progress.get(),
|
||||||
|
&mut indeterminant_animation,
|
||||||
|
&start,
|
||||||
|
&end,
|
||||||
|
);
|
||||||
|
|
||||||
|
let slider = value.slider().knobless().non_interactive();
|
||||||
|
match self.progress {
|
||||||
|
Value::Dynamic(progress) => {
|
||||||
|
progress.for_each(move |progress| {
|
||||||
|
update_progress_bar(*progress, &mut indeterminant_animation, &start, &end);
|
||||||
|
});
|
||||||
|
Data::new_wrapping(progress, slider).make_widget()
|
||||||
|
}
|
||||||
|
Value::Constant(_) => Data::new_wrapping(indeterminant_animation, slider).make_widget(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_progress_bar(
|
||||||
|
progress: Progress,
|
||||||
|
indeterminant_animation: &mut Option<AnimationHandle>,
|
||||||
|
start: &Dynamic<ZeroToOne>,
|
||||||
|
end: &Dynamic<ZeroToOne>,
|
||||||
|
) {
|
||||||
|
match progress {
|
||||||
|
Progress::Indeterminant => {
|
||||||
|
*indeterminant_animation = Some(
|
||||||
|
end.transition_to(ZeroToOne::new(0.66))
|
||||||
|
.over(Duration::from_millis(500))
|
||||||
|
.with_easing(EaseInOutSine)
|
||||||
|
.and_then(
|
||||||
|
start
|
||||||
|
.transition_to(ZeroToOne::new(0.33))
|
||||||
|
.over(Duration::from_millis(500))
|
||||||
|
.with_easing(EaseInOutSine),
|
||||||
|
)
|
||||||
|
.and_then(
|
||||||
|
end.transition_to(ZeroToOne::ONE)
|
||||||
|
.over(Duration::from_millis(500))
|
||||||
|
.with_easing(EaseInOutSine),
|
||||||
|
)
|
||||||
|
.and_then(
|
||||||
|
start
|
||||||
|
.transition_to(ZeroToOne::ONE)
|
||||||
|
.over(Duration::from_millis(500))
|
||||||
|
.with_easing(EaseInOutSine),
|
||||||
|
)
|
||||||
|
.and_then(
|
||||||
|
(
|
||||||
|
start.transition_to(ZeroToOne::ZERO),
|
||||||
|
end.transition_to(ZeroToOne::ZERO),
|
||||||
|
)
|
||||||
|
.over(Duration::ZERO),
|
||||||
|
)
|
||||||
|
.cycle()
|
||||||
|
.spawn(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Progress::Percent(value) => {
|
||||||
|
let _stopped_animation = indeterminant_animation.take();
|
||||||
|
start.update(ZeroToOne::ZERO);
|
||||||
|
end.update(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A value that can be used in a progress indicator.
|
||||||
|
pub trait Progressable<T>: IntoDynamic<T> + Sized {
|
||||||
|
/// Returns a new progress bar that displays progress from `T::MIN` to
|
||||||
|
/// `T::MAX`.
|
||||||
|
fn progress_bar(self) -> ProgressBar
|
||||||
|
where
|
||||||
|
T: Ranged + PercentBetween,
|
||||||
|
{
|
||||||
|
ProgressBar::new(
|
||||||
|
self.into_dynamic()
|
||||||
|
.map_each(|t| Progress::Percent(t.percent_between(&T::MIN, &T::MAX))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new progress bar that displays progress from `T::MIN` to
|
||||||
|
/// `max`. The maximum value can be either a `T` or an `Option<T>`. If
|
||||||
|
/// `None` is the maximum value, an indeterminant progress bar will be
|
||||||
|
/// displayed.
|
||||||
|
fn progress_bar_to(self, max: impl IntoValue<Option<T>>) -> ProgressBar
|
||||||
|
where
|
||||||
|
T: Ranged + PercentBetween + Clone + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let max = max.into_value();
|
||||||
|
match max {
|
||||||
|
Value::Constant(max) => self.progress_bar_between(max.map(|max| T::MIN..=max)),
|
||||||
|
Value::Dynamic(max) => {
|
||||||
|
self.progress_bar_between(max.map_each(|max| max.clone().map(|max| T::MIN..=max)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new progress bar that displays progress over the specified
|
||||||
|
/// `range` of `T`. The range can be either a `T..=T` or an `Option<T>`. If
|
||||||
|
/// `None` is specified as the range, an indeterminant progress bar will be
|
||||||
|
/// displayed.
|
||||||
|
fn progress_bar_between<Range>(self, range: Range) -> ProgressBar
|
||||||
|
where
|
||||||
|
T: PercentBetween + Clone + Send + Sync + 'static,
|
||||||
|
Range: IntoValue<Option<RangeInclusive<T>>>,
|
||||||
|
{
|
||||||
|
let value = self.into_dynamic();
|
||||||
|
let range = range.into_value();
|
||||||
|
match range {
|
||||||
|
Value::Constant(range) => ProgressBar::new(value.map_each(move |value| {
|
||||||
|
range
|
||||||
|
.as_ref()
|
||||||
|
.map(|range| value.percent_between(range.start(), range.end()))
|
||||||
|
.map_or(Progress::Indeterminant, Progress::Percent)
|
||||||
|
})),
|
||||||
|
Value::Dynamic(range) => {
|
||||||
|
ProgressBar::new((&range, &value).map_each(|(range, value)| {
|
||||||
|
range.clone().map_or(Progress::Indeterminant, |range| {
|
||||||
|
Progress::Percent(value.percent_between(range.start(), range.end()))
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<U, T> Progressable<U> for T where T: IntoDynamic<U> {}
|
||||||
|
|
@ -46,6 +46,8 @@ where
|
||||||
///
|
///
|
||||||
/// This defaults to `0.05`/5%.
|
/// This defaults to `0.05`/5%.
|
||||||
pub step: Value<ZeroToOne>,
|
pub step: Value<ZeroToOne>,
|
||||||
|
knob_visible: bool,
|
||||||
|
interactive: bool,
|
||||||
knob_size: UPx,
|
knob_size: UPx,
|
||||||
horizontal: bool,
|
horizontal: bool,
|
||||||
rendered_size: Px,
|
rendered_size: Px,
|
||||||
|
|
@ -88,6 +90,8 @@ where
|
||||||
value: value.into_dynamic(),
|
value: value.into_dynamic(),
|
||||||
minimum: min.into_value(),
|
minimum: min.into_value(),
|
||||||
maximum: max.into_value(),
|
maximum: max.into_value(),
|
||||||
|
knob_visible: true,
|
||||||
|
interactive: true,
|
||||||
step: Value::Constant(ZeroToOne::new(0.05)),
|
step: Value::Constant(ZeroToOne::new(0.05)),
|
||||||
knob_size: UPx::ZERO,
|
knob_size: UPx::ZERO,
|
||||||
horizontal: true,
|
horizontal: true,
|
||||||
|
|
@ -126,15 +130,32 @@ where
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates this slider to not show knobs and returns self.
|
||||||
|
///
|
||||||
|
/// This also prevents the slider from being focused.
|
||||||
|
#[must_use]
|
||||||
|
pub fn knobless(mut self) -> Self {
|
||||||
|
self.knob_visible = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates this slider to ignore all user input and returns self.
|
||||||
|
#[must_use]
|
||||||
|
pub fn non_interactive(mut self) -> Self {
|
||||||
|
self.interactive = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_track(&mut self, spec: &TrackSpec, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
|
fn draw_track(&mut self, spec: &TrackSpec, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||||
if self.horizontal {
|
if self.horizontal {
|
||||||
self.rendered_size = spec.size.width;
|
self.rendered_size = spec.size.width;
|
||||||
} else {
|
} else {
|
||||||
self.rendered_size = spec.size.height;
|
self.rendered_size = spec.size.height;
|
||||||
}
|
}
|
||||||
let half_focus_ring = (Lp::points(2).into_px(context.gfx.scale()) / 2).ceil();
|
let half_focus_ring =
|
||||||
|
spec.if_knobbed(|| (Lp::points(2).into_px(context.gfx.scale()) / 2).ceil());
|
||||||
let focus_ring = half_focus_ring * 2;
|
let focus_ring = half_focus_ring * 2;
|
||||||
let track_length = self.rendered_size - spec.knob_size - focus_ring;
|
let track_length = self.rendered_size - spec.if_knobbed(|| spec.knob_size - focus_ring);
|
||||||
let (start, end) = if let Some(end) = spec.end {
|
let (start, end) = if let Some(end) = spec.end {
|
||||||
(track_length * spec.start, track_length * end)
|
(track_length * spec.start, track_length * end)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -143,15 +164,13 @@ where
|
||||||
let inset = Point::squared(half_focus_ring);
|
let inset = Point::squared(half_focus_ring);
|
||||||
|
|
||||||
let half_track = spec.track_size / 2;
|
let half_track = spec.track_size / 2;
|
||||||
|
let start_inset = (spec.half_knob - half_track).max(Px::ZERO);
|
||||||
// Draw the track
|
// Draw the track
|
||||||
if start > 0 {
|
if start > 0 {
|
||||||
context.gfx.draw_shape(
|
context.gfx.draw_shape(
|
||||||
Shape::filled_round_rect(
|
Shape::filled_round_rect(
|
||||||
Rect::new(
|
Rect::new(
|
||||||
flipped(
|
flipped(!self.horizontal, Point::new(start_inset, start_inset)),
|
||||||
!self.horizontal,
|
|
||||||
Point::new(spec.half_knob - half_track, spec.half_knob - half_track),
|
|
||||||
),
|
|
||||||
flipped(!self.horizontal, Size::new(start, spec.track_size)),
|
flipped(!self.horizontal, Size::new(start, spec.track_size)),
|
||||||
),
|
),
|
||||||
half_track,
|
half_track,
|
||||||
|
|
@ -166,11 +185,14 @@ where
|
||||||
Rect::new(
|
Rect::new(
|
||||||
flipped(
|
flipped(
|
||||||
!self.horizontal,
|
!self.horizontal,
|
||||||
Point::new(end + spec.half_knob, spec.half_knob - half_track),
|
Point::new(end + spec.if_knobbed(|| spec.half_knob), start_inset),
|
||||||
),
|
),
|
||||||
flipped(
|
flipped(
|
||||||
!self.horizontal,
|
!self.horizontal,
|
||||||
Size::new(track_length - end + half_track, spec.track_size),
|
Size::new(
|
||||||
|
track_length - end + spec.if_knobbed(|| half_track),
|
||||||
|
spec.track_size,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
half_track,
|
half_track,
|
||||||
|
|
@ -187,13 +209,16 @@ where
|
||||||
flipped(
|
flipped(
|
||||||
!self.horizontal,
|
!self.horizontal,
|
||||||
Point::new(
|
Point::new(
|
||||||
start + spec.half_knob - half_track,
|
start + spec.if_knobbed(|| spec.half_knob - half_track),
|
||||||
spec.half_knob - half_track,
|
start_inset,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
flipped(
|
flipped(
|
||||||
!self.horizontal,
|
!self.horizontal,
|
||||||
Size::new(end - start + spec.track_size, spec.track_size),
|
Size::new(
|
||||||
|
end - start + spec.if_knobbed(|| spec.track_size),
|
||||||
|
spec.track_size,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
half_track,
|
half_track,
|
||||||
|
|
@ -204,34 +229,73 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw the knob
|
// Draw the knob
|
||||||
let focused = context.focused();
|
if spec.knob_size > 0 {
|
||||||
let this_knob_role = if spec.end.is_some() {
|
let focus = context.focused().then_some(self.focused_knob).flatten();
|
||||||
Knob::End
|
self.draw_knobs(
|
||||||
} else {
|
|
||||||
Knob::Start
|
|
||||||
};
|
|
||||||
self.draw_knob(
|
|
||||||
flipped(
|
|
||||||
!self.horizontal,
|
|
||||||
Point::new(end + spec.half_knob, spec.half_knob) + inset,
|
|
||||||
),
|
|
||||||
focused && self.focused_knob == Some(this_knob_role),
|
|
||||||
focus_ring,
|
|
||||||
spec,
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
|
|
||||||
if spec.end.is_some() {
|
|
||||||
self.draw_knob(
|
|
||||||
flipped(
|
flipped(
|
||||||
!self.horizontal,
|
!self.horizontal,
|
||||||
Point::new(start + spec.half_knob, spec.half_knob) + inset,
|
Point::new(end + spec.half_knob, spec.half_knob) + inset,
|
||||||
),
|
),
|
||||||
focused && matches!(self.focused_knob, Some(Knob::Start)),
|
spec.end.map(|_| {
|
||||||
|
flipped(
|
||||||
|
!self.horizontal,
|
||||||
|
Point::new(start + spec.half_knob, spec.half_knob) + inset,
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
focus,
|
||||||
focus_ring,
|
focus_ring,
|
||||||
spec,
|
spec,
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
|
// let this_knob_role = if spec.end.is_some() {
|
||||||
|
// Knob::End
|
||||||
|
// } else {
|
||||||
|
// Knob::Start
|
||||||
|
// };
|
||||||
|
// self.draw_knob(
|
||||||
|
// flipped(
|
||||||
|
// !self.horizontal,
|
||||||
|
// Point::new(end + spec.half_knob, spec.half_knob) + inset,
|
||||||
|
// ),
|
||||||
|
// focused && self.focused_knob == Some(this_knob_role),
|
||||||
|
// focus_ring,
|
||||||
|
// spec,
|
||||||
|
// context,
|
||||||
|
// );
|
||||||
|
|
||||||
|
// if spec.end.is_some() {
|
||||||
|
// self.draw_knob(
|
||||||
|
// flipped(
|
||||||
|
// !self.horizontal,
|
||||||
|
// Point::new(start + spec.half_knob, spec.half_knob) + inset,
|
||||||
|
// ),
|
||||||
|
// focused && matches!(self.focused_knob, Some(Knob::Start)),
|
||||||
|
// focus_ring,
|
||||||
|
// spec,
|
||||||
|
// context,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_knobs(
|
||||||
|
&mut self,
|
||||||
|
end_knob: Point<Px>,
|
||||||
|
start_knob: Option<Point<Px>>,
|
||||||
|
focus: Option<Knob>,
|
||||||
|
focus_ring_width: Px,
|
||||||
|
spec: &TrackSpec,
|
||||||
|
context: &mut GraphicsContext<'_, '_, '_, '_, '_>,
|
||||||
|
) {
|
||||||
|
let (a, a_is_focused, b) = match (start_knob, focus) {
|
||||||
|
(Some(start_knob), Some(Knob::Start)) => (end_knob, false, Some((start_knob, true))),
|
||||||
|
(Some(start_knob), focus) => (start_knob, false, Some((end_knob, focus.is_some()))),
|
||||||
|
(None, focus) => (end_knob, focus.is_some(), None),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.draw_knob(a, a_is_focused, focus_ring_width, spec, context);
|
||||||
|
if let Some((b, b_is_focused)) = b {
|
||||||
|
self.draw_knob(b, b_is_focused, focus_ring_width, spec, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -291,20 +355,16 @@ where
|
||||||
let end_percent = end.percent_between(&min, &max);
|
let end_percent = end.percent_between(&min, &max);
|
||||||
let knob_width_as_percent =
|
let knob_width_as_percent =
|
||||||
self.knob_size.into_float() / 2. / track_width.into_float();
|
self.knob_size.into_float() / 2. / track_width.into_float();
|
||||||
if percent - *start_percent <= knob_width_as_percent
|
let start_delta = percent - *start_percent;
|
||||||
&& matches!(previous_focus, Some(Knob::Start))
|
let end_delta = *end_percent - percent;
|
||||||
{
|
let on_overlapping_knobs =
|
||||||
|
end_delta <= knob_width_as_percent && start_delta <= knob_width_as_percent;
|
||||||
|
if let (true, Some(previous)) = (on_overlapping_knobs, previous_focus) {
|
||||||
|
previous
|
||||||
|
} else if start_delta < end_delta {
|
||||||
Knob::Start
|
Knob::Start
|
||||||
} else if *end_percent - percent <= knob_width_as_percent
|
|
||||||
&& matches!(previous_focus, Some(Knob::End))
|
|
||||||
{
|
|
||||||
Knob::End
|
|
||||||
} else if value <= start {
|
|
||||||
Knob::Start
|
|
||||||
} else if &value >= end {
|
|
||||||
Knob::End
|
|
||||||
} else {
|
} else {
|
||||||
Knob::Start
|
Knob::End
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match knob {
|
match knob {
|
||||||
|
|
@ -390,10 +450,10 @@ where
|
||||||
let inactive_track_color = context.get(&InactiveTrackColor);
|
let inactive_track_color = context.get(&InactiveTrackColor);
|
||||||
let knob_color = context.get(&KnobColor);
|
let knob_color = context.get(&KnobColor);
|
||||||
let knob_size = self.knob_size.into_signed();
|
let knob_size = self.knob_size.into_signed();
|
||||||
let track_size = context
|
let mut track_size = context.get(&TrackSize).into_px(context.gfx.scale());
|
||||||
.get(&TrackSize)
|
if knob_size > 0 {
|
||||||
.into_px(context.gfx.scale())
|
track_size = track_size.min(knob_size);
|
||||||
.min(knob_size);
|
}
|
||||||
|
|
||||||
let half_knob = knob_size / 2;
|
let half_knob = knob_size / 2;
|
||||||
|
|
||||||
|
|
@ -460,12 +520,24 @@ where
|
||||||
available_space: Size<ConstraintLimit>,
|
available_space: Size<ConstraintLimit>,
|
||||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||||
) -> Size<UPx> {
|
) -> Size<UPx> {
|
||||||
self.knob_size = context.get(&KnobSize).into_upx(context.gfx.scale());
|
self.knob_size = if self.knob_visible {
|
||||||
|
context.get(&KnobSize).into_upx(context.gfx.scale())
|
||||||
|
} else {
|
||||||
|
UPx::ZERO
|
||||||
|
};
|
||||||
let minimum_size = context
|
let minimum_size = context
|
||||||
.get(&MinimumSliderSize)
|
.get(&MinimumSliderSize)
|
||||||
.into_upx(context.gfx.scale());
|
.into_upx(context.gfx.scale());
|
||||||
let focus_ring_width = (Lp::points(2).into_upx(context.gfx.scale()) / 2).ceil() * 2;
|
let focus_ring_width = if self.knob_visible {
|
||||||
let focused_knob_size = self.knob_size + focus_ring_width;
|
(Lp::points(2).into_upx(context.gfx.scale()) / 2).ceil() * 2
|
||||||
|
} else {
|
||||||
|
UPx::ZERO
|
||||||
|
};
|
||||||
|
let static_side = if self.knob_visible {
|
||||||
|
self.knob_size + focus_ring_width
|
||||||
|
} else {
|
||||||
|
context.get(&TrackSize).into_upx(context.gfx.scale())
|
||||||
|
};
|
||||||
|
|
||||||
match (available_space.width, available_space.height) {
|
match (available_space.width, available_space.height) {
|
||||||
(ConstraintLimit::Fill(width), ConstraintLimit::Fill(height)) => {
|
(ConstraintLimit::Fill(width), ConstraintLimit::Fill(height)) => {
|
||||||
|
|
@ -473,17 +545,17 @@ where
|
||||||
// up with a horizontal slider.
|
// up with a horizontal slider.
|
||||||
if width < height {
|
if width < height {
|
||||||
// Vertical slider
|
// Vertical slider
|
||||||
Size::new(focused_knob_size, height.max(minimum_size))
|
Size::new(static_side, height.max(minimum_size))
|
||||||
} else {
|
} else {
|
||||||
// Horizontal slider
|
// Horizontal slider
|
||||||
Size::new(width.max(minimum_size), focused_knob_size)
|
Size::new(width.max(minimum_size), static_side)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(ConstraintLimit::Fill(width), ConstraintLimit::SizeToFit(_)) => {
|
(ConstraintLimit::Fill(width), ConstraintLimit::SizeToFit(_)) => {
|
||||||
Size::new(width.max(minimum_size), focused_knob_size)
|
Size::new(width.max(minimum_size), static_side)
|
||||||
}
|
}
|
||||||
(ConstraintLimit::SizeToFit(_), ConstraintLimit::Fill(height)) => {
|
(ConstraintLimit::SizeToFit(_), ConstraintLimit::Fill(height)) => {
|
||||||
Size::new(focused_knob_size, height.max(minimum_size))
|
Size::new(static_side, height.max(minimum_size))
|
||||||
}
|
}
|
||||||
(ConstraintLimit::SizeToFit(width), ConstraintLimit::SizeToFit(_)) => {
|
(ConstraintLimit::SizeToFit(width), ConstraintLimit::SizeToFit(_)) => {
|
||||||
// When we have no limit on our, we still want to be draggable.
|
// When we have no limit on our, we still want to be draggable.
|
||||||
|
|
@ -493,17 +565,17 @@ where
|
||||||
// user of the slider, a horizontal slider is expected. So, we
|
// user of the slider, a horizontal slider is expected. So, we
|
||||||
// set the minimum measurement based on a horizontal
|
// set the minimum measurement based on a horizontal
|
||||||
// orientation.
|
// orientation.
|
||||||
Size::new(width.min(minimum_size), focused_knob_size)
|
Size::new(width.min(minimum_size), static_side)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hit_test(&mut self, _location: Point<Px>, _context: &mut EventContext<'_, '_>) -> bool {
|
fn hit_test(&mut self, _location: Point<Px>, _context: &mut EventContext<'_, '_>) -> bool {
|
||||||
true
|
self.interactive
|
||||||
}
|
}
|
||||||
|
|
||||||
fn accept_focus(&mut self, context: &mut EventContext<'_, '_>) -> bool {
|
fn accept_focus(&mut self, context: &mut EventContext<'_, '_>) -> bool {
|
||||||
context.get(&AutoFocusableControls).is_all()
|
self.interactive && self.knob_visible && context.get(&AutoFocusableControls).is_all()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn focus(&mut self, context: &mut EventContext<'_, '_>) {
|
fn focus(&mut self, context: &mut EventContext<'_, '_>) {
|
||||||
|
|
@ -556,6 +628,10 @@ where
|
||||||
_button: MouseButton,
|
_button: MouseButton,
|
||||||
context: &mut EventContext<'_, '_>,
|
context: &mut EventContext<'_, '_>,
|
||||||
) -> EventHandling {
|
) -> EventHandling {
|
||||||
|
let true = self.interactive else {
|
||||||
|
return IGNORED;
|
||||||
|
};
|
||||||
|
|
||||||
let previous_focus = match (self.previous_focus.take(), self.focused_knob.take()) {
|
let previous_focus = match (self.previous_focus.take(), self.focused_knob.take()) {
|
||||||
(None | Some(_), Some(focus)) | (Some(focus), None) => Some(focus),
|
(None | Some(_), Some(focus)) | (Some(focus), None) => Some(focus),
|
||||||
(None, None) => None,
|
(None, None) => None,
|
||||||
|
|
@ -593,6 +669,10 @@ where
|
||||||
_is_synthetic: bool,
|
_is_synthetic: bool,
|
||||||
_context: &mut EventContext<'_, '_>,
|
_context: &mut EventContext<'_, '_>,
|
||||||
) -> EventHandling {
|
) -> EventHandling {
|
||||||
|
let true = self.interactive else {
|
||||||
|
return IGNORED;
|
||||||
|
};
|
||||||
|
|
||||||
let forwards = match input.logical_key {
|
let forwards = match input.logical_key {
|
||||||
Key::Named(NamedKey::ArrowLeft | NamedKey::ArrowUp) => false,
|
Key::Named(NamedKey::ArrowLeft | NamedKey::ArrowUp) => false,
|
||||||
Key::Named(NamedKey::ArrowRight | NamedKey::ArrowDown) => true,
|
Key::Named(NamedKey::ArrowRight | NamedKey::ArrowDown) => true,
|
||||||
|
|
@ -614,6 +694,10 @@ where
|
||||||
_phase: TouchPhase,
|
_phase: TouchPhase,
|
||||||
_context: &mut EventContext<'_, '_>,
|
_context: &mut EventContext<'_, '_>,
|
||||||
) -> EventHandling {
|
) -> EventHandling {
|
||||||
|
let true = self.interactive else {
|
||||||
|
return IGNORED;
|
||||||
|
};
|
||||||
|
|
||||||
let factor: f32 = match delta {
|
let factor: f32 = match delta {
|
||||||
MouseScrollDelta::LineDelta(_, y) => y,
|
MouseScrollDelta::LineDelta(_, y) => y,
|
||||||
MouseScrollDelta::PixelDelta(pt) => pt.y.cast(),
|
MouseScrollDelta::PixelDelta(pt) => pt.y.cast(),
|
||||||
|
|
@ -645,6 +729,19 @@ struct TrackSpec {
|
||||||
inactive_track_color: Color,
|
inactive_track_color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TrackSpec {
|
||||||
|
fn if_knobbed<R>(&self, knobbed: impl FnOnce() -> R) -> R
|
||||||
|
where
|
||||||
|
R: Default,
|
||||||
|
{
|
||||||
|
if self.knob_size > 0 {
|
||||||
|
knobbed()
|
||||||
|
} else {
|
||||||
|
R::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn flipped<T, Unit>(flip: bool, value: T) -> T
|
fn flipped<T, Unit>(flip: bool, value: T) -> T
|
||||||
where
|
where
|
||||||
T: IntoComponents<Unit> + FromComponents<Unit>,
|
T: IntoComponents<Unit> + FromComponents<Unit>,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue