Add Link support to rich_text widget

This commit is contained in:
Héctor Ramón Jiménez 2024-07-21 12:45:05 +02:00
parent 4b44079f34
commit 9bfaf2840c
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
9 changed files with 287 additions and 71 deletions

View file

@ -77,8 +77,8 @@ impl text::Paragraph for () {
fn with_text(_text: Text<&str>) -> Self {} fn with_text(_text: Text<&str>) -> Self {}
fn with_spans( fn with_spans<Link>(
_text: Text<&[text::Span<'_, Self::Font>], Self::Font>, _text: Text<&[text::Span<'_, Link, Self::Font>], Self::Font>,
) -> Self { ) -> Self {
} }
@ -107,6 +107,10 @@ impl text::Paragraph for () {
fn hit_test(&self, _point: Point) -> Option<text::Hit> { fn hit_test(&self, _point: Point) -> Option<text::Hit> {
None None
} }
fn hit_span(&self, _point: Point) -> Option<usize> {
None
}
} }
impl text::Editor for () { impl text::Editor for () {

View file

@ -223,8 +223,8 @@ pub trait Renderer: crate::Renderer {
} }
/// A span of text. /// A span of text.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone)]
pub struct Span<'a, Font = crate::Font> { pub struct Span<'a, Link = (), Font = crate::Font> {
/// The [`Fragment`] of text. /// The [`Fragment`] of text.
pub text: Fragment<'a>, pub text: Fragment<'a>,
/// The size of the [`Span`] in [`Pixels`]. /// The size of the [`Span`] in [`Pixels`].
@ -235,9 +235,11 @@ pub struct Span<'a, Font = crate::Font> {
pub font: Option<Font>, pub font: Option<Font>,
/// The [`Color`] of the [`Span`]. /// The [`Color`] of the [`Span`].
pub color: Option<Color>, pub color: Option<Color>,
/// The link of the [`Span`].
pub link: Option<Link>,
} }
impl<'a, Font> Span<'a, Font> { impl<'a, Link, Font> Span<'a, Link, Font> {
/// Creates a new [`Span`] of text with the given text fragment. /// Creates a new [`Span`] of text with the given text fragment.
pub fn new(fragment: impl IntoFragment<'a>) -> Self { pub fn new(fragment: impl IntoFragment<'a>) -> Self {
Self { Self {
@ -246,6 +248,7 @@ impl<'a, Font> Span<'a, Font> {
line_height: None, line_height: None,
font: None, font: None,
color: None, color: None,
link: None,
} }
} }
@ -285,14 +288,27 @@ impl<'a, Font> Span<'a, Font> {
self self
} }
/// Sets the link of the [`Span`].
pub fn link(mut self, link: impl Into<Link>) -> Self {
self.link = Some(link.into());
self
}
/// Sets the link of the [`Span`], if any.
pub fn link_maybe(mut self, link: Option<impl Into<Link>>) -> Self {
self.link = link.map(Into::into);
self
}
/// Turns the [`Span`] into a static one. /// Turns the [`Span`] into a static one.
pub fn to_static(self) -> Span<'static, Font> { pub fn to_static(self) -> Span<'static, Link, Font> {
Span { Span {
text: Cow::Owned(self.text.into_owned()), text: Cow::Owned(self.text.into_owned()),
size: self.size, size: self.size,
line_height: self.line_height, line_height: self.line_height,
font: self.font, font: self.font,
color: self.color, color: self.color,
link: self.link,
} }
} }
} }
@ -303,6 +319,16 @@ impl<'a, Font> From<&'a str> for Span<'a, Font> {
} }
} }
impl<'a, Link, Font: PartialEq> PartialEq for Span<'a, Link, Font> {
fn eq(&self, other: &Self) -> bool {
self.text == other.text
&& self.size == other.size
&& self.line_height == other.line_height
&& self.font == other.font
&& self.color == other.color
}
}
/// A fragment of [`Text`]. /// A fragment of [`Text`].
/// ///
/// This is just an alias to a string that may be either /// This is just an alias to a string that may be either

View file

