duat_term/
rules.rs

1//! A vertical ruler, usually to separate the [`Buffer`] from other
2//! widgets
3//!
4//! This is just a simple widget, which should only really make sense
5//! in a terminal context, since, in GUIs, you'd use styling of the
6//! widget areas in order to accomplish the same thing.
7//!
8//! [`Buffer`]: duat_core::buffer::Buffer
9use duat_core::{
10    context::Handle,
11    data::Pass,
12    text::{Text, txt},
13    ui::{PushSpecs, PushTarget, Side, Widget},
14};
15
16/// A vertical line on screen, useful, for example, for the separation
17/// of a [`Buffer`] and [`LineNumbers`].
18///
19/// By default, this [`VertRule`] will show the `'│'` character on the
20/// whole line, using the `"default.VertRule"` form. However, you can
21/// change that with the [`VertRule::sep_char`] field.
22///
23/// This field, of type [`SepChar`], can have up to 3 distinct
24/// characters, in order to differentiate between the main line's
25/// separator and even the separators above and below the main line.
26///
27/// If the main character is not the same as the other two characters,
28/// then the line will be printed with the `"rule.upper"` and
29/// `"rule.lower"` forms for the characters above and below.
30///
31/// [`Buffer`]: duat_core::buffer::Buffer
32/// [`LineNumbers`]: https://docs.rs/duat-utils/latest/duat_utils/widgets/struct.LineNumbers.html
33pub struct VertRule {
34    handle: Option<Handle>,
35    text: Text,
36    pub sep_char: SepChar,
37}
38
39impl VertRule {
40    /// Returns a [`VertRuleBuilder`], letting you push your own
41    /// [`VertRule`]s around
42    pub fn builder() -> VertRuleBuilder {
43        VertRuleBuilder::default()
44    }
45}
46
47impl Widget for VertRule {
48    fn update(pa: &mut Pass, handle: &Handle<Self>) {
49        let vr = handle.read(pa);
50        let text = if let Some(handle) = vr.handle.as_ref()
51            && let SepChar::ThreeWay(..) | SepChar::TwoWay(..) = vr.sep_char
52        {
53            let (upper, middle, lower) = {
54                let file = handle.read(pa);
55
56                let lines = file.printed_lines();
57                if let Some(main) = file.selections().get_main() {
58                    let main = main.line();
59                    let upper = lines.iter().filter(|&(line, _)| *line < main).count();
60                    let middle = lines.iter().filter(|&(line, _)| *line == main).count();
61                    let lower = lines.iter().filter(|&(line, _)| *line > main).count();
62                    (upper, middle, lower)
63                } else {
64                    (0, lines.len(), 0)
65                }
66            };
67
68            let chars = vr.sep_char.chars();
69            txt!(
70                "[rule.upper]{}[]{}[rule.lower]{}",
71                form_string(chars[0], upper),
72                form_string(chars[1], middle),
73                form_string(chars[2], lower)
74            )
75        } else {
76            let full_line =
77                format!("{}\n", vr.sep_char.chars()[1]).repeat(handle.area().height(pa) as usize);
78
79            txt!("{full_line}")
80        };
81
82        handle.write(pa).text = text;
83    }
84
85    fn needs_update(&self, pa: &Pass) -> bool {
86        matches!(self.sep_char, SepChar::ThreeWay(..) | SepChar::TwoWay(..))
87            && self.handle.as_ref().is_some_and(|fh| fh.has_changed(pa))
88    }
89
90    fn text(&self) -> &Text {
91        &self.text
92    }
93
94    fn text_mut(&mut self) -> &mut Text {
95        &mut self.text
96    }
97}
98
99/// The configurations for the [`VertRule`] widget.
100#[derive(Default, Clone)]
101pub struct VertRuleBuilder {
102    pub sep_char: SepChar,
103    pub on_the_right: bool,
104}
105
106impl VertRuleBuilder {
107    /// Pushes a [`VertRule`] to a [`PushTarget`]
108    pub fn push_on(self, pa: &mut Pass, push_target: &impl PushTarget) -> Handle<VertRule> {
109        let vert_rule = VertRule {
110            handle: push_target.try_downcast(),
111            text: Text::default(),
112            sep_char: self.sep_char,
113        };
114
115        let specs = PushSpecs {
116            side: if self.on_the_right {
117                Side::Right
118            } else {
119                Side::Left
120            },
121            width: Some(1.0),
122            ..Default::default()
123        };
124
125        push_target.push_outer(pa, vert_rule, specs)
126    }
127}
128
129/// The [`char`]s that should be printed above, equal to, and below
130/// the main line.
131#[derive(Clone)]
132pub enum SepChar {
133    Uniform(char),
134    /// Order: main line, other lines.
135    TwoWay(char, char),
136    /// Order: main line, above main line, below main line.
137    ThreeWay(char, char, char),
138}
139
140impl SepChar {
141    /// The [`char`]s above, equal to, and below the main line,
142    /// respectively.
143    fn chars(&self) -> [char; 3] {
144        match self {
145            SepChar::Uniform(uniform) => [*uniform, *uniform, *uniform],
146            SepChar::TwoWay(main, other) => [*other, *main, *other],
147            SepChar::ThreeWay(main, upper, lower) => [*upper, *main, *lower],
148        }
149    }
150}
151
152impl Default for SepChar {
153    fn default() -> Self {
154        Self::Uniform('│')
155    }
156}
157
158fn form_string(char: char, count: usize) -> String {
159    [char, '\n'].repeat(count).iter().collect()
160}