use egui::text::LayoutJob; /// View some code with syntax highlighting and selection. pub fn code_view_ui(ui: &mut egui::Ui, mut code: &str) { let language = "rs"; let theme = CodeTheme::from_memory(ui.ctx()); let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| { let layout_job = highlight(ui.ctx(), &theme, string, language); // layout_job.wrap.max_width = wrap_width; // no wrapping ui.fonts().layout_job(layout_job) }; ui.add( egui::TextEdit::multiline(&mut code) .font(egui::TextStyle::Monospace) // for cursor height .code_editor() .desired_rows(1) .lock_focus(true) .layouter(&mut layouter), ); } /// Memoized Code highlighting pub fn highlight(ctx: &egui::Context, theme: &CodeTheme, code: &str, language: &str) -> LayoutJob { impl egui::util::cache::ComputerMut<(&CodeTheme, &str, &str), LayoutJob> for Highlighter { fn compute(&mut self, (theme, code, lang): (&CodeTheme, &str, &str)) -> LayoutJob { self.highlight(theme, code, lang) } } type HighlightCache = egui::util::cache::FrameCache; let mut memory = ctx.memory(); let highlight_cache = memory.caches.cache::(); highlight_cache.get((theme, code, language)) } // ---------------------------------------------------------------------------- #[cfg(not(feature = "syntect"))] #[derive(Clone, Copy, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[derive(enum_map::Enum)] enum TokenType { Comment, Keyword, Literal, StringLiteral, Punctuation, Whitespace, } #[cfg(feature = "syntect")] #[derive(Clone, Copy, Hash, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] enum SyntectTheme { Base16EightiesDark, Base16MochaDark, Base16OceanDark, Base16OceanLight, InspiredGitHub, SolarizedDark, SolarizedLight, } #[cfg(feature = "syntect")] impl SyntectTheme { fn all() -> impl ExactSizeIterator { [ Self::Base16EightiesDark, Self::Base16MochaDark, Self::Base16OceanDark, Self::Base16OceanLight, Self::InspiredGitHub, Self::SolarizedDark, Self::SolarizedLight, ] .iter() .copied() } fn name(&self) -> &'static str { match self { Self::Base16EightiesDark => "Base16 Eighties (dark)", Self::Base16MochaDark => "Base16 Mocha (dark)", Self::Base16OceanDark => "Base16 Ocean (dark)", Self::Base16OceanLight => "Base16 Ocean (light)", Self::InspiredGitHub => "InspiredGitHub (light)", Self::SolarizedDark => "Solarized (dark)", Self::SolarizedLight => "Solarized (light)", } } fn syntect_key_name(&self) -> &'static str { match self { Self::Base16EightiesDark => "base16-eighties.dark", Self::Base16MochaDark => "base16-mocha.dark", Self::Base16OceanDark => "base16-ocean.dark", Self::Base16OceanLight => "base16-ocean.light", Self::InspiredGitHub => "InspiredGitHub", Self::SolarizedDark => "Solarized (dark)", Self::SolarizedLight => "Solarized (light)", } } pub fn is_dark(&self) -> bool { match self { Self::Base16EightiesDark | Self::Base16MochaDark | Self::Base16OceanDark | Self::SolarizedDark => true, Self::Base16OceanLight | Self::InspiredGitHub | Self::SolarizedLight => false, } } } #[derive(Clone, Hash, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct CodeTheme { dark_mode: bool, #[cfg(feature = "syntect")] syntect_theme: SyntectTheme, #[cfg(not(feature = "syntect"))] formats: enum_map::EnumMap, } impl Default for CodeTheme { fn default() -> Self { Self::dark() } } impl CodeTheme { pub fn from_style(style: &egui::Style) -> Self { if style.visuals.dark_mode { Self::dark() } else { Self::light() } } pub fn from_memory(ctx: &egui::Context) -> Self { if ctx.style().visuals.dark_mode { ctx.data() .get_persisted(egui::Id::new("dark")) .unwrap_or_else(CodeTheme::dark) } else { ctx.data() .get_persisted(egui::Id::new("light")) .unwrap_or_else(CodeTheme::light) } } pub fn store_in_memory(self, ctx: &egui::Context) { if self.dark_mode { ctx.data().insert_persisted(egui::Id::new("dark"), self); } else { ctx.data().insert_persisted(egui::Id::new("light"), self); } } } #[cfg(feature = "syntect")] impl CodeTheme { pub fn dark() -> Self { Self { dark_mode: true, syntect_theme: SyntectTheme::Base16MochaDark, } } pub fn light() -> Self { Self { dark_mode: false, syntect_theme: SyntectTheme::SolarizedLight, } } pub fn ui(&mut self, ui: &mut egui::Ui) { egui::widgets::global_dark_light_mode_buttons(ui); for theme in SyntectTheme::all() { if theme.is_dark() == self.dark_mode { ui.radio_value(&mut self.syntect_theme, theme, theme.name()); } } } } #[cfg(not(feature = "syntect"))] impl CodeTheme { pub fn dark() -> Self { let font_id = egui::FontId::monospace(10.0); use egui::{Color32, TextFormat}; Self { dark_mode: true, formats: enum_map::enum_map![ TokenType::Comment => TextFormat::simple(font_id.clone(), Color32::from_gray(120)), TokenType::Keyword => TextFormat::simple(font_id.clone(), Color32::from_rgb(255, 100, 100)), TokenType::Literal => TextFormat::simple(font_id.clone(), Color32::from_rgb(87, 165, 171)), TokenType::StringLiteral => TextFormat::simple(font_id.clone(), Color32::from_rgb(109, 147, 226)), TokenType::Punctuation => TextFormat::simple(font_id.clone(), Color32::LIGHT_GRAY), TokenType::Whitespace => TextFormat::simple(font_id.clone(), Color32::TRANSPARENT), ], } } pub fn light() -> Self { let font_id = egui::FontId::monospace(10.0); use egui::{Color32, TextFormat}; Self { dark_mode: false, #[cfg(not(feature = "syntect"))] formats: enum_map::enum_map![ TokenType::Comment => TextFormat::simple(font_id.clone(), Color32::GRAY), TokenType::Keyword => TextFormat::simple(font_id.clone(), Color32::from_rgb(235, 0, 0)), TokenType::Literal => TextFormat::simple(font_id.clone(), Color32::from_rgb(153, 134, 255)), TokenType::StringLiteral => TextFormat::simple(font_id.clone(), Color32::from_rgb(37, 203, 105)), TokenType::Punctuation => TextFormat::simple(font_id.clone(), Color32::DARK_GRAY), TokenType::Whitespace => TextFormat::simple(font_id.clone(), Color32::TRANSPARENT), ], } } pub fn ui(&mut self, ui: &mut egui::Ui) { ui.horizontal_top(|ui| { let selected_id = egui::Id::null(); let mut selected_tt: TokenType = *ui .data() .get_persisted_mut_or(selected_id, TokenType::Comment); ui.vertical(|ui| { ui.set_width(150.0); egui::widgets::global_dark_light_mode_buttons(ui); ui.add_space(8.0); ui.separator(); ui.add_space(8.0); ui.scope(|ui| { for (tt, tt_name) in [ (TokenType::Comment, "// comment"), (TokenType::Keyword, "keyword"), (TokenType::Literal, "literal"), (TokenType::StringLiteral, "\"string literal\""), (TokenType::Punctuation, "punctuation ;"), // (TokenType::Whitespace, "whitespace"), ] { let format = &mut self.formats[tt]; ui.style_mut().override_font_id = Some(format.font_id.clone()); ui.visuals_mut().override_text_color = Some(format.color); ui.radio_value(&mut selected_tt, tt, tt_name); } }); let reset_value = if self.dark_mode { CodeTheme::dark() } else { CodeTheme::light() }; if ui .add_enabled(*self != reset_value, egui::Button::new("Reset theme")) .clicked() { *self = reset_value; } }); ui.add_space(16.0); ui.data().insert_persisted(selected_id, selected_tt); egui::Frame::group(ui.style()) .inner_margin(egui::Vec2::splat(2.0)) .show(ui, |ui| { // ui.group(|ui| { ui.style_mut().override_text_style = Some(egui::TextStyle::Small); ui.spacing_mut().slider_width = 128.0; // Controls color picker size egui::widgets::color_picker::color_picker_color32( ui, &mut self.formats[selected_tt].color, egui::color_picker::Alpha::Opaque, ); }); }); } } // ---------------------------------------------------------------------------- #[cfg(feature = "syntect")] struct Highlighter { ps: syntect::parsing::SyntaxSet, ts: syntect::highlighting::ThemeSet, } #[cfg(feature = "syntect")] impl Default for Highlighter { fn default() -> Self { Self { ps: syntect::parsing::SyntaxSet::load_defaults_newlines(), ts: syntect::highlighting::ThemeSet::load_defaults(), } } } #[cfg(feature = "syntect")] impl Highlighter { #[allow(clippy::unused_self, clippy::unnecessary_wraps)] fn highlight(&self, theme: &CodeTheme, code: &str, lang: &str) -> LayoutJob { self.highlight_impl(theme, code, lang).unwrap_or_else(|| { // Fallback: LayoutJob::simple( code.into(), egui::FontId::monospace(12.0), if theme.dark_mode { egui::Color32::LIGHT_GRAY } else { egui::Color32::DARK_GRAY }, f32::INFINITY, ) }) } fn highlight_impl(&self, theme: &CodeTheme, text: &str, language: &str) -> Option { use syntect::easy::HighlightLines; use syntect::highlighting::FontStyle; use syntect::util::LinesWithEndings; let syntax = self .ps .find_syntax_by_name(language) .or_else(|| self.ps.find_syntax_by_extension(language))?; let theme = theme.syntect_theme.syntect_key_name(); let mut h = HighlightLines::new(syntax, &self.ts.themes[theme]); use egui::text::{LayoutSection, TextFormat}; let mut job = LayoutJob { text: text.into(), ..Default::default() }; for line in LinesWithEndings::from(text) { for (style, range) in h.highlight_line(line, &self.ps).ok()? { let fg = style.foreground; let text_color = egui::Color32::from_rgb(fg.r, fg.g, fg.b); let italics = style.font_style.contains(FontStyle::ITALIC); let underline = style.font_style.contains(FontStyle::ITALIC); let underline = if underline { egui::Stroke::new(1.0, text_color) } else { egui::Stroke::NONE }; job.sections.push(LayoutSection { leading_space: 0.0, byte_range: as_byte_range(text, range), format: TextFormat { font_id: egui::FontId::monospace(12.0), color: text_color, italics, underline, ..Default::default() }, }); } } Some(job) } } #[cfg(feature = "syntect")] fn as_byte_range(whole: &str, range: &str) -> std::ops::Range { let whole_start = whole.as_ptr() as usize; let range_start = range.as_ptr() as usize; assert!(whole_start <= range_start); assert!(range_start + range.len() <= whole_start + whole.len()); let offset = range_start - whole_start; offset..(offset + range.len()) } // ---------------------------------------------------------------------------- #[cfg(not(feature = "syntect"))] #[derive(Default)] struct Highlighter {} #[cfg(not(feature = "syntect"))] impl Highlighter { #[allow(clippy::unused_self, clippy::unnecessary_wraps)] fn highlight(&self, theme: &CodeTheme, mut text: &str, _language: &str) -> LayoutJob { // Extremely simple syntax highlighter for when we compile without syntect let mut job = LayoutJob::default(); while !text.is_empty() { if text.starts_with("//") { let end = text.find('\n').unwrap_or(text.len()); job.append(&text[..end], 0.0, theme.formats[TokenType::Comment].clone()); text = &text[end..]; } else if text.starts_with('"') { let end = text[1..] .find('"') .map(|i| i + 2) .or_else(|| text.find('\n')) .unwrap_or(text.len()); job.append( &text[..end], 0.0, theme.formats[TokenType::StringLiteral].clone(), ); text = &text[end..]; } else if text.starts_with(|c: char| c.is_ascii_alphanumeric()) { let end = text[1..] .find(|c: char| !c.is_ascii_alphanumeric()) .map_or_else(|| text.len(), |i| i + 1); let word = &text[..end]; let tt = if is_keyword(word) { TokenType::Keyword } else { TokenType::Literal }; job.append(word, 0.0, theme.formats[tt].clone()); text = &text[end..]; } else if text.starts_with(|c: char| c.is_ascii_whitespace()) { let end = text[1..] .find(|c: char| !c.is_ascii_whitespace()) .map_or_else(|| text.len(), |i| i + 1); job.append( &text[..end], 0.0, theme.formats[TokenType::Whitespace].clone(), ); text = &text[end..]; } else { let mut it = text.char_indices(); it.next(); let end = it.next().map_or(text.len(), |(idx, _chr)| idx); job.append( &text[..end], 0.0, theme.formats[TokenType::Punctuation].clone(), ); text = &text[end..]; } } job } } #[cfg(not(feature = "syntect"))] fn is_keyword(word: &str) -> bool { matches!( word, "as" | "async" | "await" | "break" | "const" | "continue" | "crate" | "dyn" | "else" | "enum" | "extern" | "false" | "fn" | "for" | "if" | "impl" | "in" | "let" | "loop" | "match" | "mod" | "move" | "mut" | "pub" | "ref" | "return" | "self" | "Self" | "static" | "struct" | "super" | "trait" | "true" | "type" | "unsafe" | "use" | "where" | "while" ) }