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 }