monospace 0.1.0

The interactive full-screen terminal application: the chrome around a diagram being edited
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
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
//! The bar across the top, and the drop-down it opens.
//!
//! **The bar is drawn in two pieces with the body between them, so that the drop-down covers the
//! body instead of pushing it down.** The menu widget splits itself into a line and a pop-up, and
//! the two are drawn at different moments: the line with the rest of the frame, the pop-up after
//! the region it covers.
//!
//! **The bar is one of the two leaves that hold the keyboard.** Which one holds it is the
//! application's answer — kept in `AppState` — and the bar is handed it: the widget draws its
//! selected label differently only while it has the focus, and it answers a key only while it has
//! it. **The bar asks for the keyboard and gives it back**: `F10` asks for it from either leaf,
//! `Alt` plus a navigation character brings it with the press, and `Esc` with nothing left to close
//! gives it back. **A leaf is not a meaning** (D1 of #189): these three keys move the keyboard
//! where they are read, so the bar moves it and no action names a leaf. The rules about the focus
//! are #189's.
//!
//! **The framework's keys are the framework's.** The navigation character opens a drop-down, the
//! arrows move it, `Enter` takes an entry and `Esc` closes it, and all four are the menu
//! widget's own handling. What the bar adds is what an entry means.

use rat_theme4::WidgetStyle;
use rat_theme4::theme::SalsaTheme;
use rat_widget::event::MenuOutcome;
use rat_widget::menu::menubar;
use rat_widget::menu::{MenuStyle, Menubar, MenubarLine, MenubarPopup, MenubarState, StaticMenu};
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
use ratatui_core::style::Style;
use ratatui_core::widgets::StatefulWidget;
use ratatui_crossterm::crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};

use crate::chrome::Chrome;
use crate::vocabulary::{Action, Reply};

/// The bar, and what each of its entries means.
///
/// **The shortcut on the right of the entry is drawn by the widget and answered to by the
/// application**, which is what the table in `shortcuts` is for. The two are two places that can
/// disagree, and joining them is #180's.
///
/// **Every entry is offered all the time.** The bar carries no state about what is selected or
/// whether a step can be given back, so the Edit menu's entries are active at every position of the
/// history: taking one with nothing to do is an action that changes nothing and repaints all the
/// same, the same answer the key gives (D2 of #181).
///
/// **The label's navigation character is the letter after its `_`**, and it is the only table of
/// names: `Alt` reaches the same character here that a bare press answers when the bar holds the
/// keyboard (D4 of #189). Help is last, so adding it leaves File and Edit where they were.
static MENU: StaticMenu = StaticMenu {
    menu: &[
        (
            "_File",
            &[
                "_New|Ctrl+N",
                "_Open|Ctrl+O",
                "_Save|Ctrl+S",
                "_Export as Text",
                "Open _Demo",
                "_Quit|Ctrl+Q",
            ],
        ),
        ("_Edit", &["_Undo|Ctrl+Z", "_Redo|Ctrl+Shift+Z"]),
        ("_Help", &["_About"]),
    ],
};

/// The label of the way out.
const FILE: usize = 0;

/// The entry that starts a fresh blank diagram.
const NEW: usize = 0;

/// The entry that asks for a description by name.
const OPEN: usize = 1;

/// The entry that saves a description by name.
const SAVE: usize = 2;

/// The entry that exports the diagram as plain text by name.
const EXPORT_TEXT: usize = 3;

/// The entry that loads the demonstration description.
const OPEN_DEMO: usize = 4;

/// The entry of the way out.
const QUIT: usize = 5;

/// The label of the editing commands.
const EDIT: usize = 1;

/// The entry that gives back the step behind.
const UNDO: usize = 0;

/// The entry that gives back the step ahead.
const REDO: usize = 1;

/// The label of the dialog.
const HELP: usize = 2;

/// The entry that says what the application is.
const ABOUT: usize = 0;

