git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/widget/input/checkbox.rs (36.1K)

  1 //! Narrow-trait `Checkbox` and `Toggle` (Phase 5e — first interactive widgets off `WidgetHost`).
  2 //!
  3 //! Both are inline-label widgets: they paint their own label (with hover/focus-dependent color)
  4 //! inside their rect, so they track `hovered`/`focused` themselves from the `MouseEnter`/
  5 //! `MouseLeave`/`FocusIn`/`FocusOut` events the adapter forwards — the migration shape for the
  6 //! state that becomes `Animated<f32>` in RFC §3.6.
  7 //!
  8 //! Geometry parity: `paint` emits the same conditional geometry as the legacy `extra_quads` /
  9 //! `extra_arcs` / `all_rounded_quads` overrides did, in the matching prim kinds, so the adapter's
 10 //! per-prim reverse bridges reproduce the legacy getters byte-for-byte.
 11 
 12 use crate::colors;
 13 use crate::scene::layout::Rect;
 14 use crate::scene::paint::{Field, PaintCtx};
 15 use crate::widget::{
 16     Adapted, ElementState, Event, EventCtx, Input, Justification, Layout, MouseButton, Paint,
 17 };
 18 
 19 /// How much of a checked box's side its plate takes, standing square in the
 20 /// middle of the well — enough to read as a plate, little enough to leave the
 21 /// well showing all round it, which is what tells it from an empty box. (All
 22 /// run, a box filled flush, was tried first: at a control's size its outline
 23 /// is the empty well's, and the two states were hard to tell apart. Then the
 24 /// toggle's run, half the width and the field's whole height: in a square box
 25 /// that is a tall bar, and a ticked box read as having narrowed.)
 26 const PLATE_SHARE: f32 = 0.6;
 27 
 28 /// Paint a check box — its well ([`Checkbox::box_field`]) and, checked, the
 29 /// plate in it ([`Checkbox::box_plate`]): both fields with relief on; off,
 30 /// as the toggle's — a well is its frame, the hairline every well falls back
 31 /// to (lit by `hovered` / `focused`), and the plate a lit face in it.
 32 pub(crate) fn paint_box(ctx: &mut PaintCtx, well: &Field, plate: Option<&Field>, hovered: bool, focused: bool) {
 33     if crate::layout::control_relief() {
 34         ctx.field(well);
 35         if let Some(plate) = plate {
 36             ctx.field(plate);
 37         }
 38         return;
 39     }
 40     let bw = crate::layout::toggle_border_width().max(1.0);
 41     ctx.border(well.rect, well.radii, [0.0; 4], colors::well_frame_color(hovered, focused), bw);
 42     if let Some(plate) = plate {
 43         let (face, face_r) = inset(plate.rect, plate.radii.0, plate.depth * 0.5);
 44         let lit = (0.16 * (crate::layout::bevel_depth() / 0.15)).clamp(0.0, 0.5);
 45         ctx.rounded_rect(face, face_r, (true, true, true, true), [1.0, 1.0, 1.0, lit]);
 46     }
 47 }
 48 
 49 /// A rect shrunk by `g` on every side, its uniform corner radius shrunk to
 50 /// match so the inner silhouette stays concentric with the outer one.
 51 fn inset(rect: Rect, radius: f32, g: f32) -> (Rect, f32) {
 52     (
 53         Rect {
 54             x: rect.x + g,
 55             y: rect.y + g,
 56             width: (rect.width - 2.0 * g).max(0.0),
 57             height: (rect.height - 2.0 * g).max(0.0),
 58         },
 59         (radius - g).max(0.0),
 60     )
 61 }
 62 
 63 fn parse_bool(val: &str) -> Option<bool> {
 64     match val.trim().to_lowercase().as_str() {
 65         "true" | "1" | "yes" | "on" => Some(true),
 66         "false" | "0" | "no" | "off" => Some(false),
 67         _ => None,
 68     }
 69 }
 70 
 71 /// A check box: a square well ([`Field::well`]) with an optional label to
 72 /// its right — empty unchecked; checked, a flush square plate standing in the
 73 /// middle of it ([`Field::run`] on [`PLATE_SHARE`] of its side), the well
 74 /// showing all round. Not the toggle's run with no travel, though it was: a
 75 /// run spans its field's whole height, so in a square it is a tall bar.
 76 /// Standalone, the box is the largest square in the rect, centred.
 77 ///
 78 /// A check drawn inline in text — a list row (cce-list), a markdown task
 79 /// item, the doc editor — is the same box, through [`Checkbox::paint_inline`].
 80 /// Until 2026-10-02 the widget and all of those drew a ring-and-dot mark.
 81 pub struct Checkbox {
 82     checked: bool,
 83     just_clicked: bool,
 84     pub just_changed: bool,
 85     label: Option<String>,
 86     hovered: bool,
 87     focused: bool,
 88 }
 89 
 90 impl Checkbox {
 91     /// Half the side of an inline box ([`Checkbox::paint_inline`]) at a
 92     /// list row's size: a 14px square, the size of the round mark it
 93     /// replaced.
 94     pub const INLINE_HALF: f32 = 7.0;
 95 
 96     /// The box's well: a square as tall as the control (at most a toggle's
 97     /// height) at the rect's left when labelled, the largest square in the
 98     /// rect, centred, when not ([`Checkbox::box_field`]). Lit while focused.
 99     pub fn field(&self, rect: Rect) -> Field {
100         let side = if self.label.is_some() {
101             rect.height.min(crate::layout::toggle_height())
102         } else {
103             rect.width.min(rect.height)
104         }
105         .max(1.0);
106         let x = if self.label.is_some() { rect.x } else { rect.x + (rect.width - side) * 0.5 };
107         let square = Rect { x, y: rect.y + (rect.height - side) * 0.5, width: side, height: side };
108         Self::box_field(square).with_tint(self.focused.then(crate::scene::paint::ControlPlate::focus_tint))
109     }
110 
111     /// A check box's well on `square` — its footprint — carved inside it
112     /// through [`crate::layout::carve_inside`] as every field is. With
113     /// [`Checkbox::box_plate`], the ONE description the widget and every
114     /// inline box share.
115     ///
116     /// The corner is the toggle's in PROPORTION — its radius over its height
117     /// — so a box as tall as a toggle has the toggle's corner exactly, and a
118     /// 14px box in a line of text is a rounded square rather than a disc,
119     /// which the toggle's radius taken whole would make it.
120     pub fn box_field(square: Rect) -> Field {
121         let side = square.width.min(square.height).max(1.0);
122         let r = (crate::layout::toggle_corner_radius() * side / crate::layout::toggle_height().max(1.0)).min(side * 0.5);
123         let depth = crate::layout::bevel_width().min(side * 0.2);
124         let (outline, radii) = crate::layout::carve_inside(square, (r, r, r, r), depth);
125         Field::well(outline, radii, depth)
126     }
127 
128     /// A checked box's plate in `well` ([`Checkbox::box_field`]): a square
129     /// [`PLATE_SHARE`] of its side, centred, all run — its corner the well's
130     /// in proportion, so it is the box again, smaller.
131     pub fn box_plate(well: &Field) -> Field {
132         let o = well.rect;
133         let side = (o.width.min(o.height) * PLATE_SHARE).max(1.0);
134         let rect = Rect { x: o.x + (o.width - side) * 0.5, y: o.y + (o.height - side) * 0.5, width: side, height: side };
135         let r = well.radii.0 * side / o.width.max(1.0);
136         Field::run(rect, (r, r, r, r), well.depth.min(side * 0.2))
137     }
138 
139     /// Paint a check box centred on (`cx`, `cy`), `half` its half-side, for
140     /// a host that draws one inline — cce-list's rows, a markdown task item,
141     /// the doc editor — where the widget would be a whole control. The same
142     /// box the widget draws ([`Checkbox::box_field`], [`Checkbox::box_plate`]),
143     /// relief on or off.
144     pub fn paint_inline(ctx: &mut PaintCtx, cx: f32, cy: f32, half: f32, checked: bool) {
145         let square = Rect { x: cx - half, y: cy - half, width: 2.0 * half, height: 2.0 * half };
146         let well = Self::box_field(square);
147         paint_box(ctx, &well, checked.then(|| Self::box_plate(&well)).as_ref(), false, false);
148     }
149 
150     pub fn new() -> Adapted<Checkbox> {
151         Adapted::new(Checkbox {
152             checked: false,
153             just_clicked: false,
154             just_changed: false,
155             label: None,
156             hovered: false,
157             focused: false,
158         })
159     }
160 
161     pub fn set_checked(&mut self, checked: bool) {
162         self.checked = checked;
163     }
164 
165     pub fn checked(&self) -> bool {
166         self.checked
167     }
168 
169     /// Hover state, also settable directly for immediate-mode hosts that do their own
170     /// hit-testing instead of routing `MouseEnter`/`MouseLeave` (json_layout).
171     pub fn hovered(&self) -> bool {
172         self.hovered
173     }
174 
175     pub fn set_hovered(&mut self, hovered: bool) {
176         self.hovered = hovered;
177     }
178 }
179 
180 impl Layout for Checkbox {
181     fn inline_label(&self) -> bool {
182         true
183     }
184 }
185 
186 impl Paint for Checkbox {
187     fn color(&self) -> [f32; 4] {
188         // The box is carved; the widget itself has no background.
189         [0.0, 0.0, 0.0, 0.0]
190     }
191 
192     fn widget_font(&self) -> Option<String> {
193         Some(crate::layout::control_label_font())
194     }
195 
196     fn sync_label(&mut self, label: &str) {
197         self.label = Some(label.to_string());
198     }
199 
200     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
201         let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
202         // The box leads and the label follows, a list row's reading order.
203         let field = self.field(rect);
204         let plate = self.checked.then(|| Self::box_plate(&field));
205         paint_box(ctx, &field, plate.as_ref(), self.hovered, self.focused);
206         if let Some(ref label) = self.label {
207             let (_, font_size) = crate::layout::control_label_font_parsed();
208             let ty = crate::layout::align_text_y(y, h, font_size, 0.0);
209             // From the box's footprint, not its carved outline.
210             let box_right = field.rect.x + field.rect.width + field.depth * 0.5;
211             ctx.text_with(
212                 label.clone(),
213                 box_right + 8.0,
214                 ty,
215                 font_size,
216                 colors::control_label_color_for_state(self.hovered, self.focused),
217                 None,
218                 // The label is caller text and the box is caller-sized; a
219                 // control has no business drawing past its own rect.
220                 Some([x, y, x + w, y + h]),
221             );
222         }
223     }
224 }
225 
226 impl Input for Checkbox {
227     fn focus_role(&self) -> crate::widget::FocusRole {
228         crate::widget::FocusRole::Plate
229     }
230     fn on_event(&mut self, event: &Event, _ectx: &mut EventCtx) -> bool {
231         match event {
232             Event::MouseButton { button: MouseButton::Left, state: ElementState::Pressed, .. } => {
233                 // Already hit-gated by the adapter.
234                 self.checked = !self.checked;
235                 self.just_clicked = true;
236                 self.just_changed = true;
237                 true
238             }
239             Event::MouseEnter => {
240                 self.hovered = true;
241                 false
242             }
243             Event::MouseLeave => {
244                 self.hovered = false;
245                 false
246             }
247             Event::FocusIn => {
248                 self.focused = true;
249                 false
250             }
251             Event::FocusOut => {
252                 self.focused = false;
253                 false
254             }
255             Event::KeyInput(key_event) => {
256                 // A focused plate is pressed by Enter / Space, as a Button is.
257                 if !self.focused || key_event.state != ElementState::Pressed {
258                     return false;
259                 }
260                 match key_event.logical_key {
261                     crate::widget::Key::Named(crate::widget::NamedKey::Enter)
262                     | crate::widget::Key::Named(crate::widget::NamedKey::Space) => {
263                         self.checked = !self.checked;
264                         self.just_clicked = true;
265                         self.just_changed = true;
266                         true
267                     }
268                     _ => false,
269                 }
270             }
271             _ => false,
272         }
273     }
274 
275     fn opens_context_menu(&self) -> bool {
276         true
277     }
278 
279     fn take_click(&mut self) -> bool {
280         std::mem::take(&mut self.just_clicked)
281     }
282 
283     fn take_change(&mut self) -> bool {
284         std::mem::take(&mut self.just_changed)
285     }
286 
287     fn value_string(&self) -> Option<String> {
288         Some(self.checked.to_string())
289     }
290 
291     fn set_value_string(&mut self, val: &str) -> bool {
292         let Some(new_checked) = parse_bool(val) else { return false };
293         if self.checked != new_checked {
294             self.checked = new_checked;
295             self.just_changed = true;
296             true
297         } else {
298             false
299         }
300     }
301 
302     fn value(&self) -> i32 {
303         if self.checked { 1 } else { 0 }
304     }
305 }
306 
307 /// A field whose run glides: the toggle's footprint is ONE field
308 /// ([`crate::scene::paint::Prim::Field`]) — a well carved into the plate it
309 /// sits on, holding a flush run half its width, the run at the left end
310 /// (off) or the right end (on) and gliding between them.
311 ///
312 /// It is the same object as a text row with its picker, a spinbox and its
313 /// -/+ run, and a dropdown trigger: a well in a plate with a flush plate in
314 /// it, one outline round both. Those hold their run still at the right end
315 /// (or are all run); a toggle's moves, and where it stands is the state.
316 /// Until 2026-10-02 it was a recess with a raised boss standing on its
317 /// floor, the one control whose nested plate stood ABOVE the surface where
318 /// every other stood flush with it, and whose outline turned its own corner
319 /// round the plate rather than running round the whole control.
320 ///
321 /// That is the ONE toggle style. The rocker — two flat half faces with a
322 /// hinge between them, the state half tipped out toward the light — is gone,
323 /// and with it the per-widget and per-config style switch it was chosen by
324 /// (`style.control.toggle.style`, `Toggle::with_slide`).
325 #[derive(Debug, Clone)]
326 pub struct Toggle {
327     toggled: bool,
328     just_toggled: bool,
329     label: Option<String>,
330     hovered: bool,
331     focused: bool,
332     /// Where the label sits across the track. Mirrors `Button::justify` — same enum, same
333     /// 8px edge inset — so the two read as one control set wherever they share a column.
334     justify: Justification,
335     /// Relief style: the toggle is a field, a carved well with a flush run in
336     /// it. Without it the well falls back to the hairline frame every well
337     /// shares and the run to a lit face — see `paint`.
338     raised: Option<bool>,
339     /// The run's animated position along its field, 0 (left/off) → 1 (right/on).
340     /// Chases `toggled` in `tick` after a click; programmatic state syncs
341     /// (`set_toggled`, `set_value_string`) snap it, so only user interaction
342     /// animates.
343     slide_t: f32,
344 }
345 
346 impl Toggle {
347     /// The style in force: the per-widget override (`with_raised`) when set, else
348     /// the DE's `control_relief`, read live so a runtime switch
349     /// (`layout::set_control_relief`) restyles every control at once.
350     fn raised(&self) -> bool {
351         self.raised.unwrap_or_else(crate::layout::control_relief)
352     }
353 
354     pub fn new() -> Adapted<Toggle> {
355         Adapted::new(Toggle {
356             toggled: false,
357             just_toggled: false,
358             label: None,
359             hovered: false,
360             focused: false,
361             justify: Justification::Center,
362             raised: None,
363             slide_t: 0.0,
364         })
365     }
366 
367     pub fn set_label(&mut self, label: &str) {
368         self.label = Some(label.to_string());
369     }
370 
371     pub fn set_toggled(&mut self, v: bool) {
372         self.toggled = v;
373         self.slide_t = if v { 1.0 } else { 0.0 };
374     }
375 
376     /// Set the hover directly, for a host that paints one toggle as a
377     /// STAMP over several rows and does its own hit-testing — the
378     /// designer's dialog — as `Checkbox::set_hovered` is there for.
379     pub fn set_hovered(&mut self, hovered: bool) {
380         self.hovered = hovered;
381     }
382 
383     pub fn toggled(&self) -> bool {
384         self.toggled
385     }
386 
387     /// The toggle as the [`Field`] it is, in its sliding form: the whole
388     /// footprint carved one step down — taken through
389     /// [`crate::layout::carve_inside`], so the walls stay inside the rect and
390     /// the gap beside a toggle is the gap, exactly as a TextBox's well and a
391     /// spinbox's field are taken — holding a run half its width, placed by
392     /// the animated `slide_t`: flush with the left end off, the right end on,
393     /// a well either side between. Lit while focused.
394     ///
395     /// As a text row's picker is, the run is laid out a wall wider than the
396     /// face it carries (the field insets the face half a wall from the
397     /// outline and from the seam), so the face reaches the well.
398     ///
399     /// The SINGLE source for the toggle's geometry: `paint` draws it, the
400     /// relief-off paint lights its [`Toggle::face`], and a host that draws
401     /// the relief itself (`ParametersBg::fields`) takes it whole.
402     pub fn field(&self, rect: Rect) -> Field {
403         let r = crate::layout::toggle_corner_radius();
404         let depth = crate::layout::bevel_width().min(rect.height * 0.2);
405         let (outline, radii) = crate::layout::carve_inside(rect, (r, r, r, r), depth);
406         let focus = self.focused.then(crate::scene::paint::ControlPlate::focus_tint);
407         Field::sliding_run(outline, radii, depth, outline.width * 0.5, self.slide_t).with_tint(focus)
408     }
409 
410     /// The run's FACE — what stands at the surface's level inside the
411     /// field's valley: the run inset half a wall on every side, its corners
412     /// the field's less that. `(rect, corner radius)`. The relief-off paint
413     /// lights this rect, where the field cannot be carved.
414     pub fn face(&self, rect: Rect) -> (Rect, f32) {
415         let f = self.field(rect);
416         let (a, b) = f.run_span().unwrap_or((f.rect.x, f.rect.x));
417         let run = Rect { x: a, y: f.rect.y, width: b - a, height: f.rect.height };
418         inset(run, f.radii.0, f.depth * 0.5)
419     }
420 }
421 
422 impl Adapted<Toggle> {
423     pub fn with_left_align(mut self, left_align: bool) -> Self {
424         self.justify = if left_align { Justification::Left } else { Justification::Center };
425         self
426     }
427 
428     pub fn with_justify(mut self, justify: Justification) -> Self {
429         self.justify = justify;
430         self
431     }
432 
433     /// Raised style: see the `raised` field.
434     pub fn with_raised(mut self, raised: bool) -> Self {
435         self.raised = Some(raised);
436         self
437     }
438 }
439 
440 impl Layout for Toggle {
441     fn inline_label(&self) -> bool {
442         true
443     }
444 
445     fn intrinsic_size(&self) -> Option<crate::scene::layout::Size> {
446         // Legacy `preferred_height`; width comes from the container.
447         Some(crate::scene::layout::Size::new(0.0, crate::layout::toggle_height()))
448     }
449 }
450 
451 impl Paint for Toggle {
452     /// No fill of its own: a toggle is worked out of the plate it sits on, so
453     /// the plate's material (tint, blur, whatever it is) shows through and the
454     /// state reads from light and relief alone — see [`Paint::paint`].
455     fn color(&self) -> [f32; 4] {
456         [0.0, 0.0, 0.0, 0.0]
457     }
458 
459     fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
460         let r = crate::layout::toggle_corner_radius();
461         if r > 0.0 {
462             Some((r, (true, true, true, true)))
463         } else {
464             None
465         }
466     }
467 
468     fn widget_font(&self) -> Option<String> {
469         Some(crate::layout::control_label_font())
470     }
471 
472     fn sync_label(&mut self, label: &str) {
473         self.label = Some(label.to_string());
474     }
475 
476     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
477         let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
478 
479         // A toggle paints NO fill of its own: it is worked out of the plate it
480         // sits on, so the plate's own material (tint, blur, whatever it is)
481         // shows through both the well's floor and the run's face, and the
482         // state reads from light and relief alone — the DE's transparent-face
483         // convention (closed dropdowns, inset troughs). The state colors this
484         // used to tint with (enabled/disabled/background) are retired with the
485         // rest of the toggle's palette.
486         let field = self.field(rect);
487         if self.raised() {
488             // ONE field — the well and the flush run in it under one outline,
489             // as a text row's picker and a spinbox's -/+ run are drawn. The
490             // run's position IS the read: left off, right on, animated in
491             // `tick`. A host that draws the relief itself
492             // (`ParametersBg::fields`) takes the same `field`. Focus lights
493             // the field's rim, the ring every field wears.
494             ctx.field(&field);
495         } else {
496             // Relief off: a well is its frame — the one hairline every well
497             // falls back to, lit while focused, exactly `well_rim`'s flat arm —
498             // and the run's face is a lit face, the neutral overlay the DE
499             // gives a surface it cannot carve (what the rocker's halves wore).
500             // Scaled by the relief strength, as that lighting was.
501             //
502             // The face's overlay is a ROUNDED RECT on purpose: the legacy
503             // reverse bridge reads only those, never a `Border`, so a
504             // legacy-view host still shows which end the run is at.
505             let bw = crate::layout::toggle_border_width().max(1.0);
506             ctx.border(field.rect, field.radii, [0.0; 4], colors::well_frame_color(self.hovered, self.focused), bw);
507             let (face, face_r) = self.face(rect);
508             let lit = (0.16 * (crate::layout::bevel_depth() / 0.15)).clamp(0.0, 0.5);
509             ctx.rounded_rect(face, face_r, (true, true, true, true), [1.0, 1.0, 1.0, lit]);
510         }
511 
512         if let Some(ref label) = self.label {
513             let (font_fam, font_size) = crate::layout::control_label_font_parsed();
514             let est_w = crate::widget::display::measure_text_width(label, &font_fam, font_size);
515             let tx = match self.justify {
516                 Justification::Left => x + crate::layout::CONTROL_TEXT_INSET,
517                 Justification::Right => x + w - est_w - crate::layout::CONTROL_TEXT_INSET,
518                 Justification::Center => x + (w - est_w) / 2.0,
519             };
520             // The label never moves with the state. When the run covers it
521             // the text shows through: the run carries no face of its own, and
522             // the glyphs land in the engine's later text pass either way.
523             //
524             // Focus is the field's own lit rim — the ring every other field
525             // wears, which the rocker's partial carves could not — so the
526             // label stays the label.
527             ctx.text_with(
528                 label.clone(),
529                 tx,
530                 crate::layout::align_text_y(y, h, font_size, 0.0),
531                 font_size,
532                 colors::control_label_color_for_state(self.hovered, false),
533                 None,
534                 Some([x, y, x + w, y + h]),
535             );
536         }
537     }
538 }
539 
540 impl Input for Toggle {
541     fn focus_role(&self) -> crate::widget::FocusRole {
542         crate::widget::FocusRole::Plate
543     }
544     fn on_event(&mut self, event: &Event, _ectx: &mut EventCtx) -> bool {
545         match event {
546             Event::MouseButton { button: MouseButton::Left, state: ElementState::Pressed, .. } => {
547                 self.toggled = !self.toggled;
548                 self.just_toggled = true;
549                 true
550             }
551             Event::MouseEnter => {
552                 self.hovered = true;
553                 false
554             }
555             Event::MouseLeave => {
556                 self.hovered = false;
557                 false
558             }
559             Event::FocusIn => {
560                 self.focused = true;
561                 false
562             }
563             Event::FocusOut => {
564                 self.focused = false;
565                 false
566             }
567             Event::KeyInput(key_event) => {
568                 // A focused plate is pressed by Enter / Space, as a Button is.
569                 if !self.focused || key_event.state != ElementState::Pressed {
570                     return false;
571                 }
572                 match key_event.logical_key {
573                     crate::widget::Key::Named(crate::widget::NamedKey::Enter)
574                     | crate::widget::Key::Named(crate::widget::NamedKey::Space) => {
575                         self.toggled = !self.toggled;
576                         self.just_toggled = true;
577                         true
578                     }
579                     _ => false,
580                 }
581             }
582             _ => false,
583         }
584     }
585 
586     /// The glide: the plate's position in its well chases the state after a
587     /// click (~90ms exponential settle). Programmatic syncs snap instead — see
588     /// `set_toggled` / `set_value_string` — so only user interaction animates.
589     fn tick(&mut self, dt: f32, _rect: Rect) -> bool {
590         let target = if self.toggled { 1.0 } else { 0.0 };
591         let d = target - self.slide_t;
592         if d.abs() < 0.001 {
593             return false;
594         }
595         if !crate::motion::enabled() {
596             self.slide_t = target;
597             return true;
598         }
599         self.slide_t += d * (1.0 - (-dt * 22.0).exp());
600         if (target - self.slide_t).abs() < 0.005 {
601             self.slide_t = target;
602         }
603         true
604     }
605 
606     fn take_click(&mut self) -> bool {
607         std::mem::take(&mut self.just_toggled)
608     }
609 
610     fn take_change(&mut self) -> bool {
611         std::mem::take(&mut self.just_toggled)
612     }
613 
614     fn value_string(&self) -> Option<String> {
615         Some(self.toggled.to_string())
616     }
617 
618     fn set_value_string(&mut self, val: &str) -> bool {
619         let Some(new_toggled) = parse_bool(val) else { return false };
620         if self.toggled != new_toggled {
621             self.toggled = new_toggled;
622             self.slide_t = if new_toggled { 1.0 } else { 0.0 };
623             self.just_toggled = true;
624             true
625         } else {
626             false
627         }
628     }
629 }
630 
631 #[cfg(test)]
632 mod tests {
633     use super::*;
634     use crate::widget::{WidgetHost, UiContext};
635 
636     fn click_at(x: f32, y: f32) -> Event {
637         Event::MouseButton {
638             button: MouseButton::Left,
639             state: ElementState::Pressed,
640             x,
641             y,
642             local_x: x,
643             local_y: y,
644         }
645     }
646 
647     #[test]
648     fn checkbox_click_toggles_and_polls_like_legacy() {
649         let mut ctx = UiContext::new();
650         let cb = ctx.insert(Checkbox::new());
651         let id = cb.id();
652         WidgetHost::set_rect(&mut ctx[cb], 0.0, 0.0, 20.0, 20.0);
653 
654         assert!(ctx.propagate_event(&click_at(10.0, 10.0), id), "in-rect click consumed");
655         assert!(ctx[cb].checked(), "click checked it");
656         assert!(ctx[cb].take_click(), "take_click reads once");
657         assert!(!ctx[cb].take_click(), "...then clears");
658         assert!(ctx[cb].take_change());
659 
660         assert!(!ctx.propagate_event(&click_at(100.0, 100.0), id), "miss is not consumed");
661         assert!(ctx[cb].checked(), "miss does not toggle");
662     }
663 
664     #[test]
665     fn checkbox_value_string_round_trip() {
666         let mut cb = Checkbox::new();
667         assert_eq!(cb.get_value_string(), Some("false".to_string()));
668         assert!(cb.set_value_string("on"));
669         assert!(cb.checked());
670         assert_eq!(cb.value(), 1);
671         assert!(!cb.set_value_string("on"), "unchanged value reports false");
672         assert!(!cb.set_value_string("junk"), "unparsable reports false");
673         assert!(cb.take_change(), "set_value_string marked the change");
674     }
675 
676     /// A labelled checkbox is a square field at the left of its label: an
677     /// empty well, or checked a run standing in its middle.
678     #[test]
679     fn a_checkbox_is_a_well_with_a_square_plate_in_it_or_not() {
680         use crate::scene::paint::Prim;
681         let ctx = UiContext::new();
682         let mut cb = Checkbox::new().with_label("Enable");
683         WidgetHost::set_rect(&mut cb, 0.0, 0.0, 200.0, 24.0);
684         let rect = Rect { x: 0.0, y: 0.0, width: 200.0, height: 24.0 };
685         let prims = |cb: &Adapted<Checkbox>| -> Vec<Prim> {
686             let mut pc = crate::scene::paint::PaintCtx::new();
687             crate::widget::Paint::paint(cb.inner(), rect, &mut pc);
688             pc.finish().items.into_iter().map(|i| i.prim).filter(|p| !matches!(p, Prim::Text { .. })).collect()
689         };
690 
691         // A square at the left, as tall as the control, carved inside it.
692         let f = cb.inner().field(rect);
693         let side = 24.0f32.min(crate::layout::toggle_height());
694         assert!((f.rect.width - f.rect.height).abs() < 1e-4, "square");
695         assert!(f.rect.x >= 0.0 && f.rect.x + f.rect.width <= side, "at the left, inside its square");
696         assert!(f.has_well() && f.run_span().is_none(), "all well");
697 
698         // Checked, a square plate in the middle, the well all round it: the
699         // box keeps its shape — a ticked box must not read as narrower.
700         cb.inner_mut().set_checked(true);
701         assert_eq!(cb.inner().field(rect), f, "the well is the same either way");
702         let p = Checkbox::box_plate(&f);
703         assert!((p.rect.width - p.rect.height).abs() < 1e-4, "the plate is square");
704         assert!((p.rect.width - f.rect.width * PLATE_SHARE).abs() < 1e-3);
705         let (pcx, pcy) = (p.rect.x + p.rect.width * 0.5, p.rect.y + p.rect.height * 0.5);
706         let (fcx, fcy) = (f.rect.x + f.rect.width * 0.5, f.rect.y + f.rect.height * 0.5);
707         assert!((pcx - fcx).abs() < 1e-3 && (pcy - fcy).abs() < 1e-3, "centred");
708         assert!(!p.has_well(), "the plate is all run");
709         assert!(
710             !crate::widget::shown_prims(&cb).iter().any(|p| matches!(p, crate::scene::paint::Prim::Circle { .. })),
711             "no mark"
712         );
713 
714         if crate::layout::control_relief() {
715             cb.inner_mut().set_checked(false);
716             assert!(matches!(prims(&cb)[..], [Prim::Recess { .. }]), "a well paints as the recess it groups as");
717             cb.inner_mut().set_checked(true);
718             assert!(matches!(prims(&cb)[..], [Prim::Recess { .. }, Prim::Field { .. }]), "the plate paints as a field in it");
719             cb.focused = true;
720             assert!(matches!(prims(&cb)[..], [Prim::Recess { tint: Some(_), .. }, _]), "focus lights the rim");
721         }
722 
723         // The label follows the box.
724         let labels = cb.own_text_labels();
725         assert_eq!(labels.len(), 1);
726         assert_eq!(labels[0].text, "Enable");
727         assert!((labels[0].x - (side + 8.0)).abs() < 1e-3, "{} vs {}", labels[0].x, side + 8.0);
728 
729         // Inline label => no set_rect inflation.
730         assert_eq!(WidgetHost::rect(&cb), (0.0, 0.0, 200.0, 24.0));
731         let _ = &ctx;
732     }
733 
734     /// An inline check — a list row's, a markdown task item's — is the
735     /// widget's box at the size it is given: the same field, its corner the
736     /// toggle's in proportion, so a 14px box is a rounded square and not a
737     /// disc, and a box a toggle tall has the toggle's corner exactly.
738     #[test]
739     fn an_inline_check_is_the_widgets_box() {
740         use crate::scene::paint::Prim;
741         let h = Checkbox::INLINE_HALF;
742         let square = Rect { x: 50.0 - h, y: 20.0 - h, width: 2.0 * h, height: 2.0 * h };
743         for checked in [false, true] {
744             let mut pc = crate::scene::paint::PaintCtx::new();
745             Checkbox::paint_inline(&mut pc, 50.0, 20.0, h, checked);
746             let prims: Vec<Prim> = pc.finish().items.into_iter().map(|i| i.prim).collect();
747             let want = Checkbox::box_field(square);
748             if crate::layout::control_relief() {
749                 let mut expect = crate::scene::paint::PaintCtx::new();
750                 expect.field(&want);
751                 if checked {
752                     expect.field(&Checkbox::box_plate(&want));
753                 }
754                 let expect: Vec<Prim> = expect.finish().items.into_iter().map(|i| i.prim).collect();
755                 assert_eq!(format!("{prims:?}"), format!("{expect:?}"), "the box_field and box_plate, painted");
756             }
757             assert!(want.radii.0 < want.rect.width * 0.5 - 0.5, "a rounded square, not a disc: {:?}", want.radii);
758         }
759         // A box a toggle tall has the toggle's corner, as the widget's always had.
760         let th = crate::layout::toggle_height();
761         let big = Checkbox::box_field(Rect { x: 0.0, y: 0.0, width: th, height: th });
762         let r = crate::layout::toggle_corner_radius().min(th * 0.5);
763         let (_, radii) = crate::layout::carve_inside(Rect { x: 0.0, y: 0.0, width: th, height: th }, (r, r, r, r), big.depth);
764         assert_eq!(big.radii, radii);
765     }
766 
767     #[test]
768     fn toggle_click_glides_the_run_across_its_field() {
769         let mut ctx = UiContext::new();
770         let t = ctx.insert(Toggle::new());
771         let id = t.id();
772         WidgetHost::set_rect(&mut ctx[t], 0.0, 0.0, 60.0, 30.0);
773 
774         let rect = Rect { x: 0.0, y: 0.0, width: 60.0, height: 30.0 };
775         let painted = |t: &Adapted<Toggle>| {
776             let mut pc = crate::scene::paint::PaintCtx::new();
777             crate::widget::Paint::paint(t.inner(), rect, &mut pc);
778             pc.finish().items.into_iter().map(|i| format!("{:?}", i.prim)).collect::<Vec<_>>()
779         };
780         let before = painted(&ctx[t]);
781         let plate_x = |t: &Adapted<Toggle>| t.inner().field(rect).run_span().unwrap().0;
782         let left = plate_x(&ctx[t]);
783 
784         assert!(ctx.propagate_event(&click_at(30.0, 15.0), id), "toggle consumed the click");
785         assert!(ctx[t].toggled());
786         assert!(ctx[t].take_click());
787 
788         // A click sets the target; the plate GLIDES there (`tick`), so the
789         // geometry only moves once time passes — the rocker's halves used to
790         // swap on the press itself.
791         assert_eq!(plate_x(&ctx[t]), left, "the click alone does not move the plate");
792         for _ in 0..60 {
793             crate::widget::Input::tick(ctx[t].inner_mut(), 1.0 / 60.0, rect);
794         }
795         assert!(plate_x(&ctx[t]) > left, "the plate glided toward the on end");
796         assert!(painted(&ctx[t]) != before, "toggling changes the emitted geometry");
797 
798         // preferred_height forwards the legacy toggle height.
799         assert_eq!(crate::widget::WidgetHostExt::preferred_height(&ctx[t]), Some(crate::layout::toggle_height()));
800     }
801 
802     /// The toggle is ONE field, the form a text row's picker and a spinbox's
803     /// -/+ run take: off, its run is the left half and the well the right;
804     /// on, the other way round; between, a well either side. The run's face
805     /// stands half a wall inside the run, so the run reaches the well.
806     #[test]
807     fn a_toggle_is_a_field_whose_run_glides() {
808         use crate::scene::paint::{Prim, FIELD_RUN_ONLY};
809         let rect = Rect { x: 10.0, y: 4.0, width: 120.0, height: 24.0 };
810         let mut t = Toggle::new().with_raised(true);
811         let Field { rect: field, radii, depth, .. } = t.inner().field(rect);
812         assert!(field.x >= rect.x && field.y >= rect.y, "the field carves inside the rect");
813         let (fl, fr) = (field.x, field.x + field.width);
814 
815         let fields = |t: &Adapted<Toggle>| -> Vec<Prim> {
816             let mut pc = crate::scene::paint::PaintCtx::new();
817             crate::widget::Paint::paint(t.inner(), rect, &mut pc);
818             pc.finish().items.into_iter().map(|i| i.prim).filter(|p| !matches!(p, Prim::Text { .. })).collect()
819         };
820         let off = fields(&t);
821         assert_eq!(off.len(), 1, "one prim, the field: {off:?}");
822         let Prim::Field { rect: r, radii: rr, depth: d, split, end, tint } = off[0] else { panic!("{off:?}") };
823         assert_eq!((r, rr, d, tint), (field, radii, depth, None));
824         assert!(split <= fl - FIELD_RUN_ONLY + 1.0, "off: the run reaches the field's left end — no well there");
825         assert!((end - (fl + field.width * 0.5)).abs() < 1e-4, "half the field, the well beyond it");
826 
827         t.set_toggled(true); // programmatic syncs snap, so this is the on-end geometry
828         let Prim::Field { split, end, .. } = fields(&t)[0] else { panic!() };
829         assert!((split - (fl + field.width * 0.5)).abs() < 1e-4, "on: the right half");
830         assert!(end >= fr + FIELD_RUN_ONLY - 1.0, "reaching the field's right end");
831 
832         t.slide_t = 0.5;
833         let Prim::Field { split, end, .. } = fields(&t)[0] else { panic!() };
834         assert!(split > fl + 1.0 && end < fr - 1.0, "mid-glide, a well either side of the run");
835         assert!(end < fr, "and the run's end is its own, not the field's");
836 
837         let (face, face_r) = t.inner().face(rect);
838         let (a, b) = t.inner().field(rect).run_span().unwrap();
839         assert!((face.x - (a + depth * 0.5)).abs() < 1e-4 && (face.x + face.width - (b - depth * 0.5)).abs() < 1e-4);
840         assert!((face_r - (radii.0 - depth * 0.5).max(0.0)).abs() < 1e-4, "concentric with the field");
841 
842         // Focus lights the field's rim.
843         t.focused = true;
844         assert!(matches!(fields(&t)[0], Prim::Field { tint: Some(_), .. }), "focus tints the field");
845     }
846 
847     #[test]
848     fn toggle_set_label_via_deref_reaches_paint() {
849         let mut t = Toggle::new();
850         WidgetHost::set_rect(&mut t, 0.0, 0.0, 60.0, 30.0);
851         t.set_label("ON"); // the network.rs pattern: live label updates through Deref
852         let labels = t.own_text_labels();
853         assert_eq!(labels.len(), 1);
854         assert_eq!(labels[0].text, "ON");
855     }
856 }