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}