rat_text/
line_number.rs

1//!
2//! Line numbers widget.
3//!
4
5use crate::_private::NonExhaustive;
6use crate::text_area::TextAreaState;
7use crate::{TextPosition, upos_type};
8use format_num_pattern::NumberFormat;
9use rat_event::util::MouseFlags;
10use ratatui::buffer::Buffer;
11use ratatui::layout::Rect;
12use ratatui::prelude::BlockExt;
13use ratatui::style::Style;
14use ratatui::text::Line;
15use ratatui::widgets::StatefulWidget;
16use ratatui::widgets::{Block, Widget};
17
18/// Renders line-numbers.
19///
20/// # Stateful
21/// This widget implements [`StatefulWidget`], you can use it with
22/// [`LineNumberState`] to handle common actions.
23#[derive(Debug, Default, Clone)]
24pub struct LineNumbers<'a> {
25    start: Option<upos_type>,
26    end: Option<upos_type>,
27    cursor: Option<upos_type>,
28    text_area: Option<&'a TextAreaState>,
29
30    relative: bool,
31    flags: Vec<Line<'a>>,
32    flag_width: Option<u16>,
33    margin: (u16, u16),
34
35    format: Option<NumberFormat>,
36    style: Style,
37    cursor_style: Option<Style>,
38
39    block: Option<Block<'a>>,
40}
41
42/// Styles as a package.
43#[derive(Debug, Clone)]
44pub struct LineNumberStyle {
45    pub flag_width: Option<u16>,
46    pub margin: Option<(u16, u16)>,
47    pub format: Option<NumberFormat>,
48    pub style: Style,
49    pub cursor: Option<Style>,
50    pub block: Option<Block<'static>>,
51
52    pub non_exhaustive: NonExhaustive,
53}
54
55/// State
56#[derive(Debug, Clone)]
57pub struct LineNumberState {
58    pub area: Rect,
59    pub inner: Rect,
60
61    /// First rendered line-number
62    pub start: upos_type,
63
64    /// Helper for mouse.
65    pub mouse: MouseFlags,
66
67    pub non_exhaustive: NonExhaustive,
68}
69
70impl<'a> LineNumbers<'a> {
71    pub fn new() -> Self {
72        Self::default()
73    }
74
75    /// Sync with this text-area.
76    ///
77    /// To make this work correctly, the TextArea must be rendered
78    /// first to make sure that all layout-information stored in the
79    /// state is accurate.
80    pub fn with_textarea(mut self, text_area: &'a TextAreaState) -> Self {
81        self.text_area = Some(text_area);
82        self
83    }
84
85    /// Start position.
86    pub fn start(mut self, start: upos_type) -> Self {
87        self.start = Some(start);
88        self
89    }
90
91    /// End position.
92    pub fn end(mut self, end: upos_type) -> Self {
93        self.end = Some(end);
94        self
95    }
96
97    /// Current line for highlighting.
98    pub fn cursor(mut self, cursor: upos_type) -> Self {
99        self.cursor = Some(cursor);
100        self
101    }
102
103    /// Numbering relative to cursor
104    pub fn relative(mut self, relative: bool) -> Self {
105        self.relative = relative;
106        self
107    }
108
109    /// Extra info.
110    pub fn flags(mut self, flags: Vec<Line<'a>>) -> Self {
111        self.flags = flags;
112        self
113    }
114
115    /// Required width for the flags.
116    pub fn flag_width(mut self, width: u16) -> Self {
117        self.flag_width = Some(width);
118        self
119    }
120
121    /// Extra margin as (left-margin, right-margin).
122    pub fn margin(mut self, margin: (u16, u16)) -> Self {
123        self.margin = margin;
124        self
125    }
126
127    /// Line number format.
128    pub fn format(mut self, format: NumberFormat) -> Self {
129        self.format = Some(format);
130        self
131    }
132
133    /// Complete set of styles.
134    pub fn styles(mut self, styles: LineNumberStyle) -> Self {
135        self.style = styles.style;
136        if let Some(flag_width) = styles.flag_width {
137            self.flag_width = Some(flag_width);
138        }
139        if let Some(margin) = styles.margin {
140            self.margin = margin;
141        }
142        if let Some(format) = styles.format {
143            self.format = Some(format);
144        }
145        if let Some(cursor_style) = styles.cursor {
146            self.cursor_style = Some(cursor_style);
147        }
148        if let Some(block) = styles.block {
149            self.block = Some(block);
150        }
151        self.block = self.block.map(|v| v.style(self.style));
152        self
153    }
154
155    /// Base style.
156    pub fn style(mut self, style: Style) -> Self {
157        self.style = style;
158        self.block = self.block.map(|v| v.style(style));
159        self
160    }
161
162    /// Style for current line.
163    pub fn cursor_style(mut self, style: Style) -> Self {
164        self.cursor_style = Some(style);
165        self
166    }
167
168    /// Block.
169    pub fn block(mut self, block: Block<'a>) -> Self {
170        self.block = Some(block.style(self.style));
171        self
172    }
173
174    /// Calculates the necessary width for the configuration.
175    #[deprecated(since = "1.1.0", note = "use width_for()")]
176    pub fn width(&self) -> u16 {
177        let nr_width = if let Some(text_area) = self.text_area {
178            (text_area.vscroll.offset() + 50).ilog10() as u16 + 1
179        } else if let Some(end) = self.end {
180            end.ilog10() as u16 + 1
181        } else if let Some(start) = self.start {
182            (start + 50).ilog10() as u16 + 1
183        } else {
184            3
185        };
186
187        let flag_width = if let Some(flag_width) = self.flag_width {
188            flag_width
189        } else {
190            self.flags
191                .iter()
192                .map(|v| v.width() as u16)
193                .max()
194                .unwrap_or_default()
195        };
196
197        let block_width = {
198            let area = self.block.inner_if_some(Rect::new(0, 0, 2, 2));
199            2 - area.width
200        };
201
202        nr_width + flag_width + self.margin.0 + self.margin.1 + block_width + 1
203    }
204
205    /// Required width for the line-numbers.
206    pub fn width_for(start_nr: usize, flag_width: u16, margin: (u16, u16), block: u16) -> u16 {
207        let nr_width = (start_nr + 50).ilog10() as u16 + 1;
208        nr_width + flag_width + margin.0 + margin.1 + block + 1
209    }
210}
211
212impl Default for LineNumberStyle {
213    fn default() -> Self {
214        Self {
215            flag_width: None,
216            margin: None,
217            format: None,
218            style: Default::default(),
219            cursor: None,
220            block: None,
221            non_exhaustive: NonExhaustive,
222        }
223    }
224}
225
226impl StatefulWidget for LineNumbers<'_> {
227    type State = LineNumberState;
228
229    #[allow(clippy::manual_unwrap_or_default)]
230    #[allow(clippy::manual_unwrap_or)]
231    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
232        state.area = area;
233        state.inner = self.block.inner_if_some(area);
234
235        state.start = if let Some(text_area) = self.text_area {
236            text_area.offset().1 as upos_type
237        } else if let Some(start) = self.start {
238            start
239        } else {
240            0
241        };
242        let end = if let Some(text_area) = self.text_area {
243            text_area.len_lines()
244        } else if let Some(end) = self.end {
245            end
246        } else {
247            state.start + state.inner.height as upos_type
248        };
249
250        let nr_width = if let Some(text_area) = self.text_area {
251            (text_area.vscroll.offset() + 50).ilog10() as u16 + 1
252        } else if let Some(end) = self.end {
253            end.ilog10() as u16 + 1
254        } else if let Some(start) = self.start {
255            (start + 50).ilog10() as u16 + 1
256        } else {
257            3
258        };
259
260        let flag_width = if let Some(flag_width) = self.flag_width {
261            flag_width
262        } else {
263            self.flags
264                .iter()
265                .map(|v| v.width() as u16)
266                .max()
267                .unwrap_or_default()
268        };
269
270        let format = if let Some(format) = self.format {
271            format
272        } else {
273            let mut f = "#".repeat(nr_width.saturating_sub(1) as usize);
274            f.push('0');
275            NumberFormat::new(f).expect("valid")
276        };
277
278        let cursor_style = if let Some(cursor_style) = self.cursor_style {
279            cursor_style
280        } else {
281            self.style
282        };
283
284        if let Some(block) = self.block {
285            block.render(area, buf);
286        } else {
287            buf.set_style(area, self.style);
288        }
289
290        let cursor = if let Some(text_area) = self.text_area {
291            text_area.cursor()
292        } else if let Some(cursor) = self.cursor {
293            TextPosition::new(0, cursor)
294        } else {
295            TextPosition::new(0, upos_type::MAX)
296        };
297
298        let mut tmp = String::new();
299        let mut prev_nr = upos_type::MAX;
300
301        for y in state.inner.top()..state.inner.bottom() {
302            let nr;
303            let rel_nr;
304            let render_nr;
305            let render_cursor;
306
307            if let Some(text_area) = self.text_area {
308                let rel_y = y - state.inner.y;
309                if let Some(pos) = text_area.relative_screen_to_pos((0, rel_y as i16)) {
310                    nr = pos.y;
311                    if self.relative {
312                        rel_nr = nr.abs_diff(cursor.y);
313                    } else {
314                        rel_nr = nr;
315                    }
316                    render_nr = pos.y != prev_nr;
317                    render_cursor = pos.y == cursor.y;
318                } else {
319                    nr = 0;
320                    rel_nr = 0;
321                    render_nr = false;
322                    render_cursor = false;
323                }
324            } else {
325                nr = state.start + (y - state.inner.y) as upos_type;
326                render_nr = nr < end;
327                render_cursor = Some(nr) == self.cursor;
328                if self.relative {
329                    rel_nr = nr.abs_diff(self.cursor.unwrap_or_default());
330                } else {
331                    rel_nr = nr;
332                }
333            }
334
335            tmp.clear();
336            if render_nr {
337                _ = format.fmt_to(rel_nr, &mut tmp);
338            }
339
340            let style = if render_cursor {
341                cursor_style
342            } else {
343                self.style
344            };
345
346            let nr_area = Rect::new(
347                state.inner.x + self.margin.0, //
348                y,
349                nr_width,
350                1,
351            )
352            .intersection(area);
353            buf.set_stringn(nr_area.x, nr_area.y, &tmp, nr_area.width as usize, style);
354
355            if let Some(flags) = self.flags.get((y - state.inner.y) as usize) {
356                flags.render(
357                    Rect::new(
358                        state.inner.x + self.margin.0 + nr_width + 1,
359                        y,
360                        flag_width,
361                        1,
362                    ),
363                    buf,
364                );
365            }
366
367            prev_nr = nr;
368        }
369    }
370}
371
372impl Default for LineNumberState {
373    fn default() -> Self {
374        Self {
375            area: Default::default(),
376            inner: Default::default(),
377            start: 0,
378            mouse: Default::default(),
379            non_exhaustive: NonExhaustive,
380        }
381    }
382}
383
384impl LineNumberState {
385    pub fn new() -> Self {
386        Self::default()
387    }
388}