makeover-immediate 0.50.0

The immediate-mode renderer for makeover-layout. Immediate mode is the constraint that matters, not the library: no cascade, no retained tree, one stroke per widget. Backed by egui.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
//! Tests for [`super`].

use super::*;
use crate::widget::{WidgetStyle, chart};
use makeover_layout::{Bar, Chart};

fn palette(well: Color32) -> Palette {
    Palette {
        page: Color32::from_rgb(1, 1, 1),
        raised: Color32::from_rgb(2, 2, 2),
        overlay: Color32::from_rgb(3, 3, 3),
        well,
        sunken: Color32::from_rgb(4, 4, 4),
        bevel_light: Color32::WHITE,
        bevel_dark: Color32::BLACK,
        elevation: Color32::from_black_alpha(46),
        content: Color32::from_rgb(5, 5, 5),
        content_secondary: Color32::from_rgb(55, 55, 55),
        content_muted: Color32::from_rgb(6, 6, 6),
        action: Color32::from_rgb(7, 7, 7),
        danger: Color32::from_rgb(8, 8, 8),
        success: Color32::from_rgb(9, 9, 9),
        warning: Color32::from_rgb(10, 10, 10),
        info: Color32::from_rgb(11, 11, 11),
        border: Color32::from_rgb(200, 200, 200),
        info_surface: Color32::from_rgb(201, 201, 201),
        success_surface: Color32::from_rgb(202, 202, 202),
        warning_surface: Color32::from_rgb(203, 203, 203),
        danger_surface: Color32::from_rgb(204, 204, 204),
        row_stripe: Color32::from_rgb(205, 205, 205),
        row_hover: Color32::from_rgb(206, 206, 206),
        row_rule: Color32::from_rgb(207, 207, 207),
        row_selected: Color32::from_rgb(208, 208, 208),
    }
}

/// The cast is egui's own shadow type carrying the theme's tone, which is
/// the whole of what this crate had to decide for it: unlike a bevel, egui
/// already knows how to paint one.
#[test]
fn a_unit_is_drawn_only_where_the_kind_is_a_quantity() {
    // egui-drawing has no harness here, so what is tested is the decision
    // that precedes it: which fields have a unit to draw at all. The kind
    // half comes from the description rather than from a `matches!` in this
    // crate, which is the drift `FieldKind::measurable` exists to stop.
    let ranged = makeover_layout::Field {
        unit: Some("s"),
        ..makeover_layout::Field::range("attack", "Attack", "0", "5")
    };
    assert_eq!(unit_of(&ranged), Some("s"));

    let typed = makeover_layout::Field {
        unit: Some("ms"),
        ..makeover_layout::Field::new(makeover_layout::FieldKind::Number, "fade", "Fade")
    };
    assert_eq!(unit_of(&typed), Some("ms"));

    let worded = makeover_layout::Field {
        unit: Some("s"),
        ..makeover_layout::Field::new(makeover_layout::FieldKind::Text, "name", "Name")
    };
    assert_eq!(unit_of(&worded), None);

    let bare = makeover_layout::Field::range("attack", "Attack", "0", "5");
    assert_eq!(unit_of(&bare), None);
}

#[test]
fn the_cast_hands_egui_the_themes_tone() {
    let p = palette(Color32::from_rgb(9, 9, 9));
    let cast = p.cast();
    assert_eq!(cast.color, p.elevation);
    assert!(cast.blur > 0, "a cast shadow is soft");
    assert_eq!(cast.offset, [0, 2], "it falls downward and only a little");
}

#[test]
fn a_well_resolves_to_its_own_token() {
    // No substitution left. The page-filled well was a stand-in for a
    // token that did not exist yet; it exists now.
    let w = Color32::from_rgb(9, 9, 9);
    let p = palette(w);
    assert_eq!(p.fill(Fill::Well), Some(w));
    assert_ne!(p.fill(Fill::Well), Some(p.page));
}

#[test]
fn every_intent_is_a_plain_lookup() {
    let p = palette(Color32::from_rgb(9, 9, 9));
    assert_eq!(p.fill(Fill::Page), Some(p.page));
    assert_eq!(p.fill(Fill::Raised), Some(p.raised));
    assert_eq!(p.fill(Fill::Overlay), Some(p.overlay));
}

