git.lucas.co / cce-ui
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 }