diff options
Diffstat (limited to 'examples/markdown/src')
| -rw-r--r-- | examples/markdown/src/icon.rs | 15 | ||||
| -rw-r--r-- | examples/markdown/src/main.rs | 291 | 
2 files changed, 266 insertions, 40 deletions
diff --git a/examples/markdown/src/icon.rs b/examples/markdown/src/icon.rs new file mode 100644 index 00000000..cfe32541 --- /dev/null +++ b/examples/markdown/src/icon.rs @@ -0,0 +1,15 @@ +// Generated automatically by iced_fontello at build time. +// Do not edit manually. Source: ../fonts/markdown-icons.toml +// dcd2f0c969d603e2ee9237a4b70fa86b1a6e84d86f4689046d8fdd10440b06b9 +use iced::widget::{text, Text}; +use iced::Font; + +pub const FONT: &[u8] = include_bytes!("../fonts/markdown-icons.ttf"); + +pub fn copy<'a>() -> Text<'a> { +    icon("\u{F0C5}") +} + +fn icon(codepoint: &str) -> Text<'_> { +    text(codepoint).font(Font::with_name("markdown-icons")) +} diff --git a/examples/markdown/src/main.rs b/examples/markdown/src/main.rs index ba93ee18..512d4b44 100644 --- a/examples/markdown/src/main.rs +++ b/examples/markdown/src/main.rs @@ -1,50 +1,79 @@ +mod icon; + +use iced::animation; +use iced::clipboard;  use iced::highlighter; -use iced::time::{self, milliseconds}; +use iced::task; +use iced::time::{self, milliseconds, Instant};  use iced::widget::{ -    self, hover, markdown, right, row, scrollable, text_editor, toggler, +    self, button, center_x, container, horizontal_space, hover, image, +    markdown, pop, right, row, scrollable, text_editor, toggler,  }; -use iced::{Element, Fill, Font, Subscription, Task, Theme}; +use iced::window; +use iced::{Animation, Element, Fill, Font, Subscription, Task, Theme}; + +use std::collections::HashMap; +use std::io; +use std::sync::Arc;  pub fn main() -> iced::Result {      iced::application("Markdown - Iced", Markdown::update, Markdown::view) +        .font(icon::FONT)          .subscription(Markdown::subscription)          .theme(Markdown::theme)          .run_with(Markdown::new)  }  struct Markdown { -    content: text_editor::Content, +    content: markdown::Content, +    raw: text_editor::Content, +    images: HashMap<markdown::Url, Image>,      mode: Mode,      theme: Theme, +    now: Instant,  }  enum Mode { -    Preview(Vec<markdown::Item>), -    Stream { -        pending: String, -        parsed: markdown::Content, +    Preview, +    Stream { pending: String }, +} + +enum Image { +    Loading { +        _download: task::Handle, +    }, +    Ready { +        handle: image::Handle, +        fade_in: Animation<bool>,      }, +    #[allow(dead_code)] +    Errored(Error),  }  #[derive(Debug, Clone)]  enum Message {      Edit(text_editor::Action), +    Copy(String),      LinkClicked(markdown::Url), +    ImageShown(markdown::Url), +    ImageDownloaded(markdown::Url, Result<image::Handle, Error>),      ToggleStream(bool),      NextToken, +    Animate(Instant),  }  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(INITIAL_CONTENT), -                mode: Mode::Preview(markdown::parse(INITIAL_CONTENT).collect()), -                theme, +                content: markdown::Content::parse(INITIAL_CONTENT), +                raw: text_editor::Content::with_text(INITIAL_CONTENT), +                images: HashMap::new(), +                mode: Mode::Preview, +                theme: Theme::TokyoNight, +                now: Instant::now(),              },              widget::focus_next(),          ) @@ -55,26 +84,73 @@ impl Markdown {              Message::Edit(action) => {                  let is_edit = action.is_edit(); -                self.content.perform(action); +                self.raw.perform(action);                  if is_edit { -                    self.mode = Mode::Preview( -                        markdown::parse(&self.content.text()).collect(), -                    ); +                    self.content = markdown::Content::parse(&self.raw.text()); +                    self.mode = Mode::Preview; + +                    let images = self.content.images(); +                    self.images.retain(|url, _image| images.contains(url));                  }                  Task::none()              } +            Message::Copy(content) => clipboard::write(content),              Message::LinkClicked(link) => {                  let _ = open::that_in_background(link.to_string());                  Task::none()              } +            Message::ImageShown(url) => { +                if self.images.contains_key(&url) { +                    return Task::none(); +                } + +                let (download_image, handle) = Task::future({ +                    let url = url.clone(); + +                    async move { +                        // Wait half a second for further editions before attempting download +                        tokio::time::sleep(milliseconds(500)).await; +                        download_image(url).await +                    } +                }) +                .abortable(); + +                let _ = self.images.insert( +                    url.clone(), +                    Image::Loading { +                        _download: handle.abort_on_drop(), +                    }, +                ); + +                download_image.map(move |result| { +                    Message::ImageDownloaded(url.clone(), result) +                }) +            } +            Message::ImageDownloaded(url, result) => { +                let _ = self.images.insert( +                    url, +                    result +                        .map(|handle| Image::Ready { +                            handle, +                            fade_in: Animation::new(false) +                                .quick() +                                .easing(animation::Easing::EaseInOut) +                                .go(true), +                        }) +                        .unwrap_or_else(Image::Errored), +                ); + +                Task::none() +            }              Message::ToggleStream(enable_stream) => {                  if enable_stream { +                    self.content = markdown::Content::new(); +                      self.mode = Mode::Stream { -                        pending: self.content.text(), -                        parsed: markdown::Content::new(), +                        pending: self.raw.text(),                      };                      scrollable::snap_to( @@ -82,24 +158,22 @@ impl Markdown {                          scrollable::RelativeOffset::END,                      )                  } else { -                    self.mode = Mode::Preview( -                        markdown::parse(&self.content.text()).collect(), -                    ); +                    self.mode = Mode::Preview;                      Task::none()                  }              }              Message::NextToken => {                  match &mut self.mode { -                    Mode::Preview(_) => {} -                    Mode::Stream { pending, parsed } => { +                    Mode::Preview => {} +                    Mode::Stream { pending } => {                          if pending.is_empty() { -                            self.mode = Mode::Preview(parsed.items().to_vec()); +                            self.mode = Mode::Preview;                          } else {                              let mut tokens = pending.split(' ');                              if let Some(token) = tokens.next() { -                                parsed.push_str(&format!("{token} ")); +                                self.content.push_str(&format!("{token} "));                              }                              *pending = tokens.collect::<Vec<_>>().join(" "); @@ -109,11 +183,16 @@ impl Markdown {                  Task::none()              } +            Message::Animate(now) => { +                self.now = now; + +                Task::none() +            }          }      }      fn view(&self) -> Element<Message> { -        let editor = text_editor(&self.content) +        let editor = text_editor(&self.raw)              .placeholder("Type your Markdown here...")              .on_action(Message::Edit)              .height(Fill) @@ -121,17 +200,14 @@ impl Markdown {              .font(Font::MONOSPACE)              .highlight("markdown", highlighter::Theme::Base16Ocean); -        let items = match &self.mode { -            Mode::Preview(items) => items.as_slice(), -            Mode::Stream { parsed, .. } => parsed.items(), -        }; - -        let preview = markdown( -            items, -            markdown::Settings::default(), -            markdown::Style::from_palette(self.theme.palette()), -        ) -        .map(Message::LinkClicked); +        let preview = markdown::view_with( +            self.content.items(), +            &self.theme, +            &CustomViewer { +                images: &self.images, +                now: self.now, +            }, +        );          row![              editor, @@ -159,11 +235,146 @@ impl Markdown {      }      fn subscription(&self) -> Subscription<Message> { -        match self.mode { -            Mode::Preview(_) => Subscription::none(), +        let listen_stream = match self.mode { +            Mode::Preview => Subscription::none(),              Mode::Stream { .. } => {                  time::every(milliseconds(10)).map(|_| Message::NextToken)              } +        }; + +        let animate = { +            let is_animating = self.images.values().any(|image| match image { +                Image::Ready { fade_in, .. } => fade_in.is_animating(self.now), +                _ => false, +            }); + +            if is_animating { +                window::frames().map(Message::Animate) +            } else { +                Subscription::none() +            } +        }; + +        Subscription::batch([listen_stream, animate]) +    } +} + +struct CustomViewer<'a> { +    images: &'a HashMap<markdown::Url, Image>, +    now: Instant, +} + +impl<'a> markdown::Viewer<'a, Message> for CustomViewer<'a> { +    fn on_link_click(url: markdown::Url) -> Message { +        Message::LinkClicked(url) +    } + +    fn image( +        &self, +        _settings: markdown::Settings, +        url: &'a markdown::Url, +        _title: &'a str, +        _alt: &markdown::Text, +    ) -> Element<'a, Message> { +        if let Some(Image::Ready { handle, fade_in }) = self.images.get(url) { +            center_x( +                image(handle) +                    .opacity(fade_in.interpolate(0.0, 1.0, self.now)) +                    .scale(fade_in.interpolate(1.2, 1.0, self.now)), +            ) +            .into() +        } else { +            pop(horizontal_space()) +                .key(url.as_str()) +                .on_show(|_size| Message::ImageShown(url.clone())) +                .into()          }      } + +    fn code_block( +        &self, +        settings: markdown::Settings, +        _language: Option<&'a str>, +        code: &'a str, +        lines: &'a [markdown::Text], +    ) -> Element<'a, Message> { +        let code_block = +            markdown::code_block(settings, lines, Message::LinkClicked); + +        let copy = button(icon::copy().size(12)) +            .padding(2) +            .on_press_with(|| Message::Copy(code.to_owned())) +            .style(button::text); + +        hover( +            code_block, +            right(container(copy).style(container::dark)) +                .padding(settings.spacing / 2), +        ) +    } +} + +async fn download_image(url: markdown::Url) -> Result<image::Handle, Error> { +    use std::io; +    use tokio::task; + +    println!("Trying to download image: {url}"); + +    let client = reqwest::Client::new(); + +    let bytes = client +        .get(url) +        .send() +        .await? +        .error_for_status()? +        .bytes() +        .await?; + +    let image = task::spawn_blocking(move || { +        Ok::<_, Error>( +            ::image::ImageReader::new(io::Cursor::new(bytes)) +                .with_guessed_format()? +                .decode()? +                .to_rgba8(), +        ) +    }) +    .await??; + +    Ok(image::Handle::from_rgba( +        image.width(), +        image.height(), +        image.into_raw(), +    )) +} + +#[derive(Debug, Clone)] +pub enum Error { +    RequestFailed(Arc<reqwest::Error>), +    IOFailed(Arc<io::Error>), +    JoinFailed(Arc<tokio::task::JoinError>), +    ImageDecodingFailed(Arc<::image::ImageError>), +} + +impl From<reqwest::Error> for Error { +    fn from(error: reqwest::Error) -> Self { +        Self::RequestFailed(Arc::new(error)) +    } +} + +impl From<io::Error> for Error { +    fn from(error: io::Error) -> Self { +        Self::IOFailed(Arc::new(error)) +    } +} + +impl From<tokio::task::JoinError> for Error { +    fn from(error: tokio::task::JoinError) -> Self { +        Self::JoinFailed(Arc::new(error)) +    } +} + +impl From<::image::ImageError> for Error { +    fn from(error: ::image::ImageError) -> Self { +        Self::ImageDecodingFailed(Arc::new(error)) +    }  }  | 