/// What an event means to the chrome.
///
/// **The bar is asked about every event and says what it was worth**: `Ignored` if it took no
/// interest, `Repaint` if it changed what the screen shows, `Consumed` if it took the event without
/// changing anything, and `Act` for the entry that was taken. A shortcut is not the bar's to
/// answer: the application's own are asked around it, in `callbacks` — before it the ones that
/// reach everything, after it the ones nobody has claimed — so the bar says nothing about one.
///
/// **The widget's `Continue` is `Ignored` and its `Unchanged` is `Consumed`**, and the difference
/// is the point: an event the drop-down trapped on its border is the drop-down's, and the region
/// behind it must not be asked. **The widget's outcome is not the whole answer**: `rat-menu` closes
/// the pop-up when the bar loses the keyboard and reports `Continue` all the same, so the bar
/// compares the drop-down and the keyboard before and after it asks. A change made while the event
/// was taken upgrades `Consumed` to `Repaint`; an event the bar ignored goes on to the next
/// handler, and the keyboard that left is what owes that frame. **The widget only answers a key
/// while the bar holds the keyboard**: the leaf it is handed says whether it does, and a bar that
/// does not hold it answers no key and closes the drop-down the keyboard left behind.
///
/// **The three keys that move the keyboard are read here, and the bar moves it** (rule 5 of #189).
/// `F10` asks for the bar from either leaf (D4, amended). `Alt` plus a navigation character brings
/// the keyboard with the press: the character is looked up in the labels, so the menu stays the only
/// table of names, and the keyboard is the bar's before the widget is asked (D4). `Esc` while the
/// bar holds it and no drop-down is open gives it back to the region. **A leaf is not a meaning**:
/// the keyboard moves where the event is addressed, and for these three keys that is here — which is
/// why no action names a leaf, and why this is the one handler that moves the keyboard.
///
/// **A bar that moved the keyboard answers**, with the frame owed because a label reversed is a
/// label that changed. The caller's chain stops there, so nothing after the bar is asked with the
/// keyboard somewhere other than where it was when the event arrived.
pub(crate) fn handle(raw: &Event, chrome: &mut Chrome) -> Reply {
    let bar = chrome.is_bar();

    // **`F10` asks for the bar** (D4, amended): the key every terminal reports, where a bare `Alt`
    // is not one. It asks for the keyboard and opens nothing, so the bar looks focused with no
    // drop-down in the way.
    if !bar && asks_for_the_bar(raw) {
        chrome.move_to_bar();
        return Reply::Repaint;
    }

    // **`Esc` on a bar with nothing left to close gives the keyboard back** (rule 5): the widget
    // answers the key while a drop-down is open, and this is the press after it.
    if bar && steps_back(raw, chrome.menu()) {
        chrome.move_to_region();
        return Reply::Repaint;
    }

    let translated;
    let (raw, arriving) = match alt_navigation(raw) {
        Some(character) => {
            translated = Event::Key(KeyEvent::new(KeyCode::Char(character), KeyModifiers::NONE));
            let arrived = !chrome.is_bar();
            chrome.move_to_bar();
            (&translated, arrived)
        }
        None => (raw, false),
    };

    let before_open = chrome.popup_active();
    let before_focus = chrome.leaf();
    let focused = chrome.is_bar();

    let answer = match menubar::handle_popup_events(chrome.menu_mut(), focused, raw) {
        // An entry was taken: it means an answer, and `callbacks` carries it out — which is also
        // where the keyboard goes back to the region, because a meaning carried out is left there.
        // The loop is not asked again, so an entry and the key that means the same thing end in one
        // place. An entry nobody knows still closed the drop-down, so it owes the frame.
        MenuOutcome::MenuActivated(label, entry) => {
            chrome.close_popup();
            meaning(label, entry)
        }

        // **The widget's vocabulary is finer than the application's.** Selecting, activating and
        // hiding are all "the screen changed"; `Unchanged` took the event and changed nothing, so
        // the chain stops there; `Continue` leaves it to the next handler.
        MenuOutcome::Changed
        | MenuOutcome::Hide
        | MenuOutcome::Selected(_)
        | MenuOutcome::Activated(_)
        | MenuOutcome::MenuSelected(_, _) => Reply::Repaint,

        MenuOutcome::Unchanged => Reply::Consumed,

        MenuOutcome::Continue => Reply::Ignored,
    };

    // **The widget can change the state and still say nothing.** `rat-menu` closes the drop-down
    // when the bar loses the keyboard and answers `Continue`, so a `Consumed` that changed
    // something is a frame owed rather than a silent event.
    //
    // **An ignored event stays ignored even where the state moved.** The drop-down closes because
    // the keyboard had already left the bar, and the frame that closing owes is [`crate::callbacks`]'
    // address answer: claiming the event here would stop the chain and the region would never see
    // the press that took the keyboard away.
    let state_changed = chrome.popup_active() != before_open || chrome.leaf() != before_focus;
    let answer = if state_changed && answer != Reply::Ignored {
        answer.and_repaint()
    } else {
        answer
    };

    // **The keyboard arrived with the translation** (D4): the label reversed and the drop-down
    // opened are changes the widget has answered, and the frame is owed even where it said nothing
    // — the keyboard that arrived is the screen changing. It cannot be an entry: an entry is only
    // taken from an open drop-down, and an open drop-down means the bar already held the keyboard.
    if arriving {
        answer.and_repaint()
    } else {
        answer
    }
}

