Browse Source

Code editor demo: nice syntax highlighting with customizable theme

pull/765/head
Emil Ernerfeldt 3 years ago
parent
commit
f0868c2f07
  1. 22
      Cargo.lock
  2. 2
      egui/src/containers/frame.rs
  3. 10
      egui/src/widgets/mod.rs
  4. 2
      egui_demo_lib/Cargo.toml
  5. 237
      egui_demo_lib/src/apps/demo/code_editor.rs

22
Cargo.lock

@ -865,6 +865,7 @@ dependencies = [
"criterion", "criterion",
"egui", "egui",
"ehttp", "ehttp",
"enum-map",
"epi", "epi",
"image", "image",
"serde", "serde",
@ -929,6 +930,27 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "enum-map"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e893a7ba6116821058dec84a6fb14fb2a97cd8ce5fd0f85d5a4e760ecd7329d9"
dependencies = [
"enum-map-derive",
"serde",
]
[[package]]
name = "enum-map-derive"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84278eae0af6e34ff6c1db44c11634a694aafac559ff3080e4db4e4ac35907aa"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "env_logger" name = "env_logger"
version = "0.8.4" version = "0.8.4"

2
egui/src/containers/frame.rs

@ -23,7 +23,7 @@ impl Frame {
/// For when you want to group a few widgets together within a frame. /// For when you want to group a few widgets together within a frame.
pub fn group(style: &Style) -> Self { pub fn group(style: &Style) -> Self {
Self { Self {
margin: Vec2::new(8.0, 6.0), margin: Vec2::splat(6.0), // symmetric looks best in corners when nesting
corner_radius: style.visuals.widgets.noninteractive.corner_radius, corner_radius: style.visuals.widgets.noninteractive.corner_radius,
stroke: style.visuals.widgets.noninteractive.bg_stroke, stroke: style.visuals.widgets.noninteractive.bg_stroke,
..Default::default() ..Default::default()

10
egui/src/widgets/mod.rs

@ -81,12 +81,16 @@ where
/// Show a button to reset a value to its default. /// Show a button to reset a value to its default.
/// The button is only enabled if the value does not already have its original value. /// The button is only enabled if the value does not already have its original value.
pub fn reset_button<T: Default + PartialEq>(ui: &mut Ui, value: &mut T) { pub fn reset_button<T: Default + PartialEq>(ui: &mut Ui, value: &mut T) {
let def = T::default(); reset_button_with(ui, value, T::default());
}
/// Show a button to reset a value to its default.
/// The button is only enabled if the value does not already have its original value.
pub fn reset_button_with<T: PartialEq>(ui: &mut Ui, value: &mut T, reset_value: T) {
if ui if ui
.add(Button::new("Reset").enabled(*value != def)) .add(Button::new("Reset").enabled(*value != reset_value))
.clicked() .clicked()
{ {
*value = def; *value = reset_value;
} }
} }

2
egui_demo_lib/Cargo.toml

@ -25,6 +25,8 @@ all-features = true
[dependencies] [dependencies]
egui = { version = "0.14.0", path = "../egui", default-features = false } egui = { version = "0.14.0", path = "../egui", default-features = false }
epi = { version = "0.14.0", path = "../epi" } epi = { version = "0.14.0", path = "../epi" }
enum-map = { version = "1", features = ["serde"] }
unicode_names2 = { version = "0.4.0", default-features = false } unicode_names2 = { version = "0.4.0", default-features = false }
# feature "http": # feature "http":

237
egui_demo_lib/src/apps/demo/code_editor.rs

@ -1,10 +1,128 @@
use egui::text::LayoutJob; #[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,
}
#[derive(Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
struct CodeTheme {
dark_mode: bool,
formats: enum_map::EnumMap<TokenType, egui::TextFormat>,
}
impl Default for CodeTheme {
fn default() -> Self {
Self::dark(egui::TextStyle::Monospace)
}
}
impl CodeTheme {
fn dark(text_style: egui::TextStyle) -> Self {
use egui::{Color32, TextFormat};
Self {
dark_mode: true,
formats: enum_map::enum_map![
TokenType::Comment => TextFormat::simple(text_style, Color32::from_gray(120)),
TokenType::Keyword => TextFormat::simple(text_style, Color32::from_rgb(255, 100, 100)),
TokenType::Literal => TextFormat::simple(text_style, Color32::from_rgb(178, 108, 210)),
TokenType::StringLiteral => TextFormat::simple(text_style, Color32::from_rgb(109, 147, 226)),
TokenType::Punctuation => TextFormat::simple(text_style, Color32::LIGHT_GRAY),
TokenType::Whitespace => TextFormat::simple(text_style, Color32::TRANSPARENT),
],
}
}
fn light(text_style: egui::TextStyle) -> Self {
use egui::{Color32, TextFormat};
Self {
dark_mode: false,
formats: enum_map::enum_map![
TokenType::Comment => TextFormat::simple(text_style, Color32::GRAY),
TokenType::Keyword => TextFormat::simple(text_style, Color32::from_rgb(235, 0, 0)),
TokenType::Literal => TextFormat::simple(text_style, Color32::from_rgb(153, 134, 255)),
TokenType::StringLiteral => TextFormat::simple(text_style, Color32::from_rgb(37, 203, 105)),
TokenType::Punctuation => TextFormat::simple(text_style, Color32::DARK_GRAY),
TokenType::Whitespace => TextFormat::simple(text_style, Color32::TRANSPARENT),
],
}
}
}
impl CodeTheme {
fn ui(&mut self, ui: &mut egui::Ui, reset_value: CodeTheme) {
ui.horizontal_top(|ui| {
let mut selected_tt: TokenType = *ui.memory().data.get_or(TokenType::Comment);
ui.vertical(|ui| {
egui::widgets::global_dark_light_mode_buttons(ui);
// ui.separator(); // TODO: fix forever-expand
ui.add_space(14.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_text_style = Some(format.style);
ui.visuals_mut().override_text_color = Some(format.color);
ui.radio_value(&mut selected_tt, tt, tt_name);
}
});
ui.add_space(14.0);
if ui
.add(egui::Button::new("Reset theme").enabled(*self != reset_value))
.clicked()
{
*self = reset_value;
}
});
ui.add_space(16.0);
// ui.separator(); // TODO: fix forever-expand
ui.memory().data.insert(selected_tt);
egui::Frame::group(ui.style())
.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_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))] #[cfg_attr(feature = "serde", serde(default))]
pub struct CodeEditor { pub struct CodeEditor {
code: String, theme_dark: CodeTheme,
theme_light: CodeTheme,
language: String, language: String,
code: String,
#[cfg_attr(feature = "serde", serde(skip))] #[cfg_attr(feature = "serde", serde(skip))]
highlighter: MemoizedSyntaxHighlighter, highlighter: MemoizedSyntaxHighlighter,
} }
@ -12,13 +130,15 @@ pub struct CodeEditor {
impl Default for CodeEditor { impl Default for CodeEditor {
fn default() -> Self { fn default() -> Self {
Self { Self {
theme_dark: CodeTheme::dark(egui::TextStyle::Monospace),
theme_light: CodeTheme::light(egui::TextStyle::Monospace),
language: "rs".into(),
code: "// A very simple example\n\ code: "// A very simple example\n\
fn main() {\n\ fn main() {\n\
\tprintln!(\"Hello world!\");\n\ \tprintln!(\"Hello world!\");\n\
}\n\ }\n\
" "
.into(), .into(),
language: "rs".into(),
highlighter: Default::default(), highlighter: Default::default(),
} }
} }
@ -33,6 +153,7 @@ impl super::Demo for CodeEditor {
use super::View; use super::View;
egui::Window::new(self.name()) egui::Window::new(self.name())
.open(open) .open(open)
.default_height(500.0)
.show(ctx, |ui| self.ui(ui)); .show(ctx, |ui| self.ui(ui));
} }
} }
@ -40,8 +161,10 @@ impl super::Demo for CodeEditor {
impl super::View for CodeEditor { impl super::View for CodeEditor {
fn ui(&mut self, ui: &mut egui::Ui) { fn ui(&mut self, ui: &mut egui::Ui) {
let Self { let Self {
code, theme_dark,
theme_light,
language, language,
code,
highlighter, highlighter,
} = self; } = self;
@ -63,16 +186,36 @@ impl super::View for CodeEditor {
ui.label("."); ui.label(".");
}); });
} else { } else {
ui.horizontal_wrapped(|ui|{ ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = 0.0; 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.label("Compile the demo with the ");
ui.code("syntax_highlighting");
ui.label(" feature to enable more accurate syntax highlighting using ");
ui.hyperlink_to("syntect", "https://github.com/trishume/syntect"); ui.hyperlink_to("syntect", "https://github.com/trishume/syntect");
ui.label("."); ui.label(".");
}); });
} }
ui.collapsing("Theme", |ui| {
ui.group(|ui| {
if ui.visuals().dark_mode {
let reset_value = CodeTheme::dark(egui::TextStyle::Monospace);
theme_dark.ui(ui, reset_value);
} else {
let reset_value = CodeTheme::light(egui::TextStyle::Monospace);
theme_light.ui(ui, reset_value);
}
});
});
let theme = if ui.visuals().dark_mode {
theme_dark
} else {
theme_light
};
let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| { let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
let mut layout_job = highlighter.highlight(ui.visuals().dark_mode, string, language); let mut layout_job = highlighter.highlight(theme, string, language);
layout_job.wrap_width = wrap_width; layout_job.wrap_width = wrap_width;
ui.fonts().layout_job(layout_job) ui.fonts().layout_job(layout_job)
}; };
@ -82,6 +225,7 @@ impl super::View for CodeEditor {
egui::TextEdit::multiline(code) egui::TextEdit::multiline(code)
.text_style(egui::TextStyle::Monospace) // for cursor height .text_style(egui::TextStyle::Monospace) // for cursor height
.code_editor() .code_editor()
.desired_rows(10)
.lock_focus(true) .lock_focus(true)
.desired_width(f32::INFINITY) .desired_width(f32::INFINITY)
.layouter(&mut layouter), .layouter(&mut layouter),
@ -92,9 +236,11 @@ impl super::View for CodeEditor {
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
use egui::text::LayoutJob;
#[derive(Default)] #[derive(Default)]
struct MemoizedSyntaxHighlighter { struct MemoizedSyntaxHighlighter {
is_dark_mode: bool, theme: CodeTheme,
code: String, code: String,
language: String, language: String,
output: LayoutJob, output: LayoutJob,
@ -102,24 +248,19 @@ struct MemoizedSyntaxHighlighter {
} }
impl MemoizedSyntaxHighlighter { impl MemoizedSyntaxHighlighter {
fn highlight(&mut self, is_dark_mode: bool, code: &str, language: &str) -> LayoutJob { fn highlight(&mut self, theme: &CodeTheme, code: &str, language: &str) -> LayoutJob {
if ( if (&self.theme, self.code.as_str(), self.language.as_str()) != (theme, code, language) {
self.is_dark_mode, self.theme = *theme;
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.code = code.to_owned();
self.language = language.to_owned(); self.language = language.to_owned();
self.output = self self.output = self
.highligher .highligher
.highlight(is_dark_mode, code, language) .highlight(theme, code, language)
.unwrap_or_else(|| { .unwrap_or_else(|| {
LayoutJob::simple( LayoutJob::simple(
code.into(), code.into(),
egui::TextStyle::Monospace, egui::TextStyle::Monospace,
if is_dark_mode { if theme.dark_mode {
egui::Color32::LIGHT_GRAY egui::Color32::LIGHT_GRAY
} else { } else {
egui::Color32::DARK_GRAY egui::Color32::DARK_GRAY
@ -152,7 +293,7 @@ impl Default for Highligher {
#[cfg(feature = "syntect")] #[cfg(feature = "syntect")]
impl Highligher { impl Highligher {
fn highlight(&self, is_dark_mode: bool, text: &str, language: &str) -> Option<LayoutJob> { fn highlight(&self, theme: &CodeTheme, text: &str, language: &str) -> Option<LayoutJob> {
use syntect::easy::HighlightLines; use syntect::easy::HighlightLines;
use syntect::highlighting::FontStyle; use syntect::highlighting::FontStyle;
use syntect::util::LinesWithEndings; use syntect::util::LinesWithEndings;
@ -162,7 +303,7 @@ impl Highligher {
.find_syntax_by_name(language) .find_syntax_by_name(language)
.or_else(|| self.ps.find_syntax_by_extension(language))?; .or_else(|| self.ps.find_syntax_by_extension(language))?;
let theme = if is_dark_mode { let theme = if theme.dark_mode {
"base16-mocha.dark" "base16-mocha.dark"
} else { } else {
"base16-ocean.light" "base16-ocean.light"
@ -224,54 +365,15 @@ struct Highligher {}
#[cfg(not(feature = "syntect"))] #[cfg(not(feature = "syntect"))]
impl Highligher { impl Highligher {
#[allow(clippy::unused_self, clippy::unnecessary_wraps)] #[allow(clippy::unused_self, clippy::unnecessary_wraps)]
fn highlight(&self, is_dark_mode: bool, mut text: &str, _language: &str) -> Option<LayoutJob> { fn highlight(&self, theme: &CodeTheme, mut text: &str, _language: &str) -> Option<LayoutJob> {
// Extremely simple syntax highlighter for when we compile without syntect // 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(); let mut job = LayoutJob::default();
while !text.is_empty() { while !text.is_empty() {
if text.starts_with("//") { if text.starts_with("//") {
let end = text.find('\n').unwrap_or_else(|| text.len()); let end = text.find('\n').unwrap_or_else(|| text.len());
job.append(&text[..end], 0.0, comment_format); job.append(&text[..end], 0.0, theme.formats[TokenType::Comment]);
text = &text[end..]; text = &text[end..];
} else if text.starts_with('"') { } else if text.starts_with('"') {
let end = text[1..] let end = text[1..]
@ -279,7 +381,7 @@ impl Highligher {
.map(|i| i + 2) .map(|i| i + 2)
.or_else(|| text.find('\n')) .or_else(|| text.find('\n'))
.unwrap_or_else(|| text.len()); .unwrap_or_else(|| text.len());
job.append(&text[..end], 0.0, quoted_string_format); job.append(&text[..end], 0.0, theme.formats[TokenType::StringLiteral]);
text = &text[end..]; text = &text[end..];
} else if text.starts_with(|c: char| c.is_ascii_alphanumeric()) { } else if text.starts_with(|c: char| c.is_ascii_alphanumeric()) {
let end = text[1..] let end = text[1..]
@ -287,24 +389,25 @@ impl Highligher {
.map(|i| i + 1) .map(|i| i + 1)
.unwrap_or_else(|| text.len()); .unwrap_or_else(|| text.len());
let word = &text[..end]; let word = &text[..end];
if is_keyword(word) { let tt = if is_keyword(word) {
job.append(word, 0.0, keyword_format); TokenType::Keyword
} else { } else {
job.append(word, 0.0, literal_format); TokenType::Literal
}; };
job.append(word, 0.0, theme.formats[tt]);
text = &text[end..]; text = &text[end..];
} else if text.starts_with(|c: char| c.is_ascii_whitespace()) { } else if text.starts_with(|c: char| c.is_ascii_whitespace()) {
let end = text[1..] let end = text[1..]
.find(|c: char| !c.is_ascii_whitespace()) .find(|c: char| !c.is_ascii_whitespace())
.map(|i| i + 1) .map(|i| i + 1)
.unwrap_or_else(|| text.len()); .unwrap_or_else(|| text.len());
job.append(&text[..end], 0.0, whitespace_format); job.append(&text[..end], 0.0, theme.formats[TokenType::Whitespace]);
text = &text[end..]; text = &text[end..];
} else { } else {
let mut it = text.char_indices(); let mut it = text.char_indices();
it.next(); it.next();
let end = it.next().map_or(text.len(), |(idx, _chr)| idx); let end = it.next().map_or(text.len(), |(idx, _chr)| idx);
job.append(&text[..end], 0.0, punctuation_format); job.append(&text[..end], 0.0, theme.formats[TokenType::Punctuation]);
text = &text[end..]; text = &text[end..];
} }
} }

Loading…
Cancel
Save