/// Sunken is its own colour, not the well's and not the page's. The two
/// are authored in opposite directions and an earlier cut of the
/// description conflated them.
#[test]
fn sunken_is_neither_the_well_nor_the_page() {
    let p = palette(Color32::from_rgb(9, 9, 9));
    assert_eq!(p.fill(Fill::Sunken), Some(p.sunken));
    assert_ne!(p.fill(Fill::Sunken), p.fill(Fill::Well));
    assert_ne!(p.fill(Fill::Sunken), p.fill(Fill::Page));
}

#[test]
fn a_raised_region_never_resolves_to_the_well_fill() {
    // The cross-app bug, asserted at the renderer boundary this time.
    let p = palette(Color32::from_rgb(9, 9, 9));
    let raised = Depth::Raised.fill().and_then(|f| p.fill(f));
    let well = Depth::Well.fill().and_then(|f| p.fill(f));
    assert_eq!(raised, Some(p.raised));
    assert_ne!(raised, well);
}

#[test]
fn an_overlay_is_cast_onto_the_page_and_takes_no_edge() {
    // makeover-layout 0.14.0 is what made this reachable. The answer was
    // already here at 0.10.0 and the question could not be asked.
    let p = palette(Color32::from_rgb(9, 9, 9));
    assert_eq!(
        Depth::Overlay.fill().and_then(|f| p.fill(f)),
        Some(p.overlay)
    );
    assert_eq!(Depth::Overlay.bevel(), None);
    // The shadow `frame` reaches for is the theme's tone rather than
    // egui's default, which is the whole reason `cast` exists.
    assert_eq!(p.cast().color, p.elevation);
}

#[test]
fn the_lit_edge_swaps_when_a_card_is_pressed() {
    let p = palette(Color32::from_rgb(9, 9, 9));
    let (tl, _) = Depth::Raised.bevel().unwrap().edges();
    let (ptl, _) = Depth::Raised.pressed().bevel().unwrap().edges();
    assert_eq!(p.edge(tl), p.bevel_light);
    assert_eq!(p.edge(ptl), p.bevel_dark);
}

#[test]
fn flat_asks_for_neither_fill_nor_edge() {
    assert!(Depth::Flat.fill().is_none());
    assert!(Depth::Flat.bevel().is_none());
}

#[test]
fn a_select_keeps_a_value_none_of_its_options_carries() {
    // The save-the-wrong-thing bug, asserted at the second renderer so it
    // is not re-found there. goingson's own numbers.
    let options = [
        Choice::plain("1"),
        Choice::plain("3"),
        Choice::plain("7"),
        Choice::plain("14"),
    ];
    assert_eq!(shown_label(&options, "10"), "10");
    // And a value that does match reads as its label, not as itself.
    let spelled = [Choice::new("7", "One week")];
    assert_eq!(shown_label(&spelled, "7"), "One week");
}

#[test]
fn an_unanswered_chooser_reads_its_ghost_text_and_reads_it_muted() {
    let p = palette(Color32::from_rgb(9, 9, 9));
    let options = [Choice::new("sp404", "SP-404")];
    let field = Field {
        placeholder: Some("Select device..."),
        ..Field::select("device", "Conform for device", &options)
    };

    assert_eq!(
        chosen_text(&field, "", &p),
        ("Select device...", p.content_muted),
        "ghost text is not an answer, so it takes the tone the typed kinds' ghost text does"
    );

    // Answered, and it is the label that reads rather than the value.
    assert_eq!(chosen_text(&field, "sp404", &p), ("SP-404", p.content));
}

#[test]
fn a_wrong_answer_is_not_an_absent_one() {
    // The retention-10 bug and the ghost text meet here: a value no option
    // carries still reads as itself, because the field IS answered and the
    // answer is wrong. Only the empty value is unanswered.
    let p = palette(Color32::from_rgb(9, 9, 9));
    let options = [Choice::plain("1"), Choice::plain("7")];
    let field = Field {
        placeholder: Some("Pick one"),
        ..Field::select("retention", "Keep backups for", &options)
    };

    assert_eq!(chosen_text(&field, "10", &p), ("10", p.content));
}

