use std::fmt::Debug;
use std::mem;
use std::ops::RangeInclusive;
use figures::units::{Lp, Px, UPx};
use figures::{FloatConversion, IntoSigned, Point, Ranged, Rect, Round, ScreenScale, Size, Zero};
use intentional::{Assert, Cast as _};
use kludgine::app::winit::event::{MouseButton, MouseScrollDelta, TouchPhase};
use kludgine::app::winit::keyboard::{Key, NamedKey};
use kludgine::app::winit::window::CursorIcon;
use kludgine::shapes::{Shape, StrokeOptions};
use kludgine::{Color, DrawableExt, Origin};
use crate::animation::{LinearInterpolate, PercentBetween, ZeroToOne};
use crate::context::{EventContext, GraphicsContext, LayoutContext};
use crate::styles::components::{
AutoFocusableControls, DisabledWidgetAccentColor, OpaqueWidgetColor, OutlineColor,
WidgetAccentColor,
};
use crate::styles::{Dimension, HorizontalOrder, VerticalOrder, VisualOrder};
use crate::value::{Destination, Dynamic, IntoDynamic, IntoValue, Source, Value};
use crate::widget::{EventHandling, Widget, HANDLED, IGNORED};
use crate::window::{DeviceId, KeyEvent};
use crate::ConstraintLimit;
#[derive(Debug, Clone)]
pub struct Slider<T>
where
T: SliderValue,
{
pub value: Dynamic<T>,
pub minimum: Value<T::Value>,
pub maximum: Value<T::Value>,
pub step: Value<ZeroToOne>,
knob_visible: bool,
interactive: bool,
knob_size: UPx,
horizontal: bool,
rendered_size: Px,
focused_knob: Option<Knob>,
previous_focus: Option<Knob>,
mouse_buttons_down: usize,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum Knob {
Start,
End,
}
impl<T> Slider<T>
where
T: SliderValue,
T::Value: Ranged,
{
#[must_use]
pub fn from_value(value: impl IntoDynamic<T>) -> Self {
Self::new(value, <T::Value>::MIN, <T::Value>::MAX)
}
}
impl<T> Slider<T>
where
T: SliderValue,
{
#[must_use]
pub fn new(
value: impl IntoDynamic<T>,
min: impl IntoValue<T::Value>,
max: impl IntoValue<T::Value>,
) -> Self {
Self {
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,
rendered_size: Px::ZERO,
focused_knob: None,
mouse_buttons_down: 0,
previous_focus: None,
}
}
#[must_use]
pub fn maximum(mut self, max: impl IntoValue<T::Value>) -> Self {
self.maximum = max.into_value();
self
}
#[must_use]
pub fn minimum(mut self, min: impl IntoValue<T::Value>) -> Self {
self.minimum = min.into_value();
self
}
#[must_use]
pub fn step_by(mut self, percent: impl IntoValue<ZeroToOne>) -> Self {
self.step = percent.into_value();
self
}
#[must_use]
pub fn knobless(mut self) -> Self {
self.knob_visible = false;
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 =
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.if_knobbed(|| spec.knob_size + focus_ring);
let (start, end) = if let Some(end) = spec.end {
(track_length * spec.start, track_length * end)
} else {
(Px::ZERO, track_length * spec.start)
};
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);
if start > 0 {
context.gfx.draw_shape(
Shape::filled_round_rect(
self.orient_rectangle(Rect::new(
Point::new(start_inset, start_inset),
Size::new(start, spec.track_size),
)),
half_track,
spec.inactive_track_color,
)
.translate_by(inset),
);
}
if end < track_length {
context.gfx.draw_shape(
Shape::filled_round_rect(
self.orient_rectangle(Rect::new(
Point::new(end + spec.if_knobbed(|| spec.half_knob), start_inset),
Size::new(
track_length - end + spec.if_knobbed(|| half_track),
spec.track_size,
),
)),
half_track,
spec.inactive_track_color,
)
.translate_by(inset),
);
}
if start != end {
context.gfx.draw_shape(
Shape::filled_round_rect(
self.orient_rectangle(Rect::new(
Point::new(
start + spec.if_knobbed(|| spec.half_knob - half_track),
start_inset,
),
Size::new(
end - start + spec.if_knobbed(|| spec.track_size),
spec.track_size,
),
)),
half_track,
spec.track_color,
)
.translate_by(inset),
);
}
if spec.knob_size > 0 {
let focus = context.focused(true).then_some(self.focused_knob).flatten();
Self::draw_knobs(
self.flip_pt_if_vertical(Point::new(end + spec.half_knob, spec.half_knob) + inset),
spec.end.map(|_| {
self.flip_pt_if_vertical(
Point::new(start + spec.half_knob, spec.half_knob) + inset,
)
}),
focus,
focus_ring,
spec,
context,
);
}
}
fn draw_knobs(
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);
}
}
fn draw_knob(
knob_center: Point<Px>,
is_focused: bool,
focus_ring_width: Px,
spec: &TrackSpec,
context: &mut GraphicsContext<'_, '_, '_, '_>,
) {
context.gfx.draw_shape(
Shape::filled_circle(spec.half_knob, spec.knob_color, Origin::Center)
.translate_by(knob_center),
);
if is_focused {
let focus_color = context.get(&OutlineColor);
context.gfx.draw_shape(
Shape::stroked_circle(
spec.half_knob,
Origin::Center,
StrokeOptions::px_wide(focus_ring_width).colored(focus_color),
)
.translate_by(knob_center),
);
}
}
}
impl<T> Slider<T>
where
T: SliderValue,
{
fn update_from_click(&mut self, position: Point<Px>, previous_focus: Option<Knob>) {
let knob_size = self.knob_size.into_signed();
let position = if self.horizontal {
position.x - knob_size / 2
} else {
self.rendered_size - position.y - knob_size / 2
};
let track_width = self.rendered_size - knob_size;
let position = position.clamp(Px::ZERO, track_width);
let percent = position.into_float() / track_width.into_float();
let min = self.minimum.get();
let max = self.maximum.get();
let value = min.lerp(&max, percent);
let (mut start, mut opt_end) = T::into_parts(self.value.get());
if let Some(end) = &opt_end {
let knob = if let Some(knob) = self.focused_knob {
knob
} else {
let start_percent = start.percent_between(&min, &max);
let end_percent = end.percent_between(&min, &max);
let knob_width_as_percent =
self.knob_size.into_float() / 2. / track_width.into_float();
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 {
Knob::End
}
};
match knob {
Knob::Start => {
if &value <= end {
start = value;
} else {
start = end.clone();
}
}
Knob::End => {
if value >= start {
opt_end = Some(value);
} else {
opt_end = Some(start.clone());
}
}
}
self.focused_knob = Some(knob);
} else {
start = value;
self.focused_knob = Some(Knob::Start);
}
self.value.set(T::from_parts(start, opt_end));
}
fn step(&mut self, forwards: bool, factor: f32) {
let Some(focus) = self
.focused_knob
.or_else(|| (!T::RANGED).then_some(Knob::Start))
else {
return;
};
let (current, other) = match (focus, T::into_parts(self.value.get())) {
(Knob::Start, (current, other)) => (current, other),
(Knob::End, (other, Some(current))) => (current, Some(other)),
(Knob::End, (_, None)) => unreachable!("invalid state"),
};
let min = self.minimum.get();
let max = self.maximum.get();
let step = self.step.get();
let mut current_percent = current.percent_between(&min, &max);
let new_value = loop {
let next = if forwards {
*current_percent + *step * factor
} else {
*current_percent - *step * factor
};
if next < 0. {
break min.clone();
} else if next > 1. {
break max.clone();
}
current_percent = ZeroToOne::new(next);
let generated_value = min.lerp(&max, *current_percent);
if generated_value != current {
break generated_value;
}
};
let valid_relative_to_other = match (&other, focus) {
(Some(end), Knob::Start) => new_value < *end,
(Some(start), Knob::End) => new_value > *start,
(None, _) => true,
};
if valid_relative_to_other && new_value >= min && new_value <= max {
let (start, end) = match (focus, other) {
(_, None) => (new_value, None),
(Knob::Start, Some(end)) => (new_value, Some(end)),
(Knob::End, Some(start)) => (start, Some(new_value)),
};
self.value.set(T::from_parts(start, end));
}
}
fn orient_rectangle(&self, rect: Rect<Px>) -> Rect<Px> {
if self.horizontal {
rect
} else {
let (tl, br) = rect.extents();
Rect::from_extents(
Point::new(tl.y, self.rendered_size - tl.x),
Point::new(br.y, self.rendered_size - br.x),
)
}
}
fn flip_pt_if_vertical(&self, pt: Point<Px>) -> Point<Px> {
if self.horizontal {
pt
} else {
Point::new(pt.y, self.rendered_size - pt.x)
}
}
}
impl<T> Widget for Slider<T>
where
T: SliderValue,
{
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) {
let (track_color, inactive_track_color, knob_color) = if context.enabled() {
(
context.get(&TrackColor),
context.get(&InactiveTrackColor),
context.get(&KnobColor),
)
} else {
(
context.get(&DisabledTrackColor),
context.get(&DisabledInactiveTrackColor),
context.get(&DisabledKnobColor),
)
};
let knob_size = self.knob_size.into_signed();
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;
let (mut start_value, mut end_value) =
T::into_parts(self.value.get_tracking_redraw(context));
let min = self.minimum.get_tracking_redraw(context);
let mut max = self.maximum.get_tracking_redraw(context);
if max < min {
self.maximum.map_mut(|mut max| *max = min.clone());
max = min.clone();
}
let mut value_clamped = false;
if start_value < min {
value_clamped = true;
start_value = min.clone();
} else if start_value > max {
value_clamped = true;
start_value = max.clone();
}
if let Some(end) = &mut end_value {
if *end < min {
value_clamped = true;
*end = min.clone();
} else if *end < start_value {
value_clamped = true;
mem::swap(&mut start_value, end);
} else if *end > max {
value_clamped = true;
*end = max.clone();
}
}
if value_clamped {
self.value
.map_mut(|mut v| *v = T::from_parts(start_value.clone(), end_value.clone()));
}
let start_percent = start_value.percent_between(&min, &max);
let end_percent = end_value.map(|end| *end.percent_between(&min, &max));
let size = context.gfx.region().size;
self.horizontal = size.width >= size.height;
self.draw_track(
&TrackSpec {
size,
start: *start_percent,
end: end_percent,
half_knob,
knob_size,
track_size,
knob_color,
track_color,
inactive_track_color,
},
context,
);
}
fn layout(
&mut self,
available_space: Size<ConstraintLimit>,
context: &mut LayoutContext<'_, '_, '_, '_>,
) -> Size<UPx> {
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 = 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)) => {
if width < height {
Size::new(static_side, height.max(minimum_size))
} else {
Size::new(width.max(minimum_size), static_side)
}
}
(ConstraintLimit::Fill(width), ConstraintLimit::SizeToFit(_)) => {
Size::new(width.max(minimum_size), static_side)
}
(ConstraintLimit::SizeToFit(_), ConstraintLimit::Fill(height)) => {
Size::new(static_side, height.max(minimum_size))
}
(ConstraintLimit::SizeToFit(width), ConstraintLimit::SizeToFit(_)) => {
Size::new(width.min(minimum_size), static_side)
}
}
}
fn hit_test(&mut self, _location: Point<Px>, _context: &mut EventContext<'_>) -> bool {
self.interactive
}
fn hover(
&mut self,
_location: Point<Px>,
context: &mut EventContext<'_>,
) -> Option<CursorIcon> {
(self.interactive && self.knob_visible).then_some({
if context.enabled() {
if self.mouse_buttons_down > 0 {
CursorIcon::Grabbing
} else {
CursorIcon::Grab
}
} else {
CursorIcon::NotAllowed
}
})
}
fn accept_focus(&mut self, context: &mut EventContext<'_>) -> bool {
context.enabled()
&& self.interactive
&& self.knob_visible
&& context.get(&AutoFocusableControls).is_all()
}
fn focus(&mut self, context: &mut EventContext<'_>) {
if self.mouse_buttons_down == 0 {
self.focused_knob = Some(if T::RANGED && !context.focus_is_advancing() {
Knob::End
} else {
Knob::Start
});
context.set_needs_redraw();
}
}
fn advance_focus(
&mut self,
direction: VisualOrder,
context: &mut EventContext<'_>,
) -> EventHandling {
let (true, Some(focused)) = (T::RANGED, self.focused_knob) else {
return IGNORED;
};
let new_knob = if self.horizontal {
match (direction.horizontal, focused) {
(HorizontalOrder::LeftToRight, Knob::Start) => Knob::End,
(HorizontalOrder::RightToLeft, Knob::End) => Knob::Start,
_ => return IGNORED,
}
} else {
match (direction.vertical, focused) {
(VerticalOrder::TopToBottom, Knob::Start) => Knob::End,
(VerticalOrder::BottomToTop, Knob::End) => Knob::Start,
_ => return IGNORED,
}
};
self.focused_knob = Some(new_knob);
context.set_needs_redraw();
HANDLED
}
fn blur(&mut self, context: &mut EventContext<'_>) {
self.previous_focus = self.focused_knob.take();
context.set_needs_redraw();
}
fn mouse_down(
&mut self,
location: Point<Px>,
_device_id: DeviceId,
_button: MouseButton,
context: &mut EventContext<'_>,
) -> EventHandling {
let true = self.interactive else {
return IGNORED;
};
if context.enabled() {
let previous_focus = match (self.previous_focus.take(), self.focused_knob.take()) {
(None | Some(_), Some(focus)) | (Some(focus), None) => Some(focus),
(None, None) => None,
};
self.update_from_click(location, previous_focus);
context.focus();
}
self.mouse_buttons_down += 1;
HANDLED
}
fn mouse_drag(
&mut self,
location: Point<Px>,
_device_id: DeviceId,
_button: MouseButton,
context: &mut EventContext<'_>,
) {
if context.enabled() {
self.update_from_click(location, None);
}
}
fn mouse_up(
&mut self,
_location: Option<Point<Px>>,
_device_id: DeviceId,
_button: MouseButton,
_context: &mut EventContext<'_>,
) {
self.mouse_buttons_down -= 1;
}
fn keyboard_input(
&mut self,
_device_id: DeviceId,
input: KeyEvent,
_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,
_ => return IGNORED,
};
if !input.state.is_pressed() {
return HANDLED;
}
self.step(forwards, 1.);
HANDLED
}
fn mouse_wheel(
&mut self,
_device_id: DeviceId,
delta: MouseScrollDelta,
_phase: TouchPhase,
context: &mut EventContext<'_>,
) -> EventHandling {
let true = self.interactive else {
return IGNORED;
};
if context.enabled() {
let factor: f32 = match delta {
MouseScrollDelta::LineDelta(_, y) => y,
MouseScrollDelta::PixelDelta(pt) => pt.y.cast(),
};
let (forwards, factor) = if factor.is_sign_negative() {
(false, -factor)
} else {
(true, factor)
};
self.step(forwards, factor);
}
HANDLED
}
fn summarize(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fmt.debug_struct("Slider")
.field("value", &self.value)
.field("min", &self.minimum)
.field("max", &self.maximum)
.finish()
}
}
struct TrackSpec {
size: Size<Px>,
start: f32,
end: Option<f32>,
half_knob: Px,
knob_size: Px,
track_size: Px,
knob_color: Color,
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()
}
}
}
define_components! {
Slider {
TrackSize(Dimension, "track_size", Dimension::Lp(Lp::points(5)))
KnobSize(Dimension, "knob_size", Dimension::Lp(Lp::points(14)))
MinimumSliderSize(Dimension, "minimum_size", |context| context.get(&KnobSize) * 2)
KnobColor(Color, "knob_color", @WidgetAccentColor)
DisabledKnobColor(Color, "disabled_knob_color", @DisabledWidgetAccentColor)
TrackColor(Color,"track_color", |context| context.get(&KnobColor))
DisabledTrackColor(Color,"track_color", |context| context.get(&DisabledKnobColor))
InactiveTrackColor(Color, "inactive_track_color", |context| context.get(&OpaqueWidgetColor))
DisabledInactiveTrackColor(Color, "disabled_inactive_track_color", |context| context.get(&OpaqueWidgetColor))
}
}
pub trait SliderValue: Clone + PartialEq + Send + Debug + 'static {
type Value: Clone + Debug + PartialOrd + LinearInterpolate + PercentBetween + Send + 'static;
const RANGED: bool;
fn into_parts(self) -> (Self::Value, Option<Self::Value>);
fn from_parts(min_or_value: Self::Value, max: Option<Self::Value>) -> Self;
}
impl<T> SliderValue for T
where
T: Clone + Debug + PartialOrd + LinearInterpolate + PercentBetween + Send + 'static,
{
type Value = T;
const RANGED: bool = false;
fn into_parts(self) -> (Self::Value, Option<Self::Value>) {
(self, None)
}
fn from_parts(min_or_value: Self::Value, _max: Option<Self::Value>) -> Self {
min_or_value
}
}
impl<T> SliderValue for RangeInclusive<T>
where
T: Clone + Debug + PartialOrd + LinearInterpolate + PercentBetween + Send + 'static,
{
type Value = T;
const RANGED: bool = true;
fn into_parts(self) -> (Self::Value, Option<Self::Value>) {
let (start, end) = self.into_inner();
(start, Some(end))
}
fn from_parts(min_or_value: Self::Value, max: Option<Self::Value>) -> Self {
min_or_value..=max.assert("always provided")
}
}
impl<T> SliderValue for (T, T)
where
T: Clone + Debug + PartialOrd + LinearInterpolate + PercentBetween + Send + 'static,
{
type Value = T;
const RANGED: bool = true;
fn into_parts(self) -> (Self::Value, Option<Self::Value>) {
(self.0, Some(self.1))
}
fn from_parts(min_or_value: Self::Value, max: Option<Self::Value>) -> Self {
(min_or_value, max.assert("always provided"))
}
}
pub trait Slidable<T>: IntoDynamic<T> + Sized
where
T: SliderValue,
{
fn slider(self) -> Slider<T>
where
T::Value: Ranged,
{
Slider::from_value(self.into_dynamic())
}
fn slider_between(
self,
min: impl IntoValue<T::Value>,
max: impl IntoValue<T::Value>,
) -> Slider<T> {
Slider::new(self.into_dynamic(), min, max)
}
}
impl<U, T> Slidable<U> for T
where
T: IntoDynamic<U>,
U: SliderValue,
{
}