edtui_papier/
view.rs

1//! The editors state
2pub mod explorer;
3pub mod status_line;
4pub mod theme;
5use std::time::Duration;
6
7use ratatui::prelude::*;
8pub use status_line::StatusLine;
9use synoptic::{trim, TokOpt};
10
11use self::theme::EditorTheme;
12use crate::{helper::max_col, state::EditorState, EditorMode, Index2};
13
14#[derive(Debug, Clone, Default)]
15pub struct EditorMessage {
16    message: String,
17    duration: Duration,
18}
19
20impl EditorMessage {
21    /// Creates a new instance of [`EditorMessage`].
22    #[must_use]
23    pub fn new(message: String, duration: Duration) -> Self {
24        Self { message, duration }
25    }
26}
27
28pub struct EditorView<'a, 'b> {
29    pub(crate) state: &'a mut EditorState,
30    pub(crate) theme: EditorTheme<'b>,
31    pub(crate) message: Option<EditorMessage>,
32}
33
34impl<'a, 'b> EditorView<'a, 'b> {
35    /// Creates a new instance of [`EditorView`].
36    #[must_use]
37    pub fn new(state: &'a mut EditorState) -> Self {
38        Self { state, theme: EditorTheme::default(), message: None }
39    }
40
41    /// Set the theme for the [`EditorView`]
42    /// See [`EditorTheme`] for the customizable parameters.
43    #[must_use]
44    pub fn theme(mut self, theme: EditorTheme<'b>) -> Self {
45        self.theme = theme;
46        self
47    }
48
49    /// Set the message for the [`EditorView`]
50    /// The message is displayed in the status line.
51    /// If the message is `None`, the message is not displayed.
52    /// The message will be displayed for the specified duration.
53    #[must_use]
54    pub fn message(mut self, message: Option<EditorMessage>) -> Self {
55        self.message = message;
56        self
57    }
58
59    /// Returns a reference to the [`EditorState`].
60    #[must_use]
61    pub fn get_state(&'a self) -> &'a EditorState {
62        self.state
63    }
64
65    /// Returns a mutable reference to the [`EditorState`].
66    #[must_use]
67    pub fn get_state_mut(&'a mut self) -> &'a mut EditorState {
68        self.state
69    }
70    fn highlight_colour(&self, name: &str) -> Color {
71        self.theme.highlighting.get(name).unwrap_or(Color::Reset)
72    }
73}
74
75impl Widget for EditorView<'_, '_> {
76    // type State = ViewState;
77    fn render(self, area: Rect, buf: &mut Buffer) {
78        // Draw the border.
79        buf.set_style(area, self.theme.base);
80        let area = match &self.theme.block {
81            Some(b) => {
82                let inner_area = b.inner(area);
83                b.clone().render(area, buf);
84                inner_area
85            },
86            None => area,
87        };
88
89        // Split into main section and status line
90        let [main, status] = Layout::vertical([
91            Constraint::Min(0),
92            Constraint::Length(if self.theme.status_line.is_some() { 1 } else { 0 }),
93        ])
94        .horizontal_margin(1)
95        .areas(area);
96        let [side, _, main] = Layout::horizontal([
97            Constraint::Length(if self.theme.line_numbers_style.is_some() { 3 } else { 0 }),
98            Constraint::Length(if self.theme.line_numbers_style.is_some() { 2 } else { 0 }),
99            Constraint::Min(0),
100        ])
101        .areas(main);
102
103        let width = main.width as usize;
104        let height = main.height as usize;
105
106        // Retrieve the displayed cursor position. The column of the displayed
107        // cursor is clamped to the maximum line length.
108        let cursor = displayed_cursor(self.state);
109
110        // Update the view offset. Requires the screen size and the position
111        // of the cursor. Updates the view offset only if the cursor is out
112        // side of the view port. The state is stored in the `ViewOffset`.
113        let size = (width, height);
114        let (x_off, y_off) = self.state.view.update_offset(size, cursor);
115
116        // Rendering the text and the selection.
117        let lines = &self.state.lines;
118        for (i, line) in lines.iter_row().skip(y_off).take(height).enumerate() {
119            let y = (main.top() as usize) as u16 + i as u16;
120            // Render the line number.
121            if let Some(line_numbers_style) = self.theme.line_numbers_style {
122                let line_number = (y_off + i + 1).to_string();
123                let line_number_x = side.right() - line_number.len() as u16;
124                buf.get_mut(line_number_x, y).set_symbol(&line_number).set_style(line_numbers_style);
125            }
126
127            let tokens = self.state.highlighter.line(y_off + i, &line.iter().collect());
128            let tokens = trim(&tokens, x_off);
129            let mut j = 0;
130            for token in tokens {
131                match token {
132                    TokOpt::Some(text, kind) => {
133                        let color = self.highlight_colour(&kind);
134                        for c in text.chars() {
135                            let x = (main.left() as usize) as u16 + j as u16;
136                            if let Some(selection) = &self.state.selection {
137                                let position = Index2::new(y_off + i, x_off + j);
138                                if selection.within(&position) {
139                                    buf.get_mut(x, y).set_style(self.theme.selection_style);
140                                }
141                            }
142                            if x < main.right() && y < main.bottom() {
143                                buf.get_mut(x, y).set_symbol(&c.to_string()).set_style(Style::default().fg(color));
144                                j += 1;
145                            } else {
146                                break;
147                            }
148                        }
149                    },
150                    TokOpt::None(text) => {
151                        for c in text.chars() {
152                            let x = (main.left() as usize) as u16 + j as u16;
153                            if let Some(selection) = &self.state.selection {
154                                let position = Index2::new(y_off + i, x_off + j);
155                                if selection.within(&position) {
156                                    buf.get_mut(x, y).set_style(self.theme.selection_style);
157                                }
158                            }
159                            if x < main.right() && y < main.bottom() {
160                                buf.get_mut(x, y).set_symbol(&c.to_string());
161                                j += 1;
162                            } else {
163                                break;
164                            }
165                        }
166                    },
167                }
168            }
169        }
170
171        // Rendering of the cursor. Cursor is not rendered in the loop above,
172        // as the cursor may be outside the text in input mode.
173        let x_cursor = (main.left() as usize) + width.min(cursor.col.saturating_sub(x_off));
174        let y_cursor = (main.top() as usize) + cursor.row.saturating_sub(y_off);
175        let cursor_cell = buf.get_mut(x_cursor as u16, y_cursor as u16).set_style(self.theme.cursor_style);
176        if let Some(symbol) = self.theme.cursor_symbol {
177            cursor_cell.set_symbol(&symbol.to_string());
178        }
179
180        // Render the status line.
181        if let Some(s) = self.theme.status_line {
182            s.mode(self.state.mode.name())
183                .search(if self.state.mode == EditorMode::Search {
184                    Some(self.state.search.pattern.clone())
185                } else {
186                    None
187                })
188                .command(if self.state.mode == EditorMode::Command { Some(self.state.command.clone()) } else { None })
189                .render(status, buf);
190        }
191    }
192}
193
194/// Retrieves the displayed cursor position based on the editor state.
195///
196/// Ensures that the displayed cursor position doesn't exceed the line length.
197/// If the internal cursor position exceeds the maximum column, clamp it to
198/// the maximum.
199fn displayed_cursor(state: &EditorState) -> Index2 {
200    let max_col = max_col(&state.lines, &state.cursor, state.mode);
201    Index2::new(state.cursor.row, state.cursor.col.min(max_col))
202}