diff options
Diffstat (limited to '')
| -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, +    }) +} | 
