use std::fmt::Debug;
use std::marker::PhantomData;
use crate::iced::widget::Canvas;
use crate::iced::widget::canvas::{self, Event, Frame, Geometry, Path, Stroke, Text};
use crate::iced::{Color, Element, Length, Point, Rectangle, Renderer, Theme, alignment, mouse};
use crate::param_cache::ParamCache;
use crate::param_message::{Message, ParamMessage};
use crate::theme;
use truce_core::Float;
use truce_params::Params;
const TRACK_HEIGHT: f32 = 4.0;
const THUMB_RADIUS: f32 = 6.0;
pub struct SliderWidget<'a, M> {
id: u32,
value: f64,
display: String,
label: Option<&'a str>,
width: f32,
font: crate::iced::Font,
_phantom: PhantomData<M>,
}
impl<'a, M: Clone + Debug + 'static> SliderWidget<'a, M> {
pub fn new(id: impl Into<u32>, params: &'a ParamCache<impl Params>) -> Self {
let id = id.into();
Self {
id,
value: params.get(id),
display: params.label(id).to_string(),
label: None,
width: 120.0,
font: params.font(),
_phantom: PhantomData,
}
}
#[must_use]
pub fn label(mut self, label: &'a str) -> Self {
self.label = Some(label);
self
}
#[must_use]
pub fn width(mut self, width: f32) -> Self {
self.width = width;
self
}
#[must_use]
pub fn font(mut self, font: crate::iced::Font) -> Self {
self.font = font;
self
}
#[must_use]
pub fn into_element(self) -> Element<'a, Message<M>> {
let total_h = THUMB_RADIUS * 2.0 + 30.0;
let program = SliderProgram {
id: self.id,
value: f32::from_f64(self.value),
display: self.display,
label: self.label.unwrap_or("").to_string(),
font: self.font,
};
Canvas::new(program)
.width(Length::Fixed(self.width))
.height(Length::Fixed(total_h))
.into()
}
}
impl<'a, M: Clone + Debug + 'static> From<SliderWidget<'a, M>> for Element<'a, Message<M>> {
fn from(s: SliderWidget<'a, M>) -> Self {
s.into_element()
}
}
struct SliderProgram {
id: u32,
value: f32,
display: String,
label: String,
font: crate::iced::Font,
}
#[derive(Default)]
struct SliderState {
dragging: bool,
start_value: f32,
start_x: f32,
}
impl<M: Clone + Debug + 'static> canvas::Program<Message<M>> for SliderProgram {
type State = SliderState;
fn draw(
&self,
_state: &Self::State,
renderer: &Renderer,
_theme: &Theme,
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec<Geometry> {
let mut frame = Frame::new(renderer, bounds.size());
let margin = THUMB_RADIUS;
let track_y = THUMB_RADIUS;
let track_left = margin;
let track_right = bounds.width - margin;
let track_width = track_right - track_left;
let track_bg = Path::line(
Point::new(track_left, track_y),
Point::new(track_right, track_y),
);
frame.stroke(
&track_bg,
Stroke::default()
.with_color(theme::KNOB_TRACK)
.with_width(TRACK_HEIGHT)
.with_line_cap(crate::iced::widget::canvas::LineCap::Round),
);
let fill_x = track_left + self.value * track_width;
if self.value > 0.001 {
let track_fill =
Path::line(Point::new(track_left, track_y), Point::new(fill_x, track_y));
frame.stroke(
&track_fill,
Stroke::default()
.with_color(theme::KNOB_FILL)
.with_width(TRACK_HEIGHT)
.with_line_cap(crate::iced::widget::canvas::LineCap::Round),
);
}
let thumb = Path::circle(Point::new(fill_x, track_y), THUMB_RADIUS);
frame.fill(&thumb, theme::KNOB_POINTER);
let text_y = THUMB_RADIUS * 2.0 + 4.0;
let cx = bounds.width / 2.0;
frame.fill_text(Text {
content: self.display.clone(),
position: Point::new(cx, text_y),
color: Color::from_rgb(0.90, 0.90, 0.92),
size: crate::iced::Pixels(11.0),
align_x: alignment::Horizontal::Center.into(),
align_y: alignment::Vertical::Top,
font: self.font,
..Text::default()
});
if !self.label.is_empty() {
let label_y = text_y + 14.0;
frame.fill_text(Text {
content: self.label.clone(),
position: Point::new(cx, label_y),
color: theme::TEXT_DIM,
size: crate::iced::Pixels(10.0),
align_x: alignment::Horizontal::Center.into(),
align_y: alignment::Vertical::Top,
font: self.font,
..Text::default()
});
}
vec![frame.into_geometry()]
}
fn update(
&self,
state: &mut Self::State,
event: &Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<canvas::Action<Message<M>>> {
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
if let Some(pos) = cursor.position_in(bounds) {
let track_top = 0.0;
let track_bottom = THUMB_RADIUS * 2.0;
if pos.y >= track_top && pos.y <= track_bottom {
state.dragging = true;
state.start_value = self.value;
state.start_x = pos.x;
return Some(
canvas::Action::publish(Message::Param(ParamMessage::BeginEdit(
self.id,
)))
.and_capture(),
);
}
}
}
Event::Mouse(mouse::Event::CursorMoved { .. }) if state.dragging => {
if let Some(pos) = cursor.position() {
let current_x = pos.x - bounds.x;
let track_width = bounds.width - THUMB_RADIUS * 2.0;
let delta = (current_x - state.start_x) / track_width;
let new_value = (state.start_value + delta).clamp(0.0, 1.0);
return Some(
canvas::Action::publish(Message::Param(ParamMessage::SetNormalized(
self.id,
f64::from(new_value),
)))
.and_capture(),
);
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) if state.dragging => {
state.dragging = false;
return Some(
canvas::Action::publish(Message::Param(ParamMessage::EndEdit(self.id)))
.and_capture(),
);
}
_ => {}
}
None
}
fn mouse_interaction(
&self,
state: &Self::State,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> mouse::Interaction {
if state.dragging {
return mouse::Interaction::Grabbing;
}
if let Some(pos) = cursor.position_in(bounds)
&& pos.y <= THUMB_RADIUS * 2.0
{
return mouse::Interaction::Grab;
}
mouse::Interaction::default()
}
}