Skip to main content

datui_lib/widgets/ui/
mod.rs

1//! The component kit: the pieces every surface is built from.
2//!
3//! One Surface per modal or sidebar, FormRows inside it, a Picker for pick-one
4//! lists, a HintBar for the keys, a SectionRule to divide space without borders,
5//! Working for a wait said where its result will appear.
6//! The canon these implement is the `ui-style` skill; a screen that hand-rolls
7//! one of these is a migration target.
8
9mod form_row;
10mod form_view;
11mod hintbar;
12mod picker;
13mod section_rule;
14mod surface;
15mod working;
16
17pub use form_row::{FormRow, FormValue};
18pub use form_view::{FormLine, FormView};
19pub use hintbar::HintBar;
20pub use picker::{Clicks, Picker, PickerState};
21pub use section_rule::SectionRule;
22pub use surface::Surface;
23pub use working::Working;
24
25#[cfg(test)]
26mod tests {
27    use std::path::{Path, PathBuf};
28
29    /// Source files outside the kit that may still draw a frame of their own,
30    /// relative to `src/`. Each needs a comment at the frame saying why a
31    /// Surface cannot express it.
32    const ALLOWED: &[&str] = &[];
33
34    /// What a hand-built frame looks like in source. `Borders::NONE` draws
35    /// nothing and is not one.
36    const FRAMES: &[&str] = &[
37        "Borders::",
38        "Block::bordered",
39        ".border_set(",
40        ".border_type(",
41    ];
42
43    fn rust_files(dir: &Path, out: &mut Vec<PathBuf>) {
44        for entry in std::fs::read_dir(dir).expect("source dir") {
45            let path = entry.expect("dir entry").path();
46            if path.is_dir() {
47                rust_files(&path, out);
48            } else if path.extension().is_some_and(|ext| ext == "rs") {
49                out.push(path);
50            }
51        }
52    }
53
54    /// Line number and text of every line in `source` that builds a frame.
55    fn frames_in(source: &str) -> Vec<(usize, &str)> {
56        source
57            .lines()
58            .enumerate()
59            .filter(|(_, line)| {
60                let code = line.trim_start().replace("Borders::NONE", "");
61                !code.starts_with("//") && FRAMES.iter().any(|frame| code.contains(frame))
62            })
63            .map(|(i, line)| (i + 1, line.trim()))
64            .collect()
65    }
66
67    /// Hard rule 1 of the canon: a surface gets one frame, and only Surface
68    /// draws it. #368, #369 and #370 moved every hand-drawn frame onto it;
69    /// this keeps new ones from coming back.
70    #[test]
71    fn only_the_surface_draws_a_frame() {
72        let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
73        let kit = src.join("widgets").join("ui");
74        let mut files = Vec::new();
75        rust_files(&src, &mut files);
76        for dir in ["render", "widgets"] {
77            assert!(
78                files.iter().any(|path| path.starts_with(src.join(dir))),
79                "the scan reads src/{dir}"
80            );
81        }
82        let mut found = Vec::new();
83        for path in files.iter().filter(|path| !path.starts_with(&kit)) {
84            let name = path
85                .strip_prefix(&src)
86                .expect("under src")
87                .to_string_lossy()
88                .replace('\\', "/");
89            let source = std::fs::read_to_string(path).expect("source file");
90            let frames = frames_in(&source);
91            if ALLOWED.contains(&name.as_str()) {
92                assert!(
93                    !frames.is_empty(),
94                    "{name} is allowed a frame it no longer draws"
95                );
96                continue;
97            }
98            found.extend(
99                frames
100                    .into_iter()
101                    .map(|(line, code)| format!("  {name}:{line}: {code}")),
102            );
103        }
104        assert!(
105            found.is_empty(),
106            "draw these frames with widgets::ui::Surface:\n{}",
107            found.join("\n")
108        );
109    }
110
111    /// The scan sees a frame, and not a borderless block or a comment.
112    #[test]
113    fn the_frame_scan_reads_code_only() {
114        let source = [
115            "let a = Block::default().borders(Borders::ALL);",
116            "let b = Block::bordered();",
117            "let c = Block::default().borders(Borders::NONE);",
118            "// Borders::ALL was here once.",
119            "let d = Block::default().borders(Borders::TOP | Borders::BOTTOM);",
120        ]
121        .join("\n");
122        let lines: Vec<usize> = frames_in(&source).into_iter().map(|(n, _)| n).collect();
123        assert_eq!(lines, [1, 2, 5]);
124    }
125}