@ -12,7 +12,9 @@ pub trait Paragraph: Sized + Default {
fn with_text(text: Text<&str, Self::Font>) -> Self; fn with_text(text: Text<&str, Self::Font>) -> Self;
/// Creates a new [`Paragraph`] laid out with the given [`Text`]. /// Creates a new [`Paragraph`] laid out with the given [`Text`].
fn with_spans(text: Text<&[Span<'_, Self::Font>], Self::Font>) -> Self; fn with_spans<Link>(
text: Text<&[Span<'_, Link, Self::Font>], Self::Font>,
) -> Self;
/// Lays out the [`Paragraph`] with some new boundaries. /// Lays out the [`Paragraph`] with some new boundaries.
fn resize(&mut self, new_bounds: Size); fn resize(&mut self, new_bounds: Size);
@ -35,6 +37,11 @@ pub trait Paragraph: Sized + Default {
/// [`Paragraph`], returning information about the nearest character. /// [`Paragraph`], returning information about the nearest character.
fn hit_test(&self, point: Point) -> Option<Hit>; fn hit_test(&self, point: Point) -> Option<Hit>;
/// Tests whether the provided point is within the boundaries of a
/// [`Span`] in the [`Paragraph`], returning the index of the [`Span`]
/// that was hit.
fn hit_span(&self, point: Point) -> Option<usize>;
/// Returns the distance to the given grapheme index in the [`Paragraph`]. /// Returns the distance to the given grapheme index in the [`Paragraph`].
fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>; fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>;

View file

@ -8,3 +8,5 @@ publish = false
[dependencies] [dependencies]
iced.workspace = true iced.workspace = true
iced.features = ["markdown", "highlighter", "debug"] iced.features = ["markdown", "highlighter", "debug"]
open = "5.3"

View file

@ -16,6 +16,7 @@ struct Markdown {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum Message { enum Message {
Edit(text_editor::Action), Edit(text_editor::Action),
LinkClicked(String),
} }
impl Markdown { impl Markdown {
@ -50,6 +51,9 @@ impl Markdown {
.collect(); .collect();
} }
} }
Message::LinkClicked(link) => {
let _ = open::that(link);
}
} }
} }
@ -60,7 +64,7 @@ impl Markdown {
.padding(10) .padding(10)
.font(Font::MONOSPACE); .font(Font::MONOSPACE);
let preview = markdown(&self.items); let preview = markdown(&self.items, Message::LinkClicked);
row![editor, scrollable(preview).spacing(10).height(Fill)] row![editor, scrollable(preview).spacing(10).height(Fill)]
.spacing(10) .spacing(10)

View file

@ -100,8 +100,8 @@ impl core::text::Paragraph for Paragraph {
})) }))
} }
fn with_spans(text: Text<&[Span<'_>]>) -> Self { fn with_spans<Link>(text: Text<&[Span<'_, Link>]>) -> Self {
log::trace!("Allocating rich paragraph: {:?}", text.content); log::trace!("Allocating rich paragraph: {} spans", text.content.len());
let mut font_system = let mut font_system =
text::font_system().write().expect("Write font system"); text::font_system().write().expect("Write font system");
@ -122,11 +122,9 @@ impl core::text::Paragraph for Paragraph {
buffer.set_rich_text( buffer.set_rich_text(
font_system.raw(), font_system.raw(),
text.content.iter().map(|span| { text.content.iter().enumerate().map(|(i, span)| {
let attrs = cosmic_text::Attrs::new();
let attrs = if let Some(font) = span.font { let attrs = if let Some(font) = span.font {
attrs cosmic_text::Attrs::new()
.family(text::to_family(font.family)) .family(text::to_family(font.family))
.weight(text::to_weight(font.weight)) .weight(text::to_weight(font.weight))
.stretch(text::to_stretch(font.stretch)) .stretch(text::to_stretch(font.stretch))
@ -156,7 +154,7 @@ impl core::text::Paragraph for Paragraph {
attrs attrs
}; };
(span.text.as_ref(), attrs) (span.text.as_ref(), attrs.metadata(i))
}), }),
text::to_attributes(text.font), text::to_attributes(text.font),
text::to_shaping(text.shaping), text::to_shaping(text.shaping),
@ -231,6 +229,36 @@ impl core::text::Paragraph for Paragraph {
Some(Hit::CharOffset(cursor.index)) Some(Hit::CharOffset(cursor.index))
} }
fn hit_span(&self, point: Point) -> Option<usize> {
let internal = self.internal();
let cursor = internal.buffer.hit(point.x, point.y)?;
let line = internal.buffer.lines.get(cursor.line)?;
let mut last_glyph = None;
let mut glyphs = line
.layout_opt()
.as_ref()?
.iter()
.flat_map(|line| line.glyphs.iter())
.peekable();
while let Some(glyph) = glyphs.peek() {
if glyph.start <= cursor.index && cursor.index < glyph.end {
break;
}
last_glyph = glyphs.next();
}
let glyph = match cursor.affinity {
cosmic_text::Affinity::Before => last_glyph,
cosmic_text::Affinity::After => glyphs.next(),
}?;
Some(glyph.metadata)
}
fn grapheme_position(&self, line: usize, index: usize) -> Option<Point> { fn grapheme_position(&self, line: usize, index: usize) -> Option<Point> {
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;

View file

@ -683,10 +683,11 @@ where
/// Creates a new [`Rich`] text widget with the provided spans. /// Creates a new [`Rich`] text widget with the provided spans.
/// ///
/// [`Rich`]: text::Rich /// [`Rich`]: text::Rich
pub fn rich_text<'a, Theme, Renderer>( pub fn rich_text<'a, Message, Link, Theme, Renderer>(
spans: impl Into<Cow<'a, [text::Span<'a, Renderer::Font>]>>, spans: impl Into<Cow<'a, [text::Span<'a, Link, Renderer::Font>]>>,
) -> text::Rich<'a, Theme, Renderer> ) -> text::Rich<'a, Message, Link, Theme, Renderer>
where where
Link: Clone,
Theme: text::Catalog + 'a, Theme: text::Catalog + 'a,
Renderer: core::text::Renderer, Renderer: core::text::Renderer,
{ {

View file

@ -14,13 +14,13 @@ use crate::{column, container, rich_text, row, span, text};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Item { pub enum Item {
/// A heading. /// A heading.
Heading(Vec<text::Span<'static>>), Heading(Vec<text::Span<'static, String>>),
/// A paragraph. /// A paragraph.
Paragraph(Vec<text::Span<'static>>), Paragraph(Vec<text::Span<'static, String>>),
/// A code block. /// A code block.
/// ///
/// You can enable the `highlighter` feature for syntax highligting. /// You can enable the `highlighter` feature for syntax highligting.
CodeBlock(Vec<text::Span<'static>>), CodeBlock(Vec<text::Span<'static, String>>),
/// A list. /// A list.
List { List {
/// The first number of the list, if it is ordered. /// The first number of the list, if it is ordered.
@ -46,7 +46,7 @@ pub fn parse(
let mut emphasis = false; let mut emphasis = false;
let mut metadata = false; let mut metadata = false;
let mut table = false; let mut table = false;
let mut link = false; let mut link = None;
let mut lists = Vec::new(); let mut lists = Vec::new();
#[cfg(feature = "highlighter")] #[cfg(feature = "highlighter")]
@ -93,8 +93,10 @@ pub fn parse(
emphasis = true; emphasis = true;
None None
} }
pulldown_cmark::Tag::Link { .. } if !metadata && !table => { pulldown_cmark::Tag::Link { dest_url, .. }
link = true; if !metadata && !table =>
{
link = Some(dest_url);
None None
} }
pulldown_cmark::Tag::List(first_item) if !metadata && !table => { pulldown_cmark::Tag::List(first_item) if !metadata && !table => {
@ -150,7 +152,7 @@ pub fn parse(
None None
} }
pulldown_cmark::TagEnd::Link if !metadata && !table => { pulldown_cmark::TagEnd::Link if !metadata && !table => {
link = false; link = None;
None None
} }
pulldown_cmark::TagEnd::Paragraph if !metadata && !table => { pulldown_cmark::TagEnd::Paragraph if !metadata && !table => {
@ -245,7 +247,11 @@ pub fn parse(
span span
}; };
let span = span.color_maybe(link.then_some(palette.primary)); let span = if let Some(link) = link.as_ref() {
span.color(palette.primary).link(link.to_string())
} else {
span
};
spans.push(span); spans.push(span);
@ -272,40 +278,48 @@ pub fn parse(
/// You can obtain the items with [`parse`]. /// You can obtain the items with [`parse`].
pub fn view<'a, Message, Renderer>( pub fn view<'a, Message, Renderer>(
items: impl IntoIterator<Item = &'a Item>, items: impl IntoIterator<Item = &'a Item>,
on_link_click: impl Fn(String) -> Message + Copy + 'a,
) -> Element<'a, Message, Theme, Renderer> ) -> Element<'a, Message, Theme, Renderer>
where where
Message: 'a, Message: 'a,
Renderer: core::text::Renderer<Font = Font> + 'a, Renderer: core::text::Renderer<Font = Font> + 'a,
{ {
let blocks = items.into_iter().enumerate().map(|(i, item)| match item { let blocks = items.into_iter().enumerate().map(|(i, item)| match item {
Item::Heading(heading) => container(rich_text(heading)) Item::Heading(heading) => {
.padding(padding::top(if i > 0 { 8 } else { 0 })) container(rich_text(heading).on_link_click(on_link_click))
.into(), .padding(padding::top(if i > 0 { 8 } else { 0 }))
Item::Paragraph(paragraph) => rich_text(paragraph).into(), .into()
Item::List { start: None, items } => column( }
items Item::Paragraph(paragraph) => {
.iter() rich_text(paragraph).on_link_click(on_link_click).into()
.map(|items| row!["", view(items)].spacing(10).into()), }
) Item::List { start: None, items } => {
.spacing(10) column(items.iter().map(|items| {
.into(), row!["", view(items, on_link_click)].spacing(10).into()
}))
.spacing(10)
.into()
}
Item::List { Item::List {
start: Some(start), start: Some(start),
items, items,
} => column(items.iter().enumerate().map(|(i, items)| { } => column(items.iter().enumerate().map(|(i, items)| {
row![text!("{}.", i as u64 + *start), view(items)] row![text!("{}.", i as u64 + *start), view(items, on_link_click)]
.spacing(10) .spacing(10)
.into() .into()
})) }))
.spacing(10) .spacing(10)
.into(), .into(),
Item::CodeBlock(code) => { Item::CodeBlock(code) => container(
container(rich_text(code).font(Font::MONOSPACE).size(12)) rich_text(code)
.width(Length::Fill) .font(Font::MONOSPACE)
.padding(10) .size(12)
.style(container::rounded_box) .on_link_click(on_link_click),
.into() )
} .width(Length::Fill)
.padding(10)
.style(container::rounded_box)
.into(),
}); });
Element::new(column(blocks).width(Length::Fill).spacing(16)) Element::new(column(blocks).width(Length::Fill).spacing(16))

View file

@ -1,5 +1,6 @@
use crate::core::alignment; use crate::core::alignment;
use crate::core::layout::{self, Layout}; use crate::core::event;
use crate::core::layout;
use crate::core::mouse; use crate::core::mouse;
use crate::core::renderer; use crate::core::renderer;
use crate::core::text::{Paragraph, Span}; use crate::core::text::{Paragraph, Span};
@ -8,19 +9,26 @@ use crate::core::widget::text::{
}; };
use crate::core::widget::tree::{self, Tree}; use crate::core::widget::tree::{self, Tree};
use crate::core::{ use crate::core::{
self, Color, Element, Length, Pixels, Rectangle, Size, Widget, self, Clipboard, Color, Element, Event, Layout, Length, Pixels, Rectangle,
Shell, Size, Widget,
}; };
use std::borrow::Cow; use std::borrow::Cow;
/// A bunch of [`Rich`] text. /// A bunch of [`Rich`] text.
#[derive(Debug)] #[allow(missing_debug_implementations)]
pub struct Rich<'a, Theme = crate::Theme, Renderer = crate::Renderer> pub struct Rich<
where 'a,
Message,
Link = (),
Theme = crate::Theme,
Renderer = crate::Renderer,
> where
Link: Clone + 'static,
Theme: Catalog, Theme: Catalog,
Renderer: core::text::Renderer, Renderer: core::text::Renderer,
{ {
spans: Cow<'a, [Span<'a, Renderer::Font>]>, spans: Cow<'a, [Span<'a, Link, Renderer::Font>]>,
size: Option<Pixels>, size: Option<Pixels>,
line_height: LineHeight, line_height: LineHeight,
width: Length, width: Length,
@ -29,10 +37,13 @@ where
align_x: alignment::Horizontal, align_x: alignment::Horizontal,
align_y: alignment::Vertical, align_y: alignment::Vertical,
class: Theme::Class<'a>, class: Theme::Class<'a>,
on_link_click: Option<Box<dyn Fn(Link) -> Message + 'a>>,
} }
impl<'a, Theme, Renderer> Rich<'a, Theme, Renderer> impl<'a, Message, Link, Theme, Renderer>
Rich<'a, Message, Link, Theme, Renderer>
where where
Link: Clone + 'static,
Theme: Catalog, Theme: Catalog,
Renderer: core::text::Renderer, Renderer: core::text::Renderer,
{ {
@ -48,12 +59,13 @@ where
align_x: alignment::Horizontal::Left, align_x: alignment::Horizontal::Left,
align_y: alignment::Vertical::Top, align_y: alignment::Vertical::Top,
class: Theme::default(), class: Theme::default(),
on_link_click: None,
} }
} }
/// Creates a new [`Rich`] text with the given text spans. /// Creates a new [`Rich`] text with the given text spans.
pub fn with_spans( pub fn with_spans(
spans: impl Into<Cow<'a, [Span<'a, Renderer::Font>]>>, spans: impl Into<Cow<'a, [Span<'a, Link, Renderer::Font>]>>,
) -> Self { ) -> Self {
Self { Self {
spans: spans.into(), spans: spans.into(),
@ -143,6 +155,15 @@ where
self.style(move |_theme| Style { color }) self.style(move |_theme| Style { color })
} }
/// Sets the message handler for link clicks on the [`Rich`] text.
pub fn on_link_click(
mut self,
on_link_click: impl Fn(Link) -> Message + 'a,
) -> Self {
self.on_link_click = Some(Box::new(on_link_click));
self
}
/// Sets the default style class of the [`Rich`] text. /// Sets the default style class of the [`Rich`] text.
#[cfg(feature = "advanced")] #[cfg(feature = "advanced")]
#[must_use] #[must_use]
@ -152,14 +173,19 @@ where
} }
/// Adds a new text [`Span`] to the [`Rich`] text. /// Adds a new text [`Span`] to the [`Rich`] text.
pub fn push(mut self, span: impl Into<Span<'a, Renderer::Font>>) -> Self { pub fn push(
mut self,
span: impl Into<Span<'a, Link, Renderer::Font>>,
) -> Self {
self.spans.to_mut().push(span.into()); self.spans.to_mut().push(span.into());
self self
} }
} }
impl<'a, Theme, Renderer> Default for Rich<'a, Theme, Renderer> impl<'a, Message, Link, Theme, Renderer> Default
for Rich<'a, Message, Link, Theme, Renderer>
where where
Link: Clone + 'static,
Theme: Catalog, Theme: Catalog,
Renderer: core::text::Renderer, Renderer: core::text::Renderer,
{ {
@ -168,24 +194,27 @@ where
} }
} }
struct State<P: Paragraph> { struct State<Link, P: Paragraph> {
spans: Vec<Span<'static, P::Font>>, spans: Vec<Span<'static, Link, P::Font>>,
span_pressed: Option<usize>,
paragraph: P, paragraph: P,
} }
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> impl<'a, Message, Link, Theme, Renderer> Widget<Message, Theme, Renderer>
for Rich<'a, Theme, Renderer> for Rich<'a, Message, Link, Theme, Renderer>
where where
Link: Clone + 'static,
Theme: Catalog, Theme: Catalog,
Renderer: core::text::Renderer, Renderer: core::text::Renderer,
{ {
fn tag(&self) -> tree::Tag { fn tag(&self) -> tree::Tag {
tree::Tag::of::<State<Renderer::Paragraph>>() tree::Tag::of::<State<Link, Renderer::Paragraph>>()
} }
fn state(&self) -> tree::State { fn state(&self) -> tree::State {
tree::State::new(State { tree::State::new(State::<Link, _> {
spans: Vec::new(), spans: Vec::new(),
span_pressed: None,
paragraph: Renderer::Paragraph::default(), paragraph: Renderer::Paragraph::default(),
}) })
} }
@ -204,7 +233,8 @@ where
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
layout( layout(
tree.state.downcast_mut::<State<Renderer::Paragraph>>(), tree.state
.downcast_mut::<State<Link, Renderer::Paragraph>>(),
renderer, renderer,
limits, limits,
self.width, self.width,
@ -228,7 +258,10 @@ where
_cursor_position: mouse::Cursor, _cursor_position: mouse::Cursor,
viewport: &Rectangle, viewport: &Rectangle,
) { ) {
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>(); let state = tree
.state
.downcast_ref::<State<Link, Renderer::Paragraph>>();
let style = theme.style(&self.class); let style = theme.style(&self.class);
text::draw( text::draw(
@ -240,15 +273,106 @@ where
viewport, viewport,
); );
} }
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
_viewport: &Rectangle,
) -> event::Status {
let Some(on_link_click) = self.on_link_click.as_ref() else {
return event::Status::Ignored;
};
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
if let Some(position) = cursor.position_in(layout.bounds()) {
let state = tree
.state
.downcast_mut::<State<Link, Renderer::Paragraph>>();
if let Some(span) = state.paragraph.hit_span(position) {
state.span_pressed = Some(span);
return event::Status::Captured;
}
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
let state = tree
.state
.downcast_mut::<State<Link, Renderer::Paragraph>>();
if let Some(span_pressed) = state.span_pressed {
state.span_pressed = None;
if let Some(position) = cursor.position_in(layout.bounds())
{
match state.paragraph.hit_span(position) {
Some(span) if span == span_pressed => {
if let Some(link) = state
.spans
.get(span)
.and_then(|span| span.link.clone())
{
shell.publish(on_link_click(link));
}
}
_ => {}
}
}
}
}
_ => {}
}
event::Status::Ignored
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
if self.on_link_click.is_none() {
return mouse::Interaction::None;
}
if let Some(position) = cursor.position_in(layout.bounds()) {
let state = tree
.state
.downcast_ref::<State<Link, Renderer::Paragraph>>();
if let Some(span) = state
.paragraph
.hit_span(position)
.and_then(|span| state.spans.get(span))
{
if span.link.is_some() {
return mouse::Interaction::Pointer;
}
}
}
mouse::Interaction::None
}
} }
fn layout<Renderer>( fn layout<Link, Renderer>(
state: &mut State<Renderer::Paragraph>, state: &mut State<Link, Renderer::Paragraph>,
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
width: Length, width: Length,
height: Length, height: Length,
spans: &[Span<'_, Renderer::Font>], spans: &[Span<'_, Link, Renderer::Font>],
line_height: LineHeight, line_height: LineHeight,
size: Option<Pixels>, size: Option<Pixels>,
font: Option<Renderer::Font>, font: Option<Renderer::Font>,
@ -256,6 +380,7 @@ fn layout<Renderer>(
vertical_alignment: alignment::Vertical, vertical_alignment: alignment::Vertical,
) -> layout::Node ) -> layout::Node
where where
Link: Clone,
Renderer: core::text::Renderer, Renderer: core::text::Renderer,
{ {
layout::sized(limits, width, height, |limits| { layout::sized(limits, width, height, |limits| {
@ -305,13 +430,15 @@ where
}) })
} }
impl<'a, Theme, Renderer> FromIterator<Span<'a, Renderer::Font>> impl<'a, Message, Link, Theme, Renderer>
for Rich<'a, Theme, Renderer> FromIterator<Span<'a, Link, Renderer::Font>>
for Rich<'a, Message, Link, Theme, Renderer>
where where
Link: Clone + 'static,
Theme: Catalog, Theme: Catalog,
Renderer: core::text::Renderer, Renderer: core::text::Renderer,
{ {
fn from_iter<T: IntoIterator<Item = Span<'a, Renderer::Font>>>( fn from_iter<T: IntoIterator<Item = Span<'a, Link, Renderer::Font>>>(
spans: T, spans: T,
) -> Self { ) -> Self {
Self { Self {
@ -321,14 +448,17 @@ where
} }
} }
impl<'a, Message, Theme, Renderer> From<Rich<'a, Theme, Renderer>> impl<'a, Message, Link, Theme, Renderer>
From<Rich<'a, Message, Link, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer> for Element<'a, Message, Theme, Renderer>
where where
Message: 'a,
Link: Clone + 'static,
Theme: Catalog + 'a, Theme: Catalog + 'a,
Renderer: core::text::Renderer + 'a, Renderer: core::text::Renderer + 'a,
{ {
fn from( fn from(
text: Rich<'a, Theme, Renderer>, text: Rich<'a, Message, Link, Theme, Renderer>,
) -> Element<'a, Message, Theme, Renderer> { ) -> Element<'a, Message, Theme, Renderer> {
Element::new(text) Element::new(text)
} }