use std::fmt::Debug;
use std::marker::PhantomData;
use iced::widget::Canvas;
use iced::widget::canvas::{self, Event, Frame, Geometry, Path, Stroke};
use iced::{Color, Element, Length, Point, Rectangle, Renderer, Size, Theme, mouse};
use crate::param_cache::ParamCache;
use crate::param_message::{Message, ParamMessage};
use crate::theme;
use truce_core::Float;
use truce_params::Params;
pub struct XYPadWidget<'a, M> {
x_id: u32,
y_id: u32,
x_value: f64,
y_value: f64,
label: Option<&'a str>,
width: Length,
height: Length,
font: iced::Font,
_phantom: PhantomData<M>,
}
impl<'a, M: Clone + Debug + 'static> XYPadWidget<'a, M> {
pub fn new(
x_id: impl Into<u32>,
y_id: impl Into<u32>,
params: &'a ParamCache<impl Params>,
) -> Self {
let x_id = x_id.into();
let y_id = y_id.into();
Self {
x_id,
y_id,
x_value: params.get(x_id),
y_value: params.get(y_id),
label: None,
width: Length::Fixed(120.0),
height: Length::Fixed(120.0 + LABEL_H),
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 size(mut self, size: f32) -> Self {
self.width = Length::Fixed(size);
self.height = Length::Fixed(size + LABEL_H);
self
}
#[must_use]
pub fn fill(mut self) -> Self {
self.width = Length::Fill;
self.height = Length::Fill;
self
}
#[must_use]
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
#[must_use]
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
#[must_use]
pub fn font(mut self, font: iced::Font) -> Self {
self.font = font;
self
}
#[must_use]
pub fn into_element(self) -> Element<'a, Message<M>> {
let program = XYPadProgram {
x_id: self.x_id,
y_id: self.y_id,
x_value: f32::from_f64(self.x_value),
y_value: f32::from_f64(self.y_value),
label: self.label.unwrap_or("").to_string(),
font: self.font,
};
Canvas::new(program)
.width(self.width)
.height(self.height)
.into()
}
}
const LABEL_H: f32 = 16.0;
impl<'a, M: Clone + Debug + 'static> From<XYPadWidget<'a, M>> for Element<'a, Message<M>> {
fn from(xy: XYPadWidget<'a, M>) -> Self {
xy.into_element()
}
}
struct XYPadProgram {
x_id: u32,
y_id: u32,
x_value: f32,
y_value: f32,
label: String,
font: iced::Font,
}
#[derive(Default)]
struct XYPadState {
dragging: bool,
}
impl<M: Clone + Debug + 'static> canvas::Program<Message<M>> for XYPadProgram {
type State = XYPadState;
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 label_h = if self.label.is_empty() { 0.0 } else { LABEL_H };
let pad_w = bounds.width;
let pad_h = (bounds.height - label_h).max(1.0);
let bg = Path::rectangle(Point::ORIGIN, Size::new(pad_w, pad_h));
frame.fill(&bg, theme::SURFACE);
frame.stroke(
&bg,
Stroke::default().with_color(theme::ACCENT).with_width(1.0),
);
let px = self.x_value * pad_w;
let py = (1.0 - self.y_value) * pad_h;
let h_line = Path::line(Point::new(0.0, py), Point::new(pad_w, py));
let v_line = Path::line(Point::new(px, 0.0), Point::new(px, pad_h));
let crosshair_stroke = Stroke::default()
.with_color(Color {
a: 0.3,
..theme::KNOB_FILL
})
.with_width(1.0);
frame.stroke(&h_line, crosshair_stroke);
frame.stroke(&v_line, crosshair_stroke);
let dot = Path::circle(Point::new(px, py), 5.0);
frame.fill(&dot, theme::KNOB_FILL);
if !self.label.is_empty() {
frame.fill_text(iced::widget::canvas::Text {
content: self.label.clone(),
position: Point::new(pad_w / 2.0, pad_h + 2.0),
color: theme::TEXT_DIM,
size: iced::Pixels(10.0),
align_x: iced::alignment::Horizontal::Center.into(),
align_y: iced::alignment::Vertical::Top,
font: self.font,
..Default::default()
});
}
vec![frame.into_geometry()]
}
fn update(
&self,
state: &mut Self::State,
event: &Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<canvas::Action<Message<M>>> {
let label_h = if self.label.is_empty() { 0.0 } else { LABEL_H };
let pad_w = bounds.width.max(1.0);
let pad_h = (bounds.height - label_h).max(1.0);
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
if cursor.position_in(bounds).is_some() =>
{
state.dragging = true;
return Some(
canvas::Action::publish(Message::Param(ParamMessage::Batch(vec![
ParamMessage::BeginEdit(self.x_id),
ParamMessage::BeginEdit(self.y_id),
])))
.and_capture(),
);
}
Event::Mouse(mouse::Event::CursorMoved { .. }) if state.dragging => {
if let Some(pos) = cursor.position() {
let x_norm = f64::from(((pos.x - bounds.x) / pad_w).clamp(0.0, 1.0));
let y_norm = f64::from((1.0 - (pos.y - bounds.y) / pad_h).clamp(0.0, 1.0));
return Some(
canvas::Action::publish(Message::Param(ParamMessage::Batch(vec![
ParamMessage::SetNormalized(self.x_id, x_norm),
ParamMessage::SetNormalized(self.y_id, y_norm),
])))
.and_capture(),
);
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) if state.dragging => {
state.dragging = false;
return Some(
canvas::Action::publish(Message::Param(ParamMessage::Batch(vec![
ParamMessage::EndEdit(self.x_id),
ParamMessage::EndEdit(self.y_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 cursor.position_in(bounds).is_some() {
return mouse::Interaction::Crosshair;
}
mouse::Interaction::default()
}
}