diff options
author | 2024-07-18 13:14:56 +0200 | |
---|---|---|
committer | 2024-07-18 13:14:56 +0200 | |
commit | 904704d7c1b006c850654dcf3bf9e856e23cb317 (patch) | |
tree | f26c32d5769a184c72ca3dc4accacd1ac634d915 /examples/markdown | |
parent | 910eb72a0620b34e5b3d7793bbd5ab7290e08dd6 (diff) | |
download | iced-904704d7c1b006c850654dcf3bf9e856e23cb317.tar.gz iced-904704d7c1b006c850654dcf3bf9e856e23cb317.tar.bz2 iced-904704d7c1b006c850654dcf3bf9e856e23cb317.zip |
Flesh out the `markdown` example a bit more
Diffstat (limited to 'examples/markdown')
-rw-r--r-- | examples/markdown/Cargo.toml | 2 | ||||
-rw-r--r-- | examples/markdown/overview.md | 102 | ||||
-rw-r--r-- | examples/markdown/src/main.rs | 356 |
3 files changed, 336 insertions, 124 deletions
diff --git a/examples/markdown/Cargo.toml b/examples/markdown/Cargo.toml index f9bf4042..6875ee94 100644 --- a/examples/markdown/Cargo.toml +++ b/examples/markdown/Cargo.toml @@ -7,6 +7,6 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["debug"] +iced.features = ["highlighter", "debug"] pulldown-cmark = "0.11" diff --git a/examples/markdown/overview.md b/examples/markdown/overview.md new file mode 100644 index 00000000..ca3250f1 --- /dev/null +++ b/examples/markdown/overview.md @@ -0,0 +1,102 @@ +# Overview + +Inspired by [The Elm Architecture], Iced expects you to split user interfaces +into four different concepts: + +* __State__ — the state of your application +* __Messages__ — user interactions or meaningful events that you care + about +* __View logic__ — a way to display your __state__ as widgets that + may produce __messages__ on user interaction +* __Update logic__ — a way to react to __messages__ and update your + __state__ + +We can build something to see how this works! Let's say we want a simple counter +that can be incremented and decremented using two buttons. + +We start by modelling the __state__ of our application: + +```rust +#[derive(Default)] +struct Counter { + value: i32, +} +``` + +Next, we need to define the possible user interactions of our counter: +the button presses. These interactions are our __messages__: + +```rust +#[derive(Debug, Clone, Copy)] +pub enum Message { + Increment, + Decrement, +} +``` + +Now, let's show the actual counter by putting it all together in our +__view logic__: + +```rust +use iced::widget::{button, column, text, Column}; + +impl Counter { + pub fn view(&self) -> Column<Message> { + // We use a column: a simple vertical layout + column![ + // The increment button. We tell it to produce an + // `Increment` message when pressed + button("+").on_press(Message::Increment), + + // We show the value of the counter here + text(self.value).size(50), + + // The decrement button. We tell it to produce a + // `Decrement` message when pressed + button("-").on_press(Message::Decrement), + ] + } +} +``` + +Finally, we need to be able to react to any produced __messages__ and change our +__state__ accordingly in our __update logic__: + +```rust +impl Counter { + // ... + + pub fn update(&mut self, message: Message) { + match message { + Message::Increment => { + self.value += 1; + } + Message::Decrement => { + self.value -= 1; + } + } + } +} +``` + +And that's everything! We just wrote a whole user interface. Let's run it: + +```rust +fn main() -> iced::Result { + iced::run("A cool counter", Counter::update, Counter::view) +} +``` + +Iced will automatically: + + 1. Take the result of our __view logic__ and layout its widgets. + 1. Process events from our system and produce __messages__ for our + __update logic__. + 1. Draw the resulting user interface. + +Read the [book], the [documentation], and the [examples] to learn more! + +[book]: https://book.iced.rs/ +[documentation]: https://docs.rs/iced/ +[examples]: https://github.com/iced-rs/iced/tree/master/examples#examples +[The Elm Architecture]: https://guide.elm-lang.org/architecture/ diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index 43adaf72..384645fa 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -1,7 +1,6 @@ -use iced::font; -use iced::padding; use iced::widget::{ - self, column, container, rich_text, row, span, text_editor, + self, column, container, rich_text, row, scrollable, span, text, + text_editor, }; use iced::{Element, Fill, Font, Task, Theme}; @@ -13,6 +12,8 @@ pub fn main() -> iced::Result { struct Markdown { content: text_editor::Content, + items: Vec<Item>, + theme: Theme, } #[derive(Debug, Clone)] @@ -22,11 +23,15 @@ enum Message { impl Markdown { fn new() -> (Self, Task<Message>) { + const INITIAL_CONTENT: &str = include_str!("../overview.md"); + + let theme = Theme::TokyoNight; + ( Self { - content: text_editor::Content::with_text( - "# Markdown Editor\nType your Markdown here...", - ), + content: text_editor::Content::with_text(INITIAL_CONTENT), + items: parse(INITIAL_CONTENT, &theme).collect(), + theme, }, widget::focus_next(), ) @@ -34,7 +39,14 @@ impl Markdown { fn update(&mut self, message: Message) { match message { Message::Edit(action) => { + let is_edit = action.is_edit(); + self.content.perform(action); + + if is_edit { + self.items = + parse(&self.content.text(), &self.theme).collect(); + } } } } @@ -46,127 +58,225 @@ impl Markdown { .padding(10) .font(Font::MONOSPACE); - let preview = { - let markdown = self.content.text(); - let parser = pulldown_cmark::Parser::new(&markdown); - - let mut strong = false; - let mut emphasis = false; - let mut heading = None; - let mut spans = Vec::new(); - - let items = parser.filter_map(|event| match event { - pulldown_cmark::Event::Start(tag) => match tag { - pulldown_cmark::Tag::Strong => { - strong = true; - None - } - pulldown_cmark::Tag::Emphasis => { - emphasis = true; - None - } - pulldown_cmark::Tag::Heading { level, .. } => { - heading = Some(level); - None - } - _ => None, - }, - pulldown_cmark::Event::End(tag) => match tag { - pulldown_cmark::TagEnd::Emphasis => { - emphasis = false; - None - } - pulldown_cmark::TagEnd::Strong => { - strong = false; - None - } - pulldown_cmark::TagEnd::Heading(_) => { - heading = None; - Some( - container(rich_text(spans.drain(..))) - .padding(padding::bottom(5)) - .into(), - ) - } - pulldown_cmark::TagEnd::Paragraph => Some( - container(rich_text(spans.drain(..))) - .padding(padding::bottom(15)) - .into(), - ), - pulldown_cmark::TagEnd::CodeBlock => Some( - container( - container( - rich_text(spans.drain(..)) - .font(Font::MONOSPACE), - ) - .width(Fill) - .padding(10) - .style(container::rounded_box), - ) - .padding(padding::bottom(15)) - .into(), - ), - _ => None, - }, - pulldown_cmark::Event::Text(text) => { - let span = span(text.into_string()); - - let span = match heading { - None => span, - Some(heading) => span.size(match heading { - pulldown_cmark::HeadingLevel::H1 => 32, - pulldown_cmark::HeadingLevel::H2 => 28, - pulldown_cmark::HeadingLevel::H3 => 24, - pulldown_cmark::HeadingLevel::H4 => 20, - pulldown_cmark::HeadingLevel::H5 => 16, - pulldown_cmark::HeadingLevel::H6 => 16, - }), - }; - - let span = if strong || emphasis { - span.font(Font { - weight: if strong { - font::Weight::Bold - } else { - font::Weight::Normal - }, - style: if emphasis { - font::Style::Italic - } else { - font::Style::Normal - }, - ..Font::default() - }) - } else { - span - }; - - spans.push(span); - - None - } - pulldown_cmark::Event::Code(code) => { - spans.push(span(code.into_string()).font(Font::MONOSPACE)); - None - } - pulldown_cmark::Event::SoftBreak => { - spans.push(span(" ")); - None - } - pulldown_cmark::Event::HardBreak => { - spans.push(span("\n")); - None - } - _ => None, - }); - - column(items).width(Fill) - }; + let preview = markdown(&self.items); - row![editor, preview].spacing(10).padding(10).into() + row![ + editor, + scrollable(preview).spacing(10).width(Fill).height(Fill) + ] + .spacing(10) + .padding(10) + .into() } fn theme(&self) -> Theme { Theme::TokyoNight } } + +fn markdown<'a>( + items: impl IntoIterator<Item = &'a Item>, +) -> Element<'a, Message> { + use iced::padding; + + let blocks = items.into_iter().enumerate().map(|(i, item)| match item { + Item::Heading(heading) => container(rich_text(heading)) + .padding(padding::top(if i > 0 { 8 } else { 0 })) + .into(), + Item::Paragraph(paragraph) => rich_text(paragraph).into(), + Item::List { start: None, items } => column( + items + .iter() + .map(|item| row!["•", rich_text(item)].spacing(10).into()), + ) + .spacing(10) + .into(), + Item::List { + start: Some(start), + items, + } => column(items.iter().enumerate().map(|(i, item)| { + row![text!("{}.", i as u64 + *start), rich_text(item)] + .spacing(10) + .into() + })) + .spacing(10) + .into(), + Item::CodeBlock(code) => { + container(rich_text(code).font(Font::MONOSPACE).size(12)) + .width(Fill) + .padding(10) + .style(container::rounded_box) + .into() + } + }); + + column(blocks).width(Fill).spacing(16).into() +} + +#[derive(Debug, Clone)] +enum Item { + Heading(Vec<text::Span<'static>>), + Paragraph(Vec<text::Span<'static>>), + CodeBlock(Vec<text::Span<'static>>), + List { + start: Option<u64>, + items: Vec<Vec<text::Span<'static>>>, + }, +} + +fn parse<'a>( + markdown: &'a str, + theme: &'a Theme, +) -> impl Iterator<Item = Item> + 'a { + use iced::font; + use iced::highlighter::{self, Highlighter}; + use text::Highlighter as _; + + let mut spans = Vec::new(); + let mut heading = None; + let mut strong = false; + let mut emphasis = false; + let mut link = false; + let mut list = Vec::new(); + let mut list_start = None; + let mut highlighter = None; + + let parser = pulldown_cmark::Parser::new(markdown); + + // We want to keep the `spans` capacity + #[allow(clippy::drain_collect)] + parser.filter_map(move |event| match event { + pulldown_cmark::Event::Start(tag) => match tag { + pulldown_cmark::Tag::Heading { level, .. } => { + heading = Some(level); + None + } + pulldown_cmark::Tag::Strong => { + strong = true; + None + } + pulldown_cmark::Tag::Emphasis => { + emphasis = true; + None + } + pulldown_cmark::Tag::Link { .. } => { + link = true; + None + } + pulldown_cmark::Tag::List(first_item) => { + list_start = first_item; + None + } + pulldown_cmark::Tag::CodeBlock( + pulldown_cmark::CodeBlockKind::Fenced(language), + ) => { + highlighter = Some(Highlighter::new(&highlighter::Settings { + theme: highlighter::Theme::Base16Ocean, + token: language.to_string(), + })); + + None + } + _ => None, + }, + pulldown_cmark::Event::End(tag) => match tag { + pulldown_cmark::TagEnd::Heading(_) => { + heading = None; + Some(Item::Heading(spans.drain(..).collect())) + } + pulldown_cmark::TagEnd::Emphasis => { + emphasis = false; + None + } + pulldown_cmark::TagEnd::Strong => { + strong = false; + None + } + pulldown_cmark::TagEnd::Link => { + link = false; + None + } + pulldown_cmark::TagEnd::Paragraph => { + Some(Item::Paragraph(spans.drain(..).collect())) + } + pulldown_cmark::TagEnd::List(_) => Some(Item::List { + start: list_start, + items: list.drain(..).collect(), + }), + pulldown_cmark::TagEnd::Item => { + list.push(spans.drain(..).collect()); + None + } + pulldown_cmark::TagEnd::CodeBlock => { + highlighter = None; + Some(Item::CodeBlock(spans.drain(..).collect())) + } + _ => None, + }, + pulldown_cmark::Event::Text(text) => { + if let Some(highlighter) = &mut highlighter { + for (range, highlight) in + highlighter.highlight_line(text.as_ref()) + { + let span = span(text[range].to_owned()) + .color_maybe(highlight.color()) + .font_maybe(highlight.font()); + + spans.push(span); + } + } else { + let span = span(text.into_string()); + + let span = match heading { + None => span, + Some(heading) => span.size(match heading { + pulldown_cmark::HeadingLevel::H1 => 32, + pulldown_cmark::HeadingLevel::H2 => 28, + pulldown_cmark::HeadingLevel::H3 => 24, + pulldown_cmark::HeadingLevel::H4 => 20, + pulldown_cmark::HeadingLevel::H5 => 16, + pulldown_cmark::HeadingLevel::H6 => 16, + }), + }; + + let span = if strong || emphasis { + span.font(Font { + weight: if strong { + font::Weight::Bold + } else { + font::Weight::Normal + }, + style: if emphasis { + font::Style::Italic + } else { + font::Style::Normal + }, + ..Font::default() + }) + } else { + span + }; + + let span = + span.color_maybe(link.then(|| theme.palette().primary)); + + spans.push(span); + } + + None + } + pulldown_cmark::Event::Code(code) => { + spans.push(span(code.into_string()).font(Font::MONOSPACE)); + None + } + pulldown_cmark::Event::SoftBreak => { + spans.push(span(" ")); + None + } + pulldown_cmark::Event::HardBreak => { + spans.push(span("\n")); + None + } + _ => None, + }) +} |