Skip to main content

duat_base/widgets/
line_numbers.rs

1//! Line numbers for a [`Buffer`]
2//!
3//! These are pretty standard like in most text editors. Usually,
4//! they'll be printed on the right of the [`Buffer`], 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//! selection's line number.
11//!
12//! [`Buffer`]: duat_core::buffer::Buffer
13use std::{fmt::Alignment, sync::Once};
14
15use duat_core::{
16    buffer::Buffer,
17    context::Handle,
18    data::Pass,
19    form,
20    hook::{self, BufferUpdated, OnMouseEvent},
21    mode::{MouseButton, MouseEventKind},
22    text::{Builder, Spacer, Text, TextMut},
23    ui::{PushSpecs, Side, Widget},
24};
25
26/// Shows a column of line numbers beside the [`Buffer`]
27///
28/// There are various fields that you can use to configure how the
29/// `LineNumbers` will be displayed. They control things like the
30/// line numbers and the relativeness of the number displayed.
31///
32/// This is a default struct of Duat, that is, it is automatically
33/// placed around every `Buffer`, but you can disable that behavior
34/// by [removing] the `"BufferWidgets"` hook.
35///
36/// [`Buffer`]: duat_core::buffer::Buffer
37/// [removing]: duat_core::hook::remove
38pub struct LineNumbers {
39    text: Text,
40    /// Wether to show relative numbering
41    ///
42    /// The default is `false`
43    pub relative: bool,
44    /// Where to align the numbers
45    ///
46    /// The default is [`Alignment::Left`]
47    pub align: Alignment,
48    /// Where to align the main line number
49    ///
50    /// The default is [`Alignment::Right`]
51    pub main_align: Alignment,
52    /// Wether to show wrapped line's numbers
53    ///
54    /// The default is `false`
55    pub show_wraps: bool,
56}
57
58impl LineNumbers {
59    /// Returns a [`LineNumbersOpts`], used to create a new
60    /// `LineNumbers`
61    pub fn builder() -> LineNumbersOpts {
62        LineNumbersOpts::default()
63    }
64
65    /// The minimum width that would be needed to show the last line.
66    fn calculate_width(&self, pa: &Pass, buffer: &Handle) -> f32 {
67        let len = buffer.read(pa).text().end_point().line();
68        len.ilog10() as f32
69    }
70
71    fn form_text(&self, pa: &Pass, buffer: &Handle) -> Text {
72        let (main_line_num, printed_line_numbers) = {
73            let printed_line_numbers = buffer.printed_line_numbers(pa);
74            let buf = buffer.read(pa);
75
76            let main_line = if buf.selections().is_empty() {
77                usize::MAX
78            } else {
79                buf.selections().main().caret().line()
80            };
81
82            (main_line, printed_line_numbers)
83        };
84
85        let mut builder = Text::builder();
86        let mut last_was_ghost = false;
87
88        for (idx, line) in printed_line_numbers.iter().enumerate() {
89            if line.is_ghost {
90                last_was_ghost = true;
91                builder.push("\n");
92                continue;
93            }
94
95            let align = if line.number == main_line_num {
96                self.main_align
97            } else {
98                self.align
99            };
100
101            if align != Alignment::Left {
102                builder.push(Spacer);
103            }
104
105			let is_wrapped = line.is_wrapped && idx > 0 && !last_was_ghost;
106            match (line.number == main_line_num, is_wrapped) {
107                (false, false) => {}
108                (true, false) => builder.push(form::id_of!("linenum.main")),
109                (false, true) => builder.push(form::id_of!("linenum.wrapped")),
110                (true, true) => builder.push(form::id_of!("linenum.wrapped.main")),
111            }
112            
113            push_text(&mut builder, line.number, main_line_num, is_wrapped, self);
114
115            if align == Alignment::Center {
116                builder.push(Spacer);
117            }
118
119            builder.push("\n");
120            builder.push(form::DEFAULT_ID);
121            last_was_ghost = false;
122        }
123
124        builder.build()
125    }
126}
127
128impl Widget for LineNumbers {
129    fn text(&self) -> &Text {
130        &self.text
131    }
132
133    fn text_mut(&mut self) -> TextMut<'_> {
134        self.text.as_mut()
135    }
136}
137
138/// Options for cosntructing a [`LineNumbers`] [`Widget`]
139///
140/// For most options, you can just set them in the `Widget`
141/// directly (through a [hook] or something). Right now, the
142/// only option exclusive to this struct is the [`on_the_right`]
143/// option, which places the `LineNumbers` on the right, as
144/// opposed to on the left.
145///
146/// [`on_the_right`]: Self::on_the_right
147/// [hook]: duat_core::hook
148#[derive(Clone, Copy, Debug)]
149pub struct LineNumbersOpts {
150    /// Wether to show relative numbering
151    ///
152    /// The default is `false`
153    pub relative: bool,
154    /// Where to align the numbers
155    ///
156    /// The default is [`Alignment::Left`]
157    pub align: Alignment,
158    /// Where to align the main line number
159    ///
160    /// The default is [`Alignment::Right`]
161    pub main_align: Alignment,
162    /// Wether to show wrapped line's numbers
163    ///
164    /// The default is `false`
165    pub show_wraps: bool,
166    /// Place this [`Widget`] on the right, as opposed to on the left
167    ///
168    /// The default is `false`
169    pub on_the_right: bool,
170}
171
172impl LineNumbersOpts {
173    /// Retunrs a new `LineNumbersOpts`
174    pub const fn new() -> Self {
175        Self {
176            relative: false,
177            align: Alignment::Left,
178            main_align: Alignment::Right,
179            show_wraps: false,
180            on_the_right: false,
181        }
182    }
183
184    /// Push the [`LineNumbers`] to a [`Handle`]
185    ///
186    /// The [`Widget`] will be pushed on the "outside". That is, if
187    /// there are other widgets pushed on the buffer, this one will be
188    /// placed around them.
189    pub fn push_on(self, pa: &mut Pass, buffer: &Handle) -> Handle<LineNumbers> {
190        static ONCE: Once = Once::new();
191        ONCE.call_once(|| {
192            hook::add::<BufferUpdated>(|pa, buffer| {
193                for (linenumbers, _) in buffer.get_related::<LineNumbers>(pa) {
194                    let width = linenumbers.read(pa).calculate_width(pa, buffer);
195                    linenumbers.area().set_width(pa, width + 1.0).unwrap();
196
197                    linenumbers.write(pa).text = linenumbers.read(pa).form_text(pa, buffer);
198                }
199            })
200            .lateness(usize::MAX);
201
202            hook::add::<OnMouseEvent<LineNumbers>>(|pa, event| {
203                let line = |pa, handle: &Handle| {
204                    let lines = handle.printed_line_numbers(pa);
205                    event
206                        .points
207                        .and_then(|tpp| lines.get(tpp.points().real.line()))
208                        .map(|line| line.number)
209                        .unwrap_or(handle.text(pa).end_point().line())
210                };
211
212                let (buffer, _) = event.handle.get_related::<Buffer>(pa).remove(0);
213
214                match event.kind {
215                    MouseEventKind::Down(MouseButton::Left) => {
216                        let line = line(pa, &buffer);
217
218                        buffer.selections_mut(pa).remove_extras();
219                        buffer.edit_main(pa, |mut c| {
220                            c.unset_anchor();
221                            c.move_to_coords(line, 0)
222                        })
223                    }
224                    MouseEventKind::Drag(MouseButton::Left) => {
225                        let line = line(pa, &buffer);
226
227                        buffer.selections_mut(pa).remove_extras();
228                        buffer.edit_main(pa, |mut c| {
229                            c.set_anchor_if_needed();
230                            c.move_to_coords(line, 0)
231                        })
232                    }
233                    MouseEventKind::ScrollDown => {
234                        let opts = buffer.opts(pa);
235                        let (buf, area) = buffer.write_with_area(pa);
236
237                        area.scroll_ver(buf.text(), 3, opts);
238                    }
239                    MouseEventKind::ScrollUp => {
240                        let opts = buffer.opts(pa);
241                        let (buf, area) = buffer.write_with_area(pa);
242
243                        area.scroll_ver(buf.text(), -3, opts);
244                    }
245                    _ => {}
246                }
247            });
248        });
249
250        let mut linenumbers = LineNumbers {
251            text: Text::default(),
252            relative: self.relative,
253            align: self.align,
254            main_align: self.main_align,
255            show_wraps: self.show_wraps,
256        };
257        linenumbers.text = linenumbers.form_text(pa, buffer);
258
259        let specs = PushSpecs {
260            side: if self.on_the_right {
261                Side::Right
262            } else {
263                Side::Left
264            },
265            ..Default::default()
266        };
267
268        let linenumbers = buffer.push_outer_widget(pa, linenumbers, specs);
269
270        let width = linenumbers.read(pa).calculate_width(pa, buffer);
271        linenumbers.area().set_width(pa, width + 1.0).unwrap();
272
273        linenumbers
274    }
275}
276
277impl Default for LineNumbersOpts {
278    fn default() -> Self {
279        Self::new()
280    }
281}
282
283/// Writes the text of the line number to a given [`String`].
284fn push_text(b: &mut Builder, line: usize, main: usize, is_wrapped: bool, opts: &LineNumbers) {
285    if (!is_wrapped || opts.show_wraps) && main != usize::MAX {
286        b.push(if opts.relative {
287            if line != main {
288                line.abs_diff(main)
289            } else {
290                line + 1
291            }
292        } else {
293            line + 1
294        });
295    }
296}