GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/widget/input/slider2d.rs (7.7K)
1 //! `Slider2D` — a two-axis pad control: one thumb dragged across a recessed
2 //! square well maps to an `(x, y)` pair in 0..1 × 0..1, y-up. Follows the
3 //! `Slider` narrow-trait shape: the adapter's detached label above the pad, host-driven drags through
4 //! the `Input` drag hooks, and `take_change`-based polling by composites.
5
6 use crate::scene::layout::{Rect, Size};
7 use crate::scene::paint::PaintCtx;
8 use crate::widget::{
9 Adapted, ElementState, Event, EventCtx, Input, Layout, MouseButton, MouseScrollDelta, Paint,
10 };
11
12 #[derive(Debug, Clone)]
13 pub struct Slider2D {
14 dragging: bool,
15 pub(crate) value_x: f32,
16 pub(crate) value_y: f32,
17 pub just_changed: bool,
18 label: Option<String>,
19 }
20
21 impl Slider2D {
22 /// Thumb radius; also the pad's inner inset so the thumb center's range
23 /// keeps the whole thumb inside the well.
24 const THUMB_R: f32 = 6.0;
25
26 pub fn new() -> Adapted<Slider2D> {
27 Adapted::new(Slider2D {
28 dragging: false,
29 value_x: 0.5,
30 value_y: 0.5,
31 just_changed: false,
32 label: None,
33 })
34 }
35
36 pub fn value_x(&self) -> f32 {
37 self.value_x
38 }
39
40 pub fn value_y(&self) -> f32 {
41 self.value_y
42 }
43
44 pub fn set_values(&mut self, x: f32, y: f32) {
45 self.value_x = x.clamp(0.0, 1.0);
46 self.value_y = y.clamp(0.0, 1.0);
47 }
48
49 fn thumb_center(&self, rect: Rect) -> (f32, f32) {
50 let inset = Self::THUMB_R + 2.0;
51 (
52 rect.x + inset + self.value_x * (rect.width - 2.0 * inset).max(1.0),
53 rect.y + inset + (1.0 - self.value_y) * (rect.height - 2.0 * inset).max(1.0),
54 )
55 }
56
57 fn set_from_point(&mut self, px: f32, py: f32, rect: Rect) -> bool {
58 let inset = Self::THUMB_R + 2.0;
59 let w = (rect.width - 2.0 * inset).max(1.0);
60 let h = (rect.height - 2.0 * inset).max(1.0);
61 let nx = ((px - rect.x - inset) / w).clamp(0.0, 1.0);
62 let ny = (1.0 - (py - rect.y - inset) / h).clamp(0.0, 1.0);
63 if (nx - self.value_x).abs() > 0.0001 || (ny - self.value_y).abs() > 0.0001 {
64 self.value_x = nx;
65 self.value_y = ny;
66 self.just_changed = true;
67 true
68 } else {
69 false
70 }
71 }
72 }
73
74 impl Layout for Slider2D {
75 /// A 64px pad.
76 fn intrinsic_size(&self) -> Option<Size> {
77 Some(Size::new(64.0, 64.0))
78 }
79
80 fn intrinsic_measure_width(&self) -> bool {
81 true
82 }
83 }
84
85 impl Paint for Slider2D {
86 fn color(&self) -> [f32; 4] {
87 [0.0, 0.0, 0.0, 0.0]
88 }
89
90 fn widget_font(&self) -> Option<String> {
91 Some(crate::layout::control_label_font_detached())
92 }
93
94 fn sync_label(&mut self, label: &str) {
95 self.label = Some(label.to_string());
96 }
97
98 fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
99 // The well: the shared canvas floor (`PaintCtx::well_floor`) read as an
100 // opening cut into the host's plate, rim drawn last so its shading
101 // falls over the content at the edges. Rounded like the text wells.
102 let radius = crate::layout::textbox_corner_radius();
103 ctx.well_floor(rect, radius, &crate::scene::Material::pane(), false);
104
105 // Crosshair through the thumb — the pad's read of both axis values.
106 let (cx, cy) = self.thumb_center(rect);
107 let line = [1.0, 1.0, 1.0, 0.16];
108 ctx.quad(Rect { x: rect.x + 2.0, y: cy - 0.5, width: rect.width - 4.0, height: 1.0 }, line);
109 ctx.quad(Rect { x: cx - 0.5, y: rect.y + 2.0, width: 1.0, height: rect.height - 4.0 }, line);
110
111 // Thumb: glassy fill in a thin white ring (the ramp peg look, small).
112 let fill_a = if self.dragging { 0.9 } else { 0.35 };
113 ctx.circle(cx, cy, Self::THUMB_R - 1.0, [0.5, 0.75, 1.0, fill_a]);
114 ctx.arc(
115 cx,
116 cy,
117 Self::THUMB_R + 1.0,
118 2.0,
119 0.0,
120 std::f32::consts::TAU,
121 [1.0, 1.0, 1.0, 0.9],
122 );
123
124 // The well's rim (`PaintCtx::well_rim`), carved inside the pad's rect; the
125 // control label above is the adapter's, outside the well like every control's.
126 ctx.well_rim(rect, radius, crate::layout::control_relief());
127 }
128 }
129
130 impl Input for Slider2D {
131 fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
132 match event {
133 Event::MouseButton { button: MouseButton::Left, state, x: px, y: py, .. } => {
134 match state {
135 ElementState::Pressed => {
136 let r = ectx.rect;
137 if *px >= r.x && *px <= r.x + r.width && *py >= r.y && *py <= r.y + r.height {
138 self.dragging = true;
139 self.set_from_point(*px, *py, r);
140 return true;
141 }
142 false
143 }
144 ElementState::Released => std::mem::take(&mut self.dragging),
145 }
146 }
147 Event::PointerMove { x: px, y: py, .. } => {
148 if self.dragging {
149 self.set_from_point(*px, *py, ectx.rect)
150 } else {
151 false
152 }
153 }
154 Event::MouseWheel { delta, x: px, y: py, .. } => {
155 // Vertical wheel nudges the y axis (the Slider gesture-gated
156 // pattern), hovering anywhere over the pad.
157 if let Some(ui) = ectx.ui.as_deref_mut() {
158 if !ui.scroll_gesture_new && ui.scroll_initiate_widget_id != Some(ectx.id) {
159 return false;
160 }
161 let r = ectx.rect;
162 if *px >= r.x && *px <= r.x + r.width && *py >= r.y && *py <= r.y + r.height {
163 if ui.scroll_gesture_new {
164 ui.scroll_initiate_widget_id = Some(ectx.id);
165 }
166 let amount = match delta {
167 MouseScrollDelta::LineDelta(_x, y) => *y,
168 MouseScrollDelta::PixelDelta(pos) => (pos.y as f32) / 120.0,
169 };
170 let ny = (self.value_y - amount * 0.02).clamp(0.0, 1.0);
171 if (ny - self.value_y).abs() > 0.0001 {
172 self.value_y = ny;
173 self.just_changed = true;
174 }
175 return true;
176 }
177 }
178 false
179 }
180 _ => false,
181 }
182 }
183
184 fn opens_context_menu(&self) -> bool {
185 true
186 }
187
188 fn draggable(&self, _rect: Rect) -> bool {
189 true
190 }
191 fn is_dragging(&self) -> bool {
192 self.dragging
193 }
194 fn drag_begin(&mut self, px: f32, py: f32, rect: Rect) {
195 self.dragging = true;
196 self.set_from_point(px, py, rect);
197 }
198 fn drag_update(&mut self, px: f32, py: f32, rect: Rect) -> bool {
199 self.set_from_point(px, py, rect)
200 }
201 fn drag_end(&mut self) {
202 self.dragging = false;
203 }
204
205 fn take_change(&mut self) -> bool {
206 std::mem::take(&mut self.just_changed)
207 }
208
209 fn value_string(&self) -> Option<String> {
210 Some(format!("{:.3},{:.3}", self.value_x, self.value_y))
211 }
212
213 fn set_value_string(&mut self, val: &str) -> bool {
214 let Some((xs, ys)) = val.split_once(',') else { return false };
215 let (Ok(x), Ok(y)) = (xs.trim().parse::<f32>(), ys.trim().parse::<f32>()) else {
216 return false;
217 };
218 let (x, y) = (x.clamp(0.0, 1.0), y.clamp(0.0, 1.0));
219 let changed = (x - self.value_x).abs() > 0.0005 || (y - self.value_y).abs() > 0.0005;
220 self.value_x = x;
221 self.value_y = y;
222 if changed {
223 self.just_changed = true;
224 }
225 changed
226 }
227 }