1use 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#[derive(Debug, Clone, PartialEq, Eq)]
16pub(crate) struct CodeRow {
17 pub(crate) number: Option<usize>,
18 pub(crate) pieces: Vec<(String, Token)>,
19}
20
21pub(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
63pub(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 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
90pub(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
99pub(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
105pub 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 #[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 #[must_use]
130 pub fn line_numbers(mut self, show: bool) -> Self {
131 self.line_numbers = show;
132 self
133 }
134
135 #[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 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}