#[test]
fn a_chooser_with_no_ghost_text_is_unchanged() {
    // The whole change is opt-in from the description. A field that says
    // nothing about its empty state still shows an empty box.
    let p = palette(Color32::from_rgb(9, 9, 9));
    let options = [Choice::plain("1")];
    let field = Field::select("retention", "Keep backups for", &options);
    assert_eq!(chosen_text(&field, "", &p), ("", p.content));
}

#[test]
fn a_range_is_slid_and_a_number_is_typed_into() {
    // The distinction the kind was added for, at the renderer that has to
    // act on it. A well with a figure in it is not a quiet slider.
    assert_eq!(control_shape(FieldKind::Range), Control::Slid);
    assert_eq!(control_shape(FieldKind::Number), Control::Typed);
}

#[test]
fn a_range_missing_an_end_falls_back_to_a_well() {
    // egui's `Slider` demands both ends, so inventing one would be this
    // renderer picking bounds the app never stated and the user then
    // dragging against them. A typed number takes every answer the slider
    // would.
    let whole = Field::range("review", "Review above", "0", "1");
    assert_eq!(shape_of(&whole), Control::Slid);

    let half = Field {
        max: Some("1"),
        ..Field::new(FieldKind::Range, "review", "Review above")
    };
    assert_eq!(shape_of(&half), Control::Typed);
    assert_eq!(extent(&half), None);

    // A bound this host cannot read is the same outcome by a different
    // route: the description carries bounds as text because the bound of a
    // date is a date.
    let dated = Field::range("when", "When", "2026-08-01", "2026-08-31");
    assert_eq!(extent(&dated), None);
}

#[test]
fn an_interval_is_its_own_shape_and_not_two_numbers() {
    // The distinction the kind was added for, at the renderer that has to
    // arrange it: two wells stacked are two questions on screen, whatever
    // the description says.
    assert_eq!(control_shape(FieldKind::Interval), Control::Spanned);
    assert_eq!(shape_of(&Field::interval("a", "b", "A")), Control::Spanned);

    // Unlike a range, it owes no extent: its bounds are a rule on each end
    // rather than the control, so a missing one is an open end.
    let axis = Field {
        min: Some("0"),
        max: Some("300"),
        ..Field::interval("bpm_min", "bpm_max", "BPM")
    };
    assert_eq!(shape_of(&axis), Control::Spanned);
}

#[test]
fn an_empty_end_reads_as_the_bound_it_stands_for() {
    // Which is what the shipped control did before it was described: an
    // unset minimum sits on the low edge and stores no filter. `DragValue`
    // has no empty state, and a text box instead would cost the app a
    // control on the way into being described.
    let axis = Axis {
        extent: Some(0.0..=300.0),
        step: Some("1"),
        unit: Some("BPM"),
    };
    assert!((axis.edge(Bound::Low) - 0.0).abs() < f64::EPSILON);
    assert!((axis.edge(Bound::High) - 300.0).abs() < f64::EPSILON);

    // With no extent there is no edge to sit on, and a drag box has to
    // start somewhere. The one number this renderer invents, invented where
    // the description declined to say anything.
    let open = Axis {
        extent: None,
        step: None,
        unit: None,
    };
    assert!((open.edge(Bound::Low) - 0.0).abs() < f64::EPSILON);
    assert!((open.edge(Bound::High) - 0.0).abs() < f64::EPSILON);
}

#[test]
fn the_step_decides_how_a_dragged_value_is_written_back() {
    // Without it a 0-to-1 threshold writes back whatever float the drag
    // landed on, which is the host's granularity and is what the
    // description says an absent step means.
    assert_eq!(decimals(None), None);
    assert_eq!(decimals(Some("1")), Some(0));
    assert_eq!(decimals(Some("0.01")), Some(2));
    // Trailing zeros are not precision: 0.10 is a one-decimal question.
    assert_eq!(decimals(Some("0.10")), Some(1));
}

