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