Skip to content

Commit

Permalink
Draft Viewer trait for markdown
Browse files Browse the repository at this point in the history
  • Loading branch information
hecrj committed Feb 4, 2025
1 parent c02ae0c commit 5655998
Show file tree
Hide file tree
Showing 9 changed files with 588 additions and 234 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions core/src/padding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,9 @@ impl From<Padding> for Size {
Self::new(padding.horizontal(), padding.vertical())
}
}

impl From<Pixels> for Padding {
fn from(pixels: Pixels) -> Self {
Self::from(pixels.0)
}
}
26 changes: 11 additions & 15 deletions examples/changelog/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,25 +267,21 @@ impl Generator {
} => {
let details = {
let title = rich_text![
span(&pull_request.title).size(24).link(
Message::OpenPullRequest(pull_request.id)
),
span(&pull_request.title)
.size(24)
.link(pull_request.id),
span(format!(" by {}", pull_request.author))
.font(Font {
style: font::Style::Italic,
..Font::default()
}),
]
.on_link_clicked(Message::OpenPullRequest)
.font(Font::MONOSPACE);

let description = markdown::view(
description,
markdown::Settings::default(),
markdown::Style::from_palette(
self.theme().palette(),
),
)
.map(Message::UrlClicked);
let description =
markdown::view(&self.theme(), description)
.map(Message::UrlClicked);

let labels =
row(pull_request.labels.iter().map(|label| {
Expand Down Expand Up @@ -349,11 +345,11 @@ impl Generator {
container(
scrollable(
markdown::view(
preview,
markdown::Settings::with_text_size(12),
markdown::Style::from_palette(
self.theme().palette(),
markdown::Settings::with_text_size(
12,
&self.theme(),
),
preview,
)
.map(Message::UrlClicked),
)
Expand Down
8 changes: 7 additions & 1 deletion examples/markdown/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ publish = false

[dependencies]
iced.workspace = true
iced.features = ["markdown", "highlighter", "tokio", "debug"]
iced.features = ["markdown", "highlighter", "image", "tokio", "debug"]

reqwest.version = "0.12"
reqwest.features = ["json"]

image.workspace = true
tokio.workspace = true

open = "5.3"
139 changes: 133 additions & 6 deletions examples/markdown/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
use iced::highlighter;
use iced::time::{self, milliseconds};
use iced::widget::{
self, hover, markdown, right, row, scrollable, text_editor, toggler,
self, center_x, horizontal_space, hover, image, markdown, pop, right, row,
scrollable, text_editor, toggler,
};
use iced::{Element, Fill, Font, Subscription, Task, Theme};

use tokio::task;

use std::collections::HashMap;
use std::io;
use std::sync::Arc;

pub fn main() -> iced::Result {
iced::application("Markdown - Iced", Markdown::update, Markdown::view)
.subscription(Markdown::subscription)
Expand All @@ -14,6 +21,7 @@ pub fn main() -> iced::Result {

struct Markdown {
content: text_editor::Content,
images: HashMap<markdown::Url, Image>,
mode: Mode,
theme: Theme,
}
Expand All @@ -26,10 +34,19 @@ enum Mode {
},
}

enum Image {
Loading,
Ready(image::Handle),
#[allow(dead_code)]
Errored(Error),
}

#[derive(Debug, Clone)]
enum Message {
Edit(text_editor::Action),
LinkClicked(markdown::Url),
ImageShown(markdown::Url),
ImageDownloaded(markdown::Url, Result<image::Handle, Error>),
ToggleStream(bool),
NextToken,
}
Expand All @@ -43,6 +60,7 @@ impl Markdown {
(
Self {
content: text_editor::Content::with_text(INITIAL_CONTENT),
images: HashMap::new(),
mode: Mode::Preview(markdown::parse(INITIAL_CONTENT).collect()),
theme,
},
Expand Down Expand Up @@ -70,6 +88,25 @@ impl Markdown {

Task::none()
}
Message::ImageShown(url) => {
if self.images.contains_key(&url) {
return Task::none();
}

let _ = self.images.insert(url.clone(), Image::Loading);

Task::perform(download_image(url.clone()), move |result| {
Message::ImageDownloaded(url.clone(), result)
})
}
Message::ImageDownloaded(url, result) => {
let _ = self.images.insert(
url,
result.map(Image::Ready).unwrap_or_else(Image::Errored),
);

Task::none()
}
Message::ToggleStream(enable_stream) => {
if enable_stream {
self.mode = Mode::Stream {
Expand Down Expand Up @@ -126,12 +163,13 @@ impl Markdown {
Mode::Stream { parsed, .. } => parsed.items(),
};

let preview = markdown(
let preview = markdown::view_with(
&MarkdownViewer {
images: &self.images,
},
&self.theme,
items,
markdown::Settings::default(),
markdown::Style::from_palette(self.theme.palette()),
)
.map(Message::LinkClicked);
);

row![
editor,
Expand Down Expand Up @@ -167,3 +205,92 @@ impl Markdown {
}
}
}

struct MarkdownViewer<'a> {
images: &'a HashMap<markdown::Url, Image>,
}

impl<'a> markdown::Viewer<'a, Message> for MarkdownViewer<'a> {
fn on_link_clicked(url: markdown::Url) -> Message {
Message::LinkClicked(url)
}

fn image(
&self,
_settings: markdown::Settings,
_title: &markdown::Text,
url: &'a markdown::Url,
) -> Element<'a, Message> {
if let Some(Image::Ready(handle)) = self.images.get(url) {
center_x(image(handle)).into()
} else {
pop(horizontal_space().width(0))
.key(url.as_str())
.on_show(|_size| Message::ImageShown(url.clone()))
.into()
}
}
}

async fn download_image(url: markdown::Url) -> Result<image::Handle, Error> {
use std::io;
use tokio::task;

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<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<task::JoinError> for Error {
fn from(error: 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))
}
}
6 changes: 3 additions & 3 deletions widget/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ macro_rules! text {
#[macro_export]
macro_rules! rich_text {
() => (
$crate::Column::new()
$crate::text::Rich::new()
);
($($x:expr),+ $(,)?) => (
$crate::text::Rich::from_iter([$($crate::text::Span::from($x)),+])
Expand Down Expand Up @@ -1155,9 +1155,9 @@ where
/// .into()
/// }

Check failure on line 1156 in widget/src/helpers.rs

View workflow job for this annotation

GitHub Actions / all (ubuntu-latest, beta)

type annotations needed

Check failure on line 1156 in widget/src/helpers.rs

View workflow job for this annotation

GitHub Actions / all (macOS-latest, beta)

type annotations needed

Check failure on line 1156 in widget/src/helpers.rs

View workflow job for this annotation

GitHub Actions / all (macOS-latest, 1.82)

type annotations needed
/// ```
pub fn rich_text<'a, Link, Theme, Renderer>(
pub fn rich_text<'a, Link, Message, Theme, Renderer>(
spans: impl AsRef<[text::Span<'a, Link, Renderer::Font>]> + 'a,
) -> text::Rich<'a, Link, Theme, Renderer>
) -> text::Rich<'a, Link, Message, Theme, Renderer>
where
Link: Clone + 'static,
Theme: text::Catalog + 'a,
Expand Down
Loading

0 comments on commit 5655998

Please sign in to comment.