#[test]
fn an_unavailable_option_is_drawn_muted_rather_than_dropped() {
    // The tone rule, at the one place it is a claim rather than a
    // preference: this option genuinely will not answer, so muted is the
    // truth. The available ones beside it keep the secondary intent.
    let p = palette(Color32::from_rgb(9, 9, 9));
    let options = [
        Choice::new("chromatic", "Chromatic"),
        Choice::new("multi", "Multi-sample").unless("Drop a second sample."),
    ];
    assert!(options[0].available());
    assert!(!options[1].available());
    assert_eq!(
        option_color("chromatic", options[0].value, &p),
        p.content,
        "the chosen option is the emphasised thing"
    );
    assert_eq!(
        option_color("chromatic", options[1].value, &p),
        p.content_secondary,
        "and `option_color` never mutes: the unavailable path is what does"
    );
}

#[test]
fn a_closed_chooser_carries_both_of_an_options_extra_lines_in_its_row() {
    // makeover-layout 0.39.0. The radio group has room for a second line
    // and this control does not, so the two draw the same description
    // differently and neither is a preference.
    let plain = Choice::new("24", "Small Files");
    assert_eq!(combo_row(&plain), "Small Files");

    let detailed = plain.detailing("$24/mo. Fits audio, plugins, binaries.");
    assert_eq!(
        combo_row(&detailed),
        "Small Files  $24/mo. Fits audio, plugins, binaries."
    );

    // What it is, then why it cannot be picked. An option carrying both has
    // said two things, and the order is the one the group draws.
    let both = detailed.unless("Sold out.");
    assert_eq!(
        combo_row(&both),
        "Small Files  $24/mo. Fits audio, plugins, binaries.  Sold out."
    );
    assert_eq!(
        combo_row(&plain.unless("Sold out.")),
        "Small Files  Sold out.",
        "the reason still reads the way it did before the detail existed"
    );
}

#[test]
fn only_a_required_field_is_marked() {
    let style = FieldStyle::default();
    let plain = Field::new(FieldKind::Text, "title", "Title");
    assert_eq!(label_text(&plain, &style), "Title");

    let required = Field {
        required: true,
        ..plain
    };
    assert_eq!(label_text(&required, &style), "Title *");

    // The marker is copy and the app owns it, which is why it is a knob.
    let house = FieldStyle {
        required_marker: "(required)",
        ..style
    };
    assert_eq!(label_text(&required, &house), "Title (required)");
}

#[test]
fn a_select_and_a_checkbox_are_pressed_and_everything_else_is_typed_into() {
    // What decides whether the control gets a well. A well is for what the
    // user looks into, and only one of these is.
    assert_eq!(control_shape(FieldKind::Select), Control::Chosen);
    // Not the wildcard. A theme picker falling to `Control::Typed` would
    // draw a text box over a resolved list, which is worse than the select
    // every app had before the member existed.
    assert_eq!(control_shape(FieldKind::Theme), Control::Themed);
    assert_eq!(control_shape(FieldKind::Radio), Control::Listed);
    assert_eq!(control_shape(FieldKind::Checkbox), Control::Toggled);
    for k in [
        FieldKind::Text,
        FieldKind::Secret,
        FieldKind::Number,
        FieldKind::Email,
        FieldKind::Url,
        FieldKind::Tel,
        FieldKind::Textarea,
        FieldKind::Rich,
    ] {
        assert_eq!(control_shape(k), Control::Typed, "{k:?} is typed into");
    }
    // And the two multi-line ones get a multi-line edit, which is the half
    // `control_shape` alone does not say: both are typed into, and only one
    // of the two edit shapes can hold a markdown document.
    assert!(FieldKind::Rich.multiline());
    assert!(FieldKind::Textarea.multiline());
    assert!(!FieldKind::Text.multiline());
}

#[test]
fn the_two_option_taking_kinds_are_drawn_differently_on_purpose() {
    // The description holds Select and Radio apart, and a renderer that
    // collapsed them would silently answer a question the app did not ask:
    // audiofiles' storage style is irreversible and its alternatives have
    // to be readable without opening anything. Asserting the two shapes
    // differ is asserting that distinction survives the trip.
    assert!(FieldKind::Select.offers_options());
    assert!(FieldKind::Radio.offers_options());
    assert_ne!(
        control_shape(FieldKind::Select),
        control_shape(FieldKind::Radio)
    );
}