/// The navigation character an `Alt` press names, or `None` where the event is not one.
///
/// **The answer comes from the labels** (D4): the character the menu answers to is the letter
/// after its `_`, and looking it up here keeps the bar the only table of names. **A press of any
/// other key, or with any other modifier, is not one**: a bare `Alt` is not a key, and a
/// combination that names no label changes nothing.
fn alt_navigation(raw: &Event) -> Option<char> {
    let Event::Key(key) = raw else {
        return None;
    };

    if key.kind != KeyEventKind::Press || key.modifiers != KeyModifiers::ALT {
        return None;
    }

    let KeyCode::Char(character) = key.code else {
        return None;
    };
    let character = character.to_ascii_lowercase();

    MENU.menu
        .iter()
        .any(|(label, _)| navigation(label) == Some(character))
        .then_some(character)
}

/// Whether the event is the `F10` press that asks for the bar.
///
/// **`F10` is the key every terminal reports** (D4, amended): a bare `Alt` is not one — the
/// framework asks for the terminal's extended keys only off Windows, and a terminal without them
/// sends nothing for `Alt` alone — so the bar is reachable from the region with a key that is
/// always there. **It asks only for the keyboard**: no drop-down opens, and the bar draws its first
/// label in reverse video because that is what holding the keyboard looks like. [`handle`] asks it
/// only while the bar does not hold the keyboard, and moves the keyboard there.
fn asks_for_the_bar(event: &Event) -> bool {
    matches!(
        event,
        Event::Key(KeyEvent {
            code: KeyCode::F(10),
            modifiers: KeyModifiers::NONE,
            kind: KeyEventKind::Press,
            ..
        })
    )
}

/// Whether the event tells the bar to give the keyboard back.
///
/// **A bar with a drop-down open has something to close**, so `Esc` is the widget's there; with none
/// open the key has nothing left to do here, and this is the bar's own reading of it: the bar steps
/// back and the keyboard goes to the region (rule 5). [`handle`] asks it only while the bar holds
/// the keyboard, and moves the keyboard back.
fn steps_back(event: &Event, state: &MenubarState) -> bool {
    !state.popup_active() && is_escape(event)
}

/// Whether the event is an `Esc` press, with no modifier on it and no kind but a press.
fn is_escape(event: &Event) -> bool {
    matches!(
        event,
        Event::Key(KeyEvent {
            code: KeyCode::Esc,
            modifiers: KeyModifiers::NONE,
            kind: KeyEventKind::Press,
            ..
        })
    )
}

/// The navigation character a label carries: the letter after its `_`.
fn navigation(label: &str) -> Option<char> {
    label
        .strip_prefix('_')
        .and_then(|rest| rest.chars().next())
        .map(|character| character.to_ascii_lowercase())
}

/// What an entry of the bar means to the application.
///
/// **The bar carries text and nothing else**: an entry means what its position says, and an entry
/// nobody knows means nothing — it still closed the drop-down, so it owes the frame, which is
/// [`Reply::Repaint`] said here rather than by the caller.
fn meaning(label: usize, entry: usize) -> Reply {
    match (label, entry) {
        (FILE, NEW) => Reply::Act(Action::New),
        (FILE, OPEN) => Reply::Act(Action::Open),
        (FILE, SAVE) => Reply::Act(Action::Save),
        (FILE, EXPORT_TEXT) => Reply::Act(Action::ExportText),
        (FILE, OPEN_DEMO) => Reply::Act(Action::OpenDemo),
        (FILE, QUIT) => Reply::quit(),
        (EDIT, UNDO) => Reply::Act(Action::Undo),
        (EDIT, REDO) => Reply::Act(Action::Redo),
        (HELP, ABOUT) => Reply::Act(Action::About),

        _ => Reply::Repaint,
    }
}

/// Paints the bar across the top.
///
/// **The focus is given before the frame is drawn**, because the widget paints its selected label
/// differently only while it holds the keyboard: a frame that did not say which leaf holds it
/// would draw the label the way the last event left it.
pub(crate) fn draw_bar(
    area: Rect,
    buf: &mut Buffer,
    state: &mut MenubarState,
    theme: &SalsaTheme,
    focused: bool,
) {
    let (bar, _) = widgets(theme);
    state.bar.focus.set(focused);
    bar.render(area, buf, state);
}

