mirror of
https://github.com/danbulant/cushy
synced 2026-06-19 22:41:10 +00:00
Slider
This commit is contained in:
parent
7439c6a68f
commit
eb4b24f4a9
9 changed files with 538 additions and 37 deletions
6
Cargo.lock
generated
6
Cargo.lock
generated
|
|
@ -455,9 +455,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "etagere"
|
||||
version = "0.2.9"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bf70b9ea3a235a7432b4f481854815e2d4fb2fe824c1f5fb09b8985dd06b3e9"
|
||||
checksum = "fcf22f748754352918e082e0039335ee92454a5d62bcaf69b5e8daf5907d9644"
|
||||
dependencies = [
|
||||
"euclid",
|
||||
"svg_fmt",
|
||||
|
|
@ -481,7 +481,7 @@ checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
|
|||
[[package]]
|
||||
name = "figures"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/khonsulabs/figures#f5b9ca5cf181b748897b269ad47d7a9f2d1f3eac"
|
||||
source = "git+https://github.com/khonsulabs/figures#3e12470aa10d36d0b38f0441a0e89ae03ccd448b"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"euclid",
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
use gooey::animation::ZeroToOne;
|
||||
use gooey::styles::components::{TextColor, WidgetBackground};
|
||||
use gooey::styles::{ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme, ThemePair};
|
||||
use gooey::value::{Dynamic, MapEach};
|
||||
use gooey::widget::MakeWidget;
|
||||
use gooey::widgets::{Input, Label, Stack};
|
||||
use gooey::widgets::{Label, Scroll, Slider, Stack};
|
||||
use gooey::Run;
|
||||
use kludgine::Color;
|
||||
|
||||
const PRIMARY_HUE: f32 = -120.;
|
||||
const PRIMARY_HUE: f32 = 240.;
|
||||
const SECONDARY_HUE: f32 = 0.;
|
||||
const TERTIARY_HUE: f32 = -30.;
|
||||
const TERTIARY_HUE: f32 = 330.;
|
||||
const ERROR_HUE: f32 = 30.;
|
||||
|
||||
fn main() -> gooey::Result {
|
||||
|
|
@ -41,23 +42,21 @@ fn main() -> gooey::Result {
|
|||
},
|
||||
);
|
||||
|
||||
Stack::rows(
|
||||
Stack::columns(
|
||||
Stack::columns(
|
||||
Scroll::vertical(Stack::rows(
|
||||
primary_editor
|
||||
.and(secondary_editor)
|
||||
.and(tertiary_editor)
|
||||
.and(error_editor)
|
||||
.and(neutral_editor)
|
||||
.and(neutral_variant_editor),
|
||||
)
|
||||
.and(Stack::columns(
|
||||
theme(default_theme.map_each(|theme| theme.dark), "Dark")
|
||||
.and(theme(default_theme.map_each(|theme| theme.light), "Light"))
|
||||
.and(fixed_themes(
|
||||
default_theme.map_each(|theme| theme.primary_fixed),
|
||||
default_theme.map_each(|theme| theme.secondary_fixed),
|
||||
default_theme.map_each(|theme| theme.tertiary_fixed),
|
||||
)),
|
||||
))
|
||||
.and(theme(default_theme.map_each(|theme| theme.dark), "Dark"))
|
||||
.and(theme(default_theme.map_each(|theme| theme.light), "Light"))
|
||||
.and(fixed_themes(
|
||||
default_theme.map_each(|theme| theme.primary_fixed),
|
||||
default_theme.map_each(|theme| theme.secondary_fixed),
|
||||
default_theme.map_each(|theme| theme.tertiary_fixed),
|
||||
)),
|
||||
)
|
||||
.expand()
|
||||
|
|
@ -66,13 +65,14 @@ fn main() -> gooey::Result {
|
|||
|
||||
fn color_editor(
|
||||
initial_hue: f32,
|
||||
initial_saturation: f32,
|
||||
initial_saturation: impl Into<ZeroToOne>,
|
||||
label: &str,
|
||||
) -> (Dynamic<ColorSource>, impl MakeWidget) {
|
||||
let hue_text = Dynamic::new(initial_hue.to_string());
|
||||
let hue = hue_text.map_each(|hue| hue.parse::<f32>().unwrap_or_default());
|
||||
let saturation_text = Dynamic::new(initial_saturation.to_string());
|
||||
let saturation = saturation_text.map_each(|sat| sat.parse::<f32>().unwrap_or_default());
|
||||
let hue = Dynamic::new(initial_hue);
|
||||
let hue_text = hue.map_each(|hue| hue.to_string());
|
||||
let saturation = Dynamic::new(initial_saturation.into());
|
||||
let saturation_text = saturation.map_each(|saturation| saturation.to_string());
|
||||
|
||||
let color =
|
||||
(&hue, &saturation).map_each(|(hue, saturation)| ColorSource::new(*hue, *saturation));
|
||||
|
||||
|
|
@ -80,10 +80,11 @@ fn color_editor(
|
|||
color,
|
||||
Stack::rows(
|
||||
Label::new(label)
|
||||
.and(Input::new(hue_text))
|
||||
.and(Input::new(saturation_text)),
|
||||
)
|
||||
.expand(),
|
||||
.and(Slider::<f32>::new(hue, 0., 360.))
|
||||
.and(Label::new(hue_text))
|
||||
.and(Slider::<ZeroToOne>::from_value(saturation))
|
||||
.and(Label::new(saturation_text)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,14 +40,16 @@
|
|||
pub mod easings;
|
||||
|
||||
use std::fmt::Debug;
|
||||
use std::ops::{ControlFlow, Deref};
|
||||
use std::ops::{ControlFlow, Deref, Div, Mul};
|
||||
use std::panic::{RefUnwindSafe, UnwindSafe};
|
||||
use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock, PoisonError};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use alot::{LotId, Lots};
|
||||
use intentional::Cast;
|
||||
use kempt::Set;
|
||||
use kludgine::figures::Ranged;
|
||||
use kludgine::Color;
|
||||
|
||||
use crate::animation::easings::Linear;
|
||||
|
|
@ -703,6 +705,56 @@ impl LinearInterpolate for Color {
|
|||
}
|
||||
}
|
||||
|
||||
/// Calculates the ratio of one value against a minimum and maximum.
|
||||
pub trait PercentBetween {
|
||||
/// Return the percentage that `self` is between `min` and `max`.
|
||||
fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne;
|
||||
}
|
||||
|
||||
macro_rules! impl_percent_between {
|
||||
($type:ident, $float:ident) => {
|
||||
impl PercentBetween for $type {
|
||||
fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne {
|
||||
let range = *max - *min;
|
||||
ZeroToOne::from(*self as $float / range as $float)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_percent_between!(u8, f32);
|
||||
impl_percent_between!(u16, f32);
|
||||
impl_percent_between!(u32, f32);
|
||||
impl_percent_between!(u64, f32);
|
||||
impl_percent_between!(u128, f64);
|
||||
impl_percent_between!(usize, f64);
|
||||
impl_percent_between!(i8, f32);
|
||||
impl_percent_between!(i16, f32);
|
||||
impl_percent_between!(i32, f32);
|
||||
impl_percent_between!(i64, f32);
|
||||
impl_percent_between!(i128, f64);
|
||||
impl_percent_between!(isize, f64);
|
||||
impl_percent_between!(f32, f32);
|
||||
impl_percent_between!(f64, f64);
|
||||
|
||||
impl PercentBetween for Color {
|
||||
fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne {
|
||||
fn channel_percent(
|
||||
value: Color,
|
||||
min: Color,
|
||||
max: Color,
|
||||
func: impl Fn(Color) -> u8,
|
||||
) -> ZeroToOne {
|
||||
func(value).percent_between(&func(min), &func(max))
|
||||
}
|
||||
|
||||
channel_percent(*self, *min, *max, Color::red)
|
||||
* channel_percent(*self, *min, *max, Color::green)
|
||||
* channel_percent(*self, *min, *max, Color::blue)
|
||||
* channel_percent(*self, *min, *max, Color::alpha)
|
||||
}
|
||||
}
|
||||
|
||||
/// An `f32` that is clamped between 0.0 and 1.0 and cannot be NaN or Infinity.
|
||||
///
|
||||
/// Because of these restrictions, this type implements `Ord` and `Eq`.
|
||||
|
|
@ -734,6 +786,18 @@ impl ZeroToOne {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<f32> for ZeroToOne {
|
||||
fn from(value: f32) -> Self {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for ZeroToOne {
|
||||
fn from(value: f64) -> Self {
|
||||
Self::new(value.cast())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ZeroToOne {
|
||||
fn default() -> Self {
|
||||
Self::ZERO
|
||||
|
|
@ -787,6 +851,33 @@ impl LinearInterpolate for ZeroToOne {
|
|||
}
|
||||
}
|
||||
|
||||
impl PercentBetween for ZeroToOne {
|
||||
fn percent_between(&self, min: &Self, max: &Self) -> ZeroToOne {
|
||||
self.0.percent_between(&min.0, &max.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul for ZeroToOne {
|
||||
type Output = Self;
|
||||
|
||||
fn mul(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 * rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Div for ZeroToOne {
|
||||
type Output = Self;
|
||||
|
||||
fn div(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 / rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for ZeroToOne {
|
||||
const MAX: Self = Self::ONE;
|
||||
const MIN: Self = Self::ZERO;
|
||||
}
|
||||
|
||||
/// An easing function for customizing animations.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum EasingFunction {
|
||||
|
|
|
|||
|
|
@ -47,10 +47,10 @@ impl ConstraintLimit {
|
|||
}
|
||||
|
||||
/// Converts `measured` to unsigned pixels, and adjusts it according to the
|
||||
/// contraint's intentions.
|
||||
/// constraint's intentions.
|
||||
///
|
||||
/// If this constraint is of a known size, it will return the maximum of the
|
||||
/// measured size and the contraint. If it is of an unknown size, it will
|
||||
/// measured size and the constraint. If it is of an unknown size, it will
|
||||
/// return the measured size.
|
||||
pub fn fit_measured<Unit>(self, measured: Unit, scale: Fraction) -> UPx
|
||||
where
|
||||
|
|
|
|||
|
|
@ -571,9 +571,15 @@ where
|
|||
pub struct Group(Name);
|
||||
|
||||
impl Group {
|
||||
/// Returns a new group with `name`.
|
||||
#[must_use]
|
||||
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
|
||||
Self(Name::new(name))
|
||||
}
|
||||
|
||||
/// Returns a new instance using the group name of `T`.
|
||||
#[must_use]
|
||||
pub fn new<T>() -> Self
|
||||
pub fn from_group<T>() -> Self
|
||||
where
|
||||
T: ComponentGroup,
|
||||
{
|
||||
|
|
@ -625,7 +631,7 @@ impl ComponentName {
|
|||
|
||||
/// Returns a new instance using `G` and `name`.
|
||||
pub fn named<G: ComponentGroup>(name: impl Into<Name>) -> Self {
|
||||
Self::new(Group::new::<G>(), name)
|
||||
Self::new(Group::from_group::<G>(), name)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1130,10 +1136,10 @@ impl ColorSource {
|
|||
/// Returns a new source with the given hue (in degrees) and saturation (0.0
|
||||
/// - 1.0).
|
||||
#[must_use]
|
||||
pub fn new(hue: f32, saturation: f32) -> Self {
|
||||
pub fn new(hue: impl Into<OklabHue>, saturation: impl Into<ZeroToOne>) -> Self {
|
||||
Self {
|
||||
hue: OklabHue::new(hue),
|
||||
saturation: ZeroToOne::new(saturation),
|
||||
hue: hue.into(),
|
||||
saturation: saturation.into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
37
src/value.rs
37
src/value.rs
|
|
@ -139,6 +139,18 @@ impl<T> Dynamic<T> {
|
|||
self.0.get().value
|
||||
}
|
||||
|
||||
/// Returns a clone of the currently contained value.
|
||||
///
|
||||
/// `context` will be invalidated when the value is updated.
|
||||
#[must_use]
|
||||
pub fn get_tracked(&self, context: &WidgetContext<'_, '_>) -> T
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
context.redraw_when_changed(self);
|
||||
self.get()
|
||||
}
|
||||
|
||||
/// Returns the currently stored value, replacing the current contents with
|
||||
/// `T::default()`.
|
||||
#[must_use]
|
||||
|
|
@ -669,6 +681,20 @@ impl<T> Value<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Maps the current contents to `map` and returns the result.
|
||||
///
|
||||
/// If `self` is a dynamic, `context` will be invalidated when the value is
|
||||
/// updated.
|
||||
pub fn map_tracked<R>(&self, context: &WidgetContext<'_, '_>, map: impl FnOnce(&T) -> R) -> R {
|
||||
match self {
|
||||
Value::Constant(value) => map(value),
|
||||
Value::Dynamic(dynamic) => {
|
||||
context.redraw_when_changed(dynamic);
|
||||
dynamic.map_ref(map)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps the current contents with exclusive access and returns the result.
|
||||
pub fn map_mut<R>(&mut self, map: impl FnOnce(&mut T) -> R) -> R {
|
||||
match self {
|
||||
|
|
@ -685,6 +711,17 @@ impl<T> Value<T> {
|
|||
self.map(Clone::clone)
|
||||
}
|
||||
|
||||
/// Returns a clone of the currently stored value.
|
||||
///
|
||||
/// If `self` is a dynamic, `context` will be invalidated when the value is
|
||||
/// updated.
|
||||
pub fn get_tracked(&self, context: &WidgetContext<'_, '_>) -> T
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.map_tracked(context, Clone::clone)
|
||||
}
|
||||
|
||||
/// Returns the current generation of the data stored, if the contained
|
||||
/// value is [`Dynamic`].
|
||||
pub fn generation(&self) -> Option<Generation> {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ mod input;
|
|||
pub mod label;
|
||||
mod resize;
|
||||
pub mod scroll;
|
||||
mod slider;
|
||||
mod space;
|
||||
pub mod stack;
|
||||
mod style;
|
||||
|
|
@ -21,6 +22,7 @@ pub use input::Input;
|
|||
pub use label::Label;
|
||||
pub use resize::Resize;
|
||||
pub use scroll::Scroll;
|
||||
pub use slider::Slider;
|
||||
pub use space::Space;
|
||||
pub use stack::Stack;
|
||||
pub use style::Style;
|
||||
|
|
|
|||
|
|
@ -193,12 +193,12 @@ impl Widget for Scroll {
|
|||
if self.enabled.x {
|
||||
ConstraintLimit::ClippedAfter(UPx::MAX - scroll.x.into_unsigned())
|
||||
} else {
|
||||
ConstraintLimit::Known(control_size.width.into_unsigned())
|
||||
available_space.width
|
||||
},
|
||||
if self.enabled.y {
|
||||
ConstraintLimit::ClippedAfter(UPx::MAX - scroll.y.into_unsigned())
|
||||
} else {
|
||||
ConstraintLimit::Known(control_size.height.into_unsigned())
|
||||
available_space.height
|
||||
},
|
||||
);
|
||||
let managed = self.contents.mounted(&mut context.as_event_context());
|
||||
|
|
@ -255,7 +255,22 @@ impl Widget for Scroll {
|
|||
);
|
||||
context.set_child_layout(&managed, region);
|
||||
|
||||
Size::new(available_space.width.max(), available_space.height.max())
|
||||
Size::new(
|
||||
if self.enabled.x {
|
||||
available_space
|
||||
.width
|
||||
.fit_measured(self.content_size.width, context.gfx.scale())
|
||||
} else {
|
||||
self.content_size.width.into_unsigned()
|
||||
},
|
||||
if self.enabled.y {
|
||||
available_space
|
||||
.height
|
||||
.fit_measured(self.content_size.height, context.gfx.scale())
|
||||
} else {
|
||||
self.content_size.height.into_unsigned()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn mouse_wheel(
|
||||
|
|
|
|||
349
src/widgets/slider.rs
Normal file
349
src/widgets/slider.rs
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
use std::borrow::Cow;
|
||||
use std::fmt::Debug;
|
||||
use std::panic::UnwindSafe;
|
||||
|
||||
use kludgine::app::winit::event::{DeviceId, MouseButton};
|
||||
use kludgine::figures::units::{Lp, Px, UPx};
|
||||
use kludgine::figures::{
|
||||
FloatConversion, IntoSigned, IntoUnsigned, Point, Ranged, Rect, ScreenScale, Size,
|
||||
};
|
||||
use kludgine::shapes::Shape;
|
||||
use kludgine::{Color, Origin};
|
||||
|
||||
use crate::animation::{LinearInterpolate, PercentBetween};
|
||||
use crate::context::{EventContext, GraphicsContext, LayoutContext, WidgetContext};
|
||||
use crate::styles::{ComponentDefinition, ComponentName, Dimension, Group, NamedComponent};
|
||||
use crate::value::{Dynamic, IntoDynamic, IntoValue, Value};
|
||||
use crate::widget::{EventHandling, Widget, HANDLED};
|
||||
use crate::ConstraintLimit;
|
||||
|
||||
/// A widget that allows sliding between two values.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Slider<T> {
|
||||
/// The current value.
|
||||
pub value: Dynamic<T>,
|
||||
/// The minimum value represented by this slider.
|
||||
pub minimum: Value<T>,
|
||||
/// The maximum value represented by this slider.
|
||||
pub maximum: Value<T>,
|
||||
knob_size: UPx,
|
||||
horizontal: bool,
|
||||
rendered_size: Px,
|
||||
}
|
||||
|
||||
impl<T> Slider<T>
|
||||
where
|
||||
T: Ranged,
|
||||
{
|
||||
/// Returns a new slider over `value` using the types full range.
|
||||
#[must_use]
|
||||
pub fn from_value(value: impl IntoDynamic<T>) -> Self {
|
||||
Self::new(value, T::MIN, T::MAX)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Slider<T> {
|
||||
/// Returns a new slider using `value` as the slider's value, keeping the
|
||||
/// value between `min` and `max`.
|
||||
#[must_use]
|
||||
pub fn new(value: impl IntoDynamic<T>, min: impl IntoValue<T>, max: impl IntoValue<T>) -> Self {
|
||||
Self {
|
||||
value: value.into_dynamic(),
|
||||
minimum: min.into_value(),
|
||||
maximum: max.into_value(),
|
||||
knob_size: UPx(0),
|
||||
horizontal: true,
|
||||
rendered_size: Px(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the maximum value of this slider to `max` and returns self.
|
||||
#[must_use]
|
||||
pub fn maximum(mut self, max: impl IntoValue<T>) -> Self {
|
||||
self.maximum = max.into_value();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the minimum value of this slider to `min` and returns self.
|
||||
#[must_use]
|
||||
pub fn minimum(mut self, min: impl IntoValue<T>) -> Self {
|
||||
self.minimum = min.into_value();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Slider<T>
|
||||
where
|
||||
T: LinearInterpolate + Clone,
|
||||
{
|
||||
fn update_from_click(&mut self, position: Point<Px>) {
|
||||
let position = if self.horizontal {
|
||||
position.x
|
||||
} else {
|
||||
position.y
|
||||
};
|
||||
let position = position.clamp(Px(0), self.rendered_size);
|
||||
let percent = position.into_float() / self.rendered_size.into_float();
|
||||
let min = self.minimum.get();
|
||||
let max = self.maximum.get();
|
||||
self.value.update(min.lerp(&max, percent));
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Widget for Slider<T>
|
||||
where
|
||||
T: Clone
|
||||
+ Debug
|
||||
+ PartialOrd
|
||||
+ LinearInterpolate
|
||||
+ PercentBetween
|
||||
+ UnwindSafe
|
||||
+ Send
|
||||
+ 'static,
|
||||
{
|
||||
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
|
||||
let styles = context.query_styles(&[&TrackColor, &KnobColor, &TrackSize]);
|
||||
let track_color = styles.get(&TrackColor, context);
|
||||
let knob_color = styles.get(&KnobColor, context);
|
||||
let knob_size = self.knob_size.into_signed();
|
||||
let track_size = styles
|
||||
.get(&TrackSize, context)
|
||||
.into_px(context.gfx.scale())
|
||||
.min(knob_size);
|
||||
|
||||
let half_knob = knob_size / 2;
|
||||
let half_track = track_size / 2;
|
||||
|
||||
let mut value = self.value.get_tracked(context);
|
||||
let min = self.minimum.get_tracked(context);
|
||||
let mut max = self.maximum.get_tracked(context);
|
||||
|
||||
if max < min {
|
||||
self.maximum.map_mut(|max| *max = min.clone());
|
||||
max = min.clone();
|
||||
}
|
||||
let mut value_clamped = false;
|
||||
if value < min {
|
||||
value_clamped = true;
|
||||
value = min.clone();
|
||||
} else if value > max {
|
||||
value_clamped = true;
|
||||
value = max.clone();
|
||||
}
|
||||
|
||||
if value_clamped {
|
||||
self.value.map_mut(|v| *v = value.clone());
|
||||
}
|
||||
|
||||
let percent = value.percent_between(&min, &max);
|
||||
|
||||
let size = context.gfx.region().size;
|
||||
self.horizontal = size.width >= size.height;
|
||||
|
||||
if self.horizontal {
|
||||
self.rendered_size = size.width;
|
||||
// Draw the track
|
||||
context.gfx.draw_shape(
|
||||
&Shape::filled_rect(
|
||||
Rect::new(
|
||||
Point::new(half_knob, half_knob - half_track),
|
||||
Size::new(size.width - knob_size, track_size),
|
||||
),
|
||||
track_color,
|
||||
),
|
||||
Point::default(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
// Draw the knob
|
||||
context.gfx.draw_shape(
|
||||
&Shape::filled_circle(half_knob, knob_color, Origin::Center),
|
||||
Point::new(half_knob + (size.width - knob_size) * *percent, half_knob),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
} else {
|
||||
// Vertical slider
|
||||
self.rendered_size = size.height;
|
||||
|
||||
// Draw the track
|
||||
context.gfx.draw_shape(
|
||||
&Shape::filled_rect(
|
||||
Rect::new(
|
||||
Point::new(half_knob - half_track, half_knob),
|
||||
Size::new(track_size, size.height - knob_size),
|
||||
),
|
||||
track_color,
|
||||
),
|
||||
Point::default(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
// Draw the knob
|
||||
context.gfx.draw_shape(
|
||||
&Shape::filled_circle(half_knob, knob_color, Origin::Center),
|
||||
Point::new(half_knob, half_knob + (size.height - knob_size) * *percent),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
available_space: Size<ConstraintLimit>,
|
||||
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||
) -> Size<UPx> {
|
||||
let styles = context.query_styles(&[&KnobSize, &MinimumSliderSize]);
|
||||
self.knob_size = styles
|
||||
.get(&KnobSize, context)
|
||||
.into_px(context.gfx.scale())
|
||||
.into_unsigned();
|
||||
let minimum_size = styles
|
||||
.get(&MinimumSliderSize, context)
|
||||
.into_px(context.gfx.scale())
|
||||
.into_unsigned();
|
||||
|
||||
match (available_space.width, available_space.height) {
|
||||
(ConstraintLimit::Known(width), ConstraintLimit::Known(height)) => {
|
||||
// This comparison is done such that if width == height, we end
|
||||
// up with a horizontal slider.
|
||||
if width < height {
|
||||
// Vertical slider
|
||||
Size::new(self.knob_size, height.max(minimum_size))
|
||||
} else {
|
||||
// Horizontal slider
|
||||
Size::new(width.max(minimum_size), self.knob_size)
|
||||
}
|
||||
}
|
||||
(ConstraintLimit::Known(width), ConstraintLimit::ClippedAfter(_)) => {
|
||||
Size::new(width.max(minimum_size), self.knob_size)
|
||||
}
|
||||
(ConstraintLimit::ClippedAfter(_), ConstraintLimit::Known(height)) => {
|
||||
Size::new(self.knob_size, height.max(minimum_size))
|
||||
}
|
||||
(ConstraintLimit::ClippedAfter(width), ConstraintLimit::ClippedAfter(_)) => {
|
||||
// When we have no limit on our, we still want to be draggable.
|
||||
// Since we have no limit in both directions, we have to make a
|
||||
// choice: horizontal or vertical. It seems to @ecton at the
|
||||
// time of writing this that when there is no intent from the
|
||||
// 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), self.knob_size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hit_test(&mut self, _location: Point<Px>, _context: &mut EventContext<'_, '_>) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn mouse_down(
|
||||
&mut self,
|
||||
location: Point<Px>,
|
||||
_device_id: DeviceId,
|
||||
_button: MouseButton,
|
||||
_context: &mut EventContext<'_, '_>,
|
||||
) -> EventHandling {
|
||||
self.update_from_click(location);
|
||||
HANDLED
|
||||
}
|
||||
|
||||
fn mouse_drag(
|
||||
&mut self,
|
||||
location: Point<Px>,
|
||||
_device_id: DeviceId,
|
||||
_button: MouseButton,
|
||||
_context: &mut EventContext<'_, '_>,
|
||||
) {
|
||||
self.update_from_click(location);
|
||||
}
|
||||
}
|
||||
|
||||
/// The size of the track that the knob of a [`Slider`] traversesq.
|
||||
pub struct TrackSize;
|
||||
|
||||
impl ComponentDefinition for TrackSize {
|
||||
type ComponentType = Dimension;
|
||||
|
||||
fn default_value(&self, _context: &WidgetContext<'_, '_>) -> Self::ComponentType {
|
||||
Dimension::Lp(Lp::points(5))
|
||||
}
|
||||
}
|
||||
|
||||
impl NamedComponent for TrackSize {
|
||||
fn name(&self) -> Cow<'_, ComponentName> {
|
||||
Cow::Owned(ComponentName::new(Group::new("Slider"), "track_size"))
|
||||
}
|
||||
}
|
||||
|
||||
/// The width and height of the draggable portion of a [`Slider`].
|
||||
pub struct KnobSize;
|
||||
|
||||
impl ComponentDefinition for KnobSize {
|
||||
type ComponentType = Dimension;
|
||||
|
||||
fn default_value(&self, _context: &WidgetContext<'_, '_>) -> Self::ComponentType {
|
||||
Dimension::Lp(Lp::points(14))
|
||||
}
|
||||
}
|
||||
|
||||
impl NamedComponent for KnobSize {
|
||||
fn name(&self) -> Cow<'_, ComponentName> {
|
||||
Cow::Owned(ComponentName::new(Group::new("Slider"), "knob_size"))
|
||||
}
|
||||
}
|
||||
|
||||
/// The minimum length of the slidable dimension.
|
||||
pub struct MinimumSliderSize;
|
||||
|
||||
impl ComponentDefinition for MinimumSliderSize {
|
||||
type ComponentType = Dimension;
|
||||
|
||||
fn default_value(&self, _context: &WidgetContext<'_, '_>) -> Self::ComponentType {
|
||||
Dimension::Lp(Lp::points(14))
|
||||
}
|
||||
}
|
||||
|
||||
impl NamedComponent for MinimumSliderSize {
|
||||
fn name(&self) -> Cow<'_, ComponentName> {
|
||||
Cow::Owned(ComponentName::new(Group::new("Slider"), "minimum_size"))
|
||||
}
|
||||
}
|
||||
|
||||
/// The color of the draggable portion of the knob.
|
||||
pub struct KnobColor;
|
||||
|
||||
impl ComponentDefinition for KnobColor {
|
||||
type ComponentType = Color;
|
||||
|
||||
fn default_value(&self, context: &WidgetContext<'_, '_>) -> Self::ComponentType {
|
||||
context.theme().primary.color
|
||||
}
|
||||
}
|
||||
|
||||
impl NamedComponent for KnobColor {
|
||||
fn name(&self) -> Cow<'_, ComponentName> {
|
||||
Cow::Owned(ComponentName::new(Group::new("Slider"), "knob_color"))
|
||||
}
|
||||
}
|
||||
|
||||
/// The color of the draggable portion of the knob.
|
||||
pub struct TrackColor;
|
||||
|
||||
impl ComponentDefinition for TrackColor {
|
||||
type ComponentType = Color;
|
||||
|
||||
fn default_value(&self, context: &WidgetContext<'_, '_>) -> Self::ComponentType {
|
||||
context.theme().surface.on_color_variant
|
||||
}
|
||||
}
|
||||
|
||||
impl NamedComponent for TrackColor {
|
||||
fn name(&self) -> Cow<'_, ComponentName> {
|
||||
Cow::Owned(ComponentName::new(Group::new("Slider"), "track_color"))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue