Skip to main content

vizia_core/views/
xypad.rs

1use crate::prelude::*;
2
3/// A view which allows the user to manipulate 2 floating point values simultaneously on a two dimensional pane.
4pub struct XYPad {
5    is_dragging: bool,
6
7    on_change: Option<Box<dyn Fn(&mut EventContext, f32, f32)>>,
8}
9
10impl XYPad {
11    fn normalized_from_cursor(cx: &EventContext, current: Entity, x: f32, y: f32) -> (f32, f32) {
12        let bounds = cx.transformed_bounds(current);
13        let width = bounds.width().max(f32::EPSILON);
14        let height = bounds.height().max(f32::EPSILON);
15
16        let dx = ((x - bounds.left()) / width).clamp(0.0, 1.0);
17        let dy = ((y - bounds.top()) / height).clamp(0.0, 1.0);
18
19        (dx, dy)
20    }
21
22    /// creates a new [XYPad] view.
23    pub fn new<R: Res<(f32, f32)> + 'static>(cx: &mut Context, value: R) -> Handle<Self> {
24        let value_state = value.to_signal(cx);
25        let left = Memo::new(move |_| Percentage(value_state.get().0 * 100.0));
26        let top = Memo::new(move |_| Percentage((1.0 - value_state.get().1) * 100.0));
27
28        Self { is_dragging: false, on_change: None }
29            .build(cx, |cx| {
30                // Thumb
31                Element::new(cx)
32                    .position_type(PositionType::Absolute)
33                    .left(left)
34                    .top(top)
35                    .translate(Translate::new(
36                        Length::Value(LengthValue::Px(-6.0)),
37                        Length::Value(LengthValue::Px(-6.0)),
38                    ))
39                    .size(Pixels(10.0))
40                    .corner_radius(Percentage(50.0))
41                    .border_width(Pixels(2.0))
42                    .hoverable(false)
43                    .class("thumb");
44            })
45            .overflow(Overflow::Hidden)
46            .border_width(Pixels(1.0))
47            .size(Pixels(200.0))
48    }
49}
50
51impl View for XYPad {
52    fn element(&self) -> Option<&'static str> {
53        Some("xypad")
54    }
55
56    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
57        event.map(|window_event, meta| match window_event {
58            WindowEvent::MouseDown(button) if *button == MouseButton::Left => {
59                if cx.is_disabled() {
60                    return;
61                }
62                let current = cx.current();
63                let (mouse_x, mouse_y) = cx.mouse.left.pos_down;
64                if meta.target == current || meta.target.is_descendant_of(cx.tree, current) {
65                    cx.capture();
66                    let (dx, dy) = Self::normalized_from_cursor(cx, current, mouse_x, mouse_y);
67
68                    self.is_dragging = true;
69
70                    if let Some(callback) = &self.on_change {
71                        (callback)(cx, dx, 1.0 - dy);
72                    }
73                }
74            }
75
76            WindowEvent::MouseUp(button) if *button == MouseButton::Left => {
77                cx.set_active(false);
78                cx.release();
79                self.is_dragging = false;
80            }
81
82            WindowEvent::MouseMove(x, y) => {
83                if self.is_dragging {
84                    let current = cx.current();
85                    let (dx, dy) = Self::normalized_from_cursor(cx, current, *x, *y);
86
87                    if let Some(callback) = &self.on_change {
88                        (callback)(cx, dx, 1.0 - dy);
89                    }
90                }
91            }
92
93            _ => {}
94        });
95    }
96}
97
98impl Handle<'_, XYPad> {
99    /// Set the callback which will be triggered when the XYPad is manipulated.
100    pub fn on_change<F: Fn(&mut EventContext, f32, f32) + 'static>(self, callback: F) -> Self {
101        self.modify(|xypad| xypad.on_change = Some(Box::new(callback)))
102    }
103}