Skip to main content

qframe/widgets/
code_view.rs

1//! Highlighted code.
2
3use unicode_segmentation::UnicodeSegmentation;
4
5use super::cells;
6use super::highlight::{Language, Token, highlight};
7use crate::event::Event;
8use crate::geometry::{Rect, Size, clamp_u16};
9use crate::keymap::Key;
10use crate::text;
11use crate::theme::State;
12use crate::widget::{EventCx, MeasureCx, PaintCx, Widget};
13
14/// One visual row of code: a line number on the first row of a source line, and coloured pieces.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub(crate) struct CodeRow {
17    pub(crate) number: Option<usize>,
18    pub(crate) pieces: Vec<(String, Token)>,
19}
20
21/// Lays `code` out in rows no wider than `width` cells, wrapping long lines. Continuation
22/// rows are indented by two cells.
23pub(crate) fn code_rows(code: &str, language: Language, width: u16) -> Vec<CodeRow> {
24    let tokens = highlight(code, language);
25    let mut rows = Vec::new();
26    let mut line_start = 0;
27    for (index, line) in code.split('\n').enumerate() {
28        let line_end = line_start + line.len();
29        let mut row = CodeRow { number: Some(index + 1), pieces: Vec::new() };
30        let mut used = 0u16;
31        for (range, token) in &tokens {
32            let start = range.start.max(line_start);
33            let end = range.end.min(line_end);
34            if start >= end {
35                continue;
36            }
37            for grapheme in code[start..end].graphemes(true) {
38                let cell = if grapheme == "\t" { "    " } else { grapheme };
39                let w = text::width(cell);
40                if used.saturating_add(w) > width && used > 0 {
41                    rows.push(std::mem::replace(
42                        &mut row,
43                        CodeRow { number: None, pieces: vec![("  ".to_owned(), Token::Plain)] },
44                    ));
45                    used = 2;
46                }
47                match row.pieces.last_mut() {
48                    Some((piece, last)) if last == token => piece.push_str(cell),
49                    _ => row.pieces.push((cell.to_owned(), *token)),
50                }
51                used = used.saturating_add(w);
52            }
53        }
54        rows.push(row);
55        line_start = line_end + 1;
56    }
57    if code.ends_with('\n') {
58        rows.pop();
59    }
60    rows
61}
62
63/// Paints `rows` in `area` using the `code-token.<kind>` and `code-line-number` styles.
64pub(crate) fn paint_rows(cx: &mut PaintCx<'_>, area: Rect, rows: &[CodeRow], gutter: u16) {
65    for (y, row) in rows.iter().enumerate() {
66        let Ok(y) = u16::try_from(y) else { break };
67        if y >= area.height {
68            break;
69        }
70        let row_y = area.y + i32::from(y);
71        // Line numbers help reading, not copying: clean copies leave the gutter out.
72        if gutter > 0 {
73            cx.decoration(Rect::new(area.x, row_y, gutter, 1));
74        }
75        if gutter > 0
76            && let Some(number) = row.number
77        {
78            let style = cx.style("code-line-number", None, &[]).text();
79            let label = format!("{number:>width$}", width = usize::from(gutter - 2));
80            cx.text(area.x, row_y, &label, style, gutter);
81        }
82        let mut x = area.x + i32::from(gutter);
83        for (piece, token) in &row.pieces {
84            let style = cx.style("code-token", Some(token.variant()), &[]).text();
85            x += i32::from(cx.text(x, row_y, piece, style, area.right().saturating_sub(x).try_into().unwrap_or(0)));
86        }
87    }
88}
89
90/// Marks the padding of a code block at `rect` around `inner` as decoration, so clean copies of
91/// a selection across the block keep only the code.
92pub(crate) fn padding_decoration(cx: &mut PaintCx<'_>, rect: Rect, inner: Rect) {
93    cx.decoration(Rect::new(rect.x, rect.y, rect.width, clamp_u16(inner.y - rect.y)));
94    cx.decoration(Rect::new(rect.x, inner.bottom(), rect.width, clamp_u16(rect.bottom() - inner.bottom())));
95    cx.decoration(Rect::new(rect.x, inner.y, clamp_u16(inner.x - rect.x), inner.height));
96    cx.decoration(Rect::new(inner.right(), inner.y, clamp_u16(rect.right() - inner.right()), inner.height));
97}
98
99/// Width of the line number column for `code`, including two cells of spacing.
100pub(crate) fn gutter_width(code: &str) -> u16 {
101    let lines = code.split('\n').count();
102    text::width(&lines.to_string()).saturating_add(2)
103}
104
105/// Code with syntax colours, line numbers and wrapping of long lines.
106///
107/// While focused, `c` copies the code to the clipboard and flashes. The code is a text selection
108/// region: a mouse drag selects inside it (turn it off with
109/// [`NodeMut::selectable`](crate::widget::NodeMut::selectable)), and clean copies leave out the
110/// line numbers. Style keys: `code` (`bg`,
111/// `padding`) with `focus` and `pressed`; `code-line-number`; `code-token.<kind>` where kind is
112/// `keyword`, `type`, `function`, `macro`, `string`, `number`, `comment`, `attribute`,
113/// `lifetime`, `punctuation`, `table`, `key` or `plain`.
114pub struct CodeView<Msg> {
115    code: String,
116    language: Language,
117    line_numbers: bool,
118    on_copy: Option<Msg>,
119}
120
121impl<Msg: 'static> CodeView<Msg> {
122    /// Shows `code` in `language`.
123    #[must_use]
124    pub fn new(code: impl Into<String>, language: Language) -> Self {
125        Self { code: code.into(), language, line_numbers: true, on_copy: None }
126    }
127
128    /// Shows or hides line numbers; shown by default.
129    #[must_use]
130    pub fn line_numbers(mut self, show: bool) -> Self {
131        self.line_numbers = show;
132        self
133    }
134
135    /// Message sent after the code was copied with `c`.
136    #[must_use]
137    pub fn on_copy(mut self, message: Msg) -> Self {
138        self.on_copy = Some(message);
139        self
140    }
141
142    fn gutter(&self) -> u16 {
143        if self.line_numbers { gutter_width(&self.code) } else { 0 }
144    }
145}
146
147impl<Msg: Clone + 'static> Widget<Msg> for CodeView<Msg> {
148    fn measure(&self, cx: &mut MeasureCx<'_>, available: Size) -> Size {
149        let padding = cx.env().theme().style("code", None, &[]).pair("padding").unwrap_or((1, 2));
150        let content_width = available.width.saturating_sub(cells::sum([padding.1.saturating_mul(2), self.gutter()]));
151        let rows = code_rows(&self.code, self.language, content_width.max(1));
152        let widest = rows
153            .iter()
154            .map(|row| cells::sum(row.pieces.iter().map(|(piece, _)| text::width(piece))))
155            .max()
156            .unwrap_or(0);
157        Size::new(
158            cells::sum([widest, self.gutter(), padding.1.saturating_mul(2)]),
159            clamp_u16(i32::try_from(rows.len()).unwrap_or(i32::MAX)).saturating_add(padding.0.saturating_mul(2)),
160        )
161        .min(available)
162    }
163
164    fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
165        let mut states = cx.states();
166        states.retain(|state| *state != State::Hover);
167        let style = cx.style("code", None, &states);
168        if let Some(bg) = style.text().bg {
169            cx.clear(area, bg);
170        }
171        cx.register_hit(area);
172        let inner = area.inset(style.padding());
173        // The padding is surface, not code: a selection starts and stays inside it.
174        cx.selectable(inner);
175        let gutter = self.gutter();
176        let rows = code_rows(&self.code, self.language, inner.width.saturating_sub(gutter).max(1));
177        paint_rows(cx, inner, &rows, gutter);
178    }
179
180    fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
181        let Event::Key(key) = event else {
182            return false;
183        };
184        if !key.is_plain(Key::Char('c')) {
185            return false;
186        }
187        cx.copy(self.code.clone());
188        cx.flash();
189        if let Some(message) = &self.on_copy {
190            cx.emit(message.clone());
191        }
192        true
193    }
194
195    fn focusable(&self) -> bool {
196        true
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use crate::runtime::{App, Command, Harness};
204    use crate::widget::View;
205
206    struct Demo {
207        copies: u32,
208    }
209
210    impl App for Demo {
211        type Msg = ();
212        fn update(&mut self, _: ()) -> Command<()> {
213            self.copies += 1;
214            Command::none()
215        }
216        fn view(&self, ui: &mut View<'_, ()>) {
217            let code = "fn main() {\n    println!(\"a fairly long line that wraps\");\n}\n";
218            ui.add(CodeView::new(code, Language::Rust).on_copy(())).fill();
219        }
220    }
221
222    #[test]
223    fn numbers_colours_and_wraps() {
224        let h = Harness::new(Demo { copies: 0 }, 36, 7);
225        let screen = h.screen();
226        assert_eq!(
227            screen,
228            "\n  1  fn main() {\n  2      println!(\"a fairly long l\n       ine that wraps\");\n  3  }\n\n\n"
229        );
230        let keyword = h.env().theme().style("code-token", Some("keyword"), &[]).paint("fg");
231        assert!(keyword.is_some());
232        let (x, y) = h.find("fn").map(|(x, y)| (x as u16, y as u16)).unwrap_or_default();
233        assert_eq!(h.fg(x, y), h.env().theme().color("accent"));
234    }
235
236    #[test]
237    fn copies_on_c() {
238        let mut h = Harness::new(Demo { copies: 0 }, 40, 6);
239        h.press("tab").press("c");
240        assert_eq!(h.app().copies, 1);
241        assert!(h.copied()[0].starts_with("fn main()"));
242    }
243}