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