Add a VERY experimental markdown viewer
This commit is contained in:
parent
7d8ebb4c8f
commit
6029a438a2
9 changed files with 300 additions and 12 deletions
57
egui/src/experimental/markdown.rs
Normal file
57
egui/src/experimental/markdown.rs
Normal file
|
@ -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
|
||||||
|
}
|
153
egui/src/experimental/markdown_parser.rs
Normal file
153
egui/src/experimental/markdown_parser.rs
Normal file
|
@ -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<Self::Item> {
|
||||||
|
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")
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
8
egui/src/experimental/mod.rs
Normal file
8
egui/src/experimental/mod.rs
Normal file
|
@ -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;
|
|
@ -82,6 +82,7 @@ mod animation_manager;
|
||||||
pub mod containers;
|
pub mod containers;
|
||||||
mod context;
|
mod context;
|
||||||
mod data;
|
mod data;
|
||||||
|
pub mod experimental;
|
||||||
pub(crate) mod grid;
|
pub(crate) mod grid;
|
||||||
mod id;
|
mod id;
|
||||||
mod input_state;
|
mod input_state;
|
||||||
|
|
|
@ -13,17 +13,19 @@ struct Demos {
|
||||||
impl Default for Demos {
|
impl Default for Demos {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let demos: Vec<Box<dyn super::Demo>> = vec![
|
let demos: Vec<Box<dyn super::Demo>> = 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::dancing_strings::DancingStrings::default()),
|
||||||
Box::new(super::drag_and_drop::DragAndDropDemo::default()),
|
Box::new(super::drag_and_drop::DragAndDropDemo::default()),
|
||||||
Box::new(super::tests::Tests::default()),
|
Box::new(super::font_book::FontBook::default()),
|
||||||
Box::new(super::window_options::WindowOptions::default()),
|
Box::new(super::markdown_editor::MarkdownEditor::default()),
|
||||||
|
Box::new(super::painting::Painting::default()),
|
||||||
Box::new(super::scrolling::Scrolling::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::layout_test::LayoutTest::default()),
|
||||||
|
Box::new(super::tests::IdTest::default()),
|
||||||
|
Box::new(super::input_test::InputTest::default()),
|
||||||
];
|
];
|
||||||
Self {
|
Self {
|
||||||
open: vec![false; demos.len()],
|
open: vec![false; demos.len()],
|
||||||
|
|
|
@ -29,7 +29,7 @@ impl Default for LayoutTest {
|
||||||
|
|
||||||
impl super::Demo for LayoutTest {
|
impl super::Demo for LayoutTest {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
"🖹 Layouts"
|
"🗺 Layout Test"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) {
|
fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) {
|
||||||
|
|
66
egui_demo_lib/src/apps/demo/markdown_editor.rs
Normal file
66
egui_demo_lib/src/apps/demo/markdown_editor.rs
Normal file
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ pub mod font_contents_emoji;
|
||||||
pub mod font_contents_ubuntu;
|
pub mod font_contents_ubuntu;
|
||||||
pub mod input_test;
|
pub mod input_test;
|
||||||
pub mod layout_test;
|
pub mod layout_test;
|
||||||
|
pub mod markdown_editor;
|
||||||
pub mod painting;
|
pub mod painting;
|
||||||
pub mod scrolling;
|
pub mod scrolling;
|
||||||
pub mod sliders;
|
pub mod sliders;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Tests {}
|
pub struct IdTest {}
|
||||||
|
|
||||||
impl super::Demo for Tests {
|
impl super::Demo for IdTest {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
"📋 Tests"
|
"📋 ID Test"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) {
|
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) {
|
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
ui.heading("Name collision example");
|
ui.heading("Name collision example");
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue