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