Skip to main content

rpi_tui/
dynamic_border.rs

1//! Dynamic border component — a horizontal rule that stretches to the viewport
2//! width.
3//!
4//! Port of `packages/coding-agent/src/modes/interactive/components/dynamic-border.ts`.
5//! The TS version takes a color function; the Rust port takes an optional
6//! [`Color`] (None = theme border color) and uses [`Color::fg`] at render time.
7
8use std::any::Any;
9
10use super::component::Component;
11use crate::ansi::dim;
12use crate::theme::{theme, Color};
13
14/// A single-row horizontal rule (`─` repeated to `width`) in a chosen color.
15///
16/// `None` resolves to the low-contrast `border_muted` theme token and is also
17/// rendered faint, so command separators never compete with the editor border.
18/// Explicit colors remain full strength for intentional accent components.
19pub struct DynamicBorder {
20    color: Option<Color>,
21}
22
23impl DynamicBorder {
24    /// Create a border using the current theme's border color.
25    pub fn new() -> Self {
26        Self { color: None }
27    }
28
29    /// Create a border with an explicit color.
30    pub fn with_color(color: Color) -> Self {
31        Self { color: Some(color) }
32    }
33}
34
35impl Default for DynamicBorder {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41impl Component for DynamicBorder {
42    fn render(&self, width: usize) -> Vec<String> {
43        let rule = "─".repeat(width.max(1));
44        let rendered = match self.color {
45            Some(color) => color.fg(&rule),
46            None => dim(&theme().colors.border_muted.fg(&rule)),
47        };
48        vec![rendered]
49    }
50
51    fn invalidate(&self) {
52        // No cached state.
53    }
54
55    fn as_any(&self) -> &dyn Any {
56        self
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use crate::ansi::visible_width;
64
65    #[test]
66    fn test_dynamic_border_renders_one_line() {
67        let border = DynamicBorder::new();
68        let lines = border.render(40);
69        assert_eq!(lines.len(), 1);
70        // Visible width is stable and default separators are deliberately faint.
71        assert_eq!(visible_width(&lines[0]), 40);
72        assert!(lines[0].contains("\x1b[2m"));
73    }
74
75    #[test]
76    fn test_dynamic_border_zero_width_safe() {
77        // Must not panic on a 0-width viewport.
78        let border = DynamicBorder::new();
79        let lines = border.render(0);
80        assert_eq!(lines.len(), 1);
81        assert_eq!(visible_width(&lines[0]), 1);
82    }
83
84    #[test]
85    fn test_dynamic_border_with_color_contains_escape() {
86        let border = DynamicBorder::with_color(Color::Ansi256(39));
87        let lines = border.render(10);
88        assert!(lines[0].contains('\x1b'));
89    }
90}