Draft Viewer trait for markdown
This commit is contained in:
parent
c02ae0c4a4
commit
5655998761
9 changed files with 588 additions and 234 deletions
|
|
@ -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| {
|
||||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -14,6 +21,7 @@ pub fn main() -> iced::Result {
|
|||
|
||||
struct Markdown {
|
||||
content: text_editor::Content,
|
||||
images: HashMap<markdown::Url, Image>,
|
||||
mode: Mode,
|
||||
theme: Theme,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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,
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue