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    /// Calculates the necessary width for the configuration.
203    #[deprecated(since = "1.1.0", note = "use width_for()")]
204    pub fn width(&self) -> u16 {
205        let nr_width = if let Some(text_area) = self.text_area {
206            (text_area.vscroll.offset() + 50).ilog10() as u16 + 1
207        } else if let Some(end) = self.end {
208            end.ilog10() as u16 + 1
209        } else if let Some(start) = self.start {
210            (start + 50).ilog10() as u16 + 1
211        } else {
212            3
213        };
214
215        let flag_width = if let Some(flag_width) = self.flag_width {
216            flag_width
217        } else {
218            self.flags
219                .iter()
220                .map(|v| v.width() as u16)
221                .max()
222                .unwrap_or_default()
223        };
224
225        let block_width = {
226            let area = self.block.inner_if_some(Rect::new(0, 0, 2, 2));
227            2 - area.width
228        };
229
230        nr_width + flag_width + self.margin.0 + self.margin.1 + block_width + 1
231    }
232
233    /// Required width for the line-numbers.
234    pub fn width_for(start_nr: usize, flag_width: u16, margin: (u16, u16), block: u16) -> u16 {
235        let nr_width = (start_nr + 50).ilog10() as u16 + 1;
236        nr_width + flag_width + margin.0 + margin.1 + block + 1
237    }
238}
239
240impl Default for LineNumberStyle {
241    fn default() -> Self {
242        Self {
243            flag_width: None,
244            margin: None,
245            format: None,
246            style: Default::default(),
247            cursor: None,
248            block: None,
249            non_exhaustive: NonExhaustive,
250        }
251    }
252}
253
254impl StatefulWidget for LineNumbers<'_> {
255    type State = LineNumberState;
256
257    #[allow(clippy::manual_unwrap_or_default)]
258    #[allow(clippy::manual_unwrap_or)]
259    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
260        state.area = area;
261        state.inner = self.block.inner_if_some(area);
262
263        state.start = if let Some(text_area) = self.text_area {
264            text_area.offset().1 as upos_type
265        } else if let Some(start) = self.start {
266            start
267        } else {
268            0
269        };
270        let end = if let Some(text_area) = self.text_area {
271            text_area.len_lines()
272        } else if let Some(end) = self.end {
273            end
274        } else {
275            state.start + state.inner.height as upos_type
276        };
277
278        let nr_width = if let Some(text_area) = self.text_area {
279            (text_area.vscroll.offset() + 50).ilog10() as u16 + 1
280        } else if let Some(end) = self.end {
281            end.ilog10() as u16 + 1
282        } else if let Some(start) = self.start {
283            (start + 50).ilog10() as u16 + 1
284        } else {
285            3
286        };
287
288        let flag_width = if let Some(flag_width) = self.flag_width {
289            flag_width
290        } else {
291            self.flags
292                .iter()
293                .map(|v| v.width() as u16)
294                .max()
295                .unwrap_or_default()
296        };
297
298        let format = if let Some(format) = self.format {
299            format
300        } else {
301            let mut f = "#".repeat(nr_width.saturating_sub(1) as usize);
302            f.push('0');
303            NumberFormat::new(f).expect("valid")
304        };
305
306        let cursor_style = if let Some(cursor_style) = self.cursor_style {
307            cursor_style
308        } else {
309            self.style
310        };
311
312        if let Some(block) = self.block {
313            block.render(area, buf);
314        } else {
315            buf.set_style(area, self.style);
316        }
317
318        let cursor = if let Some(text_area) = self.text_area {
319            text_area.cursor()
320        } else if let Some(cursor) = self.cursor {
321            TextPosition::new(0, cursor)
322        } else {
323            TextPosition::new(0, upos_type::MAX)
324        };
325
326        let mut tmp = String::new();
327        let mut prev_nr = upos_type::MAX;
328
329        for y in state.inner.top()..state.inner.bottom() {
330            let nr;
331            let rel_nr;
332            let render_nr;
333            let render_cursor;
334
335            if let Some(text_area) = self.text_area {
336                let rel_y = y - state.inner.y;
337                if let Some(pos) = text_area.relative_screen_to_pos((0, rel_y as i16)) {
338                    nr = pos.y;
339                    if self.relative {
340                        rel_nr = nr.abs_diff(cursor.y);
341                    } else {
342                        rel_nr = nr;
343                    }
344                    render_nr = pos.y != prev_nr;
345                    render_cursor = pos.y == cursor.y;
346                } else {
347                    nr = 0;
348                    rel_nr = 0;
349                    render_nr = false;
350                    render_cursor = false;
351                }
352            } else {
353                nr = state.start + (y - state.inner.y) as upos_type;
354                render_nr = nr < end;
355                render_cursor = Some(nr) == self.cursor;
356                if self.relative {
357                    rel_nr = nr.abs_diff(self.cursor.unwrap_or_default());
358                } else {
359                    rel_nr = nr;
360                }
361            }
362
363            tmp.clear();
364            if render_nr {
365                _ = format.fmt_to(rel_nr, &mut tmp);
366            }
367
368            let style = if render_cursor {
369                cursor_style
370            } else {
371                self.style
372            };
373
374            let nr_area = Rect::new(
375                state.inner.x + self.margin.0, //
376                y,
377                nr_width,
378                1,
379            )
380            .intersection(area);
381            buf.set_stringn(nr_area.x, nr_area.y, &tmp, nr_area.width as usize, style);
382
383            if let Some(flags) = self.flags.get((y - state.inner.y) as usize) {
384                flags.render(
385                    Rect::new(
386                        state.inner.x + self.margin.0 + nr_width + 1,
387                        y,
388                        flag_width,
389                        1,
390                    ),
391                    buf,
392                );
393            }
394
395            prev_nr = nr;
396        }
397    }
398}
399
400impl Default for LineNumberState {
401    fn default() -> Self {
402        Self {
403            area: Default::default(),
404            inner: Default::default(),
405            start: 0,
406            mouse: Default::default(),
407            non_exhaustive: NonExhaustive,
408        }
409    }
410}
411
412impl LineNumberState {
413    pub fn new() -> Self {
414        Self::default()
415    }
416}