mirror of
https://github.com/danbulant/cushy
synced 2026-06-22 08:02:32 +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::value::Dynamic;
|
||||
use gooey::widget::MakeWidget;
|
||||
use gooey::widgets::progress::Progressable;
|
||||
use gooey::{Run, WithClone};
|
||||
use kludgine::figures::units::Lp;
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
let animation = Dynamic::new(AnimationHandle::new());
|
||||
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
|
||||
// animation to create a delay. This can also be used to call a function
|
||||
|
|
@ -20,7 +21,13 @@ fn main() -> gooey::Result {
|
|||
"To 0"
|
||||
.into_button()
|
||||
.on_click(animate_to(&animation, &value, 0))
|
||||
.and(label)
|
||||
.and(
|
||||
value
|
||||
.clone()
|
||||
.progress_bar_to(100)
|
||||
.width(Lp::inches(3))
|
||||
.centered(),
|
||||
)
|
||||
.and(
|
||||
"To 100"
|
||||
.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.
|
||||
#[derive(Clone)]
|
||||
pub struct DynamicTransition<T> {
|
||||
/// The dynamic value to change.
|
||||
pub dynamic: Dynamic<T>,
|
||||
|
|
@ -226,6 +227,7 @@ where
|
|||
/// [`Duration`], using the `Easing` generic parameter to control how the value
|
||||
/// is interpolated.
|
||||
#[must_use = "animations are not performed until they are spawned"]
|
||||
#[derive(Clone)]
|
||||
pub struct Animation<Target, Easing = Linear>
|
||||
where
|
||||
Target: AnimationTarget,
|
||||
|
|
@ -321,6 +323,22 @@ pub trait IntoAnimate: Sized + Send + Sync {
|
|||
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.
|
||||
fn on_complete<F>(self, on_complete: F) -> OnCompleteAnimation<Self>
|
||||
where
|
||||
|
|
@ -473,6 +491,7 @@ impl Drop for AnimationHandle {
|
|||
}
|
||||
|
||||
/// An animation combinator that runs animation `A`, then animation `B`.
|
||||
#[derive(Clone)]
|
||||
pub struct Chain<A: IntoAnimate, B: IntoAnimate>(A, B);
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// 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>
|
||||
where
|
||||
T: Clone,
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ mod canvas;
|
|||
pub mod checkbox;
|
||||
pub mod container;
|
||||
mod custom;
|
||||
mod data;
|
||||
mod expand;
|
||||
pub mod input;
|
||||
pub mod label;
|
||||
mod mode_switch;
|
||||
pub mod progress;
|
||||
mod resize;
|
||||
pub mod scroll;
|
||||
pub mod slider;
|
||||
|
|
@ -26,10 +28,12 @@ pub use canvas::Canvas;
|
|||
pub use checkbox::Checkbox;
|
||||
pub use container::Container;
|
||||
pub use custom::Custom;
|
||||
pub use data::Data;
|
||||
pub use expand::Expand;
|
||||
pub use input::Input;
|
||||
pub use label::Label;
|
||||
pub use mode_switch::ThemedMode;
|
||||
pub use progress::ProgressBar;
|
||||
pub use resize::Resize;
|
||||
pub use scroll::Scroll;
|
||||
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%.
|
||||
pub step: Value<ZeroToOne>,
|
||||
knob_visible: bool,
|
||||
interactive: bool,
|
||||
knob_size: UPx,
|
||||
horizontal: bool,
|
||||
rendered_size: Px,
|
||||
|
|
@ -88,6 +90,8 @@ where
|
|||
value: value.into_dynamic(),
|
||||
minimum: min.into_value(),
|
||||
maximum: max.into_value(),
|
||||
knob_visible: true,
|
||||
interactive: true,
|
||||
step: Value::Constant(ZeroToOne::new(0.05)),
|
||||
knob_size: UPx::ZERO,
|
||||
horizontal: true,
|
||||
|
|
@ -126,15 +130,32 @@ where
|
|||
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<'_, '_, '_, '_, '_>) {
|
||||
if self.horizontal {
|
||||
self.rendered_size = spec.size.width;
|
||||
} else {
|
||||
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 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 {
|
||||
(track_length * spec.start, track_length * end)
|
||||
} else {
|
||||
|
|
@ -143,15 +164,13 @@ where
|
|||
let inset = Point::squared(half_focus_ring);
|
||||
|
||||
let half_track = spec.track_size / 2;
|
||||
let start_inset = (spec.half_knob - half_track).max(Px::ZERO);
|
||||
// Draw the track
|
||||
if start > 0 {
|
||||
context.gfx.draw_shape(
|
||||
Shape::filled_round_rect(
|
||||
Rect::new(
|
||||
flipped(
|
||||
!self.horizontal,
|
||||
Point::new(spec.half_knob - half_track, spec.half_knob - half_track),
|
||||
),
|
||||
flipped(!self.horizontal, Point::new(start_inset, start_inset)),
|
||||
flipped(!self.horizontal, Size::new(start, spec.track_size)),
|
||||
),
|
||||
half_track,
|
||||
|
|
@ -166,11 +185,14 @@ where
|
|||
Rect::new(
|
||||
flipped(
|
||||
!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(
|
||||
!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,
|
||||
|
|
@ -187,13 +209,16 @@ where
|
|||
flipped(
|
||||
!self.horizontal,
|
||||
Point::new(
|
||||
start + spec.half_knob - half_track,
|
||||
spec.half_knob - half_track,
|
||||
start + spec.if_knobbed(|| spec.half_knob - half_track),
|
||||
start_inset,
|
||||
),
|
||||
),
|
||||
flipped(
|
||||
!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,
|
||||
|
|
@ -204,34 +229,73 @@ where
|
|||
}
|
||||
|
||||
// Draw the knob
|
||||
let focused = context.focused();
|
||||
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(
|
||||
if spec.knob_size > 0 {
|
||||
let focus = context.focused().then_some(self.focused_knob).flatten();
|
||||
self.draw_knobs(
|
||||
flipped(
|
||||
!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,
|
||||
spec,
|
||||
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 knob_width_as_percent =
|
||||
self.knob_size.into_float() / 2. / track_width.into_float();
|
||||
if percent - *start_percent <= knob_width_as_percent
|
||||
&& matches!(previous_focus, Some(Knob::Start))
|
||||
{
|
||||
let start_delta = percent - *start_percent;
|
||||
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
|
||||
} 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 {
|
||||
Knob::Start
|
||||
Knob::End
|
||||
}
|
||||
};
|
||||
match knob {
|
||||
|
|
@ -390,10 +450,10 @@ where
|
|||
let inactive_track_color = context.get(&InactiveTrackColor);
|
||||
let knob_color = context.get(&KnobColor);
|
||||
let knob_size = self.knob_size.into_signed();
|
||||
let track_size = context
|
||||
.get(&TrackSize)
|
||||
.into_px(context.gfx.scale())
|
||||
.min(knob_size);
|
||||
let mut track_size = context.get(&TrackSize).into_px(context.gfx.scale());
|
||||
if knob_size > 0 {
|
||||
track_size = track_size.min(knob_size);
|
||||
}
|
||||
|
||||
let half_knob = knob_size / 2;
|
||||
|
||||
|
|
@ -460,12 +520,24 @@ where
|
|||
available_space: Size<ConstraintLimit>,
|
||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||
) -> 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
|
||||
.get(&MinimumSliderSize)
|
||||
.into_upx(context.gfx.scale());
|
||||
let focus_ring_width = (Lp::points(2).into_upx(context.gfx.scale()) / 2).ceil() * 2;
|
||||
let focused_knob_size = self.knob_size + focus_ring_width;
|
||||
let focus_ring_width = if self.knob_visible {
|
||||
(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) {
|
||||
(ConstraintLimit::Fill(width), ConstraintLimit::Fill(height)) => {
|
||||
|
|
@ -473,17 +545,17 @@ where
|
|||
// up with a horizontal slider.
|
||||
if width < height {
|
||||
// Vertical slider
|
||||
Size::new(focused_knob_size, height.max(minimum_size))
|
||||
Size::new(static_side, height.max(minimum_size))
|
||||
} else {
|
||||
// Horizontal slider
|
||||
Size::new(width.max(minimum_size), focused_knob_size)
|
||||
Size::new(width.max(minimum_size), static_side)
|
||||
}
|
||||
}
|
||||
(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)) => {
|
||||
Size::new(focused_knob_size, height.max(minimum_size))
|
||||
Size::new(static_side, height.max(minimum_size))
|
||||
}
|
||||
(ConstraintLimit::SizeToFit(width), ConstraintLimit::SizeToFit(_)) => {
|
||||
// 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
|
||||
// set the minimum measurement based on a horizontal
|
||||
// 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 {
|
||||
true
|
||||
self.interactive
|
||||
}
|
||||
|
||||
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<'_, '_>) {
|
||||
|
|
@ -556,6 +628,10 @@ where
|
|||
_button: MouseButton,
|
||||
context: &mut EventContext<'_, '_>,
|
||||
) -> EventHandling {
|
||||
let true = self.interactive else {
|
||||
return IGNORED;
|
||||
};
|
||||
|
||||
let previous_focus = match (self.previous_focus.take(), self.focused_knob.take()) {
|
||||
(None | Some(_), Some(focus)) | (Some(focus), None) => Some(focus),
|
||||
(None, None) => None,
|
||||
|
|
@ -593,6 +669,10 @@ where
|
|||
_is_synthetic: bool,
|
||||
_context: &mut EventContext<'_, '_>,
|
||||
) -> EventHandling {
|
||||
let true = self.interactive else {
|
||||
return IGNORED;
|
||||
};
|
||||
|
||||
let forwards = match input.logical_key {
|
||||
Key::Named(NamedKey::ArrowLeft | NamedKey::ArrowUp) => false,
|
||||
Key::Named(NamedKey::ArrowRight | NamedKey::ArrowDown) => true,
|
||||
|
|
@ -614,6 +694,10 @@ where
|
|||
_phase: TouchPhase,
|
||||
_context: &mut EventContext<'_, '_>,
|
||||
) -> EventHandling {
|
||||
let true = self.interactive else {
|
||||
return IGNORED;
|
||||
};
|
||||
|
||||
let factor: f32 = match delta {
|
||||
MouseScrollDelta::LineDelta(_, y) => y,
|
||||
MouseScrollDelta::PixelDelta(pt) => pt.y.cast(),
|
||||
|
|
@ -645,6 +729,19 @@ struct TrackSpec {
|
|||
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
|
||||
where
|
||||
T: IntoComponents<Unit> + FromComponents<Unit>,
|
||||
|
|
|
|||
Loading…
Reference in a new issue