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