//! A small code editor: monospace text with syntax colours, Tab to indent, Enter that keeps the //! indentation, and a scroll area that follows the caret. It colours Python (for code in cells, //! where `xl()` reads the sheet) and LaTeX (for equation source); reached as //! `widgets::code_editor`. The tokenizer and the key edits are pure functions, tested below. use std::ops::Range; use egui::text::{CCursor, CCursorRange}; use egui::{FontFamily, FontId, Key, Response, TextEdit, Ui}; use crate::theme::{self, Syntax, Tokens}; /// The language a code editor colours. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Lang { Python, Latex, Plain, } /// A coloured span's kind. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Token { Keyword, String, Number, Comment, Builtin, } /// Spaces one Tab inserts. pub const INDENT: &str = " "; const PY_KEYWORDS: &[&str] = &[ "False", "None", "True", "and", "as", "assert", "async", "await", "break", "class", "continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "match", "case", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while", "with", "yield", ]; const PY_BUILTINS: &[&str] = &[ "abs", "all", "any", "bool", "dict", "enumerate", "filter", "float", "format", "int", "isinstance", "len", "list", "map", "max", "min", "print", "range", "reversed", "round", "set", "sorted", "str", "sum", "tuple", "type", "zip", "xl", ]; /// The coloured spans of `text` (byte ranges on char boundaries, in order, not overlapping). /// Anything not covered is plain text. Never panics, whatever the input. pub fn highlight(text: &str, lang: Lang) -> Vec<(Range, Token)> { match lang { Lang::Python => python(text), Lang::Latex => latex(text), Lang::Plain => Vec::new(), } } /// End (exclusive, in chars) of the Python string literal opening with quote `c` at char `i`. fn python_string_end(chars: &[(usize, char)], i: usize, c: char) -> usize { let triple = chars.get(i + 1).is_some_and(|(_, d)| *d == c) && chars.get(i + 2).is_some_and(|(_, d)| *d == c); let mut j = if triple { i + 3 } else { i + 1 }; loop { match chars.get(j) { None => break, Some((_, '\\')) => j += 2, Some((_, '\n')) if !triple => break, Some((_, d)) if *d == c => { if !triple { j += 1; break; } if chars.get(j + 1).is_some_and(|(_, d)| *d == c) && chars.get(j + 2).is_some_and(|(_, d)| *d == c) { j += 3; break; } j += 1; } Some(_) => j += 1, } } j.min(chars.len()) } fn python(text: &str) -> Vec<(Range, Token)> { let chars: Vec<(usize, char)> = text.char_indices().collect(); let end_of = |i: usize| chars.get(i).map(|(b, _)| *b).unwrap_or(text.len()); let mut out = Vec::new(); let mut i = 0; while let Some(&(start, c)) = chars.get(i) { if c == '#' { let mut j = i; while chars.get(j).is_some_and(|(_, c)| *c != '\n') { j += 1; } out.push((start..end_of(j), Token::Comment)); i = j; } else if c == '"' || c == '\'' { let j = python_string_end(&chars, i, c); out.push((start..end_of(j), Token::String)); i = j; } else if c.is_ascii_digit() || (c == '.' && chars.get(i + 1).is_some_and(|(_, d)| d.is_ascii_digit())) { let mut j = i + 1; while chars.get(j).is_some_and(|(_, d)| d.is_ascii_alphanumeric() || *d == '_' || *d == '.') { j += 1; } out.push((start..end_of(j), Token::Number)); i = j; } else if c.is_alphabetic() || c == '_' { let mut j = i + 1; while chars.get(j).is_some_and(|(_, d)| d.is_alphanumeric() || *d == '_') { j += 1; } let word = text.get(start..end_of(j)).unwrap_or(""); if PY_KEYWORDS.contains(&word) { out.push((start..end_of(j), Token::Keyword)); } else if PY_BUILTINS.contains(&word) { out.push((start..end_of(j), Token::Builtin)); } i = j; } else { i += 1; } } out } fn latex(text: &str) -> Vec<(Range, Token)> { let chars: Vec<(usize, char)> = text.char_indices().collect(); let end_of = |i: usize| chars.get(i).map(|(b, _)| *b).unwrap_or(text.len()); let mut out = Vec::new(); let mut i = 0; while let Some(&(start, c)) = chars.get(i) { match c { '%' => { let mut j = i; while chars.get(j).is_some_and(|(_, c)| *c != '\n') { j += 1; } out.push((start..end_of(j), Token::Comment)); i = j; } '\\' => { let mut j = i + 1; if chars.get(j).is_some_and(|(_, d)| d.is_ascii_alphabetic()) { while chars.get(j).is_some_and(|(_, d)| d.is_ascii_alphabetic()) { j += 1; } } else if chars.get(j).is_some() { j += 1; // a control symbol such as \{ or \% } out.push((start..end_of(j), Token::Keyword)); i = j; } '^' | '_' | '&' => { out.push((start..end_of(i + 1), Token::Builtin)); i += 1; } d if d.is_ascii_digit() => { let mut j = i + 1; while chars.get(j).is_some_and(|(_, d)| d.is_ascii_digit() || *d == '.') { j += 1; } out.push((start..end_of(j), Token::Number)); i = j; } _ => i += 1, } } out } fn color(s: &Syntax, token: Token) -> egui::Color32 { match token { Token::Keyword => s.keyword, Token::String => s.string, Token::Number => s.number, Token::Comment => s.comment, Token::Builtin => s.builtin, } } /// The code editor's font: the monospace face at 12.5 px. pub fn font() -> FontId { FontId::new(12.5, FontFamily::Name(theme::MONO.into())) } /// A layout job for `text` with syntax colours. pub fn layout_job(text: &str, lang: Lang, font: &FontId, plain: egui::Color32, syntax: &Syntax, wrap_width: f32) -> egui::text::LayoutJob { let mut job = egui::text::LayoutJob::default(); let mut pos = 0usize; for (range, token) in highlight(text, lang) { if range.start < pos { continue; } if let (Some(before), Some(span)) = (text.get(pos..range.start), text.get(range.clone())) { job.append(before, 0.0, egui::TextFormat::simple(font.clone(), plain)); job.append(span, 0.0, egui::TextFormat::simple(font.clone(), color(syntax, token))); pos = range.end; } } job.append(text.get(pos..).unwrap_or(""), 0.0, egui::TextFormat::simple(font.clone(), plain)); job.wrap.max_width = wrap_width; job } /// Underlines the byte range `mark` of `job` in `color` (an error's position), splitting the /// sections it crosses. An empty range marks the character before it (the end of the text, for /// an error about missing input). A range past the end of the text is clamped to it, and its /// ends are moved out to char boundaries: the mark may come from an earlier version of the text /// (the equation editor computes it before this frame's edit), and egui slices the text at every /// section boundary. pub fn underline(job: &mut egui::text::LayoutJob, mark: Range, color: egui::Color32) { let text = job.text.as_str(); let floor = |i: usize| (0..=i.min(text.len())).rev().find(|&b| text.is_char_boundary(b)).unwrap_or(0); let ceil = |i: usize| (i.min(text.len())..=text.len()).find(|&b| text.is_char_boundary(b)).unwrap_or(text.len()); let start = floor(mark.start); let mut mark = start..ceil(mark.end).max(start); if mark.is_empty() { let start = text.get(..mark.start).and_then(|t| t.char_indices().next_back()).map_or(0, |(i, _)| i); mark = start..mark.start.max(start + text.get(start..).and_then(|t| t.chars().next()).map_or(0, char::len_utf8)); } let mut out = Vec::with_capacity(job.sections.len() + 2); for sec in job.sections.drain(..) { let r = sec.byte_range.start.0..sec.byte_range.end.0; let (a, b) = (mark.start.clamp(r.start, r.end), mark.end.clamp(r.start, r.end)); for (piece, marked) in [(r.start..a, false), (a..b, true), (b..r.end, false)] { if piece.is_empty() { continue; } let mut s = sec.clone(); s.byte_range = egui::text::ByteIndex(piece.start)..egui::text::ByteIndex(piece.end); if marked { s.format.underline = egui::Stroke::new(1.5, color); s.format.color = color; } if s.byte_range.start.0 != r.start { s.leading_space = 0.0; } out.push(s); } } job.sections = out; } fn byte_of(text: &str, char_index: usize) -> usize { text.char_indices().nth(char_index).map(|(b, _)| b).unwrap_or(text.len()) } /// Start (char index) of the line holding char `at`. fn line_start(text: &str, at: usize) -> usize { let b = byte_of(text, at); text.get(..b).and_then(|s| s.rfind('\n')).map(|nl| text.get(..nl + 1).map(|s| s.chars().count()).unwrap_or(0)).unwrap_or(0) } /// What a key did to the text: the new caret (and selection anchor), as char indices. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Edit { pub anchor: usize, pub caret: usize, } /// Enter: a newline that keeps the current line's indentation (one level more after a Python `:`). /// `sel` is the selection as char indices (anchor, caret); it is replaced. pub fn newline(text: &mut String, sel: (usize, usize), lang: Lang) -> Edit { let count = text.chars().count(); let (a, b) = (sel.0.min(sel.1).min(count), sel.0.max(sel.1).min(count)); let ls = line_start(text, a); let line_before: String = text.chars().skip(ls).take(a - ls.min(a)).collect(); let mut indent: String = line_before.chars().take_while(|c| *c == ' ' || *c == '\t').collect(); if lang == Lang::Python && line_before.trim_end().ends_with(':') { indent.push_str(INDENT); } let insert = format!("\n{indent}"); let (ba, bb) = (byte_of(text, a), byte_of(text, b)); text.replace_range(ba..bb, &insert); let caret = a + insert.chars().count(); Edit { anchor: caret, caret } } /// Typed text (or a paste) replacing the selection. pub fn insert(text: &mut String, sel: (usize, usize), s: &str) -> Edit { let count = text.chars().count(); let (a, b) = (sel.0.min(sel.1).min(count), sel.0.max(sel.1).min(count)); let (ba, bb) = (byte_of(text, a), byte_of(text, b)); text.replace_range(ba..bb, s); let caret = a + s.chars().count(); Edit { anchor: caret, caret } } /// Backspace (`back`) or Delete: the selection goes, or the character before or after the caret. pub fn delete(text: &mut String, sel: (usize, usize), back: bool) -> Edit { let count = text.chars().count(); let (mut a, mut b) = (sel.0.min(sel.1).min(count), sel.0.max(sel.1).min(count)); if a == b { if back { a = a.saturating_sub(1); } else { b = (b + 1).min(count); } } let (ba, bb) = (byte_of(text, a), byte_of(text, b)); text.replace_range(ba..bb, ""); Edit { anchor: a, caret: a } } /// An edit the editor makes itself, read off an input event by `classify`. #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum EditKey<'a> { Insert(&'a str), Enter, Tab { dedent: bool }, Backspace, Delete, } /// What one input event means to the editor. See `apply_in_order`. #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Class<'a> { /// The editor applies it: typed text, a paste, Enter, Tab, Shift+Tab, Backspace, Delete. Edit(EditKey<'a>), /// The editor takes it off the queue and does nothing: Tab under a command or option /// modifier, which the field would turn into a tab character in place of the selection. Dropped, /// The text field acts on it: a caret or selection key, a command shortcut (select all, /// undo, word deletion), IME, Copy, Cut, or a click, which the field reads from the pointer /// state. Field, /// Neither reads it from the queue: a key release, a printable key press (its character /// arrives as the `Text` event after it, also under Shift or Option), a pointer move, Escape /// (the focus system acts on it when the pass begins). Other, } fn classify(ev: &egui::Event) -> Class<'_> { use egui::Event; match ev { // The field treats a lone newline as the Enter key's business, and ignores it. Event::Text(t) if t != "\n" && t != "\r" => Class::Edit(EditKey::Insert(t)), Event::Text(_) => Class::Other, Event::Paste(t) => Class::Edit(EditKey::Insert(t)), Event::Key { key, pressed: true, modifiers, .. } => { let command = modifiers.command || modifiers.ctrl || modifiers.mac_cmd; match key { // The field makes a newline of Enter under any modifier but a command one, and // a tab character of Tab under every modifier; the editor takes all of those. Key::Enter if !command => Class::Edit(EditKey::Enter), Key::Tab if !command && !modifiers.alt => Class::Edit(EditKey::Tab { dedent: modifiers.shift }), Key::Tab => Class::Dropped, Key::Backspace if modifiers.is_none() => Class::Edit(EditKey::Backspace), Key::Delete if modifiers.is_none() => Class::Edit(EditKey::Delete), // The keys egui's text field reads: caret and selection moves, word and line // deletion, and its command shortcuts (select all, undo, redo, the Emacs keys). Key::Enter | Key::Backspace | Key::Delete => Class::Field, Key::ArrowLeft | Key::ArrowRight | Key::ArrowUp | Key::ArrowDown | Key::Home | Key::End => Class::Field, _ if command => Class::Field, _ => Class::Other, } } Event::Copy | Event::Cut | Event::Ime(_) | Event::PointerButton { pressed: true, .. } => Class::Field, _ => Class::Other, } } /// Whether an event is part of the keyboard stream, whose order the editor keeps. Pointer, /// touch, scroll and window events are not: egui folds them into its input state when the /// pass begins, so they act in the pass they arrive in whatever their place among the keys. fn is_keyboard(ev: &egui::Event) -> bool { use egui::Event; matches!(ev, Event::Key { .. } | Event::Text(_) | Event::Paste(_) | Event::Copy | Event::Cut | Event::Ime(_)) } fn apply(key: EditKey<'_>, text: &mut String, sel: (usize, usize), lang: Lang) -> Edit { match key { EditKey::Insert(s) => insert(text, sel, s), EditKey::Enter => newline(text, sel, lang), EditKey::Tab { dedent } => indent(text, sel, dedent), EditKey::Backspace => delete(text, sel, true), EditKey::Delete => delete(text, sel, false), } } /// What `apply_in_order` did with a pass's events. #[derive(Debug, Default, PartialEq)] pub struct Applied { /// Whether the text or the selection changed. pub edited: bool, /// The keyboard events held back for the next pass, in order: the editor's first key after /// an event the text field acts on, and every keyboard event after that key. pub held: Vec, /// The caller's key the pass stopped at (see [`apply_in_order_until`]). pub stop: Option, } /// Applies the pass's input to `text` in the order it came. A pass can carry several events /// (typing between repaints, key repeat), and two readers take them: this editor, for typed /// text, pastes, Enter, Tab, Shift+Tab, Backspace and Delete, and then egui's text field, for /// the caret and selection keys, command shortcuts, IME and clicks. The editor runs first, so /// it can only apply its keys up to the first event the field acts on: its edits before that /// event are applied and taken from the queue, the field's event and the run after it stay in /// the queue for the field, and from the editor's next key on the keyboard events are held /// back (`Applied::held`) for the next pass, where `code_editor` puts them ahead of that /// pass's input and they go through this again. So select all, Tab in one pass indents every /// line in two passes, and the field never gets an Enter or a Tab of a focused editor. /// Printable key presses do not split the stream: the field reads the `Text` event that /// follows each one, not the key. `sel` is the selection as char indices (anchor, caret), /// updated. pub fn apply_in_order(events: &mut Vec, text: &mut String, sel: &mut (usize, usize), lang: Lang) -> Applied { apply_in_order_until(events, text, sel, lang, &|_| false) } /// [`apply_in_order`], stopping at the first event `stop` picks: a key the caller acts on itself /// (commit, cancel, a mode switch). It counts as one of the editor's keys for the order: the /// typing before it is applied first, and after an event the text field acts on it is held back /// for the next pass with the rest. Once the pass stops (`Applied::stop`), the keyboard events /// after the stop key are dropped: the caller acts on the text as it was when the key was /// pressed, and what followed it was typed after it. pub fn apply_in_order_until( events: &mut Vec, text: &mut String, sel: &mut (usize, usize), lang: Lang, stop: &dyn Fn(&egui::Event) -> bool, ) -> Applied { let mut out = Applied::default(); // An event the field acts on has gone by: the editor's keys after it wait for the next pass. let mut field_seen = false; // A key is waiting: every keyboard event after it waits too, so the order holds. let mut holding = false; let mut keep = Vec::with_capacity(events.len()); for ev in std::mem::take(events) { if out.stop.is_some() { if !is_keyboard(&ev) { keep.push(ev); } continue; } if holding && is_keyboard(&ev) { out.held.push(ev); continue; } if is_keyboard(&ev) && stop(&ev) { if field_seen { holding = true; out.held.push(ev); } else { out.stop = Some(ev); } continue; } match classify(&ev) { Class::Edit(_) if field_seen => { holding = true; out.held.push(ev); } Class::Edit(key) => { let e = apply(key, text, *sel, lang); *sel = (e.anchor, e.caret); out.edited = true; } Class::Dropped => {} Class::Field => { field_seen = true; keep.push(ev); } Class::Other => keep.push(ev), } } *events = keep; out } /// The keyboard events a code editor held back, with the pass that held them. #[derive(Clone, Debug, Default)] struct Held { pass: u64, events: Vec, } fn held_id(id: egui::Id) -> egui::Id { id.with("held") } /// The events the editor `id` held back in the previous pass, to go ahead of this pass's input /// (see `apply_in_order`). Only the editor they were typed into gets them, only while it still /// has the focus, and only in the pass right after: once the focus has moved on, or the editor /// was not shown in between, they are dropped rather than typed into another widget. fn take_held(ui: &Ui, id: egui::Id, focused: bool) -> Vec { let pass = ui.ctx().cumulative_pass_nr(); ui.ctx() .data_mut(|d| d.remove_temp::(held_id(id))) .filter(|h| focused && h.pass.checked_add(1) == Some(pass)) .map(|h| h.events) .unwrap_or_default() } /// Holds `events` back for the editor's next pass, and asks for that pass. fn hold(ui: &Ui, id: egui::Id, events: Vec) { let pass = ui.ctx().cumulative_pass_nr(); ui.ctx().data_mut(|d| d.insert_temp(held_id(id), Held { pass, events })); ui.ctx().request_repaint(); } /// Tab (indent) or Shift+Tab (dedent). A selection over several lines indents or dedents each /// line; otherwise Tab inserts four spaces at the caret. pub fn indent(text: &mut String, sel: (usize, usize), dedent: bool) -> Edit { let count = text.chars().count(); let (a, b) = (sel.0.min(sel.1).min(count), sel.0.max(sel.1).min(count)); let multi = text.chars().skip(a).take(b - a).any(|c| c == '\n'); if !dedent && !multi { let (ba, bb) = (byte_of(text, a), byte_of(text, b)); text.replace_range(ba..bb, INDENT); let caret = a + INDENT.len(); return Edit { anchor: caret, caret }; } // Line starts (char indices) of every line the selection touches. A selection that ends at // the start of the next line (`"a\n"` in `"a\nb"`) does not touch that line. let mut starts = vec![line_start(text, a)]; for (i, c) in text.chars().enumerate().skip(a).take(b - a) { if c == '\n' && i + 1 < b { starts.push(i + 1); } } let (mut na, mut nb) = (a, b); // Work from the last line back so earlier indices stay valid. for &s in starts.iter().rev() { let bs = byte_of(text, s); let n = if dedent { let n = text.get(bs..).map(|rest| rest.chars().take(INDENT.len()).take_while(|c| *c == ' ').count()).unwrap_or(0); text.replace_range(bs..bs + n, ""); n } else { text.insert_str(bs, INDENT); 0 }; let shift = |p: usize| { if dedent { if p > s { p - n.min(p - s) } else { p } } else if p >= s { p + INDENT.len() } else { p } }; na = shift(na); nb = shift(nb); } if sel.1 >= sel.0 { Edit { anchor: na, caret: nb } } else { Edit { anchor: nb, caret: na } } } /// The code editor. `max_lines` rows are shown before it scrolls; the scroll area follows the /// caret. pub fn code_editor(ui: &mut Ui, id: egui::Id, text: &mut String, lang: Lang, max_lines: usize) -> Response { code_editor_marked(ui, id, text, lang, max_lines, None) } /// [`code_editor`] with the byte range `mark` underlined in the error colour (see [`underline`]). pub fn code_editor_marked(ui: &mut Ui, id: egui::Id, text: &mut String, lang: Lang, max_lines: usize, mark: Option>) -> Response { let t = Tokens::get(ui.ctx()); let row_h = ui.ctx().fonts_mut(|f| f.row_height(&font())); let max_lines = max_lines.clamp(1, 200); let mut response = None; egui::Frame::new().fill(t.input_bg).stroke(egui::Stroke::new(1.0, t.input_border)).corner_radius(4.0).inner_margin(4.0).show(ui, |ui| { let options = FieldOptions { max_height: row_h * max_lines as f32, mark: mark.clone(), ..FieldOptions::new(lang) }; response = Some(code_field(ui, id, text, &options).response); }); response.unwrap_or_else(|| ui.allocate_response(egui::Vec2::ZERO, egui::Sense::hover())) } /// How a [`code_field`] is laid out, and the keys its caller takes. pub struct FieldOptions<'a> { pub lang: Lang, /// The field's width (`f32::INFINITY`: all there is). pub width: f32, /// The height shown before it scrolls; the scroll area follows the caret. pub max_height: f32, /// Whether it takes input. A dialog or message box over the field has the keyboard. pub interactive: bool, /// Keys the caller acts on itself, in input order (see [`apply_in_order_until`]). pub stop: &'a dyn Fn(&egui::Event) -> bool, /// Esc stays with the field (for a caller that stops at it) instead of releasing the focus. pub keep_escape: bool, /// Puts the caret here (a char index) before the pass's input: the caller changed the text. pub caret: Option, /// The field's name for screen readers. pub name: &'a str, /// A byte range underlined in the error colour (see [`underline`]). pub mark: Option>, } impl FieldOptions<'_> { pub fn new(lang: Lang) -> FieldOptions<'static> { FieldOptions { lang, width: f32::INFINITY, max_height: 100.0, interactive: true, stop: &|_| false, keep_escape: false, caret: None, // The editor is a generic code field, so the name is not part of the UI's translated // strings unless the caller gives one. name: "Code Editor", mark: None, } } } /// What a [`code_field`] laid out and did. pub struct FieldOutput { pub response: Response, /// The caller's key the pass stopped at, if any. pub stop: Option, /// Laid-out rows of the text. pub lines: usize, /// The caret on screen, while the field has the focus. pub caret: Option, /// The caret and the other end of the selection (char indices). pub selection: (usize, usize), } /// The code editor's text field without a frame: monospace text with syntax colours, Enter that /// keeps the indentation, Tab and Shift+Tab, applied in input order (see [`apply_in_order`]). pub fn code_field(ui: &mut Ui, id: egui::Id, text: &mut String, options: &FieldOptions<'_>) -> FieldOutput { let t = Tokens::get(ui.ctx()); let font = font(); let lang = options.lang; // The input the editor handles itself (Enter with indentation, Tab, Shift+Tab), applied in // input order with the typing around it, before the text field sees the rest; what had to // wait for the field in the previous pass comes first. let focused = options.interactive && ui.memory(|m| m.has_focus(id)); let held = take_held(ui, id, focused); let mut edited = None; let mut stop = None; let count = text.chars().count(); if let Some(caret) = options.caret { let mut state = TextEdit::load_state(ui.ctx(), id).unwrap_or_default(); let caret = caret.min(count); state.cursor.set_char_range(Some(CCursorRange::one(CCursor::new(caret)))); state.store(ui.ctx(), id); edited = Some(Edit { anchor: caret, caret }); } if focused { let mut state = TextEdit::load_state(ui.ctx(), id).unwrap_or_default(); let mut sel = state.cursor.char_range().map(|r| (r.secondary.index.into(), r.primary.index.into())).unwrap_or((count, count)); let applied = ui.input_mut(|i| { if !held.is_empty() { let mut events = held; events.append(&mut i.events); i.events = events; } apply_in_order_until(&mut i.events, text, &mut sel, lang, options.stop) }); if applied.edited { state.cursor.set_char_range(Some(CCursorRange::two(CCursor::new(sel.0), CCursor::new(sel.1)))); state.store(ui.ctx(), id); edited = Some(Edit { anchor: sel.0, caret: sel.1 }); } if !applied.held.is_empty() { hold(ui, id, applied.held); } stop = applied.stop; } let syntax = t.syntax; let plain = t.text; let danger = t.danger; let mut layouter = |ui: &Ui, buf: &dyn egui::TextBuffer, wrap_width: f32| { let mut job = layout_job(buf.as_str(), lang, &self::font(), plain, &syntax, wrap_width); if let Some(m) = options.mark.clone() { underline(&mut job, m, danger); } ui.fonts_mut(|f| f.layout_job(job)) }; let filter = egui::EventFilter { tab: true, horizontal_arrows: true, vertical_arrows: true, escape: options.keep_escape }; let scrolled = egui::ScrollArea::vertical().id_salt(id.with("scroll")).max_height(options.max_height.max(1.0)).auto_shrink([false, true]).show(ui, |ui| { let out = TextEdit::multiline(text) .id(id) .font(font.clone()) .code_editor() .frame(egui::Frame::NONE) .desired_width(options.width) .desired_rows(1) .interactive(options.interactive) .event_filter(filter) .layouter(&mut layouter) .show(ui); if let Some(e) = edited { let caret = out.galley.pos_from_cursor(CCursor::new(e.caret)).translate(out.galley_pos.to_vec2()); ui.scroll_to_rect(caret, None); } out }); let out = scrolled.inner; // The field has no visible label; name it for screen readers and the strict kittest // accessibility check. let mut response = out.response.response.accessible_name(options.name); if edited.is_some() { response.mark_changed(); } let selection = out.cursor_range.map(|r| (r.primary.index.into(), r.secondary.index.into())).unwrap_or((count, count)); let caret = out .cursor_range .map(|r| out.galley.pos_from_cursor(r.primary).translate(out.galley_pos.to_vec2())) .filter(|_| response.has_focus()) .map(|r| r.intersect(scrolled.inner_rect.expand(1.0))); FieldOutput { response, stop, lines: out.galley.rows.len().max(1), caret, selection } } #[cfg(test)] mod tests;