Skip to main content

sequoia_elements/
svpad.rs

1#![forbid(unsafe_code)]
2
3use sequoia_core::{Color, Edges, Length, Point, Rect};
4use sequoia_input::{CursorIcon, Event, EventPhase, PointerButton, PointerEvent};
5use sequoia_layout::LayoutStyle;
6use sequoia_reactive::Signal;
7use sequoia_style::VisualState;
8use sequoia_view::{EventCtx, Pad2D as PadMap, PaintCtx, Painter, Unit, View};
9
10pub const KNOB_R: f32 = 7.0;
11
12pub const HEIGHT: f32 = 198.0;
13
14#[derive(Clone)]
15pub struct SvPad {
16    sat: Signal<f32>,
17    val: Signal<f32>,
18    hue: Signal<f32>,
19    knob_radius: f32,
20    style: LayoutStyle,
21    margin: Edges,
22}
23
24impl SvPad {
25    pub fn new(sat: Signal<f32>, val: Signal<f32>, hue: Signal<f32>) -> Self {
26        Self {
27            sat,
28            val,
29            hue,
30            knob_radius: KNOB_R,
31            style: LayoutStyle {
32                width: Length::Fill(1.0),
33                height: Length::Px(HEIGHT),
34                ..LayoutStyle::default()
35            },
36            margin: Edges::ZERO,
37        }
38    }
39
40    pub fn knob_radius(mut self, r: f32) -> Self {
41        self.knob_radius = r.clamp(3.0, 16.0);
42        self
43    }
44
45    pub fn height(mut self, height: impl Into<Length>) -> Self {
46        self.style = self.style.height(height.into());
47        self
48    }
49
50    pub fn width(mut self, width: impl Into<Length>) -> Self {
51        self.style = self.style.width(width.into());
52        self
53    }
54
55    pub fn flex(mut self, weight: f32) -> Self {
56        self.style = self.style.flex(weight);
57        self
58    }
59
60    pub fn expand(self) -> Self {
61        self.flex(1.0)
62    }
63
64    pub fn margin(mut self, margin: impl Into<Edges>) -> Self {
65        let m = margin.into();
66        self.margin = Edges::only(
67            sanitize_margin(m.left),
68            sanitize_margin(m.top),
69            sanitize_margin(m.right),
70            sanitize_margin(m.bottom),
71        );
72        self
73    }
74
75    pub fn margin_xy(self, x: f32, y: f32) -> Self {
76        self.margin(Edges::xy(x, y))
77    }
78
79    pub fn margin_x(mut self, x: f32) -> Self {
80        let x = sanitize_margin(x);
81        self.margin.left = x;
82        self.margin.right = x;
83        self
84    }
85
86    pub fn margin_y(mut self, y: f32) -> Self {
87        let y = sanitize_margin(y);
88        self.margin.top = y;
89        self.margin.bottom = y;
90        self
91    }
92
93    pub fn margin_left(mut self, v: f32) -> Self {
94        self.margin.left = sanitize_margin(v);
95        self
96    }
97
98    pub fn margin_right(mut self, v: f32) -> Self {
99        self.margin.right = sanitize_margin(v);
100        self
101    }
102
103    pub fn margin_top(mut self, v: f32) -> Self {
104        self.margin.top = sanitize_margin(v);
105        self
106    }
107
108    pub fn margin_bottom(mut self, v: f32) -> Self {
109        self.margin.bottom = sanitize_margin(v);
110        self
111    }
112
113    fn apply_at(&self, rect: Rect, pos: Point) {
114        let (s, v) = PadMap::new(rect, 0.0).sv_of(pos);
115        self.sat.set(s);
116        self.val.set(v);
117    }
118
119    fn knob_center(&self, rect: Rect) -> Point {
120        PadMap::new(rect, 0.0).pos_of(
121            self.sat.get_untracked().clamp(0.0, 1.0),
122            self.val.get_untracked().clamp(0.0, 1.0),
123        )
124    }
125
126    fn cur_color(&self) -> Color {
127        Color::from_hsva(
128            self.hue.get_untracked() / 360.0,
129            self.sat.get_untracked().clamp(0.0, 1.0),
130            self.val.get_untracked().clamp(0.0, 1.0),
131            1.0,
132        )
133    }
134}
135
136impl View for SvPad {
137    fn body(&self) -> impl View + 'static {
138        let _ = self.sat.get();
139        let _ = self.val.get();
140        let _ = self.hue.get();
141        Unit
142    }
143
144    fn is_leaf(&self) -> bool {
145        true
146    }
147
148    fn focusable(&self) -> bool {
149        false
150    }
151
152    fn captures_pointer(&self) -> bool {
153        true
154    }
155
156    fn has_visual_state(&self) -> bool {
157        true
158    }
159
160    fn cursor_icon(&self, _state: VisualState) -> CursorIcon {
161        CursorIcon::Crosshair
162    }
163
164    fn layout_style(&self) -> LayoutStyle {
165        let mut s = self.style;
166        s.margin = self.margin;
167        s
168    }
169
170    fn clip_self(&self) -> bool {
171        false
172    }
173
174    fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
175        if ctx.phase != EventPhase::Target {
176            return;
177        }
178        match event {
179            Event::Pointer(PointerEvent::Down {
180                button: PointerButton::Left,
181                pos,
182                ..
183            }) => {
184                self.apply_at(ctx.rect, *pos);
185                ctx.capture_pointer();
186                ctx.handled();
187            }
188            Event::Pointer(PointerEvent::Move { pos, .. }) if ctx.state.pressed => {
189                self.apply_at(ctx.rect, *pos);
190                ctx.handled();
191            }
192            _ => {}
193        }
194    }
195
196    fn paint_self(&self, ctx: &mut PaintCtx) {
197        let mut g = Painter::new(ctx);
198        let rect = g.rect();
199        if rect.is_empty() {
200            return;
201        }
202        let overflow = rect.inflate(self.knob_radius, self.knob_radius);
203        g.clipped(overflow, |g| {
204            g.sv_field(rect, self.hue.get_untracked() / 360.0);
205            g.knob(self.knob_center(rect), self.knob_radius, self.cur_color());
206        });
207    }
208}
209
210fn sanitize_margin(v: f32) -> f32 {
211    if v.is_finite() { v.max(0.0) } else { 0.0 }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn defaults_fill_width_fixed_height() {
220        let p = SvPad::new(
221            Signal::new(0.5f32),
222            Signal::new(0.5f32),
223            Signal::new(200.0f32),
224        );
225        assert_eq!(p.layout_style().width, Length::Fill(1.0));
226        assert_eq!(p.layout_style().height, Length::Px(HEIGHT));
227        assert!(!p.clip_self());
228    }
229
230    #[test]
231    fn drag_clamps_to_corners() {
232        let (s, v, h) = (
233            Signal::new(0.5f32),
234            Signal::new(0.5f32),
235            Signal::new(0.0f32),
236        );
237        let p = SvPad::new(s.clone(), v.clone(), h);
238        let rect = Rect::from_xywh(0.0, 0.0, 200.0, 200.0);
239        p.apply_at(rect, Point::new(-50.0, -50.0));
240        assert_eq!((s.get_untracked(), v.get_untracked()), (0.0, 1.0));
241        p.apply_at(rect, Point::new(500.0, 500.0));
242        assert_eq!((s.get_untracked(), v.get_untracked()), (1.0, 0.0));
243    }
244}