/// Paints the drop-down the bar opens, over what is under it.
///
/// The area is not the bar's: the pop-up hangs from the bar and covers the region, and the
/// widget works out where that is from the frame it was drawn in.
pub(crate) fn draw_drop_down(
    area: Rect,
    buf: &mut Buffer,
    state: &mut MenubarState,
    theme: &SalsaTheme,
) {
    let (_, drop_down) = widgets(theme);
    drop_down.render(area, buf, state);
}

/// The bar and the drop-down, as the two widgets the menu splits itself into.
///
/// **The focus is reverse video over the theme's own colors** (D3 of #189): the two focus styles
/// are replaced with the modifier alone, so the base style painted under the label keeps its
/// colors and the widget adds `Modifier::REVERSED` to the cells it owns. The theme's `focus`
/// background is not used, which is what makes the bar and the dialog's `Ok` say the same thing.
fn widgets(theme: &SalsaTheme) -> (MenubarLine<'static>, MenubarPopup<'static>) {
    let mut style: MenuStyle = theme.style(WidgetStyle::MENU);
    style.focus = Some(Style::new().reversed());
    style.popup_focus = Some(Style::new().reversed());
    Menubar::new(&MENU).styles(style).into_widgets()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::chrome::{Chrome, Focus};
    use crate::config::Config;
    use ratatui_crossterm::crossterm::event::{
        KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
    };

    /// The width of the screen these tests draw.
    const WIDTH: u16 = 40;

    /// The height of the screen these tests draw.
    const HEIGHT: u16 = 10;

    /// The theme the chrome is drawn in.
    fn theme() -> SalsaTheme {
        Config::default().theme
    }

    /// The bar and its drop-down, drawn into a buffer of the whole screen.
    fn draw(chrome: &mut Chrome) -> Buffer {
        draw_with(chrome, true)
    }

    /// The bar and its drop-down, with the keyboard on the bar or not.
    fn draw_with(chrome: &mut Chrome, focused: bool) -> Buffer {
        let area = Rect::new(0, 0, WIDTH, HEIGHT);
        let mut buffer = Buffer::empty(area);
        draw_bar(
            Rect::new(0, 0, WIDTH, 1),
            &mut buffer,
            chrome.menu_mut(),
            &theme(),
            focused,
        );
        draw_drop_down(area, &mut buffer, chrome.menu_mut(), &theme());
        buffer
    }

    /// The bar, drawn once so that the widget knows where its labels are.
    fn drawn() -> Chrome {
        let mut chrome = Chrome::new();
        draw(&mut chrome);
        chrome
    }

    /// The picture of the frame, as text.
    fn picture(buffer: &Buffer) -> String {
        (buffer.area.top()..buffer.area.bottom())
            .map(|line| row_of(buffer, line))
            .collect::<Vec<String>>()
            .join("\n")
    }

    /// One row of the frame, as text.
    fn row_of(buffer: &Buffer, line: u16) -> String {
        (buffer.area.left()..buffer.area.right())
            .map(|column| buffer[(column, line)].symbol().to_string())
            .collect()
    }

    /// Where a piece of text is on the frame, found in the frame itself so that a growing
    /// label does not move the click away from it.
    fn cell_of(buffer: &Buffer, text: &str) -> (u16, u16) {
        crate::frame::cell_of(buffer, text)
    }

    /// Whether the cell at a frame position is painted in reverse video.
    fn reversed(buffer: &Buffer, column: u16, line: u16) -> bool {
        buffer[(column, line)]
            .style()
            .add_modifier
            .contains(ratatui_core::style::Modifier::REVERSED)
    }

    /// Hands the event to the bar with the keyboard on it, where the test is not about the focus.
    fn on_the_bar(event: &Event, chrome: &mut Chrome) -> Reply {
        chrome.move_to_bar();
        handle(event, chrome)
    }

    /// Opens the drop-down of the label, the way a person does.
    ///
    /// **The frame is drawn after the key as well as before it**: the widget knows what it
    /// holds only once it has been drawn, and a frame that is not drawn leaves the drop-down
    /// without an entry to take.
    fn open_with(chrome: &mut Chrome, navigation: char) {
        let _ = on_the_bar(&key(KeyCode::Char(navigation)), chrome);
        draw(chrome);
    }

    /// Opens the File drop-down.
    fn open(chrome: &mut Chrome) {
        open_with(chrome, 'f');
    }

    /// Takes the entry whose text is `text`, by clicking it on the frame, the way a person does.
    ///
    /// **The entry is found by its name**: a menu that gains entries above one moves where it is
    /// but not what it says, and a test that pressed `Enter` would take whatever happens to be
    /// first. **The pointer moves onto it before the button goes down**, because the widget
    /// activates the entry it has selected and a press alone selects nothing.
    fn take(chrome: &mut Chrome, text: &str) -> Reply {
        let buffer = draw(chrome);
        let (column, row) = cell_of(&buffer, text);
        let _ = on_the_bar(&moved(column, row), chrome);

        on_the_bar(&click(column, row), chrome)
    }

    fn key(code: KeyCode) -> Event {
        Event::Key(KeyEvent::new(code, KeyModifiers::NONE))
    }

    fn key_with(code: KeyCode, modifiers: KeyModifiers) -> Event {
        Event::Key(KeyEvent::new(code, modifiers))
    }

    fn click(column: u16, row: u16) -> Event {
        Event::Mouse(MouseEvent {
            kind: MouseEventKind::Down(MouseButton::Left),
            column,
            row,
            modifiers: KeyModifiers::NONE,
        })
    }

    /// The event the terminal reports for the pointer moving to `(column, row)`.
    fn moved(column: u16, row: u16) -> Event {
        Event::Mouse(MouseEvent {
            kind: MouseEventKind::Moved,
            column,
            row,
            modifiers: KeyModifiers::NONE,
        })
    }

    /// **The control the keyboard is on is drawn in reverse video over the theme's colors**
    /// (D3 of #189): the focused label gains the modifier and keeps the foreground and background
    /// the base style gave it, so the theme's `focus` background is not what says where the
    /// keyboard is.
    #[test]
    fn a_control_the_keyboard_is_on_is_drawn_in_reverse_video() {
        let mut without = Chrome::new();
        let unfocused = draw_with(&mut without, false);
        let (column, row) = cell_of(&unfocused, "File");
        let plain = unfocused[(column, row)].style();

        let mut with = drawn();
        let focused = draw(&mut with);
        let lit = focused[(column, row)].style();

        assert!(
            !reversed(&unfocused, column, row),
            "a label is reversed while the bar does not hold the keyboard"
        );
        assert!(
            reversed(&focused, column, row),
            "the label the keyboard is on is not reversed"
        );
        assert_eq!(lit.fg, plain.fg, "the focus changed the label's foreground");
        assert_eq!(
            lit.bg, plain.bg,
            "the theme's focus background is used, not reverse video"
        );

        // The rest of the bar keeps the plain style: the cue is one label, not the whole row.
        let (column, row) = cell_of(&focused, "Edit");
        assert!(
            !reversed(&focused, column, row),
            "another label is reversed with the one the keyboard is on"
        );
    }

    #[test]
    fn the_navigation_character_opens_the_drop_down_and_escape_closes_it() {
        let mut chrome = drawn();

        let outcome = on_the_bar(&key(KeyCode::Char('f')), &mut chrome);

        assert!(
            matches!(outcome, Reply::Repaint),
            "the navigation character did not report a screen change: {outcome:?}"
        );
        assert!(chrome.popup_active(), "the drop-down did not open");

        let outcome = on_the_bar(&key(KeyCode::Esc), &mut chrome);

        assert!(
            matches!(outcome, Reply::Repaint),
            "escape did not report a screen change: {outcome:?}"
        );
        assert!(!chrome.popup_active(), "escape did not close the drop-down");
    }

    /// **The key that leaves the bar is a press and nothing else** (rule 5): a release, a repeat,
    /// and a press with a modifier on it are not the bar stepping back, and neither is a press
    /// while a drop-down is open.
    #[test]
    fn an_escape_press_is_the_bars_way_out() {
        let mut chrome = drawn();

        assert!(
            steps_back(&key(KeyCode::Esc), chrome.menu()),
            "the escape press was not read"
        );

        for kind in [KeyEventKind::Release, KeyEventKind::Repeat] {
            let event = Event::Key(KeyEvent::new_with_kind(
                KeyCode::Esc,
                KeyModifiers::NONE,
                kind,
            ));

            assert!(
                !steps_back(&event, chrome.menu()),
                "{kind:?} was read as it"
            );
        }

        assert!(
            !steps_back(
                &key_with(KeyCode::Esc, KeyModifiers::CONTROL),
                chrome.menu()
            ),
            "a modified escape was read as it"
        );
        assert!(
            !steps_back(&key(KeyCode::Enter), chrome.menu()),
            "another key was read as it"
        );

        open(&mut chrome);
        assert!(
            !steps_back(&key(KeyCode::Esc), chrome.menu()),
            "the escape that closes the drop-down was read as the bar stepping back"
        );
    }

    /// **The key that asks for the bar is `F10` and nothing else** (D4, amended): not with a
    /// modifier on it, not released or repeated, and not a character a label already answers to.
    #[test]
    fn a_key_that_asks_for_the_bar_is_a_press_and_nothing_else() {
        assert!(
            asks_for_the_bar(&key(KeyCode::F(10))),
            "F10 did not ask for the bar"
        );
        assert!(
            !asks_for_the_bar(&key_with(KeyCode::F(10), KeyModifiers::SHIFT)),
            "a shifted F10 asked for the bar"
        );
        assert!(
            !asks_for_the_bar(&key(KeyCode::F(9))),
            "another function key asked for the bar"
        );
        assert!(
            !asks_for_the_bar(&key(KeyCode::Char('f'))),
            "a navigation character asked for the bar on its own"
        );

        for kind in [KeyEventKind::Release, KeyEventKind::Repeat] {
            let event = Event::Key(KeyEvent::new_with_kind(
                KeyCode::F(10),
                KeyModifiers::NONE,
                kind,
            ));

            assert!(!asks_for_the_bar(&event), "{kind:?} asked for the bar");
        }
    }

    /// **`F10` moves the keyboard where the key is read** (rule 5 and D4, amended): the bar is what
    /// knows the key, so the bar is where the leaf changes, and the label that stops being reversed
    /// is why the frame is owed. It opens nothing on the way.
    #[test]
    fn f10_moves_the_keyboard_to_the_bar_and_owes_the_frame() {
        let mut chrome = drawn();

        let outcome = handle(&key(KeyCode::F(10)), &mut chrome);

        assert_eq!(
            chrome.leaf(),
            Focus::Bar,
            "F10 left the keyboard where it was"
        );
        assert!(
            matches!(outcome, Reply::Repaint),
            "F10 answered {outcome:?}"
        );
        assert!(!chrome.popup_active(), "F10 opened a drop-down");
    }

    /// **The escape that closes nothing gives the keyboard back where it is read** (rule 5): the
    /// bar has nothing left to close, so this is the press that leaves, and the frame is owed
    /// because the label stops being reversed.
    #[test]
    fn the_escape_that_closes_nothing_gives_the_keyboard_back() {
        let mut chrome = drawn();
        chrome.move_to_bar();

        let outcome = handle(&key(KeyCode::Esc), &mut chrome);

        assert_eq!(
            chrome.leaf(),
            Focus::Region,
            "the escape left the keyboard on the bar"
        );
        assert!(
            matches!(outcome, Reply::Repaint),
            "the escape answered {outcome:?}"
        );
    }

    /// **A press the drop-down traps is consumed and not ignored** (rule 3 of #189): `rat-menu`
    /// answers `Unchanged` for a cell that is not an entry — the border or the padding — which is
    /// the bar's event with nothing changed. Mapping that to `Ignored` is what once let the press
    /// reach the diagram behind it.
    #[test]
    fn a_press_the_drop_down_traps_is_consumed_and_not_ignored() {
        let mut chrome = drawn();
        open_with(&mut chrome, 'e');

        let popup = chrome.menu().popup.area;
        let mut trapped = None;

        for row in popup.top()..popup.bottom() {
            for column in popup.left()..popup.right() {
                if chrome.menu().popup.item_at((column, row)).is_none() {
                    trapped = Some((column, row));
                    break;
                }
            }

            if trapped.is_some() {
                break;
            }
        }

        let (column, row) = trapped.expect("the drop-down has no cell that is not an entry");

        let outcome = on_the_bar(&click(column, row), &mut chrome);

        assert_eq!(
            outcome,
            Reply::Consumed,
            "a press the drop-down trapped answered {outcome:?}"
        );
        assert!(chrome.popup_active(), "the press closed the drop-down");
    }

    /// **The File menu's first entry is New, which mints [`Action::New`].**
    #[test]
    fn taking_the_new_entry_becomes_the_action_of_new() {
        let mut chrome = drawn();
        open(&mut chrome);

        let outcome = take(&mut chrome, "New");

        assert!(
            matches!(outcome, Reply::Act(Action::New)),
            "the entry did not become the action of new: {outcome:?}"
        );
        assert!(
            !chrome.popup_active(),
            "the drop-down stayed open behind it"
        );
    }

    /// **The File menu's second entry is Open, which mints [`Action::Open`].**
    #[test]
    fn taking_the_open_entry_becomes_the_action_of_the_open_dialog() {
        let mut chrome = drawn();
        open(&mut chrome);

        let outcome = take(&mut chrome, "Open");

        assert!(
            matches!(outcome, Reply::Act(Action::Open)),
            "the entry did not become the action of the open dialog: {outcome:?}"
        );
        assert!(
            !chrome.popup_active(),
            "the drop-down stayed open behind it"
        );
    }

    /// **The File menu's third entry is Save, which mints [`Action::Save`].**
    #[test]
    fn taking_the_save_entry_becomes_the_action_of_the_save_dialog() {
        let mut chrome = drawn();
        open(&mut chrome);

        let outcome = take(&mut chrome, "Save");

        assert!(
            matches!(outcome, Reply::Act(Action::Save)),
            "the entry did not become the action of the save dialog: {outcome:?}"
        );
        assert!(
            !chrome.popup_active(),
            "the drop-down stayed open behind it"
        );
    }

    /// **The File menu's fourth entry is Open Demo, which mints [`Action::OpenDemo`].**
    #[test]
    fn taking_the_open_demo_entry_becomes_the_action_of_open_demo() {
        let mut chrome = drawn();
        open(&mut chrome);

        let outcome = take(&mut chrome, "Open Demo");

        assert!(
            matches!(outcome, Reply::Act(Action::OpenDemo)),
            "the entry did not become the action of open demo: {outcome:?}"
        );
        assert!(
            !chrome.popup_active(),
            "the drop-down stayed open behind it"
        );
    }

    #[test]
    fn taking_the_quit_entry_becomes_the_answer_of_the_way_out() {
        let mut chrome = drawn();
        open(&mut chrome);

        let outcome = take(&mut chrome, "Quit");

        assert_eq!(
            outcome,
            Reply::quit(),
            "the entry did not become the answer of the way out"
        );
        assert!(
            !chrome.popup_active(),
            "the drop-down stayed open behind it"
        );
    }

    /// **The Edit menu's entries are the keys' actions.** The undo is the first, and taking it says
    /// `Action::Undo` rather than a second spelling of the walk.
    #[test]
    fn taking_the_undo_entry_becomes_the_action_of_the_key() {
        let mut chrome = drawn();
        open_with(&mut chrome, 'e');

        let outcome = take(&mut chrome, "Undo");

        assert!(
            matches!(outcome, Reply::Act(Action::Undo)),
            "the entry did not become the action of the undo key: {outcome:?}"
        );
        assert!(
            !chrome.popup_active(),
            "the drop-down stayed open behind it"
        );
    }

    /// **Help is the third label and About its only entry, with no shortcut drawn** (rule 8):
    /// About opens a dialog rather than a command, so there is no key to show beside it.
    #[test]
    fn the_help_menu_holds_about_and_draws_no_shortcut() {
        let mut chrome = drawn();
        open_with(&mut chrome, 'h');

        let buffer = draw(&mut chrome);
        let frame = picture(&buffer);

        assert!(frame.contains("Help"), "the bar has no Help: {frame:?}");
        assert!(
            frame.contains("About"),
            "the Help menu offers no About: {frame:?}"
        );

        let (_, row) = cell_of(&buffer, "About");
        let line = row_of(&buffer, row);

        assert!(
            !line.contains("Ctrl") && !line.contains("Del"),
            "the About entry draws a shortcut: {line:?}"
        );
    }

    /// **Taking About mints `Action::About`** (rule 8): the entry is a meaning, and the dialog is
    /// opened where the action is carried out rather than by the bar.
    #[test]
    fn taking_the_about_entry_mints_the_about_action() {
        let mut chrome = drawn();
        open_with(&mut chrome, 'h');

        let outcome = take(&mut chrome, "About");

        assert!(
            matches!(outcome, Reply::Act(Action::About)),
            "the entry did not become the action of the About dialog: {outcome:?}"
        );
        assert!(
            !chrome.popup_active(),
            "the drop-down stayed open behind it"
        );
    }

    /// **The redo entry is the redo key's action**, the same way.
    #[test]
    fn taking_the_redo_entry_becomes_the_action_of_the_key() {
        let mut chrome = drawn();
        open_with(&mut chrome, 'e');

        let outcome = take(&mut chrome, "Redo");

        assert!(
            matches!(outcome, Reply::Act(Action::Redo)),
            "the entry did not become the action of the redo key: {outcome:?}"
        );
        assert!(
            !chrome.popup_active(),
            "the drop-down stayed open behind it"
        );
    }

    /// **The Edit menu holds Undo and Redo with their shortcuts**: Undo and Redo, each with the
    /// shortcut the table answers to (rule 9).
    #[test]
    fn the_edit_menu_shows_undo_and_redo_with_their_shortcuts() {
        let mut chrome = drawn();
        open_with(&mut chrome, 'e');

        let frame = picture(&draw(&mut chrome));

        assert!(
            frame.contains("Undo"),
            "the Edit menu offers no undo: {frame:?}"
        );
        assert!(
            frame.contains("Redo"),
            "the Edit menu offers no redo: {frame:?}"
        );
        assert!(
            frame.contains("Ctrl+Z"),
            "the undo entry shows no shortcut: {frame:?}"
        );
        assert!(
            frame.contains("Ctrl+Shift+Z"),
            "the redo entry shows no shortcut: {frame:?}"
        );

        let undo = frame.find("Undo").expect("the undo entry is on the frame");
        let redo = frame.find("Redo").expect("the redo entry is on the frame");

        assert!(undo < redo, "the entries are not Undo, Redo: {frame:?}");
    }

    #[test]
    fn the_edit_menu_shows_undo_and_redo_and_nothing_else() {
        let mut chrome = drawn();
        open_with(&mut chrome, 'e');

        let frame = picture(&draw(&mut chrome));

        assert!(
            frame.contains("Edit"),
            "the bar has no Edit menu: {frame:?}"
        );
        assert!(
            !frame.contains("Delete"),
            "the Edit menu still offers delete: {frame:?}"
        );
        assert!(
            !frame.contains("Del"),
            "the Edit menu still shows Del: {frame:?}"
        );
    }

    #[test]
    fn the_shortcuts_are_not_the_bars() {
        let mut chrome = drawn();
        chrome.move_to_bar();

        let outcome = handle(
            &key_with(KeyCode::Char('q'), KeyModifiers::CONTROL),
            &mut chrome,
        );

        assert_eq!(
            outcome,
            Reply::Ignored,
            "the bar answered a shortcut: {outcome:?}"
        );
        assert_eq!(chrome.leaf(), Focus::Bar, "the bar gave the keyboard away");
        assert!(!chrome.popup_active(), "a shortcut opened a drop-down");
    }

    #[test]
    fn a_click_on_the_entry_becomes_the_same_answer() {
        let mut chrome = drawn();
        open(&mut chrome);

        let outcome = take(&mut chrome, "Quit");

        assert_eq!(
            outcome,
            Reply::quit(),
            "the click did not become the answer of the way out"
        );
    }

    #[test]
    fn a_click_on_the_label_opens_its_drop_down() {
        let mut chrome = drawn();
        let buffer = draw(&mut chrome);
        let (column, row) = cell_of(&buffer, "File");

        let outcome = on_the_bar(&click(column, row), &mut chrome);

        assert!(
            matches!(outcome, Reply::Repaint),
            "the click did not report a screen change: {outcome:?}"
        );
        assert!(
            chrome.popup_active(),
            "the click did not open the drop-down"
        );
    }

    #[test]
    fn a_key_the_bar_does_not_use_is_nothing() {
        let mut chrome = drawn();

        let outcome = on_the_bar(&key(KeyCode::Char('z')), &mut chrome);

        assert_eq!(outcome, Reply::Ignored, "{outcome:?}");
        assert!(!chrome.popup_active(), "a key opened a drop-down");
    }

    #[test]
    fn the_drop_down_shows_the_shortcut_the_entry_answers_to() {
        let mut chrome = drawn();
        open(&mut chrome);

        let buffer = draw(&mut chrome);
        let frame = picture(&buffer);

        assert!(
            frame.contains("Ctrl+Q"),
            "the drop-down shows no shortcut: {frame:?}"
        );
    }

    #[test]
    fn taking_the_export_as_text_entry_becomes_the_action_of_the_export_dialog() {
        let mut chrome = drawn();
        open(&mut chrome);

        let outcome = take(&mut chrome, "Export as Text");

        assert!(
            matches!(outcome, Reply::Act(Action::ExportText)),
            "the entry did not become the action of the export dialog: {outcome:?}"
        );
        assert!(
            !chrome.popup_active(),
            "the drop-down stayed open behind it"
        );
    }
}