Skip to main content

qframe/widgets/code_view/
mod.rs

1//! Highlighted code.
2
3use std::cell::RefCell;
4use std::ops::{Bound, Range, RangeBounds};
5use std::sync::{Arc, Mutex, PoisonError};
6
7use unicode_segmentation::UnicodeSegmentation;
8
9use super::cells;
10use super::highlight::{Language, Token, highlight};
11use crate::event::Event;
12use crate::geometry::{Rect, Size, clamp_u16};
13use crate::keymap::Key;
14use crate::style::CellStyle;
15use crate::text;
16use crate::theme::State;
17use crate::widget::{EventCx, MeasureCx, PaintCx, Widget};
18
19/// One visual row of code: a line number on the first row of a source line, and coloured pieces.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub(crate) struct CodeRow {
22    /// The source line the row shows part of, counted from 1.
23    pub(crate) line: usize,
24    pub(crate) number: Option<usize>,
25    pub(crate) pieces: Vec<(String, Token)>,
26}
27
28/// Lays `code` out in rows no wider than `width` cells, wrapping long lines. Continuation
29/// rows are indented by two cells.
30pub(crate) fn code_rows(code: &str, language: Language, width: u16) -> Vec<CodeRow> {
31    let tokens = highlight(code, language);
32    let mut rows = Vec::new();
33    let mut line_start = 0;
34    // The tokens cover the text from start to end without gaps or overlaps, so walking them for
35    // every line would read the whole file once per line and cost the square of its size: a
36    // megabyte then takes minutes rather than milliseconds. `first` is the first token that
37    // still reaches this line, and it only ever moves forward. It is not moved inside the loop
38    // below, because a token can span several lines and the next line needs it again.
39    let mut first = 0;
40    for (index, line) in code.split('\n').enumerate() {
41        let line_end = line_start + line.len();
42        while first < tokens.len() && tokens[first].0.end <= line_start {
43            first += 1;
44        }
45        let mut row = CodeRow { line: index + 1, number: Some(index + 1), pieces: Vec::new() };
46        let mut used = 0u16;
47        for (range, token) in tokens[first..].iter().take_while(|(range, _)| range.start < line_end) {
48            let start = range.start.max(line_start);
49            let end = range.end.min(line_end);
50            if start >= end {
51                continue;
52            }
53            for grapheme in code[start..end].graphemes(true) {
54                let cell = if grapheme == "\t" { "    " } else { grapheme };
55                let w = text::width(cell);
56                if used.saturating_add(w) > width && used > 0 {
57                    rows.push(std::mem::replace(
58                        &mut row,
59                        CodeRow { line: index + 1, number: None, pieces: vec![("  ".to_owned(), Token::Plain)] },
60                    ));
61                    used = 2;
62                }
63                match row.pieces.last_mut() {
64                    Some((piece, last)) if last == token => piece.push_str(cell),
65                    _ => row.pieces.push((cell.to_owned(), *token)),
66                }
67                used = used.saturating_add(w);
68            }
69        }
70        rows.push(row);
71        line_start = line_end + 1;
72    }
73    if code.ends_with('\n') {
74        rows.pop();
75    }
76    rows
77}
78
79/// Paints `rows` in `area` using the `code-token.<kind>` and `code-line-number` styles.
80pub(crate) fn paint_rows(cx: &mut PaintCx<'_>, area: Rect, rows: &[CodeRow], gutter: u16) {
81    let visible = visible_rows(cx, area, rows.len());
82    for (y, row) in rows.iter().enumerate().skip(visible.start).take(visible.len()) {
83        let Ok(y) = u16::try_from(y) else { break };
84        if y >= area.height {
85            break;
86        }
87        let row_y = area.y + i32::from(y);
88        // Line numbers help reading, not copying: clean copies leave the gutter out.
89        if gutter > 0 {
90            cx.decoration(Rect::new(area.x, row_y, gutter, 1));
91        }
92        if gutter > 0
93            && let Some(number) = row.number
94        {
95            let style = cx.style("code-line-number", None, &[]).text();
96            let label = format!("{number:>width$}", width = usize::from(gutter - 2));
97            cx.text(area.x, row_y, &label, style, gutter);
98        }
99        let mut x = area.x + i32::from(gutter);
100        for (piece, token) in &row.pieces {
101            let style = cx.style("code-token", Some(token.variant()), &[]).text();
102            x += i32::from(cx.text(x, row_y, piece, style, area.right().saturating_sub(x).try_into().unwrap_or(0)));
103        }
104    }
105}
106
107/// The indices of the `count` rows laid from the top of `area` that fall inside the visible area.
108/// A scroll view hands its content the whole height of the file and clips it to the screen, so a
109/// long file would otherwise style and draw every row it has only to have all but a screenful
110/// thrown away.
111fn visible_rows(cx: &PaintCx<'_>, area: Rect, count: usize) -> Range<usize> {
112    let clip = cx.clip();
113    let top = clip.y.max(area.y);
114    let bottom = clip.bottom().min(area.bottom()).max(top);
115    let row = |y: i32| usize::try_from(y - area.y).unwrap_or(0).min(count);
116    row(top)..row(bottom)
117}
118
119/// Marks the padding of a code block at `rect` around `inner` as decoration, so clean copies of
120/// a selection across the block keep only the code.
121pub(crate) fn padding_decoration(cx: &mut PaintCx<'_>, rect: Rect, inner: Rect) {
122    cx.decoration(Rect::new(rect.x, rect.y, rect.width, clamp_u16(inner.y - rect.y)));
123    cx.decoration(Rect::new(rect.x, inner.bottom(), rect.width, clamp_u16(rect.bottom() - inner.bottom())));
124    cx.decoration(Rect::new(rect.x, inner.y, clamp_u16(inner.x - rect.x), inner.height));
125    cx.decoration(Rect::new(inner.right(), inner.y, clamp_u16(rect.right() - inner.right()), inner.height));
126}
127
128/// Width of the line number column for `code`, including two cells of spacing.
129pub(crate) fn gutter_width(code: &str) -> u16 {
130    gutter_for(code.split('\n').count())
131}
132
133/// Width of the line number column for code of `lines` lines, including two cells of spacing.
134fn gutter_for(lines: usize) -> u16 {
135    text::width(&lines.to_string()).saturating_add(2)
136}
137
138/// How a line of a diff changed; see [`CodeView::line_marks`].
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
140pub enum LineMark {
141    /// The line is in both versions; drawn as any other line.
142    #[default]
143    Unchanged,
144    /// The line is new: a success tint and a `+` sign.
145    Added,
146    /// The line is gone: a danger tint and a `−` sign.
147    Removed,
148}
149
150/// The tone of highlighted lines; see [`CodeView::highlight_lines`].
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
152pub enum LineTone {
153    /// A line to look at, such as the one a "go to line" reached: an accent tint and the pillar.
154    #[default]
155    Accent,
156    /// A line worth a careful look, such as a finding of a review: a warning tint and the
157    /// warning icon.
158    Warning,
159}
160
161impl LineTone {
162    fn variant(self) -> &'static str {
163        match self {
164            Self::Accent => "accent",
165            Self::Warning => "warning",
166        }
167    }
168}
169
170/// The sign of a marked or highlighted line.
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172enum Sign {
173    /// An icon on the first row of the line.
174    Icon(&'static str),
175    /// The theme's pillar on every row of the line.
176    Pillar,
177}
178
179/// Rows of context kept around a revealed line, so it does not land on the very edge of the
180/// scroll view.
181const REVEAL_CONTEXT: u16 = 2;
182
183/// What a code view remembers between frames.
184#[derive(Debug, Default)]
185struct CodeMemory {
186    /// The line last revealed, so the view scrolls to a line once and then lets the user move.
187    revealed: Option<usize>,
188}
189
190/// How many sources a thread remembers. A screen shows a handful of code views at once, and a
191/// guide page in the showcase shows a dozen short ones beside its demo.
192const CACHED_SOURCES: usize = 16;
193
194/// How many widths a source remembers its layout for. A scroll view may measure its content with
195/// and without room for its scrollbar before it paints.
196const CACHED_LAYOUTS: usize = 4;
197
198thread_local! {
199    /// The sources laid out last on this thread, most recently used first.
200    static SOURCES: RefCell<Vec<Arc<Source>>> = const { RefCell::new(Vec::new()) };
201}
202
203/// Code in a language, with the layouts computed for it.
204struct Source {
205    code: String,
206    language: Language,
207    /// The number of source lines, counted once because the gutter's width follows it.
208    lines: usize,
209    /// Layouts at recent widths, most recently used first.
210    layouts: Mutex<Vec<Arc<Layout>>>,
211}
212
213/// Code laid out at one width.
214struct Layout {
215    width: u16,
216    rows: Vec<CodeRow>,
217    /// Cells taken by the widest row.
218    widest: u16,
219}
220
221impl Source {
222    /// The source for `code` in `language`: the remembered one when this thread showed the same
223    /// code recently, otherwise a fresh one that is remembered in place of the oldest.
224    ///
225    /// A view is built anew every frame and a scroll view measures its content more than once,
226    /// so without this a megabyte of code is coloured and wrapped several times a frame.
227    fn cached(code: String, language: Language) -> Arc<Self> {
228        SOURCES.with_borrow_mut(|sources| {
229            let source = match sources.iter().position(|source| source.language == language && source.code == code) {
230                Some(index) => sources.remove(index),
231                None => {
232                    let lines = code.split('\n').count();
233                    Arc::new(Self { code, language, lines, layouts: Mutex::new(Vec::new()) })
234                }
235            };
236            sources.insert(0, Arc::clone(&source));
237            sources.truncate(CACHED_SOURCES);
238            source
239        })
240    }
241
242    /// The code laid out `width` cells wide; computed once per width and remembered.
243    fn layout(&self, width: u16) -> Arc<Layout> {
244        let mut layouts = self.layouts.lock().unwrap_or_else(PoisonError::into_inner);
245        let layout = match layouts.iter().position(|layout| layout.width == width) {
246            Some(index) => layouts.remove(index),
247            None => {
248                let rows = code_rows(&self.code, self.language, width);
249                let widest = rows
250                    .iter()
251                    .map(|row| cells::sum(row.pieces.iter().map(|(piece, _)| text::width(piece))))
252                    .max()
253                    .unwrap_or(0);
254                Arc::new(Layout { width, rows, widest })
255            }
256        };
257        layouts.insert(0, Arc::clone(&layout));
258        layouts.truncate(CACHED_LAYOUTS);
259        layout
260    }
261}
262
263/// Code with syntax colours, line numbers and wrapping of long lines.
264///
265/// Building one in `view` every frame is cheap: the last few sources shown on a thread are
266/// remembered with how they were laid out at the last few widths, and only the rows on screen
267/// are drawn, so an unchanged file is neither coloured nor wrapped again however long it is.
268///
269/// While focused, `c` copies the code to the clipboard and flashes. The code is a text selection
270/// region: a mouse drag selects inside it (turn it off with
271/// [`NodeMut::selectable`](crate::widget::NodeMut::selectable)), and clean copies leave out the
272/// line numbers and the signs of marked lines.
273///
274/// For reviews, [`line_marks`](Self::line_marks) shows a diff and
275/// [`highlight_lines`](Self::highlight_lines) tints lines to look at; either adds a sign column
276/// at the left edge. [`reveal`](Self::reveal) scrolls the enclosing
277/// [`ScrollView`](crate::widgets::ScrollView) to a line.
278///
279/// Style keys: `code` (`bg`, `padding`) with `focus` and `pressed`; `code-line-number`;
280/// `code-token.<kind>` where kind is `keyword`, `type`, `function`, `macro`, `string`,
281/// `number`, `comment`, `attribute`, `lifetime`, `punctuation`, `table`, `key`, `variable` or
282/// `plain`; `code-line.<look>` (`bg` for the line, `fg` for its sign) where look is `added`,
283/// `removed`, `accent` or `warning`. Icons: `line-added`, `line-removed`, `warning` and the
284/// pillar.
285pub struct CodeView<Msg> {
286    source: Arc<Source>,
287    line_numbers: bool,
288    marks: Vec<LineMark>,
289    /// The number each source line carries, when the caller gave them outright.
290    numbers: Option<Vec<Option<usize>>>,
291    highlights: Vec<(usize, usize, LineTone)>,
292    reveal: Option<usize>,
293    reveal_number: Option<usize>,
294    on_copy: Option<Msg>,
295}
296
297impl<Msg: 'static> CodeView<Msg> {
298    /// Shows `code` in `language`, reusing its layout when this thread showed the same code a
299    /// moment ago.
300    #[must_use]
301    pub fn new(code: impl Into<String>, language: Language) -> Self {
302        Self {
303            source: Source::cached(code.into(), language),
304            line_numbers: true,
305            marks: Vec::new(),
306            numbers: None,
307            highlights: Vec::new(),
308            reveal: None,
309            reveal_number: None,
310            on_copy: None,
311        }
312    }
313
314    /// Marks lines as a diff: the first mark belongs to line 1, the next to line 2, and lines
315    /// past the last mark are unchanged. Added lines are tinted with the success colour and
316    /// signed `+`, removed ones with the danger colour and `−`, in the sign column at the left
317    /// edge.
318    ///
319    /// The line numbers then follow the files rather than the text: a diff puts the lines of two
320    /// versions one after another, so counting from the top would number neither file. A removed
321    /// line carries the old file's number, an added line the new file's, and a line in both
322    /// carries the new file's, which is the one a finding such as `PKGBUILD:22` means. Give
323    /// [`line_numbers_from`](Self::line_numbers_from) instead when the diff starts part way into
324    /// the file, and reach a line by its number with [`reveal_number`](Self::reveal_number).
325    #[must_use]
326    pub fn line_marks(mut self, marks: impl IntoIterator<Item = LineMark>) -> Self {
327        self.marks = marks.into_iter().collect();
328        self
329    }
330
331    /// Tints `lines` (counted from 1, like the line numbers) in `tone`, over any diff mark, and
332    /// puts the tone's sign in the sign column. Call it again for more lines; where ranges meet,
333    /// the later call wins. The tint is separate from a text selection, which draws over it.
334    #[must_use]
335    pub fn highlight_lines(mut self, lines: impl RangeBounds<usize>, tone: LineTone) -> Self {
336        let first = match lines.start_bound() {
337            Bound::Included(&n) => n,
338            Bound::Excluded(&n) => n.saturating_add(1),
339            Bound::Unbounded => 1,
340        };
341        let last = match lines.end_bound() {
342            Bound::Included(&n) => n,
343            Bound::Excluded(&n) => n.saturating_sub(1),
344            Bound::Unbounded => usize::MAX,
345        };
346        self.highlights.push((first.max(1), last, tone));
347        self
348    }
349
350    /// Scrolls the enclosing [`ScrollView`](crate::widgets::ScrollView) just enough to show
351    /// `line` (counted from 1; past the end, the last line) with two rows of context, gliding
352    /// there unless motion is reduced. It happens when the revealed line changes, so the user
353    /// can scroll away afterwards; outside a scroll view it does nothing.
354    #[must_use]
355    pub fn reveal(mut self, line: usize) -> Self {
356        self.reveal = Some(line);
357        self
358    }
359
360    /// Shows or hides line numbers; shown by default.
361    #[must_use]
362    pub fn line_numbers(mut self, show: bool) -> Self {
363        self.line_numbers = show;
364        self
365    }
366
367    /// Gives each line its own number outright: the first number belongs to the first line of the
368    /// code, and `None` leaves that line's column blank, as a hunk header has no number of its
369    /// own. Lines past the last number are blank too.
370    ///
371    /// This is for a diff that starts part way into a file, where nothing in the text says the
372    /// hunk began at line 120. A whole-file diff needs only [`line_marks`](Self::line_marks),
373    /// which numbers the lines from the marks. Numbers given here win over that.
374    #[must_use]
375    pub fn line_numbers_from(mut self, numbers: impl IntoIterator<Item = Option<usize>>) -> Self {
376        self.numbers = Some(numbers.into_iter().collect());
377        self
378    }
379
380    /// Scrolls to the line whose number is `number`, the way [`reveal`](Self::reveal) scrolls to
381    /// a line of the text. In a diff the two are not the same line, so this is what a finding
382    /// that names a file and a line asks for.
383    ///
384    /// Two lines can carry one number — the line a version lost and the line that took its place.
385    /// The line the new file numbers that way is the one reached, because that is the file a
386    /// finding is about; a number only a removed line carries reaches that line. A number no line
387    /// carries scrolls nowhere. Given as well as [`reveal`](Self::reveal), this wins.
388    #[must_use]
389    pub fn reveal_number(mut self, number: usize) -> Self {
390        self.reveal_number = Some(number);
391        self
392    }
393
394    /// The number each source line is drawn with: the ones given outright, else the numbers the
395    /// diff marks imply, else the line's own place in the text.
396    fn numbers(&self) -> Vec<Option<usize>> {
397        let lines = self.source.lines;
398        if let Some(given) = &self.numbers {
399            return (0..lines).map(|index| given.get(index).copied().flatten()).collect();
400        }
401        if self.marks.is_empty() {
402            return (1..=lines).map(Some).collect();
403        }
404        let (mut old, mut new) = (0, 0);
405        (0..lines)
406            .map(|index| match self.marks.get(index) {
407                Some(LineMark::Removed) => {
408                    old += 1;
409                    Some(old)
410                }
411                Some(LineMark::Added) => {
412                    new += 1;
413                    Some(new)
414                }
415                Some(LineMark::Unchanged) | None => {
416                    old += 1;
417                    new += 1;
418                    Some(new)
419                }
420            })
421            .collect()
422    }
423
424    /// The source line `number` names, preferring the line the new file numbers that way over one
425    /// the old file lost.
426    fn line_of_number(&self, number: usize) -> Option<usize> {
427        let numbers = self.numbers();
428        let carries = |index: &usize| numbers.get(*index).copied().flatten() == Some(number);
429        let kept = |index: &usize| !matches!(self.marks.get(*index), Some(LineMark::Removed));
430        let index = (0..numbers.len())
431            .find(|index| carries(index) && kept(index))
432            .or_else(|| (0..numbers.len()).find(carries))?;
433        Some(index + 1)
434    }
435
436    /// Message sent after the code was copied with `c`.
437    #[must_use]
438    pub fn on_copy(mut self, message: Msg) -> Self {
439        self.on_copy = Some(message);
440        self
441    }
442
443    fn gutter(&self) -> u16 {
444        if !self.line_numbers {
445            return 0;
446        }
447        if self.numbers.is_none() && self.marks.is_empty() {
448            return gutter_for(self.source.lines);
449        }
450        // A diff's numbers are the files' own, which can be wider than the count of lines shown.
451        let widest = self.numbers().into_iter().flatten().max().unwrap_or(1);
452        text::width(&widest.to_string()).saturating_add(2)
453    }
454
455    /// Width of the sign column: a sign and a space when lines are marked or highlighted.
456    fn signs(&self) -> u16 {
457        if self.marks.is_empty() && self.highlights.is_empty() { 0 } else { 2 }
458    }
459
460    /// How `line` is drawn: the `code-line` variant of its tint and its sign, if any.
461    fn look(&self, line: usize) -> Option<(&'static str, Sign)> {
462        if let Some((_, _, tone)) =
463            self.highlights.iter().rev().find(|(first, last, _)| (*first..=*last).contains(&line))
464        {
465            let sign = match tone {
466                LineTone::Accent => Sign::Pillar,
467                LineTone::Warning => Sign::Icon("warning"),
468            };
469            return Some((tone.variant(), sign));
470        }
471        match self.marks.get(line.checked_sub(1)?) {
472            Some(LineMark::Added) => Some(("added", Sign::Icon("line-added"))),
473            Some(LineMark::Removed) => Some(("removed", Sign::Icon("line-removed"))),
474            Some(LineMark::Unchanged) | None => None,
475        }
476    }
477
478    /// `rows` with each first row of a source line carrying the number that line is drawn with,
479    /// when that is not its place in the text.
480    fn renumbered(&self, rows: &[CodeRow]) -> Option<Vec<CodeRow>> {
481        if self.numbers.is_none() && self.marks.is_empty() {
482            return None;
483        }
484        let numbers = self.numbers();
485        let mut rows = rows.to_vec();
486        for row in &mut rows {
487            if row.number.is_some() {
488                row.number = row.line.checked_sub(1).and_then(|index| numbers.get(index).copied().flatten());
489            }
490        }
491        Some(rows)
492    }
493
494    /// Tints marked and highlighted rows across `area` and draws their signs at `x`.
495    fn paint_looks(&self, cx: &mut PaintCx<'_>, area: Rect, x: i32, top: i32, rows: &[CodeRow]) {
496        let visible = visible_rows(cx, Rect::new(area.x, top, area.width, clamp_u16(area.bottom() - top)), rows.len());
497        for (index, row) in rows.iter().enumerate().skip(visible.start).take(visible.len()) {
498            let Some((variant, sign)) = self.look(row.line) else { continue };
499            // A wrapped line signs only its first row, and a line without a number of its own —
500            // a hunk header — still signs.
501            let first_row = index == 0 || rows[index - 1].line != row.line;
502            let y = top + i32::try_from(index).unwrap_or(i32::MAX);
503            let style = cx.style("code-line", Some(variant), &[]);
504            if let Some(bg) = style.color("bg") {
505                cx.fill(Rect::new(area.x, y, area.width, 1), bg);
506            }
507            let color = style.color("fg").unwrap_or_else(|| cx.color("text"));
508            match sign {
509                Sign::Pillar => cx.pillar(x, y, color),
510                Sign::Icon(icon) if first_row => {
511                    let glyph = cx.env().icons().glyph(icon).into_owned();
512                    cx.text(x, y, &glyph, CellStyle::fg(color), 1);
513                }
514                Sign::Icon(_) => {}
515            }
516        }
517    }
518
519    /// Asks the enclosing scroll view to show the revealed line, once per line.
520    fn request_reveal(&self, cx: &mut PaintCx<'_>, area: Rect, top: i32, rows: &[CodeRow]) {
521        let asked = match self.reveal_number {
522            Some(number) => self.line_of_number(number),
523            None => self.reveal,
524        };
525        let wanted = asked.map(|line| line.clamp(1, rows.last().map_or(1, |row| row.line)));
526        let memory = cx.memory::<CodeMemory>();
527        if memory.revealed == wanted {
528            return;
529        }
530        memory.revealed = wanted;
531        let Some(line) = wanted else { return };
532        let Some(first) = rows.iter().position(|row| row.line == line) else { return };
533        let count = rows[first..].iter().take_while(|row| row.line == line).count();
534        let context = i32::from(REVEAL_CONTEXT);
535        let y = (top + i32::try_from(first).unwrap_or(i32::MAX) - context).max(area.y);
536        let bottom = (top + i32::try_from(first + count).unwrap_or(i32::MAX) + context).min(area.bottom());
537        cx.reveal(Rect::new(area.x, y, area.width, clamp_u16(bottom - y)));
538    }
539}
540
541impl<Msg: Clone + 'static> Widget<Msg> for CodeView<Msg> {
542    fn measure(&self, cx: &mut MeasureCx<'_>, available: Size) -> Size {
543        let padding = cx.env().theme().style("code", None, &[]).pair("padding").unwrap_or((1, 2));
544        let content_width =
545            available.width.saturating_sub(cells::sum([padding.1.saturating_mul(2), self.signs(), self.gutter()]));
546        let layout = self.source.layout(content_width.max(1));
547        Size::new(
548            cells::sum([layout.widest, self.signs(), self.gutter(), padding.1.saturating_mul(2)]),
549            clamp_u16(i32::try_from(layout.rows.len()).unwrap_or(i32::MAX)).saturating_add(padding.0.saturating_mul(2)),
550        )
551        .min(available)
552    }
553
554    fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
555        let mut states = cx.states();
556        states.retain(|state| *state != State::Hover);
557        let style = cx.style("code", None, &states);
558        if let Some(bg) = style.text().bg {
559            cx.clear(area, bg);
560        }
561        cx.register_hit(area);
562        let inner = area.inset(style.padding());
563        // The padding is surface, not code: a selection starts and stays inside it.
564        cx.selectable(inner);
565        let (signs, gutter) = (self.signs(), self.gutter());
566        let width = inner.width.saturating_sub(signs).saturating_sub(gutter).max(1);
567        let layout = self.source.layout(width);
568        let renumbered = self.renumbered(&layout.rows);
569        let rows = renumbered.as_deref().unwrap_or(&layout.rows);
570        if signs > 0 {
571            // Signs say how a line changed; copies of the code leave them out like line numbers.
572            cx.decoration(Rect::new(inner.x, inner.y, signs, inner.height));
573            self.paint_looks(cx, area, inner.x, inner.y, rows);
574        }
575        paint_rows(
576            cx,
577            Rect::new(inner.x + i32::from(signs), inner.y, inner.width.saturating_sub(signs), inner.height),
578            rows,
579            gutter,
580        );
581        self.request_reveal(cx, area, inner.y, rows);
582    }
583
584    fn event(&self, cx: &mut EventCx<'_, Msg>, event: &Event) -> bool {
585        let Event::Key(key) = event else {
586            return false;
587        };
588        if !key.is_plain(Key::Char('c')) {
589            return false;
590        }
591        cx.copy(self.source.code.clone());
592        cx.flash();
593        if let Some(message) = &self.on_copy {
594            cx.emit(message.clone());
595        }
596        true
597    }
598
599    fn focusable(&self) -> bool {
600        true
601    }
602}
603
604#[cfg(test)]
605mod tests;