From 7a9805dfb305d5f431673d8f8af12454a8a668e8 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 6 Sep 2021 08:14:47 +0200 Subject: [PATCH] demo: highlight easymark editor field with different fonts and colors --- .../src/easy_mark/easy_mark_editor.rs | 79 ++++++-- .../src/easy_mark/easy_mark_highlighter.rs | 191 ++++++++++++++++++ egui_demo_lib/src/easy_mark/mod.rs | 2 + 3 files changed, 258 insertions(+), 14 deletions(-) create mode 100644 egui_demo_lib/src/easy_mark/easy_mark_highlighter.rs diff --git a/egui_demo_lib/src/easy_mark/easy_mark_editor.rs b/egui_demo_lib/src/easy_mark/easy_mark_editor.rs index 8558d249..746eeece 100644 --- a/egui_demo_lib/src/easy_mark/easy_mark_editor.rs +++ b/egui_demo_lib/src/easy_mark/easy_mark_editor.rs @@ -1,15 +1,30 @@ use egui::*; #[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] -#[derive(PartialEq)] +#[cfg_attr(feature = "persistence", serde(default))] pub struct EasyMarkEditor { code: String, + highlight_editor: bool, + show_rendered: bool, + + #[cfg_attr(feature = "persistence", serde(skip))] + highlighter: crate::easy_mark::MemoizedEasymarkHighlighter, +} + +impl PartialEq for EasyMarkEditor { + fn eq(&self, other: &Self) -> bool { + (&self.code, self.highlight_editor, self.show_rendered) + == (&other.code, other.highlight_editor, other.show_rendered) + } } impl Default for EasyMarkEditor { fn default() -> Self { Self { code: DEFAULT_CODE.trim().to_owned(), + highlight_editor: true, + show_rendered: true, + highlighter: Default::default(), } } } @@ -28,28 +43,64 @@ impl epi::App for EasyMarkEditor { impl EasyMarkEditor { fn ui(&mut self, ui: &mut egui::Ui) { - ui.vertical_centered(|ui| { + egui::Grid::new("controls").show(ui, |ui| { + ui.checkbox(&mut self.highlight_editor, "Highlight editor"); egui::reset_button(ui, self); + ui.end_row(); + + ui.checkbox(&mut self.show_rendered, "Show rendered"); ui.add(crate::__egui_github_link_file!()); }); + ui.separator(); - ui.columns(2, |columns| { + + if self.show_rendered { + ui.columns(2, |columns| { + ScrollArea::vertical() + .id_source("source") + .show(&mut columns[0], |ui| self.editor_ui(ui)); + ScrollArea::vertical() + .id_source("rendered") + .show(&mut columns[1], |ui| { + // TODO: we can save some more CPU by caching the rendered output. + crate::easy_mark::easy_mark(ui, &self.code); + }); + }); + } else { ScrollArea::vertical() .id_source("source") - .show(&mut columns[0], |ui| { - ui.add(TextEdit::multiline(&mut self.code).text_style(TextStyle::Monospace)); - // let cursor = TextEdit::cursor(response.id); - // TODO: cmd-i, cmd-b, etc for italics, bold, .... - }); - ScrollArea::vertical() - .id_source("rendered") - .show(&mut columns[1], |ui| { - crate::easy_mark::easy_mark(ui, &self.code); - }); - }); + .show(ui, |ui| self.editor_ui(ui)) + } + } + + fn editor_ui(&mut self, ui: &mut egui::Ui) { + let Self { + code, highlighter, .. + } = self; + + if self.highlight_editor { + let mut layouter = |ui: &egui::Ui, easymark: &str, wrap_width: f32| { + let mut layout_job = highlighter.highlight(ui.visuals(), easymark); + layout_job.wrap_width = wrap_width; + ui.fonts().layout_job(layout_job) + }; + + ui.add( + egui::TextEdit::multiline(code) + .desired_width(f32::INFINITY) + .text_style(egui::TextStyle::Monospace) // for cursor height + .layouter(&mut layouter), + ); + } else { + ui.add(egui::TextEdit::multiline(code).desired_width(f32::INFINITY)); + } + // let cursor = TextEdit::cursor(response.id); + // TODO: cmd-i, cmd-b, etc for italics, bold, .... } } +// ---------------------------------------------------------------------------- + const DEFAULT_CODE: &str = r#" # EasyMark EasyMark is a markup language, designed for extreme simplicity. diff --git a/egui_demo_lib/src/easy_mark/easy_mark_highlighter.rs b/egui_demo_lib/src/easy_mark/easy_mark_highlighter.rs new file mode 100644 index 00000000..008b8a1a --- /dev/null +++ b/egui_demo_lib/src/easy_mark/easy_mark_highlighter.rs @@ -0,0 +1,191 @@ +use crate::easy_mark::easy_mark_parser; + +/// Highlight easymark, memoizing previous output to save CPU. +/// +/// In practice, the highlighter is fast enough not to need any caching. +#[derive(Default)] +pub struct MemoizedEasymarkHighlighter { + visuals: egui::Visuals, + code: String, + output: egui::text::LayoutJob, +} + +impl MemoizedEasymarkHighlighter { + pub fn highlight(&mut self, visuals: &egui::Visuals, code: &str) -> egui::text::LayoutJob { + if (&self.visuals, self.code.as_str()) != (visuals, code) { + self.visuals = visuals.clone(); + self.code = code.to_owned(); + self.output = highlight_easymark(visuals, code) + } + self.output.clone() + } +} + +pub fn highlight_easymark(visuals: &egui::Visuals, mut text: &str) -> egui::text::LayoutJob { + let mut job = egui::text::LayoutJob::default(); + let mut style = easy_mark_parser::Style::default(); + let mut start_of_line = true; + + while !text.is_empty() { + if start_of_line && text.starts_with("```") { + let end = text.find("\n```").map(|i| i + 4).unwrap_or(text.len()); + job.append( + &text[..end], + 0.0, + format_from_style( + visuals, + &easy_mark_parser::Style { + code: true, + ..Default::default() + }, + ), + ); + text = &text[end..]; + style = Default::default(); + continue; + } + + if text.starts_with("`") { + style.code = true; + let end = text[1..] + .find(&['`', '\n'][..]) + .map(|i| i + 2) + .unwrap_or(text.len()); + job.append(&text[..end], 0.0, format_from_style(visuals, &style)); + text = &text[end..]; + style.code = false; + continue; + } + + let mut skip; + + if text.starts_with('\\') && text.len() >= 2 { + skip = 2; + } else if start_of_line && text.starts_with(' ') { + // indentation we don't preview indentation, because it is confusing + skip = 1; + } else if start_of_line && text.starts_with("# ") { + style.heading = true; + skip = 2; + } else if start_of_line && text.starts_with("> ") { + style.quoted = true; + skip = 2; + // indentation we don't preview indentation, because it is confusing + } else if start_of_line && text.starts_with("- ") { + skip = 2; + // indentation we don't preview indentation, because it is confusing + } else if text.starts_with('*') { + skip = 1; + if style.strong { + // Include the character that i ending ths style: + job.append(&text[..skip], 0.0, format_from_style(visuals, &style)); + text = &text[skip..]; + skip = 0; + } + style.strong ^= true; + } else if text.starts_with('$') { + skip = 1; + if style.small { + // Include the character that i ending ths style: + job.append(&text[..skip], 0.0, format_from_style(visuals, &style)); + text = &text[skip..]; + skip = 0; + } + style.small ^= true; + } else if text.starts_with('^') { + skip = 1; + if style.raised { + // Include the character that i ending ths style: + job.append(&text[..skip], 0.0, format_from_style(visuals, &style)); + text = &text[skip..]; + skip = 0; + } + style.raised ^= true; + } else { + skip = 0; + } + // Note: we don't preview underline, strikethrough and italics because it confuses things. + + // Swallow everything up to the next special character: + let line_end = text[skip..] + .find('\n') + .map(|i| (skip + i + 1)) + .unwrap_or_else(|| text.len()); + let end = text[skip..] + .find(&['*', '`', '~', '_', '/', '$', '^', '\\', '<', '['][..]) + .map(|i| (skip + i).max(1)) // make sure we swallow at least one character + .unwrap_or_else(|| text.len()); + + if line_end <= end { + job.append(&text[..line_end], 0.0, format_from_style(visuals, &style)); + text = &text[line_end..]; + start_of_line = true; + style = Default::default(); + } else { + job.append(&text[..end], 0.0, format_from_style(visuals, &style)); + text = &text[end..]; + start_of_line = false; + } + } + + job +} + +fn format_from_style( + visuals: &egui::Visuals, + emark_style: &easy_mark_parser::Style, +) -> egui::text::TextFormat { + use egui::{Align, Color32, Stroke, TextStyle}; + + let color = if emark_style.strong || emark_style.heading { + visuals.strong_text_color() + } else if emark_style.quoted { + visuals.weak_text_color() + } else { + visuals.text_color() + }; + + let text_style = if emark_style.heading { + TextStyle::Heading + } else if emark_style.code { + TextStyle::Monospace + } else if emark_style.small | emark_style.raised { + TextStyle::Small + } else { + TextStyle::Body + }; + + let background = if emark_style.code { + visuals.code_bg_color + } else { + Color32::TRANSPARENT + }; + + let underline = if emark_style.underline { + Stroke::new(1.0, color) + } else { + Stroke::none() + }; + + let strikethrough = if emark_style.strikethrough { + Stroke::new(1.0, color) + } else { + Stroke::none() + }; + + let valign = if emark_style.raised { + Align::TOP + } else { + Align::BOTTOM + }; + + egui::text::TextFormat { + style: text_style, + color, + background, + italics: emark_style.italics, + underline, + strikethrough, + valign, + } +} diff --git a/egui_demo_lib/src/easy_mark/mod.rs b/egui_demo_lib/src/easy_mark/mod.rs index 8c56bd8d..d56b4fa0 100644 --- a/egui_demo_lib/src/easy_mark/mod.rs +++ b/egui_demo_lib/src/easy_mark/mod.rs @@ -1,9 +1,11 @@ //! Experimental markup language mod easy_mark_editor; +mod easy_mark_highlighter; pub mod easy_mark_parser; mod easy_mark_viewer; pub use easy_mark_editor::EasyMarkEditor; +pub use easy_mark_highlighter::MemoizedEasymarkHighlighter; pub use easy_mark_parser as parser; pub use easy_mark_viewer::easy_mark;