use std::rc::Rc;
use gpui::{
App, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce, SharedString,
Styled, Window, div, prelude::FluentBuilder, px,
};
use gpui_kit_semantics::{NodeSpec, Role, Semantic};
use gpui_kit_theme::{ActiveTheme, ControlSize, Space, TypeScale};
use crate::foundation::{
Disableable, FocusRing, Ident, Sizable, StyledExt, text as foundation_text,
};
use crate::layout::measure;
use crate::motion::{self, keyed};
#[derive(Default)]
struct PointerDriven(bool);
type ChangeHandler = Rc<dyn Fn(f32, &mut Window, &mut App)>;
#[derive(IntoElement)]
pub struct Slider {
ident: Ident,
label: Option<SharedString>,
min: f32,
max: f32,
value: f32,
step: Option<f32>,
size: ControlSize,
disabled: bool,
display: Option<SharedString>,
on_change: Option<ChangeHandler>,
}
impl std::fmt::Debug for Slider {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("Slider")
.field("ident", &self.ident)
.field("range", &(self.min, self.max))
.field("value", &self.value)
.field("disabled", &self.disabled)
.field("has_handler", &self.on_change.is_some())
.finish()
}
}
impl Slider {
pub fn new(ident: impl Into<Ident>) -> Self {
Self {
ident: ident.into(),
label: None,
min: 0.0,
max: 1.0,
value: 0.0,
step: None,
size: ControlSize::Md,
disabled: false,
display: None,
on_change: None,
}
}
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
self.label = Some(label.into());
self
}
pub fn range(mut self, min: f32, max: f32) -> Self {
let (min, max) = if min <= max { (min, max) } else { (max, min) };
self.min = min;
self.max = if (max - min).abs() < f32::EPSILON {
min + 1.0
} else {
max
};
self
}
pub fn value(mut self, value: f32) -> Self {
self.value = value;
self
}
pub fn step(mut self, step: f32) -> Self {
self.step = (step > 0.0).then_some(step);
self
}
pub fn display(mut self, display: impl Into<SharedString>) -> Self {
self.display = Some(display.into());
self
}
pub fn on_change(mut self, handler: impl Fn(f32, &mut Window, &mut App) + 'static) -> Self {
self.on_change = Some(Rc::new(handler));
self
}
fn clamped(&self) -> f32 {
self.value.clamp(self.min, self.max)
}
fn fraction(&self) -> f32 {
(self.clamped() - self.min) / (self.max - self.min)
}
}
impl Disableable for Slider {
fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
}
impl Sizable for Slider {
fn control_size(mut self, size: ControlSize) -> Self {
self.size = size;
self
}
}
impl RenderOnce for Slider {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme().clone();
let metrics = theme.control.get(self.size);
let actionable = !self.disabled && self.on_change.is_some();
let dragging = keyed::slot::<PointerDriven>(&self.ident.semantic_id(), cx);
let snap = std::mem::take(&mut dragging.borrow_mut().0);
let fraction = motion::tracked_or_snap(
&self.ident.semantic_id(),
self.fraction(),
motion::tracking(&theme),
snap,
window,
cx,
);
let track_height = px(4.0);
let knob = px(metrics.icon_size);
let track_id = self.ident.clone();
let measured = measure::cell(&track_id.semantic_id(), cx);
let mut track = div()
.id(track_id.element_id())
.relative()
.w_full()
.h(knob)
.flex()
.items_center()
.when(actionable, |element| element.cursor_pointer())
.child(
div()
.absolute()
.left_0()
.right_0()
.h(track_height)
.rounded_full()
.bg(theme.colors.hairline_strong),
)
.child(
div()
.absolute()
.left_0()
.w(gpui::relative(fraction))
.h(track_height)
.rounded_full()
.bg(theme.colors.accent),
)
.child(
div()
.absolute()
.left(gpui::relative(fraction))
.ml(-(knob / 2.0))
.size(knob)
.rounded_full()
.bg(theme.colors.text)
.border(px(theme.borders.hairline))
.border_color(theme.colors.hairline_strong),
);
if actionable && let Some(handler) = self.on_change.clone() {
let (min, max, step) = (self.min, self.max, self.step);
let down = Rc::clone(&handler);
let down_bounds = Rc::clone(&measured);
let down_dragging = Rc::clone(&dragging);
track = track.on_mouse_down(MouseButton::Left, move |event, window, cx| {
let bounds = down_bounds.get();
let width = f32::from(bounds.size.width);
if width <= 0.0 {
return;
}
down_dragging.borrow_mut().0 = true;
let fraction =
(f32::from(event.position.x - bounds.left()) / width).clamp(0.0, 1.0);
down(
quantize(min + fraction * (max - min), min, max, step),
window,
cx,
);
});
let drag = Rc::clone(&handler);
let move_bounds = Rc::clone(&measured);
let move_dragging = Rc::clone(&dragging);
track = track.on_mouse_move(move |event, window, cx| {
if event.pressed_button != Some(MouseButton::Left) {
return;
}
let bounds = move_bounds.get();
let width = f32::from(bounds.size.width);
if width <= 0.0 {
return;
}
move_dragging.borrow_mut().0 = true;
let fraction =
(f32::from(event.position.x - bounds.left()) / width).clamp(0.0, 1.0);
drag(
quantize(min + fraction * (max - min), min, max, step),
window,
cx,
);
});
}
let value = self.clamped();
let keyboard_step = self.step.unwrap_or((self.max - self.min) / 20.0);
let mut spec = NodeSpec::new(self.ident.semantic_id(), Role::Slider)
.disabled(self.disabled)
.range(self.min, self.max, value);
if let Some(label) = self.label.clone() {
spec = spec.text(label);
}
if let Some(display) = self.display.clone() {
spec = spec.value(display);
}
let mut frame = div()
.id(self.ident.child("frame").element_id())
.flex()
.flex_col()
.gap(px(theme.space(Space::Xs)))
.w_full()
.when(self.disabled, |element| {
element.opacity(theme.opacity.disabled)
})
.when(actionable, |element| {
element.tab_index(0).focus_ring(&theme)
})
.when_some(self.label.clone(), |element, label| {
element.child(
div()
.flex()
.flex_row()
.justify_between()
.child(
foundation_text(&theme, TypeScale::Label, label)
.text_size(px(metrics.font_size))
.text_tone(&theme, gpui_kit_theme::TextTone::Muted),
)
.when_some(self.display.clone(), |element, display| {
element.child(
foundation_text(&theme, TypeScale::Label, display)
.text_size(px(metrics.font_size)),
)
}),
)
})
.child(
div()
.w_full()
.on_children_prepainted({
let measured = Rc::clone(&measured);
move |bounds, window, _| {
if let Some(first) = bounds.first() {
measure::record(&measured, *first, window);
}
}
})
.child(track)
.semantic_in(cx, spec),
);
if actionable && let Some(handler) = self.on_change.clone() {
let (min, max) = (self.min, self.max);
frame.interactivity().on_key_down(move |event, window, cx| {
let next = match event.keystroke.key.as_str() {
"left" | "down" => value - keyboard_step,
"right" | "up" => value + keyboard_step,
"home" => min,
"end" => max,
_ => return,
};
handler(next.clamp(min, max), window, cx);
cx.stop_propagation();
});
}
frame
}
}
fn quantize(value: f32, min: f32, max: f32, step: Option<f32>) -> f32 {
let value = value.clamp(min, max);
match step {
Some(step) => {
let steps = ((value - min) / step).round();
(min + steps * step).clamp(min, max)
}
None => value,
}
}