#[test]
fn an_unchosen_option_is_secondary_and_never_muted() {
    let p = palette(Color32::from_rgb(4, 4, 4));
    assert_eq!(option_color("wav", "wav", &p), p.content);
    assert_eq!(option_color("wav", "aiff", &p), p.content_secondary);
    // The whole point of the distinction: muted is what Disabled resolves
    // to, so an option wearing it would claim it does not answer a press.
    assert_ne!(option_color("wav", "aiff", &p), p.content_muted);
}

/// What the accessibility tree says a screen drew, as `(role, name)` pairs.
///
/// egui builds this from the same `WidgetInfo` every widget already reports,
/// so it is what a screen reader would be handed rather than a second
/// opinion about it. A name that arrives through `labelled_by` is resolved
/// the way a client resolves it: the relation names another node, and that
/// node's text is the control's accessible name.
fn announced(draw: impl FnMut(&mut egui::Ui)) -> Vec<(egui::accesskit::Role, String)> {
    let ctx = egui::Context::default();
    ctx.enable_accesskit();
    let mut draw = draw;
    let input = || egui::RawInput {
        screen_rect: Some(egui::Rect::from_min_size(
            egui::Pos2::ZERO,
            egui::vec2(800.0, 600.0),
        )),
        ..Default::default()
    };
    // Two passes: egui lays out against the previous frame, so the first
    // sees widgets at the wrong rect.
    let _ = ctx.run_ui(input(), &mut draw);
    let out = ctx.run_ui(input(), &mut draw);

    let update = out
        .platform_output
        .accesskit_update
        .expect("accesskit is on, so a tree was built");
    let by_id: std::collections::HashMap<_, _> = update.nodes.iter().cloned().collect();
    update
        .nodes
        .iter()
        .map(|(_, node)| {
            let named = node.label().map(str::to_owned).or_else(|| {
                node.labelled_by()
                    .iter()
                    .find_map(|id| by_id.get(id))
                    .and_then(|by| by.label().or_else(|| by.value()).map(str::to_owned))
            });
            (node.role(), named.unwrap_or_default())
        })
        .collect()
}

#[test]
fn a_fields_label_names_its_control_rather_than_sitting_beside_it() {
    let f = Field::new(FieldKind::Text, "name", "Vault name");
    let p = palette(Color32::from_rgb(9, 9, 9));
    let drawn = announced(|ui| {
        let mut text = String::new();
        field(
            ui,
            &f,
            Filling::Text(&mut text),
            None,
            &p,
            &FieldStyle::default(),
        );
    });

    let box_ = drawn
        .iter()
        .find(|(role, _)| *role == egui::accesskit::Role::TextInput)
        .expect("a text field draws a text input");
    assert_eq!(
        box_.1, "Vault name",
        "the box is announced by the question rather than unnamed: {drawn:?}"
    );
}

#[test]
fn a_prefilled_box_is_still_announced_by_its_question() {
    // The sharper half. With nothing attached, a box carrying a value is
    // announced as that value, so a rename field read out the name it was
    // seeded with and never said what was being asked.
    let f = Field::new(FieldKind::Text, "name", "New name");
    let p = palette(Color32::from_rgb(9, 9, 9));
    let drawn = announced(|ui| {
        let mut text = String::from("Drums");
        field(
            ui,
            &f,
            Filling::Text(&mut text),
            None,
            &p,
            &FieldStyle::default(),
        );
    });

    let box_ = drawn
        .iter()
        .find(|(role, _)| *role == egui::accesskit::Role::TextInput)
        .expect("a text field draws a text input");
    assert_eq!(box_.1, "New name", "{drawn:?}");
}

