From 16d66bd22d74387fe1d55ee7563b5113bcb05227 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 28 Jan 2021 23:50:23 +0100 Subject: [PATCH] Replace markdown editor with new 'EasyMark' markup language --- egui/src/experimental/easy_mark_parser.rs | 337 ++++++++++++++++++ egui/src/experimental/easy_mark_viewer.rs | 132 +++++++ egui/src/experimental/markdown.rs | 57 --- egui/src/experimental/markdown_parser.rs | 153 -------- egui/src/experimental/mod.rs | 6 +- egui_demo_lib/src/apps/demo/demo_windows.rs | 1 - .../src/apps/demo/markdown_editor.rs | 66 ---- egui_demo_lib/src/apps/demo/mod.rs | 1 - egui_demo_lib/src/apps/easy_mark_editor.rs | 117 ++++++ egui_demo_lib/src/apps/mod.rs | 2 + egui_demo_lib/src/wrap_app.rs | 2 + 11 files changed, 593 insertions(+), 281 deletions(-) create mode 100644 egui/src/experimental/easy_mark_parser.rs create mode 100644 egui/src/experimental/easy_mark_viewer.rs delete mode 100644 egui/src/experimental/markdown.rs delete mode 100644 egui/src/experimental/markdown_parser.rs delete mode 100644 egui_demo_lib/src/apps/demo/markdown_editor.rs create mode 100644 egui_demo_lib/src/apps/easy_mark_editor.rs diff --git a/egui/src/experimental/easy_mark_parser.rs b/egui/src/experimental/easy_mark_parser.rs new file mode 100644 index 00000000..f47e5d4d --- /dev/null +++ b/egui/src/experimental/easy_mark_parser.rs @@ -0,0 +1,337 @@ +//! A parser for `EasyMark`: a very simple markup language. +//! +//! WARNING: `EasyMark` is subject to change. +//! +//! This module does not depend on anything else in egui +//! and should perhaps be its own crate. +// +//! # `EasyMark` design goals: +//! 1. easy to parse +//! 2. easy to learn +//! 3. similar to markdown + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Item<'a> { + /// `\n` + Newline, + /// + Text(Style, &'a str), + /// title, url + Hyperlink(Style, &'a str, &'a str), + /// leading space before e.g. a [`Self::BulletPoint`]. + Indentation(usize), + /// > + QuoteIndent, + /// - a point well made. + BulletPoint, + /// 1. numbered list. The string is the number(s). + NumberedPoint(&'a str), + /// --- + Separator, + /// language, code + CodeBlock(&'a str, &'a str), +} + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct Style { + /// # heading (large text) + pub heading: bool, + /// > quoted (slightly dimmer color or other font style) + pub quoted: bool, + /// `code` (monospace, some other color) + pub code: bool, + /// self.strong* (emphasized, e.g. bold) + pub strong: bool, + /// _underline_ + pub underline: bool, + /// -strikethrough- + pub strikethrough: bool, + /// /italics/ + pub italics: bool, +} + +/// Parser for the `EasyMark` markup language. +/// +/// See the module-level documentation for details. +/// +/// # Example: +/// ``` +/// # use egui::experimental::easy_mark_parser::Parser; +/// for item in Parser::new("Hello *world*!") { +/// } +/// +/// ``` +pub struct Parser<'a> { + /// The remainer of the input text + s: &'a str, + /// Are we at the start of a line? + start_of_line: bool, + /// Current self.style. Reset after a newline. + style: Style, +} + +impl<'a> Parser<'a> { + pub fn new(s: &'a str) -> Self { + Self { + s, + start_of_line: true, + style: Style::default(), + } + } + + /// `1. `, `42. ` etc. + fn numbered_list(&mut self) -> Option> { + let bytes = self.s.as_bytes(); + // 1. numbered bullet + if bytes.len() >= 3 && bytes[0].is_ascii_digit() && bytes[1] == b'.' && bytes[2] == b' ' { + let number = &self.s[0..1]; + self.s = &self.s[3..]; + self.start_of_line = false; + return Some(Item::NumberedPoint(number)); + } + // 42. double-digit numbered bullet + if bytes.len() >= 4 + && bytes[0].is_ascii_digit() + && bytes[1].is_ascii_digit() + && bytes[2] == b'.' + && bytes[3] == b' ' + { + let number = &self.s[0..2]; + self.s = &self.s[4..]; + self.start_of_line = false; + return Some(Item::NumberedPoint(number)); + } + // There is no triple-digit numbered bullet. Please don't make numbered lists that long. + None + } + + // ```{language}\n{code}``` + fn code_block(&mut self) -> Option> { + if let Some(language_start) = self.s.strip_prefix("```") { + if let Some(newline) = language_start.find('\n') { + let language = &language_start[..newline]; + let code_start = &language_start[newline + 1..]; + if let Some(end) = code_start.find("\n```") { + let code = &code_start[..end].trim(); + self.s = &code_start[end + 4..]; + self.start_of_line = false; + return Some(Item::CodeBlock(language, code)); + } else { + self.s = ""; + return Some(Item::CodeBlock(language, code_start)); + } + } + } + None + } + + // `code` + fn inline_code(&mut self) -> Option> { + if let Some(rest) = self.s.strip_prefix('`') { + self.s = rest; + self.start_of_line = false; + self.style.code = true; + let rest_of_line = &self.s[..self.s.find('\n').unwrap_or_else(|| self.s.len())]; + if let Some(end) = rest_of_line.find('`') { + let item = Item::Text(self.style, &self.s[..end]); + self.s = &self.s[end + 1..]; + self.style.code = false; + return Some(item); + } else { + let end = rest_of_line.len(); + let item = Item::Text(self.style, rest_of_line); + self.s = &self.s[end..]; + self.style.code = false; + return Some(item); + } + } + None + } + + /// `` or `[link](url)` + fn url(&mut self) -> Option> { + if self.s.starts_with('<') { + let this_line = &self.s[..self.s.find('\n').unwrap_or_else(|| self.s.len())]; + if let Some(url_end) = this_line.find('>') { + let url = &self.s[1..url_end]; + self.s = &self.s[url_end + 1..]; + self.start_of_line = false; + return Some(Item::Hyperlink(self.style, url, url)); + } + } + + // [text](url) + if self.s.starts_with('[') { + let this_line = &self.s[..self.s.find('\n').unwrap_or_else(|| self.s.len())]; + if let Some(bracket_end) = this_line.find(']') { + let text = &this_line[1..bracket_end]; + if this_line[bracket_end + 1..].starts_with('(') { + if let Some(parens_end) = this_line[bracket_end + 2..].find(')') { + let parens_end = bracket_end + 2 + parens_end; + let url = &self.s[bracket_end + 2..parens_end]; + self.s = &self.s[parens_end + 1..]; + self.start_of_line = false; + return Some(Item::Hyperlink(self.style, text, url)); + } + } + } + } + None + } +} + +impl<'a> Iterator for Parser<'a> { + type Item = Item<'a>; + + fn next(&mut self) -> Option { + loop { + if self.s.is_empty() { + return None; + } + + // \n + if self.s.starts_with('\n') { + self.s = &self.s[1..]; + self.start_of_line = true; + self.style = Style::default(); + return Some(Item::Newline); + } + + // Ignore line break (continue on the same line) + if self.s.starts_with("\\\n") && self.s.len() >= 2 { + self.s = &self.s[2..]; + self.start_of_line = false; + continue; + } + + // \ escape (to show e.g. a backtick) + if self.s.starts_with('\\') && self.s.len() >= 2 { + let text = &self.s[1..2]; + self.s = &self.s[2..]; + self.start_of_line = false; + return Some(Item::Text(self.style, text)); + } + + if self.start_of_line { + // leading space (indentation) + if self.s.starts_with(' ') { + let length = self.s.find(|c| c != ' ').unwrap_or_else(|| self.s.len()); + self.s = &self.s[length..]; + self.start_of_line = true; // indentation doesn't count + return Some(Item::Indentation(length)); + } + + // # Heading + if let Some(after) = self.s.strip_prefix("# ") { + self.s = after; + self.start_of_line = false; + self.style.heading = true; + continue; + } + + // > quote + if let Some(after) = self.s.strip_prefix("> ") { + self.s = after; + self.start_of_line = true; // quote indentation doesn't count + self.style.quoted = true; + return Some(Item::QuoteIndent); + } + + // - bullet point + if self.s.starts_with("- ") { + self.s = &self.s[2..]; + self.start_of_line = false; + return Some(Item::BulletPoint); + } + + // `1. `, `42. ` etc. + if let Some(item) = self.numbered_list() { + return Some(item); + } + + // --- separator + if let Some(after) = self.s.strip_prefix("---") { + self.s = after.trim_start_matches('-'); // remove extra dashes + self.s = self.s.strip_prefix('\n').unwrap_or(self.s); // remove trailing newline + self.start_of_line = false; + return Some(Item::Separator); + } + + // ```{language}\n{code}``` + if let Some(item) = self.code_block() { + return Some(item); + } + } + + // `code` + if let Some(item) = self.inline_code() { + return Some(item); + } + + if let Some(rest) = self.s.strip_prefix('*') { + self.s = rest; + self.start_of_line = false; + self.style.strong = !self.style.strong; + continue; + } + if let Some(rest) = self.s.strip_prefix('_') { + self.s = rest; + self.start_of_line = false; + self.style.underline = !self.style.underline; + continue; + } + if let Some(rest) = self.s.strip_prefix('~') { + self.s = rest; + self.start_of_line = false; + self.style.strikethrough = !self.style.strikethrough; + continue; + } + if let Some(rest) = self.s.strip_prefix('/') { + self.s = rest; + self.start_of_line = false; + self.style.italics = !self.style.italics; + continue; + } + + // `` or `[link](url)` + if let Some(item) = self.url() { + return Some(item); + } + + // Swallow everything up to the next special character: + let special = self.s[1..] + .find(&['*', '`', '~', '_', '/', '\\', '<', '[', '\n'][..]) + .map(|i| i + 1) + .unwrap_or_else(|| self.s.len()); + let item = Item::Text(self.style, &self.s[..special]); + self.s = &self.s[special..]; + self.start_of_line = false; + return Some(item); + } + } +} + +#[test] +fn test_easy_mark_parser() { + let items: Vec<_> = Parser::new("~strikethrough `code`~").collect(); + assert_eq!( + items, + vec![ + Item::Text( + Style { + strikethrough: true, + ..Default::default() + }, + "strikethrough " + ), + Item::Text( + Style { + code: true, + strikethrough: true, + ..Default::default() + }, + "code" + ), + ] + ); +} diff --git a/egui/src/experimental/easy_mark_viewer.rs b/egui/src/experimental/easy_mark_viewer.rs new file mode 100644 index 00000000..b790e2dd --- /dev/null +++ b/egui/src/experimental/easy_mark_viewer.rs @@ -0,0 +1,132 @@ +use super::easy_mark_parser as easy_mark; +use crate::*; + +/// Parse and display a VERY simple and small subset of Markdown. +pub fn easy_mark(ui: &mut Ui, easy_mark: &str) { + ui.horizontal_wrapped(|ui| { + let row_height = ui.fonts()[TextStyle::Body].row_height(); + let one_indent = row_height / 2.0; + let spacing = vec2(0.0, 2.0); + let style = ui.style_mut(); + style.spacing.interact_size.y = row_height; + style.spacing.item_spacing = spacing; + + for item in easy_mark::Parser::new(easy_mark) { + match item { + easy_mark::Item::Newline => { + // ui.label("\n"); // too much spacing (paragraph spacing) + ui.allocate_exact_size(vec2(0.0, row_height), Sense::hover()); // make sure we take up some height + ui.end_row(); + } + + easy_mark::Item::Text(style, text) => { + ui.add(label_from_style(text, style)); + } + easy_mark::Item::Hyperlink(style, text, url) => { + let label = label_from_style(text, style); + ui.add(Hyperlink::from_label_and_url(label, url)); + } + + easy_mark::Item::Separator => { + ui.add(Separator::new().horizontal()); + } + easy_mark::Item::Indentation(indent) => { + let indent = indent as f32 * one_indent; + ui.allocate_exact_size(vec2(indent, row_height), Sense::hover()); + } + easy_mark::Item::QuoteIndent => { + let rect = ui + .allocate_exact_size(vec2(row_height, row_height), Sense::hover()) + .0; + let rect = rect.expand2(spacing * 0.5); + ui.painter().line_segment( + [rect.center_top(), rect.center_bottom()], + (1.0, ui.style().visuals.weak_text_color()), + ); + } + easy_mark::Item::BulletPoint => { + ui.allocate_exact_size(vec2(one_indent, row_height), Sense::hover()); + bullet_point(ui, one_indent); + ui.allocate_exact_size(vec2(one_indent, row_height), Sense::hover()); + } + easy_mark::Item::NumberedPoint(number) => { + let width = 3.0 * one_indent; + numbered_point(ui, width, number); + ui.allocate_exact_size(vec2(one_indent, row_height), Sense::hover()); + } + easy_mark::Item::CodeBlock(_language, code) => { + let where_to_put_background = ui.painter().add(Shape::Noop); + let mut rect = ui.monospace(code).rect; + rect = rect.expand(1.0); // looks better + rect.max.x = ui.max_rect_finite().max.x; + let code_bg_color = ui.style().visuals.code_bg_color; + ui.painter().set( + where_to_put_background, + Shape::rect_filled(rect, 1.0, code_bg_color), + ); + } + }; + } + }); +} + +fn label_from_style(text: &str, style: easy_mark::Style) -> Label { + let easy_mark::Style { + heading, + quoted, + code, + strong, + underline, + strikethrough, + italics, + } = style; + + let mut label = Label::new(text); + if heading { + label = label.heading().strong(); + } + if code { + label = label.code(); + } + if strong { + label = label.strong(); + } else if quoted { + label = label.weak(); + } + if underline { + label = label.underline(); + } + if strikethrough { + label = label.strikethrough(); + } + if italics { + label = label.italics(); + } + label +} + +fn bullet_point(ui: &mut Ui, width: f32) -> Response { + let row_height = ui.fonts()[TextStyle::Body].row_height(); + let (rect, response) = ui.allocate_exact_size(vec2(width, row_height), Sense::hover()); + ui.painter().circle_filled( + rect.center(), + rect.height() / 8.0, + ui.style().visuals.strong_text_color(), + ); + response +} + +fn numbered_point(ui: &mut Ui, width: f32, number: &str) -> Response { + let row_height = ui.fonts()[TextStyle::Body].row_height(); + let (rect, response) = ui.allocate_exact_size(vec2(width, row_height), Sense::hover()); + let text = format!("{}.", number); + let text_color = ui.style().visuals.strong_text_color(); + ui.painter().text( + rect.right_center(), + Align2::RIGHT_CENTER, + text, + TextStyle::Body, + text_color, + ); + response +} diff --git a/egui/src/experimental/markdown.rs b/egui/src/experimental/markdown.rs deleted file mode 100644 index 8bf1f7e7..00000000 --- a/egui/src/experimental/markdown.rs +++ /dev/null @@ -1,57 +0,0 @@ -use super::markdown_parser::*; -use crate::*; - -/// Parse and display a VERY simple and small subset of Markdown. -pub fn markdown(ui: &mut Ui, markdown: &str) { - ui.horizontal_wrapped(|ui| { - let row_height = ui.fonts()[TextStyle::Body].row_height(); - let style = ui.style_mut(); - style.spacing.interact_size.y = row_height; - style.spacing.item_spacing = vec2(0.0, 2.0); - - for item in MarkdownParser::new(markdown) { - match item { - MarkdownItem::Newline => { - // ui.label("\n"); // too much spacing (paragraph spacing) - ui.allocate_exact_size(vec2(0.0, row_height), Sense::hover()); // make sure we take up some height - ui.end_row(); - } - MarkdownItem::Separator => { - ui.add(Separator::new().horizontal()); - } - MarkdownItem::BulletPoint(indent) => { - let indent = indent as f32 * row_height / 3.0; - ui.allocate_exact_size(vec2(indent, row_height), Sense::hover()); - bullet_point(ui); - } - MarkdownItem::Body(range) => { - ui.label(range); - } - MarkdownItem::Heading(range) => { - ui.heading(range); - } - MarkdownItem::Emphasis(range) => { - ui.colored_label(Color32::WHITE, range); - } - MarkdownItem::InlineCode(range) => { - ui.code(range); - } - MarkdownItem::Hyperlink(text, url) => { - ui.add(Hyperlink::new(url).text(text)); - } - }; - } - }); -} - -fn bullet_point(ui: &mut Ui) -> Response { - let row_height = ui.fonts()[TextStyle::Body].row_height(); - - let (rect, response) = ui.allocate_exact_size(vec2(row_height, row_height), Sense::hover()); - ui.painter().circle_filled( - rect.center(), - rect.height() / 5.0, - ui.style().visuals.text_color(), - ); - response -} diff --git a/egui/src/experimental/markdown_parser.rs b/egui/src/experimental/markdown_parser.rs deleted file mode 100644 index f484c966..00000000 --- a/egui/src/experimental/markdown_parser.rs +++ /dev/null @@ -1,153 +0,0 @@ -//! A parser for a VERY (and intentionally so) strict and limited sub-set of Markdown. -//! -//! WARNING: the parsed dialect is subject to change. -//! -//! Does not depend on anything else in egui (could perhaps be its own crate if it grows). - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum MarkdownItem<'a> { - Newline, - Separator, - BulletPoint(usize), - Body(&'a str), - Heading(&'a str), - Emphasis(&'a str), - InlineCode(&'a str), - Hyperlink(&'a str, &'a str), -} - -pub struct MarkdownParser<'a> { - s: &'a str, - start_of_line: bool, -} - -impl<'a> MarkdownParser<'a> { - pub fn new(s: &'a str) -> Self { - Self { - s, - start_of_line: true, - } - } -} - -impl<'a> Iterator for MarkdownParser<'a> { - type Item = MarkdownItem<'a>; - - fn next(&mut self) -> Option { - let Self { s, start_of_line } = self; - - if s.is_empty() { - return None; - } - - // - if s.starts_with("---\n") { - *s = &s[4..]; - *start_of_line = true; - return Some(MarkdownItem::Separator); - } - - // - if s.starts_with('\n') { - *s = &s[1..]; - *start_of_line = true; - return Some(MarkdownItem::Newline); - } - - // # Heading - if *start_of_line && s.starts_with("# ") { - *s = &s[2..]; - *start_of_line = false; - let end = s.find('\n').unwrap_or_else(|| s.len()); - let item = MarkdownItem::Heading(&s[..end]); - *s = &s[end..]; - return Some(item); - } - - // Ugly way to parse bullet points with indentation. - // TODO: parse leading spaces separately as `MarkdownItem::Indentation`. - for bullet in &["* ", " * ", " * ", " * ", " * "] { - // * bullet point - if *start_of_line && s.starts_with(bullet) { - *s = &s[bullet.len()..]; - *start_of_line = false; - return Some(MarkdownItem::BulletPoint(bullet.len() - 2)); - } - } - - // `code` - if s.starts_with('`') { - *s = &s[1..]; - *start_of_line = false; - if let Some(end) = s.find('`') { - let item = MarkdownItem::InlineCode(&s[..end]); - *s = &s[end + 1..]; - return Some(item); - } else { - let end = s.len(); - let item = MarkdownItem::InlineCode(&s[..end]); - *s = &s[end..]; - return Some(item); - } - } - - // *emphasis* - if s.starts_with('*') { - *s = &s[1..]; - *start_of_line = false; - if let Some(end) = s.find('*') { - let item = MarkdownItem::Emphasis(&s[..end]); - *s = &s[end + 1..]; - return Some(item); - } else { - let end = s.len(); - let item = MarkdownItem::Emphasis(&s[..end]); - *s = &s[end..]; - return Some(item); - } - } - - // [text](url) - if s.starts_with('[') { - if let Some(bracket_end) = s.find(']') { - let text = &s[1..bracket_end]; - if s[bracket_end + 1..].starts_with('(') { - if let Some(parens_end) = s[bracket_end + 2..].find(')') { - let parens_end = bracket_end + 2 + parens_end; - let url = &s[bracket_end + 2..parens_end]; - *s = &s[parens_end + 1..]; - *start_of_line = false; - return Some(MarkdownItem::Hyperlink(text, url)); - } - } - } - } - - let end = s[1..] - .find(&['#', '*', '`', '[', '\n'][..]) - .map(|i| i + 1) - .unwrap_or_else(|| s.len()); - let item = MarkdownItem::Body(&s[..end]); - *s = &s[end..]; - *start_of_line = false; - Some(item) - } -} - -#[test] -fn test_markdown() { - let parts: Vec<_> = MarkdownParser::new("# Hello\nworld `of` *fun* [link](url)").collect(); - assert_eq!( - parts, - vec![ - MarkdownItem::Heading("Hello"), - MarkdownItem::Newline, - MarkdownItem::Body("world "), - MarkdownItem::InlineCode("of"), - MarkdownItem::Body(" "), - MarkdownItem::Emphasis("fun"), - MarkdownItem::Body(" "), - MarkdownItem::Hyperlink("link", "url") - ] - ); -} diff --git a/egui/src/experimental/mod.rs b/egui/src/experimental/mod.rs index 8395bc6b..5a6084cf 100644 --- a/egui/src/experimental/mod.rs +++ b/egui/src/experimental/mod.rs @@ -2,7 +2,7 @@ //! //! Be very careful about depending on the experimental parts of egui! -mod markdown; -pub mod markdown_parser; +pub mod easy_mark_parser; +mod easy_mark_viewer; -pub use markdown::markdown; +pub use easy_mark_viewer::easy_mark; diff --git a/egui_demo_lib/src/apps/demo/demo_windows.rs b/egui_demo_lib/src/apps/demo/demo_windows.rs index d7c14438..08f48a9a 100644 --- a/egui_demo_lib/src/apps/demo/demo_windows.rs +++ b/egui_demo_lib/src/apps/demo/demo_windows.rs @@ -16,7 +16,6 @@ impl Default for Demos { Box::new(super::dancing_strings::DancingStrings::default()), Box::new(super::drag_and_drop::DragAndDropDemo::default()), Box::new(super::font_book::FontBook::default()), - Box::new(super::markdown_editor::MarkdownEditor::default()), Box::new(super::painting::Painting::default()), Box::new(super::scrolling::Scrolling::default()), Box::new(super::sliders::Sliders::default()), diff --git a/egui_demo_lib/src/apps/demo/markdown_editor.rs b/egui_demo_lib/src/apps/demo/markdown_editor.rs deleted file mode 100644 index 7c5ff82f..00000000 --- a/egui_demo_lib/src/apps/demo/markdown_editor.rs +++ /dev/null @@ -1,66 +0,0 @@ -use egui::*; - -#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] -#[derive(PartialEq)] -pub struct MarkdownEditor { - markdown: String, -} - -impl Default for MarkdownEditor { - fn default() -> Self { - Self { - markdown: r#" -# Markdown editor -Markdown support in egui is experimental, and *very* limited. There are: - -* bullet points - * with sub-points -* `inline code` -* *emphasis* -* [hyperlinks](https://github.com/emilk/egui) - ---- - -Also the separator - - "# - .trim_start() - .to_owned(), - } - } -} - -impl super::Demo for MarkdownEditor { - fn name(&self) -> &str { - "🖹 Markdown Editor" - } - - fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) { - egui::Window::new(self.name()).open(open).show(ctx, |ui| { - use super::View; - self.ui(ui); - }); - } -} - -impl super::View for MarkdownEditor { - fn ui(&mut self, ui: &mut egui::Ui) { - ui.vertical_centered(|ui| { - egui::reset_button(ui, self); - ui.add(crate::__egui_github_link_file!()); - }); - ui.separator(); - ui.columns(2, |columns| { - ScrollArea::auto_sized() - .id_source("source") - .show(&mut columns[0], |ui| { - ui.text_edit_multiline(&mut self.markdown); - }); - ScrollArea::auto_sized() - .id_source("rendered") - .show(&mut columns[1], |ui| { - egui::experimental::markdown(ui, &self.markdown); - }); - }); - } -} diff --git a/egui_demo_lib/src/apps/demo/mod.rs b/egui_demo_lib/src/apps/demo/mod.rs index 65b6fc2d..b46b61da 100644 --- a/egui_demo_lib/src/apps/demo/mod.rs +++ b/egui_demo_lib/src/apps/demo/mod.rs @@ -14,7 +14,6 @@ pub mod font_contents_emoji; pub mod font_contents_ubuntu; pub mod input_test; pub mod layout_test; -pub mod markdown_editor; pub mod painting; pub mod scrolling; pub mod sliders; diff --git a/egui_demo_lib/src/apps/easy_mark_editor.rs b/egui_demo_lib/src/apps/easy_mark_editor.rs new file mode 100644 index 00000000..735d21d1 --- /dev/null +++ b/egui_demo_lib/src/apps/easy_mark_editor.rs @@ -0,0 +1,117 @@ +use egui::*; + +#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] +#[derive(PartialEq)] +pub struct EasyMarkEditor { + code: String, +} + +impl Default for EasyMarkEditor { + fn default() -> Self { + Self { + code: DEFAULT_CODE.trim().to_owned(), + } + } +} + +impl epi::App for EasyMarkEditor { + fn name(&self) -> &str { + "🖹 EasyMark editor" + } + + fn update(&mut self, ctx: &egui::CtxRef, _frame: &mut epi::Frame<'_>) { + egui::CentralPanel::default().show(ctx, |ui| { + self.ui(ui); + }); + } +} + +impl EasyMarkEditor { + fn ui(&mut self, ui: &mut egui::Ui) { + ui.vertical_centered(|ui| { + egui::reset_button(ui, self); + ui.add(crate::__egui_github_link_file!()); + }); + ui.separator(); + ui.columns(2, |columns| { + ScrollArea::auto_sized() + .id_source("source") + .show(&mut columns[0], |ui| { + // ui.text_edit_multiline(&mut self.code); + ui.add(TextEdit::multiline(&mut self.code).text_style(TextStyle::Monospace)); + }); + ScrollArea::auto_sized() + .id_source("rendered") + .show(&mut columns[1], |ui| { + egui::experimental::easy_mark(ui, &self.code); + }); + }); + } +} + +const DEFAULT_CODE: &str = r#" +# EasyMark +EasyMark is a markup language, designed for extreme simplicity. + +``` +WARNING: EasyMark is still an evolving specification, +and is also missing some features. +``` + +---------------- + +# At a glance +- inline text: + - normal, `code`, *strong*, ~strikethrough~, _underline_, /italics/ + - `\` escapes the next character + - [hyperlink](https://github.com/emilk/egui) + - Embedded URL: +- `# ` header +- `---` separator (horizontal line) +- `> ` quote +- `- ` bullet list +- `1. ` numbered list +- \`\`\` code fence + +# Design +> /"Why do what everyone else is doing, when everyone else is already doing it?" +> \- Emil + +Goals: +1. easy to parse +2. easy to learn +3. similar to markdown + +[The reference parser](https://github.com/emilk/egui/blob/master/egui/src/experimental/easy_mark_parser.rs) is \~250 lines of code, using only the Rust standard library. The parser uses no look-ahead or recursion. + +There is never more than one way to accomplish the same thing, and each special character is only used for one thing. For instance `*` is used for *strong* and `-` is used for bullet lists. There is no alternative way to specify the *strong* style or getting a bullet list. + +Similarity to markdown is kept when possible, but with much less ambiguity and some improvements (like _underlining_). + +# Details +All style changes are single characters, so it is `*strong*`, NOT `**strong**`. Style is reset by a matching character, or at the end of the line. + +Style change characters and escapes (`\`) work everywhere except for in inline code, code blocks and in URLs. + +You can mix styles. For instance: /italics _underline_/ and *strong `code`*. + +You can use styles on URLs: ~my webpage is at ~. + +Newlines are preserved. If you want to continue text on the same line, just do so. Alternatively, escape the newline by ending the line with a backslash (`\`). \ +Escaping the newline effectively ignores it. + +The style characters are chosen to be similar to what they are representing: + `_` = _underline_ + `~` = ~strikethrough~ (`-` is too common in normal text) + `/` = /italics/ + `*` = *strong* + +# TODO +- Sub-headers (`## h2`, `### h3` etc) +- Images + - we want to be able to optionally specify size (width and\/or height) + - centering of images is very desirable + - captioning (image with a text underneath it) + - `![caption=My image][width=200][center](url)` ? +- Tables +"#; diff --git a/egui_demo_lib/src/apps/mod.rs b/egui_demo_lib/src/apps/mod.rs index f801529f..0693c74c 100644 --- a/egui_demo_lib/src/apps/mod.rs +++ b/egui_demo_lib/src/apps/mod.rs @@ -1,11 +1,13 @@ mod color_test; mod demo; +mod easy_mark_editor; mod fractal_clock; #[cfg(feature = "http")] mod http_app; pub use color_test::ColorTest; pub use demo::DemoApp; +pub use easy_mark_editor::EasyMarkEditor; pub use fractal_clock::FractalClock; #[cfg(feature = "http")] pub use http_app::HttpApp; diff --git a/egui_demo_lib/src/wrap_app.rs b/egui_demo_lib/src/wrap_app.rs index 01e006bd..b4e915cb 100644 --- a/egui_demo_lib/src/wrap_app.rs +++ b/egui_demo_lib/src/wrap_app.rs @@ -4,6 +4,7 @@ #[cfg_attr(feature = "persistence", serde(default))] pub struct Apps { demo: crate::apps::DemoApp, + easy_mark_editor: crate::apps::EasyMarkEditor, #[cfg(feature = "http")] http: crate::apps::HttpApp, clock: crate::apps::FractalClock, @@ -14,6 +15,7 @@ impl Apps { fn iter_mut(&mut self) -> impl Iterator { vec![ ("demo", &mut self.demo as &mut dyn epi::App), + ("easymark", &mut self.easy_mark_editor as &mut dyn epi::App), #[cfg(feature = "http")] ("http", &mut self.http as &mut dyn epi::App), ("clock", &mut self.clock as &mut dyn epi::App),