duat_core/widgets/
line_numbers.rs

1//! Line numbers for a [`File`]
2//!
3//! These are pretty standard like in most text editors. Usually,
4//! they'll be printed on the right of the [`File`], but there is an
5//! option to print them on the right, if you need such functionality.
6//!
7//! You can also change other things, like the
8//! relativeness/absoluteness of the numbers, as well as the alignment
9//! of the numbers, with one more option to change that of the main
10//! cursor's line number.
11//!
12//! [`File`]: super::File
13use std::{fmt::Alignment, marker::PhantomData};
14
15use crate::{
16    context::{self, FileReader},
17    form::{self, Form},
18    text::{Builder, Tag, Text, text},
19    ui::{Area, Constraint, PushSpecs, Ui},
20    widgets::{Widget, WidgetCfg},
21};
22
23pub struct LineNumbers<U: Ui> {
24    reader: FileReader<U>,
25    text: Text,
26    cfg: LineNumbersCfg<U>,
27}
28
29impl<U: Ui> LineNumbers<U> {
30    /// The minimum width that would be needed to show the last line.
31    fn calculate_width(&mut self) -> f32 {
32        // "+ 1" because we index from 1, not from 0.
33        let len = self.reader.inspect(|file, _| file.text().len().line()) + 1;
34        len.ilog10() as f32
35    }
36
37    fn update_text(&mut self) {
38        self.text = self.reader.inspect(|file, _| {
39            let printed_lines = file.printed_lines();
40            let cursors = file.cursors().unwrap();
41            let main_line = match cursors.is_empty() {
42                true => usize::MAX,
43                false => cursors.main().line(),
44            };
45
46            let mut builder = Text::builder();
47            text!(builder, { tag_from_align(self.cfg.align) });
48
49            for (index, (line, is_wrapped)) in printed_lines.iter().enumerate() {
50                if main_line == *line {
51                    text!(builder, { tag_from_align(self.cfg.main_align) });
52                }
53
54                match (main_line == *line, is_wrapped) {
55                    (false, false) => text!(builder, [LineNum]),
56                    (true, false) => text!(builder, [MainLineNum]),
57                    (false, true) => text!(builder, [WrappedLineNum]),
58                    (true, true) => text!(builder, [WrappedMainLineNum]),
59                }
60
61                let is_wrapped = *is_wrapped && index > 0;
62                push_text(&mut builder, *line, main_line, is_wrapped, &self.cfg);
63
64                if main_line == *line {
65                    text!(builder, { tag_from_align(self.cfg.align) });
66                }
67            }
68
69            builder.finish()
70        });
71    }
72
73    pub fn get_cfg(&self) -> LineNumbersCfg<U> {
74        self.cfg
75    }
76
77    pub fn reconfigure(&mut self, cfg: LineNumbersCfg<U>) {
78        self.cfg = cfg
79    }
80}
81
82impl<U: Ui> Widget<U> for LineNumbers<U> {
83    type Cfg = LineNumbersCfg<U>;
84
85    fn cfg() -> Self::Cfg {
86        LineNumbersCfg::new()
87    }
88
89    fn update(&mut self, area: &U::Area) {
90        let width = self.calculate_width();
91        area.constrain_hor(Constraint::Length(width + 1.0)).unwrap();
92
93        self.update_text();
94    }
95
96    fn text(&self) -> &Text {
97        &self.text
98    }
99
100    fn text_mut(&mut self) -> &mut Text {
101        &mut self.text
102    }
103
104    fn once() -> crate::Result<(), ()> {
105        form::set_weak("LineNum", Form::grey());
106        form::set_weak("MainLineNum", Form::yellow());
107        form::set_weak("WrappedLineNum", Form::cyan().italic());
108        form::set_weak("WrappedMainLineNum", "WrappedLineNum");
109        Ok(())
110    }
111}
112
113/// How to show the line numbers on screen.
114#[derive(Default, Debug, Copy, Clone)]
115pub enum LineNum {
116    #[default]
117    /// Line numbers relative to the beginning of the file.
118    Abs,
119    /// Line numbers relative to the main cursor's line, including
120    /// that line.
121    Rel,
122    /// Relative line numbers on every line, except the main cursor's.
123    RelAbs,
124}
125
126/// Configuration options for the [`LineNumbers<U>`] widget.
127#[derive(Debug)]
128pub struct LineNumbersCfg<U> {
129    pub num_rel: LineNum,
130    pub align: Alignment,
131    pub main_align: Alignment,
132    pub show_wraps: bool,
133    pub specs: PushSpecs,
134    pub ghost: PhantomData<U>,
135}
136
137impl<U> Copy for LineNumbersCfg<U> {}
138impl<U> Clone for LineNumbersCfg<U> {
139    fn clone(&self) -> Self {
140        *self
141    }
142}
143
144impl<U> Default for LineNumbersCfg<U> {
145    fn default() -> Self {
146        Self::new()
147    }
148}
149
150impl<U> LineNumbersCfg<U> {
151    pub fn new() -> Self {
152        Self {
153            num_rel: LineNum::Abs,
154            align: Alignment::Left,
155            main_align: Alignment::Right,
156            show_wraps: false,
157            specs: PushSpecs::left(),
158            ghost: PhantomData,
159        }
160    }
161
162    pub fn absolute(self) -> Self {
163        Self { num_rel: LineNum::Abs, ..self }
164    }
165
166    pub fn relative(self) -> Self {
167        Self { num_rel: LineNum::Rel, ..self }
168    }
169
170    pub fn rel_abs(self) -> Self {
171        Self { num_rel: LineNum::RelAbs, ..self }
172    }
173
174    pub fn align_left(self) -> Self {
175        Self {
176            main_align: Alignment::Left,
177            align: Alignment::Left,
178            ..self
179        }
180    }
181
182    pub fn align_center(self) -> Self {
183        Self {
184            main_align: Alignment::Center,
185            align: Alignment::Center,
186            ..self
187        }
188    }
189
190    pub fn align_right(self) -> Self {
191        Self {
192            main_align: Alignment::Right,
193            align: Alignment::Right,
194            ..self
195        }
196    }
197
198    pub fn align_main_left(self) -> Self {
199        Self { main_align: Alignment::Left, ..self }
200    }
201
202    pub fn align_main_center(self) -> Self {
203        Self { main_align: Alignment::Center, ..self }
204    }
205
206    pub fn align_main_right(self) -> Self {
207        Self { main_align: Alignment::Right, ..self }
208    }
209
210    pub fn show_wraps(self) -> Self {
211        Self { show_wraps: true, ..self }
212    }
213
214    pub fn hide_wraps(self) -> Self {
215        Self { show_wraps: false, ..self }
216    }
217
218    pub fn on_the_right(self) -> Self {
219        Self { specs: self.specs.to_right(), ..self }
220    }
221}
222
223impl<U: Ui> WidgetCfg<U> for LineNumbersCfg<U> {
224    type Widget = LineNumbers<U>;
225
226    fn build(self, _: bool) -> (Self::Widget, impl Fn() -> bool, PushSpecs) {
227        let reader = context::cur_file().unwrap().fixed_reader();
228        let specs = self.specs;
229
230        let mut widget = LineNumbers {
231            reader: reader.clone(),
232            text: Text::default(),
233            cfg: self,
234        };
235        widget.update_text();
236
237        (widget, move || reader.has_changed(), specs)
238    }
239}
240
241/// Writes the text of the line number to a given [`String`].
242fn push_text<U>(
243    builder: &mut Builder,
244    line: usize,
245    main: usize,
246    is_wrapped: bool,
247    cfg: &LineNumbersCfg<U>,
248) {
249    if is_wrapped && !cfg.show_wraps {
250        text!(*builder, "\n");
251    } else if main != usize::MAX {
252        let num = match cfg.num_rel {
253            LineNum::Abs => line + 1,
254            LineNum::Rel => line.abs_diff(main),
255            LineNum::RelAbs => {
256                if line != main {
257                    line.abs_diff(main)
258                } else {
259                    line + 1
260                }
261            }
262        };
263
264        text!(*builder, num "\n");
265    } else {
266        text!(*builder, { line + 1 } "\n");
267    }
268}
269
270fn tag_from_align(alignment: Alignment) -> Option<Tag> {
271    match alignment {
272        Alignment::Left => None,
273        Alignment::Right => Some(Tag::StartAlignRight),
274        Alignment::Center => Some(Tag::StartAlignCenter),
275    }
276}