egui/egui_demo_lib/src/apps/demo/code_editor.rs
Emil Ernerfeldt 5f88d89f74
Faster galley cache (#699)
* Speed up galley cache by only using the hash as key

This hashes the job but doesn't compare them with Eq,
which speeds up demo_with_tessellate__realistic by 5-6%,
winning back all the performance lost in
https://github.com/emilk/egui/pull/682

* Remove custom Eq/PartialEq code for LayoutJob and friends

* Silence clippy

* Unrelated clippy fixes
2021-09-04 10:19:58 +02:00

358 lines
11 KiB
Rust

use egui::text::LayoutJob;
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "persistence", serde(default))]
pub struct CodeEditor {
code: String,
language: String,
#[cfg_attr(feature = "persistence", serde(skip))]
highlighter: MemoizedSyntaxHighlighter,
}
impl Default for CodeEditor {
fn default() -> Self {
Self {
code: "// A very simple example\n\
fn main() {\n\
\tprintln!(\"Hello world!\");\n\
}\n\
"
.into(),
language: "rs".into(),
highlighter: Default::default(),
}
}
}
impl super::Demo for CodeEditor {
fn name(&self) -> &'static str {
"🖮 Code Editor"
}
fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) {
use super::View;
egui::Window::new(self.name())
.open(open)
.show(ctx, |ui| self.ui(ui));
}
}
impl super::View for CodeEditor {
fn ui(&mut self, ui: &mut egui::Ui) {
let Self {
code,
language,
highlighter,
} = self;
ui.horizontal(|ui| {
ui.set_height(0.0);
ui.label("An example of syntax highlighting in a TextEdit.");
ui.add(crate::__egui_github_link_file!());
});
if cfg!(feature = "syntect") {
ui.horizontal(|ui| {
ui.label("Language:");
ui.text_edit_singleline(language);
});
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("Syntax highlighting powered by ");
ui.hyperlink_to("syntect", "https://github.com/trishume/syntect");
ui.label(".");
});
} else {
ui.horizontal_wrapped(|ui|{
ui.spacing_mut().item_spacing.x = 0.0;
ui.label("Compile the demo with the 'syntax_highlighting' feature to enable much nicer syntax highlighting using ");
ui.hyperlink_to("syntect", "https://github.com/trishume/syntect");
ui.label(".");
});
}
let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
let mut layout_job = highlighter.highlight(ui.visuals().dark_mode, string, language);
layout_job.wrap_width = wrap_width;
ui.fonts().layout_job(layout_job)
};
egui::ScrollArea::vertical().show(ui, |ui| {
ui.add(
egui::TextEdit::multiline(code)
.text_style(egui::TextStyle::Monospace) // for cursor height
.code_editor()
.lock_focus(true)
.desired_width(f32::INFINITY)
.layouter(&mut layouter),
);
});
}
}
// ----------------------------------------------------------------------------
#[derive(Default)]
struct MemoizedSyntaxHighlighter {
is_dark_mode: bool,
code: String,
language: String,
output: LayoutJob,
highligher: Highligher,
}
impl MemoizedSyntaxHighlighter {
fn highlight(&mut self, is_dark_mode: bool, code: &str, language: &str) -> LayoutJob {
if (
self.is_dark_mode,
self.code.as_str(),
self.language.as_str(),
) != (is_dark_mode, code, language)
{
self.is_dark_mode = is_dark_mode;
self.code = code.to_owned();
self.language = language.to_owned();
self.output = self
.highligher
.highlight(is_dark_mode, code, language)
.unwrap_or_else(|| {
LayoutJob::simple(
code.into(),
egui::TextStyle::Monospace,
if is_dark_mode {
egui::Color32::LIGHT_GRAY
} else {
egui::Color32::DARK_GRAY
},
f32::INFINITY,
)
});
}
self.output.clone()
}
}
// ----------------------------------------------------------------------------
#[cfg(feature = "syntect")]
struct Highligher {
ps: syntect::parsing::SyntaxSet,
ts: syntect::highlighting::ThemeSet,
}
#[cfg(feature = "syntect")]
impl Default for Highligher {
fn default() -> Self {
Self {
ps: syntect::parsing::SyntaxSet::load_defaults_newlines(),
ts: syntect::highlighting::ThemeSet::load_defaults(),
}
}
}
#[cfg(feature = "syntect")]
impl Highligher {
fn highlight(&self, is_dark_mode: bool, text: &str, language: &str) -> Option<LayoutJob> {
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 = if is_dark_mode {
"base16-mocha.dark"
} else {
"base16-ocean.light"
};
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, &self.ps) {
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 {
style: egui::TextStyle::Monospace,
color: text_color,
italics,
underline,
..Default::default()
},
});
}
}
Some(job)
}
}
#[cfg(feature = "syntect")]
fn as_byte_range(whole: &str, range: &str) -> std::ops::Range<usize> {
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 Highligher {}
#[cfg(not(feature = "syntect"))]
impl Highligher {
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
fn highlight(&self, is_dark_mode: bool, mut text: &str, _language: &str) -> Option<LayoutJob> {
// Extremely simple syntax highlighter for when we compile without syntect
use egui::text::TextFormat;
use egui::Color32;
let monospace = egui::TextStyle::Monospace;
let comment_format = TextFormat::simple(monospace, Color32::GRAY);
let quoted_string_format = TextFormat::simple(
monospace,
if is_dark_mode {
Color32::KHAKI
} else {
Color32::BROWN
},
);
let keyword_format = TextFormat::simple(
monospace,
if is_dark_mode {
Color32::LIGHT_RED
} else {
Color32::DARK_RED
},
);
let literal_format = TextFormat::simple(
monospace,
if is_dark_mode {
Color32::LIGHT_GREEN
} else {
Color32::DARK_GREEN
},
);
let whitespace_format = TextFormat::simple(monospace, Color32::WHITE);
let punctuation_format = TextFormat::simple(
monospace,
if is_dark_mode {
Color32::LIGHT_GRAY
} else {
Color32::DARK_GRAY
},
);
let mut job = LayoutJob::default();
while !text.is_empty() {
if text.starts_with("//") {
let end = text.find('\n').unwrap_or_else(|| text.len());
job.append(&text[..end], 0.0, comment_format);
text = &text[end..];
} else if text.starts_with('"') {
let end = text[1..]
.find('"')
.map(|i| i + 2)
.or_else(|| text.find('\n'))
.unwrap_or_else(|| text.len());
job.append(&text[..end], 0.0, quoted_string_format);
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(|i| i + 1)
.unwrap_or_else(|| text.len());
let word = &text[..end];
if is_keyword(word) {
job.append(word, 0.0, keyword_format);
} else {
job.append(word, 0.0, literal_format);
};
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(|i| i + 1)
.unwrap_or_else(|| text.len());
job.append(&text[..end], 0.0, whitespace_format);
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, punctuation_format);
text = &text[end..];
}
}
Some(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"
)
}