diff --git a/egui/src/experimental/markdown.rs b/egui/src/experimental/markdown.rs new file mode 100644 index 00000000..8bf1f7e7 --- /dev/null +++ b/egui/src/experimental/markdown.rs @@ -0,0 +1,57 @@ +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 new file mode 100644 index 00000000..f484c966 --- /dev/null +++ b/egui/src/experimental/markdown_parser.rs @@ -0,0 +1,153 @@ +//! 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 new file mode 100644 index 00000000..8395bc6b --- /dev/null +++ b/egui/src/experimental/mod.rs @@ -0,0 +1,8 @@ +//! Experimental parts of egui, that may change suddenly or get removed. +//! +//! Be very careful about depending on the experimental parts of egui! + +mod markdown; +pub mod markdown_parser; + +pub use markdown::markdown; diff --git a/egui/src/lib.rs b/egui/src/lib.rs index cc2a9389..1c73fd98 100644 --- a/egui/src/lib.rs +++ b/egui/src/lib.rs @@ -82,6 +82,7 @@ mod animation_manager; pub mod containers; mod context; mod data; +pub mod experimental; pub(crate) mod grid; mod id; mod input_state; diff --git a/egui_demo_lib/src/apps/demo/demo_windows.rs b/egui_demo_lib/src/apps/demo/demo_windows.rs index 9913b539..d7c14438 100644 --- a/egui_demo_lib/src/apps/demo/demo_windows.rs +++ b/egui_demo_lib/src/apps/demo/demo_windows.rs @@ -13,17 +13,19 @@ struct Demos { impl Default for Demos { fn default() -> Self { let demos: Vec> = vec![ - Box::new(super::widget_gallery::WidgetGallery::default()), - Box::new(super::sliders::Sliders::default()), - Box::new(super::input_test::InputTest::default()), - Box::new(super::font_book::FontBook::default()), - Box::new(super::painting::Painting::default()), Box::new(super::dancing_strings::DancingStrings::default()), Box::new(super::drag_and_drop::DragAndDropDemo::default()), - Box::new(super::tests::Tests::default()), - Box::new(super::window_options::WindowOptions::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()), + Box::new(super::widget_gallery::WidgetGallery::default()), + Box::new(super::window_options::WindowOptions::default()), + // Tests: Box::new(super::layout_test::LayoutTest::default()), + Box::new(super::tests::IdTest::default()), + Box::new(super::input_test::InputTest::default()), ]; Self { open: vec![false; demos.len()], diff --git a/egui_demo_lib/src/apps/demo/layout_test.rs b/egui_demo_lib/src/apps/demo/layout_test.rs index e219098e..5dd569f1 100644 --- a/egui_demo_lib/src/apps/demo/layout_test.rs +++ b/egui_demo_lib/src/apps/demo/layout_test.rs @@ -29,7 +29,7 @@ impl Default for LayoutTest { impl super::Demo for LayoutTest { fn name(&self) -> &str { - "🖹 Layouts" + "🗺 Layout Test" } fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) { diff --git a/egui_demo_lib/src/apps/demo/markdown_editor.rs b/egui_demo_lib/src/apps/demo/markdown_editor.rs new file mode 100644 index 00000000..7c5ff82f --- /dev/null +++ b/egui_demo_lib/src/apps/demo/markdown_editor.rs @@ -0,0 +1,66 @@ +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 b46b61da..65b6fc2d 100644 --- a/egui_demo_lib/src/apps/demo/mod.rs +++ b/egui_demo_lib/src/apps/demo/mod.rs @@ -14,6 +14,7 @@ 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/demo/tests.rs b/egui_demo_lib/src/apps/demo/tests.rs index f1bb08b6..2287a7b8 100644 --- a/egui_demo_lib/src/apps/demo/tests.rs +++ b/egui_demo_lib/src/apps/demo/tests.rs @@ -1,9 +1,9 @@ #[derive(Default)] -pub struct Tests {} +pub struct IdTest {} -impl super::Demo for Tests { +impl super::Demo for IdTest { fn name(&self) -> &str { - "📋 Tests" + "📋 ID Test" } fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) { @@ -14,7 +14,7 @@ impl super::Demo for Tests { } } -impl super::View for Tests { +impl super::View for IdTest { fn ui(&mut self, ui: &mut egui::Ui) { ui.heading("Name collision example");