Draft Viewer trait for markdown
This commit is contained in:
parent
c02ae0c4a4
commit
5655998761
9 changed files with 588 additions and 234 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
|
@ -3299,7 +3299,10 @@ name = "markdown"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"iced",
|
||||
"image",
|
||||
"open",
|
||||
"reqwest",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)),+])
|
||||
|
|
@ -1155,9 +1155,9 @@ where
|
|||
/// .into()
|
||||
/// }
|
||||
/// ```
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@
|
|||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
#![allow(missing_docs)]
|
||||
use crate::core::border;
|
||||
use crate::core::font::{self, Font};
|
||||
use crate::core::padding;
|
||||
|
|
@ -144,6 +145,7 @@ impl Content {
|
|||
let mut state = State {
|
||||
leftover: String::new(),
|
||||
references: self.state.references.clone(),
|
||||
images: HashSet::new(),
|
||||
highlighter: None,
|
||||
};
|
||||
|
||||
|
|
@ -153,6 +155,7 @@ impl Content {
|
|||
self.items[*index] = item;
|
||||
}
|
||||
|
||||
self.state.images.extend(state.images.drain());
|
||||
drop(state);
|
||||
}
|
||||
|
||||
|
|
@ -167,6 +170,11 @@ impl Content {
|
|||
pub fn items(&self) -> &[Item] {
|
||||
&self.items
|
||||
}
|
||||
|
||||
/// Returns the URLs of the Markdown images present in the [`Content`].
|
||||
pub fn images(&self) -> impl Iterator<Item = &Url> {
|
||||
self.state.images.iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Markdown item.
|
||||
|
|
@ -187,130 +195,13 @@ pub enum Item {
|
|||
/// The items of the list.
|
||||
items: Vec<Vec<Item>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Item {
|
||||
/// Displays a Markdown [`Item`] using the default, built-in look for its children.
|
||||
pub fn view<'a, 'b, Theme, Renderer>(
|
||||
&'b self,
|
||||
settings: Settings,
|
||||
style: Style,
|
||||
index: usize,
|
||||
) -> Element<'a, Url, Theme, Renderer>
|
||||
where
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: core::text::Renderer<Font = Font> + 'a,
|
||||
{
|
||||
self.view_with(index, settings, style, &DefaultView)
|
||||
}
|
||||
|
||||
/// Displays a Markdown [`Item`] using the given [`View`] for its children.
|
||||
pub fn view_with<'a, 'b, Theme, Renderer>(
|
||||
&'b self,
|
||||
index: usize,
|
||||
settings: Settings,
|
||||
style: Style,
|
||||
view: &dyn View<'a, 'b, Url, Theme, Renderer>,
|
||||
) -> Element<'a, Url, Theme, Renderer>
|
||||
where
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: core::text::Renderer<Font = Font> + 'a,
|
||||
{
|
||||
let Settings {
|
||||
text_size,
|
||||
h1_size,
|
||||
h2_size,
|
||||
h3_size,
|
||||
h4_size,
|
||||
h5_size,
|
||||
h6_size,
|
||||
code_size,
|
||||
spacing,
|
||||
} = settings;
|
||||
|
||||
match self {
|
||||
Item::Heading(level, heading) => {
|
||||
container(rich_text(heading.spans(style)).size(match level {
|
||||
pulldown_cmark::HeadingLevel::H1 => h1_size,
|
||||
pulldown_cmark::HeadingLevel::H2 => h2_size,
|
||||
pulldown_cmark::HeadingLevel::H3 => h3_size,
|
||||
pulldown_cmark::HeadingLevel::H4 => h4_size,
|
||||
pulldown_cmark::HeadingLevel::H5 => h5_size,
|
||||
pulldown_cmark::HeadingLevel::H6 => h6_size,
|
||||
}))
|
||||
.padding(padding::top(if index > 0 {
|
||||
text_size / 2.0
|
||||
} else {
|
||||
Pixels::ZERO
|
||||
}))
|
||||
.into()
|
||||
}
|
||||
Item::Paragraph(paragraph) => {
|
||||
rich_text(paragraph.spans(style)).size(text_size).into()
|
||||
}
|
||||
Item::List { start: None, items } => {
|
||||
column(items.iter().map(|items| {
|
||||
row![
|
||||
text("•").size(text_size),
|
||||
view_with(
|
||||
items,
|
||||
Settings {
|
||||
spacing: settings.spacing * 0.6,
|
||||
..settings
|
||||
},
|
||||
style,
|
||||
view
|
||||
)
|
||||
]
|
||||
.spacing(spacing)
|
||||
.into()
|
||||
}))
|
||||
.spacing(spacing * 0.75)
|
||||
.into()
|
||||
}
|
||||
Item::List {
|
||||
start: Some(start),
|
||||
items,
|
||||
} => column(items.iter().enumerate().map(|(i, items)| {
|
||||
row![
|
||||
text!("{}.", i as u64 + *start).size(text_size),
|
||||
view_with(
|
||||
items,
|
||||
Settings {
|
||||
spacing: settings.spacing * 0.6,
|
||||
..settings
|
||||
},
|
||||
style,
|
||||
view
|
||||
)
|
||||
]
|
||||
.spacing(spacing)
|
||||
.into()
|
||||
}))
|
||||
.spacing(spacing * 0.75)
|
||||
.into(),
|
||||
Item::CodeBlock(lines) => container(
|
||||
scrollable(
|
||||
container(column(lines.iter().map(|line| {
|
||||
rich_text(line.spans(style))
|
||||
.font(Font::MONOSPACE)
|
||||
.size(code_size)
|
||||
.into()
|
||||
})))
|
||||
.padding(spacing.0 / 2.0),
|
||||
)
|
||||
.direction(scrollable::Direction::Horizontal(
|
||||
scrollable::Scrollbar::default()
|
||||
.width(spacing.0 / 2.0)
|
||||
.scroller_width(spacing.0 / 2.0),
|
||||
)),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.padding(spacing.0 / 2.0)
|
||||
.class(Theme::code_block())
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
/// An image.
|
||||
Image {
|
||||
/// The destination URL of the image.
|
||||
url: Url,
|
||||
/// The title of the image.
|
||||
title: Text,
|
||||
},
|
||||
}
|
||||
|
||||
/// A bunch of parsed Markdown text.
|
||||
|
|
@ -470,6 +361,7 @@ pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ {
|
|||
struct State {
|
||||
leftover: String,
|
||||
references: HashMap<String, String>,
|
||||
images: HashSet<Url>,
|
||||
#[cfg(feature = "highlighter")]
|
||||
highlighter: Option<Highlighter>,
|
||||
}
|
||||
|
|
@ -560,6 +452,10 @@ fn parse_with<'a>(
|
|||
mut state: impl BorrowMut<State> + 'a,
|
||||
markdown: &'a str,
|
||||
) -> impl Iterator<Item = (Item, &'a str, HashSet<String>)> + 'a {
|
||||
enum Scope {
|
||||
List(List),
|
||||
}
|
||||
|
||||
struct List {
|
||||
start: Option<u64>,
|
||||
items: Vec<Vec<Item>>,
|
||||
|
|
@ -575,7 +471,8 @@ fn parse_with<'a>(
|
|||
let mut metadata = false;
|
||||
let mut table = false;
|
||||
let mut link = None;
|
||||
let mut lists = Vec::new();
|
||||
let mut image = None;
|
||||
let mut stack = Vec::new();
|
||||
|
||||
#[cfg(feature = "highlighter")]
|
||||
let mut highlighter = None;
|
||||
|
|
@ -616,10 +513,18 @@ fn parse_with<'a>(
|
|||
}
|
||||
|
||||
let produce = move |state: &mut State,
|
||||
lists: &mut Vec<List>,
|
||||
stack: &mut Vec<Scope>,
|
||||
item,
|
||||
source: Range<usize>| {
|
||||
if lists.is_empty() {
|
||||
if let Some(scope) = stack.last_mut() {
|
||||
match scope {
|
||||
Scope::List(list) => {
|
||||
list.items.last_mut().expect("item context").push(item);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
} else {
|
||||
state.leftover = markdown[source.start..].to_owned();
|
||||
|
||||
Some((
|
||||
|
|
@ -627,16 +532,6 @@ fn parse_with<'a>(
|
|||
&markdown[source.start..source.end],
|
||||
broken_links.take(),
|
||||
))
|
||||
} else {
|
||||
lists
|
||||
.last_mut()
|
||||
.expect("list context")
|
||||
.items
|
||||
.last_mut()
|
||||
.expect("item context")
|
||||
.push(item);
|
||||
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -673,31 +568,36 @@ fn parse_with<'a>(
|
|||
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Tag::Image { dest_url, .. }
|
||||
if !metadata && !table =>
|
||||
{
|
||||
image = Url::parse(&dest_url).ok();
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Tag::List(first_item) if !metadata && !table => {
|
||||
let prev = if spans.is_empty() {
|
||||
None
|
||||
} else {
|
||||
produce(
|
||||
state.borrow_mut(),
|
||||
&mut lists,
|
||||
&mut stack,
|
||||
Item::Paragraph(Text::new(spans.drain(..).collect())),
|
||||
source,
|
||||
)
|
||||
};
|
||||
|
||||
lists.push(List {
|
||||
stack.push(Scope::List(List {
|
||||
start: first_item,
|
||||
items: Vec::new(),
|
||||
});
|
||||
}));
|
||||
|
||||
prev
|
||||
}
|
||||
pulldown_cmark::Tag::Item => {
|
||||
lists
|
||||
.last_mut()
|
||||
.expect("list context")
|
||||
.items
|
||||
.push(Vec::new());
|
||||
if let Some(Scope::List(list)) = stack.last_mut() {
|
||||
list.items.push(Vec::new());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
pulldown_cmark::Tag::CodeBlock(
|
||||
|
|
@ -726,7 +626,7 @@ fn parse_with<'a>(
|
|||
} else {
|
||||
produce(
|
||||
state.borrow_mut(),
|
||||
&mut lists,
|
||||
&mut stack,
|
||||
Item::Paragraph(Text::new(spans.drain(..).collect())),
|
||||
source,
|
||||
)
|
||||
|
|
@ -748,7 +648,7 @@ fn parse_with<'a>(
|
|||
pulldown_cmark::TagEnd::Heading(level) if !metadata && !table => {
|
||||
produce(
|
||||
state.borrow_mut(),
|
||||
&mut lists,
|
||||
&mut stack,
|
||||
Item::Heading(level, Text::new(spans.drain(..).collect())),
|
||||
source,
|
||||
)
|
||||
|
|
@ -770,12 +670,16 @@ fn parse_with<'a>(
|
|||
None
|
||||
}
|
||||
pulldown_cmark::TagEnd::Paragraph if !metadata && !table => {
|
||||
produce(
|
||||
state.borrow_mut(),
|
||||
&mut lists,
|
||||
Item::Paragraph(Text::new(spans.drain(..).collect())),
|
||||
source,
|
||||
)
|
||||
if spans.is_empty() {
|
||||
None
|
||||
} else {
|
||||
produce(
|
||||
state.borrow_mut(),
|
||||
&mut stack,
|
||||
Item::Paragraph(Text::new(spans.drain(..).collect())),
|
||||
source,
|
||||
)
|
||||
}
|
||||
}
|
||||
pulldown_cmark::TagEnd::Item if !metadata && !table => {
|
||||
if spans.is_empty() {
|
||||
|
|
@ -783,18 +687,20 @@ fn parse_with<'a>(
|
|||
} else {
|
||||
produce(
|
||||
state.borrow_mut(),
|
||||
&mut lists,
|
||||
&mut stack,
|
||||
Item::Paragraph(Text::new(spans.drain(..).collect())),
|
||||
source,
|
||||
)
|
||||
}
|
||||
}
|
||||
pulldown_cmark::TagEnd::List(_) if !metadata && !table => {
|
||||
let list = lists.pop().expect("list context");
|
||||
let scope = stack.pop()?;
|
||||
|
||||
let Scope::List(list) = scope;
|
||||
|
||||
produce(
|
||||
state.borrow_mut(),
|
||||
&mut lists,
|
||||
&mut stack,
|
||||
Item::List {
|
||||
start: list.start,
|
||||
items: list.items,
|
||||
|
|
@ -802,6 +708,15 @@ fn parse_with<'a>(
|
|||
source,
|
||||
)
|
||||
}
|
||||
pulldown_cmark::TagEnd::Image if !metadata && !table => {
|
||||
let url = image.take()?;
|
||||
let title = Text::new(spans.drain(..).collect());
|
||||
|
||||
let state = state.borrow_mut();
|
||||
let _ = state.images.insert(url.clone());
|
||||
|
||||
produce(state, &mut stack, Item::Image { url, title }, source)
|
||||
}
|
||||
pulldown_cmark::TagEnd::CodeBlock if !metadata && !table => {
|
||||
#[cfg(feature = "highlighter")]
|
||||
{
|
||||
|
|
@ -810,7 +725,7 @@ fn parse_with<'a>(
|
|||
|
||||
produce(
|
||||
state.borrow_mut(),
|
||||
&mut lists,
|
||||
&mut stack,
|
||||
Item::CodeBlock(code.drain(..).collect()),
|
||||
source,
|
||||
)
|
||||
|
|
@ -910,15 +825,25 @@ pub struct Settings {
|
|||
pub code_size: Pixels,
|
||||
/// The spacing to be used between elements.
|
||||
pub spacing: Pixels,
|
||||
/// The styling of the Markdown.
|
||||
pub style: Style,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
/// Creates new [`Settings`] with default text size and the given [`Style`].
|
||||
pub fn with_style(style: impl Into<Style>) -> Self {
|
||||
Self::with_text_size(16, style)
|
||||
}
|
||||
|
||||
/// Creates new [`Settings`] with the given base text size in [`Pixels`].
|
||||
///
|
||||
/// Heading levels will be adjusted automatically. Specifically,
|
||||
/// the first level will be twice the base size, and then every level
|
||||
/// after that will be 25% smaller.
|
||||
pub fn with_text_size(text_size: impl Into<Pixels>) -> Self {
|
||||
pub fn with_text_size(
|
||||
text_size: impl Into<Pixels>,
|
||||
style: impl Into<Style>,
|
||||
) -> Self {
|
||||
let text_size = text_size.into();
|
||||
|
||||
Self {
|
||||
|
|
@ -931,13 +856,14 @@ impl Settings {
|
|||
h6_size: text_size,
|
||||
code_size: text_size * 0.75,
|
||||
spacing: text_size * 0.875,
|
||||
style: style.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self::with_text_size(16)
|
||||
impl From<&Theme> for Settings {
|
||||
fn from(theme: &Theme) -> Self {
|
||||
Self::with_style(Style::from(theme))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -969,6 +895,18 @@ impl Style {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<theme::Palette> for Style {
|
||||
fn from(palette: theme::Palette) -> Self {
|
||||
Self::from_palette(palette)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Theme> for Style {
|
||||
fn from(theme: &Theme) -> Self {
|
||||
Self::from_palette(theme.palette())
|
||||
}
|
||||
}
|
||||
|
||||
/// Display a bunch of Markdown items.
|
||||
///
|
||||
/// You can obtain the items with [`parse`].
|
||||
|
|
@ -1015,16 +953,15 @@ impl Style {
|
|||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn view<'a, 'b, Theme, Renderer>(
|
||||
items: impl IntoIterator<Item = &'b Item>,
|
||||
settings: Settings,
|
||||
style: Style,
|
||||
pub fn view<'a, Theme, Renderer>(
|
||||
settings: impl Into<Settings>,
|
||||
items: impl IntoIterator<Item = &'a Item>,
|
||||
) -> Element<'a, Url, Theme, Renderer>
|
||||
where
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: core::text::Renderer<Font = Font> + 'a,
|
||||
{
|
||||
view_with(items, settings, style, &DefaultView)
|
||||
view_with(&DefaultViewer, settings, items)
|
||||
}
|
||||
|
||||
/// Runs [`view`] but with a custom [`View`] to turn an [`Item`] into
|
||||
|
|
@ -1035,56 +972,288 @@ where
|
|||
///
|
||||
/// You can use [`Item::view`] and [`Item::view_with`] for the default
|
||||
/// look.
|
||||
pub fn view_with<'a, 'b, Message, Theme, Renderer>(
|
||||
items: impl IntoIterator<Item = &'b Item>,
|
||||
settings: Settings,
|
||||
style: Style,
|
||||
view: &dyn View<'a, 'b, Message, Theme, Renderer>,
|
||||
pub fn view_with<'a, Message, Theme, Renderer>(
|
||||
viewer: &impl Viewer<'a, Message, Theme, Renderer>,
|
||||
settings: impl Into<Settings>,
|
||||
items: impl IntoIterator<Item = &'a Item>,
|
||||
) -> Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: core::text::Renderer<Font = Font> + 'a,
|
||||
{
|
||||
let settings = settings.into();
|
||||
|
||||
let blocks = items
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(move |(i, item)| view.view(settings, style, item, i));
|
||||
.map(|(i, item_)| item(viewer, settings, item_, i));
|
||||
|
||||
Element::new(column(blocks).spacing(settings.spacing))
|
||||
}
|
||||
|
||||
/// A view strategy to display a Markdown [`Item`].
|
||||
pub trait View<'a, 'b, Message, Theme, Renderer> {
|
||||
/// Displays a Markdown [`Item`] by projecting it into an [`Element`].
|
||||
///
|
||||
/// You can use [`Item::view`] and [`Item::view_with`] for the default
|
||||
/// look.
|
||||
fn view(
|
||||
pub fn item<'a, Message, Theme, Renderer>(
|
||||
viewer: &impl Viewer<'a, Message, Theme, Renderer>,
|
||||
settings: Settings,
|
||||
item: &'a Item,
|
||||
index: usize,
|
||||
) -> Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: core::text::Renderer<Font = Font> + 'a,
|
||||
{
|
||||
match item {
|
||||
Item::Image { title, url } => viewer.image(settings, title, url),
|
||||
Item::Heading(level, text) => {
|
||||
viewer.heading(settings, index, level, text)
|
||||
}
|
||||
Item::Paragraph(text) => viewer.paragraph(settings, text),
|
||||
Item::CodeBlock(lines) => viewer.code_block(settings, lines),
|
||||
Item::List { start: None, items } => {
|
||||
viewer.unordered_list(settings, items)
|
||||
}
|
||||
Item::List {
|
||||
start: Some(start),
|
||||
items,
|
||||
} => viewer.ordered_list(settings, *start, items),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn heading<'a, Message, Theme, Renderer>(
|
||||
settings: Settings,
|
||||
index: usize,
|
||||
level: &'a HeadingLevel,
|
||||
text: &'a Text,
|
||||
on_link_clicked: impl Fn(Url) -> Message + 'a,
|
||||
) -> Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: core::text::Renderer<Font = Font> + 'a,
|
||||
{
|
||||
let Settings {
|
||||
h1_size,
|
||||
h2_size,
|
||||
h3_size,
|
||||
h4_size,
|
||||
h5_size,
|
||||
h6_size,
|
||||
text_size,
|
||||
..
|
||||
} = settings;
|
||||
|
||||
container(
|
||||
rich_text(text.spans(settings.style))
|
||||
.on_link_clicked(on_link_clicked)
|
||||
.size(match level {
|
||||
pulldown_cmark::HeadingLevel::H1 => h1_size,
|
||||
pulldown_cmark::HeadingLevel::H2 => h2_size,
|
||||
pulldown_cmark::HeadingLevel::H3 => h3_size,
|
||||
pulldown_cmark::HeadingLevel::H4 => h4_size,
|
||||
pulldown_cmark::HeadingLevel::H5 => h5_size,
|
||||
pulldown_cmark::HeadingLevel::H6 => h6_size,
|
||||
}),
|
||||
)
|
||||
.padding(padding::top(if index > 0 {
|
||||
text_size / 2.0
|
||||
} else {
|
||||
Pixels::ZERO
|
||||
}))
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn paragraph<'a, Message, Theme, Renderer>(
|
||||
settings: Settings,
|
||||
text: &'a Text,
|
||||
on_link_clicked: impl Fn(Url) -> Message + 'a,
|
||||
) -> Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: core::text::Renderer<Font = Font> + 'a,
|
||||
{
|
||||
rich_text(text.spans(settings.style))
|
||||
.size(settings.text_size)
|
||||
.on_link_clicked(on_link_clicked)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn unordered_list<'a, Message, Theme, Renderer>(
|
||||
viewer: &impl Viewer<'a, Message, Theme, Renderer>,
|
||||
settings: Settings,
|
||||
items: &'a [Vec<Item>],
|
||||
) -> Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: core::text::Renderer<Font = Font> + 'a,
|
||||
{
|
||||
column(items.iter().map(|items| {
|
||||
row![
|
||||
text("•").size(settings.text_size),
|
||||
view_with(
|
||||
viewer,
|
||||
Settings {
|
||||
spacing: settings.spacing * 0.6,
|
||||
..settings
|
||||
},
|
||||
items,
|
||||
)
|
||||
]
|
||||
.spacing(settings.spacing)
|
||||
.into()
|
||||
}))
|
||||
.spacing(settings.spacing * 0.75)
|
||||
.padding([0.0, settings.spacing.0])
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn ordered_list<'a, Message, Theme, Renderer>(
|
||||
viewer: &impl Viewer<'a, Message, Theme, Renderer>,
|
||||
settings: Settings,
|
||||
start: u64,
|
||||
items: &'a [Vec<Item>],
|
||||
) -> Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: core::text::Renderer<Font = Font> + 'a,
|
||||
{
|
||||
column(items.iter().enumerate().map(|(i, items)| {
|
||||
row![
|
||||
text!("{}.", i as u64 + start).size(settings.text_size),
|
||||
view_with(
|
||||
viewer,
|
||||
Settings {
|
||||
spacing: settings.spacing * 0.6,
|
||||
..settings
|
||||
},
|
||||
items,
|
||||
)
|
||||
]
|
||||
.spacing(settings.spacing)
|
||||
.into()
|
||||
}))
|
||||
.spacing(settings.spacing * 0.75)
|
||||
.padding([0.0, settings.spacing.0])
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn code_block<'a, Message, Theme, Renderer>(
|
||||
settings: Settings,
|
||||
lines: &'a [Text],
|
||||
on_link_clicked: impl Fn(Url) -> Message + Clone + 'a,
|
||||
) -> Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: core::text::Renderer<Font = Font> + 'a,
|
||||
{
|
||||
container(
|
||||
scrollable(
|
||||
container(column(lines.iter().map(|line| {
|
||||
rich_text(line.spans(settings.style))
|
||||
.on_link_clicked(on_link_clicked.clone())
|
||||
.font(Font::MONOSPACE)
|
||||
.size(settings.code_size)
|
||||
.into()
|
||||
})))
|
||||
.padding(settings.spacing.0 / 2.0),
|
||||
)
|
||||
.direction(scrollable::Direction::Horizontal(
|
||||
scrollable::Scrollbar::default()
|
||||
.width(settings.spacing.0 / 2.0)
|
||||
.scroller_width(settings.spacing.0 / 2.0),
|
||||
)),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.padding(settings.spacing.0 / 2.0)
|
||||
.class(Theme::code_block())
|
||||
.into()
|
||||
}
|
||||
|
||||
/// A view strategy to display a Markdown [`Item`].j
|
||||
pub trait Viewer<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
|
||||
where
|
||||
Self: Sized + 'a,
|
||||
Message: 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: core::text::Renderer<Font = Font> + 'a,
|
||||
{
|
||||
fn on_link_clicked(url: Url) -> Message;
|
||||
|
||||
fn image(
|
||||
&self,
|
||||
settings: Settings,
|
||||
title: &Text,
|
||||
url: &'a Url,
|
||||
) -> Element<'a, Message, Theme, Renderer> {
|
||||
let _url = url;
|
||||
|
||||
container(
|
||||
rich_text(title.spans(settings.style))
|
||||
.on_link_clicked(Self::on_link_clicked),
|
||||
)
|
||||
.padding(settings.spacing.0)
|
||||
.class(Theme::code_block())
|
||||
.into()
|
||||
}
|
||||
|
||||
fn heading(
|
||||
&self,
|
||||
settings: Settings,
|
||||
style: Style,
|
||||
item: &'b Item,
|
||||
index: usize,
|
||||
) -> Element<'a, Message, Theme, Renderer>;
|
||||
level: &'a HeadingLevel,
|
||||
text: &'a Text,
|
||||
) -> Element<'a, Message, Theme, Renderer> {
|
||||
heading(settings, index, level, text, Self::on_link_clicked)
|
||||
}
|
||||
|
||||
fn paragraph(
|
||||
&self,
|
||||
settings: Settings,
|
||||
text: &'a Text,
|
||||
) -> Element<'a, Message, Theme, Renderer> {
|
||||
paragraph(settings, text, Self::on_link_clicked)
|
||||
}
|
||||
|
||||
fn code_block(
|
||||
&self,
|
||||
settings: Settings,
|
||||
lines: &'a [Text],
|
||||
) -> Element<'a, Message, Theme, Renderer> {
|
||||
code_block(settings, lines, Self::on_link_clicked)
|
||||
}
|
||||
|
||||
fn unordered_list(
|
||||
&self,
|
||||
settings: Settings,
|
||||
items: &'a [Vec<Item>],
|
||||
) -> Element<'a, Message, Theme, Renderer> {
|
||||
unordered_list(self, settings, items)
|
||||
}
|
||||
|
||||
fn ordered_list(
|
||||
&self,
|
||||
settings: Settings,
|
||||
start: u64,
|
||||
items: &'a [Vec<Item>],
|
||||
) -> Element<'a, Message, Theme, Renderer> {
|
||||
ordered_list(self, settings, start, items)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct DefaultView;
|
||||
struct DefaultViewer;
|
||||
|
||||
impl<'a, 'b, Theme, Renderer> View<'a, 'b, Url, Theme, Renderer> for DefaultView
|
||||
impl<'a, Theme, Renderer> Viewer<'a, Url, Theme, Renderer> for DefaultViewer
|
||||
where
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: core::text::Renderer<Font = Font> + 'a,
|
||||
{
|
||||
fn view(
|
||||
&self,
|
||||
settings: Settings,
|
||||
style: Style,
|
||||
item: &'b Item,
|
||||
index: usize,
|
||||
) -> Element<'a, Url, Theme, Renderer> {
|
||||
item.view(settings, style, index)
|
||||
fn on_link_clicked(url: Url) -> Url {
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use crate::core::layout;
|
|||
use crate::core::mouse;
|
||||
use crate::core::overlay;
|
||||
use crate::core::renderer;
|
||||
use crate::core::text;
|
||||
use crate::core::widget;
|
||||
use crate::core::widget::tree::{self, Tree};
|
||||
use crate::core::window;
|
||||
|
|
@ -17,6 +18,7 @@ use crate::core::{
|
|||
#[allow(missing_debug_implementations)]
|
||||
pub struct Pop<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> {
|
||||
content: Element<'a, Message, Theme, Renderer>,
|
||||
key: Option<text::Fragment<'a>>,
|
||||
on_show: Option<Box<dyn Fn(Size) -> Message + 'a>>,
|
||||
on_resize: Option<Box<dyn Fn(Size) -> Message + 'a>>,
|
||||
on_hide: Option<Message>,
|
||||
|
|
@ -34,6 +36,7 @@ where
|
|||
) -> Self {
|
||||
Self {
|
||||
content: content.into(),
|
||||
key: None,
|
||||
on_show: None,
|
||||
on_resize: None,
|
||||
on_hide: None,
|
||||
|
|
@ -66,6 +69,14 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the key of the [`Pop`] widget, for continuity.
|
||||
///
|
||||
/// If the key changes, the [`Pop`] widget will trigger again.
|
||||
pub fn key(mut self, key: impl text::IntoFragment<'a>) -> Self {
|
||||
self.key = Some(key.into_fragment());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the distance in [`Pixels`] to use in anticipation of the
|
||||
/// content popping into view.
|
||||
///
|
||||
|
|
@ -77,10 +88,11 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct State {
|
||||
has_popped_in: bool,
|
||||
last_size: Option<Size>,
|
||||
last_key: Option<String>,
|
||||
}
|
||||
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
|
|
@ -118,8 +130,16 @@ where
|
|||
) {
|
||||
if let Event::Window(window::Event::RedrawRequested(_)) = &event {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
let bounds = layout.bounds();
|
||||
|
||||
if state.has_popped_in
|
||||
&& state.last_key.as_deref() != self.key.as_deref()
|
||||
{
|
||||
state.has_popped_in = false;
|
||||
state.last_key =
|
||||
self.key.as_ref().cloned().map(text::Fragment::into_owned);
|
||||
}
|
||||
|
||||
let bounds = layout.bounds();
|
||||
let top_left_distance = viewport.distance(bounds.position());
|
||||
|
||||
let bottom_right_distance = viewport
|
||||
|
|
|
|||
|
|
@ -14,8 +14,13 @@ use crate::core::{
|
|||
|
||||
/// A bunch of [`Rich`] text.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Rich<'a, Link, Theme = crate::Theme, Renderer = crate::Renderer>
|
||||
where
|
||||
pub struct Rich<
|
||||
'a,
|
||||
Link,
|
||||
Message,
|
||||
Theme = crate::Theme,
|
||||
Renderer = crate::Renderer,
|
||||
> where
|
||||
Link: Clone + 'static,
|
||||
Theme: Catalog,
|
||||
Renderer: core::text::Renderer,
|
||||
|
|
@ -31,9 +36,11 @@ where
|
|||
wrapping: Wrapping,
|
||||
class: Theme::Class<'a>,
|
||||
hovered_link: Option<usize>,
|
||||
on_link_clicked: Option<Box<dyn Fn(Link) -> Message + 'a>>,
|
||||
}
|
||||
|
||||
impl<'a, Link, Theme, Renderer> Rich<'a, Link, Theme, Renderer>
|
||||
impl<'a, Link, Message, Theme, Renderer>
|
||||
Rich<'a, Link, Message, Theme, Renderer>
|
||||
where
|
||||
Link: Clone + 'static,
|
||||
Theme: Catalog,
|
||||
|
|
@ -54,6 +61,7 @@ where
|
|||
wrapping: Wrapping::default(),
|
||||
class: Theme::default(),
|
||||
hovered_link: None,
|
||||
on_link_clicked: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -127,6 +135,16 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the message that will be produced when a link of the [`Rich`] text
|
||||
/// is clicked.
|
||||
pub fn on_link_clicked(
|
||||
mut self,
|
||||
on_link_clicked: impl Fn(Link) -> Message + 'a,
|
||||
) -> Self {
|
||||
self.on_link_clicked = Some(Box::new(on_link_clicked));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the default style of the [`Rich`] text.
|
||||
#[must_use]
|
||||
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
|
||||
|
|
@ -164,7 +182,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Link, Theme, Renderer> Default for Rich<'a, Link, Theme, Renderer>
|
||||
impl<'a, Link, Message, Theme, Renderer> Default
|
||||
for Rich<'a, Link, Message, Theme, Renderer>
|
||||
where
|
||||
Link: Clone + 'a,
|
||||
Theme: Catalog,
|
||||
|
|
@ -182,8 +201,8 @@ struct State<Link, P: Paragraph> {
|
|||
paragraph: P,
|
||||
}
|
||||
|
||||
impl<Link, Theme, Renderer> Widget<Link, Theme, Renderer>
|
||||
for Rich<'_, Link, Theme, Renderer>
|
||||
impl<Link, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Rich<'_, Link, Message, Theme, Renderer>
|
||||
where
|
||||
Link: Clone + 'static,
|
||||
Theme: Catalog,
|
||||
|
|
@ -252,7 +271,8 @@ where
|
|||
let style = theme.style(&self.class);
|
||||
|
||||
for (index, span) in self.spans.as_ref().as_ref().iter().enumerate() {
|
||||
let is_hovered_link = Some(index) == self.hovered_link;
|
||||
let is_hovered_link = self.on_link_clicked.is_some()
|
||||
&& Some(index) == self.hovered_link;
|
||||
|
||||
if span.highlight.is_some()
|
||||
|| span.underline
|
||||
|
|
@ -363,9 +383,13 @@ where
|
|||
cursor: mouse::Cursor,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Link>,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let Some(on_link_clicked) = &self.on_link_clicked else {
|
||||
return;
|
||||
};
|
||||
|
||||
let was_hovered = self.hovered_link.is_some();
|
||||
|
||||
if let Some(position) = cursor.position_in(layout.bounds()) {
|
||||
|
|
@ -414,7 +438,7 @@ where
|
|||
.get(span)
|
||||
.and_then(|span| span.link.clone())
|
||||
{
|
||||
shell.publish(link);
|
||||
shell.publish(on_link_clicked(link));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -509,8 +533,9 @@ where
|
|||
})
|
||||
}
|
||||
|
||||
impl<'a, Link, Theme, Renderer> FromIterator<Span<'a, Link, Renderer::Font>>
|
||||
for Rich<'a, Link, Theme, Renderer>
|
||||
impl<'a, Link, Message, Theme, Renderer>
|
||||
FromIterator<Span<'a, Link, Renderer::Font>>
|
||||
for Rich<'a, Link, Message, Theme, Renderer>
|
||||
where
|
||||
Link: Clone + 'a,
|
||||
Theme: Catalog,
|
||||
|
|
@ -524,16 +549,18 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Link, Theme, Renderer> From<Rich<'a, Link, Theme, Renderer>>
|
||||
for Element<'a, Link, Theme, Renderer>
|
||||
impl<'a, Link, Message, Theme, Renderer>
|
||||
From<Rich<'a, Link, Message, Theme, Renderer>>
|
||||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Link: Clone + 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: core::text::Renderer + 'a,
|
||||
{
|
||||
fn from(
|
||||
text: Rich<'a, Link, Theme, Renderer>,
|
||||
) -> Element<'a, Link, Theme, Renderer> {
|
||||
text: Rich<'a, Link, Message, Theme, Renderer>,
|
||||
) -> Element<'a, Message, Theme, Renderer> {
|
||||
Element::new(text)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue