rat_text/
line_number.rs

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