1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-01 22:30:03 -04:00

Move all crates into a crates directory (#1940)

This commit is contained in:
Emil Ernerfeldt
2022-08-20 10:41:49 +02:00
committed by GitHub
parent 5c63648c02
commit 041f2e64ba
260 changed files with 59 additions and 57 deletions

View File

@@ -0,0 +1,260 @@
use egui::{text_edit::CCursorRange, *};
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct EasyMarkEditor {
code: String,
highlight_editor: bool,
show_rendered: bool,
#[cfg_attr(feature = "serde", 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(),
}
}
}
impl EasyMarkEditor {
pub fn panels(&mut self, ctx: &egui::Context) {
egui::TopBottomPanel::bottom("easy_mark_bottom").show(ctx, |ui| {
let layout = egui::Layout::top_down(egui::Align::Center).with_main_justify(true);
ui.allocate_ui_with_layout(ui.available_size(), layout, |ui| {
ui.add(crate::egui_github_link_file!())
})
});
egui::CentralPanel::default().show(ctx, |ui| {
self.ui(ui);
});
}
pub fn ui(&mut self, ui: &mut egui::Ui) {
egui::Grid::new("controls").show(ui, |ui| {
let _ = ui.button("Hotkeys").on_hover_ui(nested_hotkeys_ui);
ui.checkbox(&mut self.show_rendered, "Show rendered");
ui.checkbox(&mut self.highlight_editor, "Highlight editor");
egui::reset_button(ui, self);
ui.end_row();
});
ui.separator();
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(emilk): 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(ui, |ui| self.editor_ui(ui));
}
}
fn editor_ui(&mut self, ui: &mut egui::Ui) {
let Self {
code, highlighter, ..
} = self;
let response = if self.highlight_editor {
let mut layouter = |ui: &egui::Ui, easymark: &str, wrap_width: f32| {
let mut layout_job = highlighter.highlight(ui.style(), easymark);
layout_job.wrap.max_width = wrap_width;
ui.fonts().layout_job(layout_job)
};
ui.add(
egui::TextEdit::multiline(code)
.desired_width(f32::INFINITY)
.font(egui::TextStyle::Monospace) // for cursor height
.layouter(&mut layouter),
)
} else {
ui.add(egui::TextEdit::multiline(code).desired_width(f32::INFINITY))
};
if let Some(mut state) = TextEdit::load_state(ui.ctx(), response.id) {
if let Some(mut ccursor_range) = state.ccursor_range() {
let any_change = shortcuts(ui, code, &mut ccursor_range);
if any_change {
state.set_ccursor_range(Some(ccursor_range));
state.store(ui.ctx(), response.id);
}
}
}
}
}
fn nested_hotkeys_ui(ui: &mut egui::Ui) {
let _ = ui.label("CTRL+B *bold*");
let _ = ui.label("CTRL+N `code`");
let _ = ui.label("CTRL+I /italics/");
let _ = ui.label("CTRL+L $subscript$");
let _ = ui.label("CTRL+Y ^superscript^");
let _ = ui.label("ALT+SHIFT+Q ~strikethrough~");
let _ = ui.label("ALT+SHIFT+W _underline_");
let _ = ui.label("ALT+SHIFT+E two spaces"); // Placeholder for tab indent
}
fn shortcuts(ui: &Ui, code: &mut dyn TextBuffer, ccursor_range: &mut CCursorRange) -> bool {
let mut any_change = false;
if ui
.input_mut()
.consume_key(egui::Modifiers::ALT_SHIFT, Key::E)
{
// This is a placeholder till we can indent the active line
any_change = true;
let [primary, _secondary] = ccursor_range.sorted();
let advance = code.insert_text(" ", primary.index);
ccursor_range.primary.index += advance;
ccursor_range.secondary.index += advance;
}
for (modifier, key, surrounding) in [
(egui::Modifiers::COMMAND, Key::B, "*"), // *bold*
(egui::Modifiers::COMMAND, Key::N, "`"), // `code`
(egui::Modifiers::COMMAND, Key::I, "/"), // /italics/
(egui::Modifiers::COMMAND, Key::L, "$"), // $subscript$
(egui::Modifiers::COMMAND, Key::Y, "^"), // ^superscript^
(egui::Modifiers::ALT_SHIFT, Key::Q, "~"), // ~strikethrough~
(egui::Modifiers::ALT_SHIFT, Key::W, "_"), // _underline_
] {
if ui.input_mut().consume_key(modifier, key) {
any_change = true;
toggle_surrounding(code, ccursor_range, surrounding);
};
}
any_change
}
/// E.g. toggle *strong* with `toggle_surrounding(&mut text, &mut cursor, "*")`
fn toggle_surrounding(
code: &mut dyn TextBuffer,
ccursor_range: &mut CCursorRange,
surrounding: &str,
) {
let [primary, secondary] = ccursor_range.sorted();
let surrounding_ccount = surrounding.chars().count();
let prefix_crange = primary.index.saturating_sub(surrounding_ccount)..primary.index;
let suffix_crange = secondary.index..secondary.index.saturating_add(surrounding_ccount);
let already_surrounded = code.char_range(prefix_crange.clone()) == surrounding
&& code.char_range(suffix_crange.clone()) == surrounding;
if already_surrounded {
code.delete_char_range(suffix_crange);
code.delete_char_range(prefix_crange);
ccursor_range.primary.index -= surrounding_ccount;
ccursor_range.secondary.index -= surrounding_ccount;
} else {
code.insert_text(surrounding, secondary.index);
let advance = code.insert_text(surrounding, primary.index);
ccursor_range.primary.index += advance;
ccursor_range.secondary.index += advance;
}
}
// ----------------------------------------------------------------------------
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/, ^raised^, $small$
- `\` escapes the next character
- [hyperlink](https://github.com/emilk/egui)
- Embedded URL: <https://github.com/emilk/egui>
- `# ` header
- `---` separator (horizontal line)
- `> ` quote
- `- ` bullet list
- `1. ` numbered list
- \`\`\` code fence
- a^2^ + b^2^ = c^2^
- $Remember to read the small print$
# 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_demo_lib/src/easy_mark/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 <http://www.example.com>~.
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 used for bullet points)
`/` = /italics/
`*` = *strong*
`$` = $small$
`^` = ^raised^
# TODO
- Sub-headers (`## h2`, `### h3` etc)
- Hotkey Editor
- International keyboard algorithm for non-letter keys
- ALT+SHIFT+Num1 is not a functioning hotkey
- Tab Indent Increment/Decrement CTRL+], CTRL+[
- 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)` ?
- Nicer URL:s
- `<url>` and `[url](url)` do the same thing yet look completely different.
- let's keep similarity with images
- Tables
- Inspiration: <https://mycorrhiza.lesarbr.es/page/mycomarkup>
"#;

View File

@@ -0,0 +1,192 @@
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 {
style: egui::Style,
code: String,
output: egui::text::LayoutJob,
}
impl MemoizedEasymarkHighlighter {
pub fn highlight(&mut self, egui_style: &egui::Style, code: &str) -> egui::text::LayoutJob {
if (&self.style, self.code.as_str()) != (egui_style, code) {
self.style = egui_style.clone();
self.code = code.to_owned();
self.output = highlight_easymark(egui_style, code);
}
self.output.clone()
}
}
pub fn highlight_easymark(egui_style: &egui::Style, 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_or_else(|| text.len(), |i| i + 4);
job.append(
&text[..end],
0.0,
format_from_style(
egui_style,
&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_or_else(|| text.len(), |i| i + 2);
job.append(&text[..end], 0.0, format_from_style(egui_style, &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(' ') {
// 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;
// we don't preview indentation, because it is confusing
} else if start_of_line && text.starts_with("- ") {
skip = 2;
// we don't preview indentation, because it is confusing
} else if text.starts_with('*') {
skip = 1;
if style.strong {
// Include the character that is ending this style:
job.append(&text[..skip], 0.0, format_from_style(egui_style, &style));
text = &text[skip..];
skip = 0;
}
style.strong ^= true;
} else if text.starts_with('$') {
skip = 1;
if style.small {
// Include the character that is ending this style:
job.append(&text[..skip], 0.0, format_from_style(egui_style, &style));
text = &text[skip..];
skip = 0;
}
style.small ^= true;
} else if text.starts_with('^') {
skip = 1;
if style.raised {
// Include the character that is ending this style:
job.append(&text[..skip], 0.0, format_from_style(egui_style, &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_or_else(|| text.len(), |i| (skip + i + 1));
let end = text[skip..]
.find(&['*', '`', '~', '_', '/', '$', '^', '\\', '<', '['][..])
.map_or_else(|| text.len(), |i| (skip + i).max(1));
if line_end <= end {
job.append(
&text[..line_end],
0.0,
format_from_style(egui_style, &style),
);
text = &text[line_end..];
start_of_line = true;
style = Default::default();
} else {
job.append(&text[..end], 0.0, format_from_style(egui_style, &style));
text = &text[end..];
start_of_line = false;
}
}
job
}
fn format_from_style(
egui_style: &egui::Style,
emark_style: &easy_mark_parser::Style,
) -> egui::text::TextFormat {
use egui::{Align, Color32, Stroke, TextStyle};
let color = if emark_style.strong || emark_style.heading {
egui_style.visuals.strong_text_color()
} else if emark_style.quoted {
egui_style.visuals.weak_text_color()
} else {
egui_style.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 {
egui_style.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 {
font_id: text_style.resolve(egui_style),
color,
background,
italics: emark_style.italics,
underline,
strikethrough,
valign,
}
}

View File

@@ -0,0 +1,356 @@
//! A parser for `EasyMark`: a very simple markup language.
//!
//! WARNING: `EasyMark` is subject to change.
//
//! # `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`
// TODO(emilk): add Style here so empty heading still uses up the right amount of space.
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,
/// $small$
pub small: bool,
/// ^raised^
pub raised: bool,
}
/// Parser for the `EasyMark` markup language.
///
/// See the module-level documentation for details.
///
/// # Example:
/// ```
/// # use egui_demo_lib::easy_mark::parser::Parser;
/// for item in Parser::new("Hello *world*!") {
/// }
///
/// ```
pub struct Parser<'a> {
/// The remainder 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<Item<'a>> {
let n_digits = self.s.chars().take_while(|c| c.is_ascii_digit()).count();
if n_digits > 0 && self.s.chars().skip(n_digits).take(2).eq(". ".chars()) {
let number = &self.s[..n_digits];
self.s = &self.s[(n_digits + 2)..];
self.start_of_line = false;
return Some(Item::NumberedPoint(number));
}
None
}
// ```{language}\n{code}```
fn code_block(&mut self) -> Option<Item<'a>> {
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<Item<'a>> {
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(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
}
/// `<url>` or `[link](url)`
fn url(&mut self) -> Option<Item<'a>> {
if self.s.starts_with('<') {
let this_line = &self.s[..self.s.find('\n').unwrap_or(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(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<Self::Item> {
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(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;
}
if let Some(rest) = self.s.strip_prefix('$') {
self.s = rest;
self.start_of_line = false;
self.style.small = !self.style.small;
continue;
}
if let Some(rest) = self.s.strip_prefix('^') {
self.s = rest;
self.start_of_line = false;
self.style.raised = !self.style.raised;
continue;
}
// `<url>` or `[link](url)`
if let Some(item) = self.url() {
return Some(item);
}
// Swallow everything up to the next special character:
let end = self
.s
.find(&['*', '`', '~', '_', '/', '$', '^', '\\', '<', '[', '\n'][..])
.map_or_else(|| self.s.len(), |special| special.max(1));
let item = Item::Text(self.style, &self.s[..end]);
self.s = &self.s[end..];
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"
),
]
);
}

View File

@@ -0,0 +1,159 @@
use super::easy_mark_parser as easy_mark;
use egui::*;
/// Parse and display a VERY simple and small subset of Markdown.
pub fn easy_mark(ui: &mut Ui, easy_mark: &str) {
easy_mark_it(ui, easy_mark::Parser::new(easy_mark));
}
pub fn easy_mark_it<'em>(ui: &mut Ui, items: impl Iterator<Item = easy_mark::Item<'em>>) {
let initial_size = vec2(
ui.available_width(),
ui.spacing().interact_size.y, // Assume there will be
);
let layout = Layout::left_to_right(Align::BOTTOM).with_main_wrap(true);
ui.allocate_ui_with_layout(initial_size, layout, |ui| {
ui.spacing_mut().item_spacing.x = 0.0;
let row_height = ui.text_style_height(&TextStyle::Body);
ui.set_row_height(row_height);
for item in items {
item_ui(ui, item);
}
});
}
pub fn item_ui(ui: &mut Ui, item: easy_mark::Item<'_>) {
let row_height = ui.text_style_height(&TextStyle::Body);
let one_indent = row_height / 2.0;
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();
ui.set_row_height(row_height);
}
easy_mark::Item::Text(style, text) => {
ui.label(rich_text_from_style(text, &style));
}
easy_mark::Item::Hyperlink(style, text, url) => {
let label = rich_text_from_style(text, &style);
ui.add(Hyperlink::from_label_and_url(label, url));
}
easy_mark::Item::Separator => {
ui.add(Separator::default().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(2.0 * one_indent, row_height), Sense::hover())
.0;
let rect = rect.expand2(ui.style().spacing.item_spacing * 0.5);
ui.painter().line_segment(
[rect.center_top(), rect.center_bottom()],
(1.0, ui.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().max.x;
let code_bg_color = ui.visuals().code_bg_color;
ui.painter().set(
where_to_put_background,
Shape::rect_filled(rect, 1.0, code_bg_color),
);
}
};
}
fn rich_text_from_style(text: &str, style: &easy_mark::Style) -> RichText {
let easy_mark::Style {
heading,
quoted,
code,
strong,
underline,
strikethrough,
italics,
small,
raised,
} = *style;
let small = small || raised; // Raised text is also smaller
let mut rich_text = RichText::new(text);
if heading && !small {
rich_text = rich_text.heading().strong();
}
if small && !heading {
rich_text = rich_text.small();
}
if code {
rich_text = rich_text.code();
}
if strong {
rich_text = rich_text.strong();
} else if quoted {
rich_text = rich_text.weak();
}
if underline {
rich_text = rich_text.underline();
}
if strikethrough {
rich_text = rich_text.strikethrough();
}
if italics {
rich_text = rich_text.italics();
}
if raised {
rich_text = rich_text.raised();
}
rich_text
}
fn bullet_point(ui: &mut Ui, width: f32) -> Response {
let row_height = ui.text_style_height(&TextStyle::Body);
let (rect, response) = ui.allocate_exact_size(vec2(width, row_height), Sense::hover());
ui.painter().circle_filled(
rect.center(),
rect.height() / 8.0,
ui.visuals().strong_text_color(),
);
response
}
fn numbered_point(ui: &mut Ui, width: f32, number: &str) -> Response {
let font_id = TextStyle::Body.resolve(ui.style());
let row_height = ui.fonts().row_height(&font_id);
let (rect, response) = ui.allocate_exact_size(vec2(width, row_height), Sense::hover());
let text = format!("{}.", number);
let text_color = ui.visuals().strong_text_color();
ui.painter().text(
rect.right_center(),
Align2::RIGHT_CENTER,
text,
font_id,
text_color,
);
response
}

View File

@@ -0,0 +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;