#[test]
fn both_ends_of_an_interval_are_named_by_the_one_question() {
    // A union response carries the first id, so labelling the pair outside
    // the arm named the lower box and left the upper one announced as
    // whatever number was in it.
    let f = Field::new(FieldKind::Interval, "bpm", "BPM Range");
    let p = palette(Color32::from_rgb(9, 9, 9));
    let drawn = announced(|ui| {
        let mut lower = String::from("90");
        let mut upper = String::from("300");
        field(
            ui,
            &f,
            Filling::Between {
                lower: &mut lower,
                upper: &mut upper,
            },
            None,
            &p,
            &FieldStyle::default(),
        );
    });

    // A `SpinButton`: an interval's ends are drag values, not text boxes.
    let ends: Vec<_> = drawn
        .iter()
        .filter(|(role, _)| *role == egui::accesskit::Role::SpinButton)
        .collect();
    assert_eq!(ends.len(), 2, "an interval draws two boxes: {drawn:?}");
    for end in ends {
        assert_eq!(end.1, "BPM Range", "{drawn:?}");
    }
}

#[test]
fn a_checkbox_keeps_naming_itself() {
    // `FieldKind::labels_itself` routes past the label, and egui names a
    // checkbox from its own text, so there is nothing to attach and
    // attaching one would say the name twice.
    let f = Field::new(FieldKind::Checkbox, "loop", "Loop playback");
    let p = palette(Color32::from_rgb(9, 9, 9));
    let drawn = announced(|ui| {
        let mut ticked = false;
        field(
            ui,
            &f,
            Filling::On(&mut ticked),
            None,
            &p,
            &FieldStyle::default(),
        );
    });

    assert!(
        drawn
            .iter()
            .any(|(role, name)| *role == egui::accesskit::Role::CheckBox
                && name == "Loop playback"),
        "{drawn:?}"
    );
}

#[test]
fn a_hidden_field_draws_nothing_and_answers_nothing() {
    // Where the two renderers legitimately part: a webview still emits an
    // input because the form submits, and there is no form here.
    let f = Field::new(FieldKind::Hidden, "id", "Id");
    let p = palette(Color32::from_rgb(9, 9, 9));
    egui::__run_test_ui(|ui| {
        let drawn = field(ui, &f, Filling::Absent, None, &p, &FieldStyle::default());
        assert!(drawn.is_none());
    });
}

#[test]
fn a_disabled_field_stops_answering_and_an_unstated_one_does_not() {
    let f = Field::new(FieldKind::Text, "title", "Title");
    let p = palette(Color32::from_rgb(9, 9, 9));
    let style = FieldStyle::default();
    egui::__run_test_ui(|ui| {
        let mut text = String::from("x");
        let disabled = field(
            ui,
            &f,
            Filling::Text(&mut text),
            Some(State::Disabled),
            &p,
            &style,
        )
        .unwrap();
        assert!(!disabled.enabled());

        // Stating no state is the ordinary case and answers. Focus used to
        // be the counter-example here; it is egui's now and a description
        // cannot state it at all.
        let mut text = String::from("x");
        let plain = field(ui, &f, Filling::Text(&mut text), None, &p, &style).unwrap();
        assert!(plain.enabled(), "an unstated field still answers");
    });
}

#[test]
fn a_field_described_one_way_and_filled_another_is_drawn_inert() {
    // No panic and no write-through. A checkbox handed a string cannot be
    // filled, so it is drawn off and left alone.
    let f = Field::new(FieldKind::Checkbox, "done", "Done");
    let p = palette(Color32::from_rgb(9, 9, 9));
    let mut text = String::from("untouched");
    egui::__run_test_ui(|ui| {
        let drawn = field(
            ui,
            &f,
            Filling::Text(&mut text),
            None,
            &p,
            &FieldStyle::default(),
        );
        assert!(drawn.is_some());
    });
    assert_eq!(text, "untouched");
}

#[test]
fn the_disclosure_belongs_to_the_form_and_not_to_the_field() {
    let fields = [
        Field::new(FieldKind::Text, "title", "Title"),
        Field {
            extended: true,
            ..Field::new(FieldKind::Text, "notes", "Notes")
        },
    ];
    let style = FieldStyle::default();

    let mut closed = Vec::new();
    egui::__run_test_ui(|ui| {
        group(ui, &fields, false, &style, |_, f| closed.push(f.name));
    });
    assert_eq!(closed, ["title"]);

    let mut open = Vec::new();
    egui::__run_test_ui(|ui| {
        group(ui, &fields, true, &style, |_, f| open.push(f.name));
    });
    assert_eq!(open, ["title", "notes"]);
}

#[test]
fn the_default_frame_is_square_and_one_point() {
    let d = FrameStyle::default();
    assert_eq!(d.radius, CornerRadius::ZERO);
    assert_eq!(d.margin, Margin::ZERO);
    assert!((d.stroke - 1.0).abs() < f32::EPSILON);
}

#[test]
fn a_theme_picker_reads_its_chosen_theme_by_name() {
    const THEMES: &[makeover_layout::ThemeChoice<'_>] = &[makeover_layout::ThemeChoice::new(
        "carbonfox",
        "Carbonfox",
        ThemeVariant::Dark,
        makeover_layout::Contrast::High,
    )];
    let field =
        Field::theme("theme", "Theme", THEMES).following(Choice::new("system", "Follow System"));

    assert_eq!(themed_text(&field, "carbonfox"), "Carbonfox");
    assert_eq!(themed_text(&field, "system"), "Follow System");
    // A stored id whose theme has been deleted reads as itself rather than
    // as an empty box, which is the honest report on the config as it
    // stands.
    assert_eq!(themed_text(&field, "gone"), "gone");
}

/// A chart paints one column per bar, each reaching its share of the figure's
/// height, and a bar of nothing still occupies the axis rather than vanishing.
///
/// Asserted through the painter's own shapes rather than through a screenshot:
/// what matters is that the heights are in proportion to the description, which
/// is the one thing this renderer could get wrong on its own.
#[test]
fn the_columns_reach_their_share_of_the_height() {
    let p = palette(Color32::from_rgb(9, 9, 9));
    let style = WidgetStyle::default();
    let chart_of = Chart::new(20);
    let bars = [
        Bar::at("Mar 3").of(20),
        Bar::at("Mar 4").of(10),
        Bar::at("Mar 5").of(0),
    ];
    egui::__run_test_ui(|ui| {
        chart(ui, &chart_of, &bars, &p, &style);
    });

    // The arithmetic the painting is driven by, checked where it is decided.
    // `fraction` is what both this renderer and `makeover-tui` divide with, so
    // pinning it here pins the drawing in both.
    assert!((bars[0].fraction(&chart_of) - 1.0).abs() < f32::EPSILON);
    assert!((bars[1].fraction(&chart_of) - 0.5).abs() < f32::EPSILON);
    assert!(bars[2].fraction(&chart_of).abs() < f32::EPSILON);
}

/// An axis of zero divides by nothing and paints nothing, rather than panicking
/// or filling every bar.
#[test]
fn an_empty_axis_paints_flat_bars() {
    let p = palette(Color32::from_rgb(9, 9, 9));
    let empty = Chart::new(0);
    let bars = [Bar::at("Mar 3").of(5)];
    egui::__run_test_ui(|ui| {
        chart(ui, &empty, &bars, &p, &WidgetStyle::default());
    });
    assert!(bars[0].fraction(&empty).abs() < f32::EPSILON);
}

#[test]
fn a_badge_fills_with_its_tone_surface_and_edges_in_its_tone() {
    // The webview's badge: a neutral one is raised inside the border, and a
    // status one is its tone surface inside its tone. The tone is never the
    // ink, so it is never asked for here.
    let p = palette(Color32::from_rgb(4, 4, 4));
    assert_eq!(p.tone_surface(Tone::Neutral), p.raised);
    assert_eq!(p.tone_edge(Tone::Neutral), p.border);
    for (tone, surface, edge) in [
        (Tone::Info, p.info_surface, p.info),
        (Tone::Success, p.success_surface, p.success),
        (Tone::Warning, p.warning_surface, p.warning),
        (Tone::Danger, p.danger_surface, p.danger),
    ] {
        assert_eq!(p.tone_surface(tone), surface, "{tone:?}");
        assert_eq!(p.tone_edge(tone), edge, "{tone:?}");
    }
}