aboutsummaryrefslogtreecommitdiff
path: root/src/text/rich.rs
diff options
context:
space:
mode:
authorPolesznyák Márk <contact@pml68.dev>2025-10-19 12:17:01 +0200
committerPolesznyák Márk <contact@pml68.dev>2025-10-19 12:17:01 +0200
commit7d3bacc69df0ba1151836c8da40bace5dfc5f037 (patch)
tree1f0d531f3810b32a8b5ab6e6d63f856b98c51931 /src/text/rich.rs
parentfeat: initial commit (diff)
downloadiced_selection-7d3bacc69df0ba1151836c8da40bace5dfc5f037.tar.gz
feat: add selectable `Rich` widget with an example
Diffstat (limited to '')
-rw-r--r--src/text/rich.rs841
1 files changed, 841 insertions, 0 deletions
diff --git a/src/text/rich.rs b/src/text/rich.rs
index e69de29..ae8bbab 100644
--- a/src/text/rich.rs
+++ b/src/text/rich.rs
@@ -0,0 +1,841 @@
+use iced_widget::graphics::text::Paragraph;
+
+use crate::core::alignment;
+use crate::core::clipboard;
+use crate::core::keyboard;
+use crate::core::keyboard::key;
+use crate::core::layout;
+use crate::core::mouse;
+use crate::core::renderer;
+use crate::core::text::{Paragraph as _, Span};
+use crate::core::touch;
+use crate::core::widget::text::{Alignment, LineHeight, Shaping, Wrapping};
+use crate::core::widget::tree::{self, Tree};
+use crate::core::{
+ self, Clipboard, Element, Event, Font, Layout, Length, Pixels, Point,
+ Rectangle, Shell, Size, Vector, Widget,
+};
+use crate::selection::{Selection, SelectionEnd};
+use crate::text::{Catalog, Style, StyleFn};
+
+/// A bunch of [`Rich`] text.
+pub struct Rich<
+ 'a,
+ Link,
+ Message,
+ Theme = iced_widget::Theme,
+ Renderer = iced_widget::Renderer,
+> where
+ Link: Clone + 'static,
+ Theme: Catalog,
+ Renderer: core::text::Renderer,
+{
+ spans: Box<dyn AsRef<[Span<'a, Link, Renderer::Font>]> + 'a>,
+ size: Option<Pixels>,
+ line_height: LineHeight,
+ width: Length,
+ height: Length,
+ font: Option<Renderer::Font>,
+ align_x: Alignment,
+ align_y: alignment::Vertical,
+ wrapping: Wrapping,
+ class: Theme::Class<'a>,
+ hovered_link: Option<usize>,
+ on_link_click: Option<Box<dyn Fn(Link) -> Message + 'a>>,
+}
+
+impl<'a, Link, Message, Theme, Renderer>
+ Rich<'a, Link, Message, Theme, Renderer>
+where
+ Link: Clone + 'static,
+ Theme: Catalog,
+ Renderer: core::text::Renderer,
+ Renderer::Font: 'a,
+{
+ /// Creates a new empty [`Rich`] text.
+ pub fn new() -> Self {
+ Self {
+ spans: Box::new([]),
+ size: None,
+ line_height: LineHeight::default(),
+ width: Length::Shrink,
+ height: Length::Shrink,
+ font: None,
+ align_x: Alignment::Default,
+ align_y: alignment::Vertical::Top,
+ wrapping: Wrapping::default(),
+ class: Theme::default(),
+ hovered_link: None,
+ on_link_click: None,
+ }
+ }
+
+ /// Creates a new [`Rich`] text with the given text spans.
+ pub fn with_spans(
+ spans: impl AsRef<[Span<'a, Link, Renderer::Font>]> + 'a,
+ ) -> Self {
+ Self {
+ spans: Box::new(spans),
+ ..Self::new()
+ }
+ }
+
+ /// Sets the default size of the [`Rich`] text.
+ pub fn size(mut self, size: impl Into<Pixels>) -> Self {
+ self.size = Some(size.into());
+ self
+ }
+
+ /// Sets the default [`LineHeight`] of the [`Rich`] text.
+ pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
+ self.line_height = line_height.into();
+ self
+ }
+
+ /// Sets the default font of the [`Rich`] text.
+ pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
+ self.font = Some(font.into());
+ self
+ }
+
+ /// Sets the width of the [`Rich`] text boundaries.
+ pub fn width(mut self, width: impl Into<Length>) -> Self {
+ self.width = width.into();
+ self
+ }
+
+ /// Sets the height of the [`Rich`] text boundaries.
+ pub fn height(mut self, height: impl Into<Length>) -> Self {
+ self.height = height.into();
+ self
+ }
+
+ /// Centers the [`Rich`] text, both horizontally and vertically.
+ pub fn center(self) -> Self {
+ self.align_x(alignment::Horizontal::Center)
+ .align_y(alignment::Vertical::Center)
+ }
+
+ /// Sets the [`alignment::Horizontal`] of the [`Rich`] text.
+ pub fn align_x(mut self, alignment: impl Into<Alignment>) -> Self {
+ self.align_x = alignment.into();
+ self
+ }
+
+ /// Sets the [`alignment::Vertical`] of the [`Rich`] text.
+ pub fn align_y(
+ mut self,
+ alignment: impl Into<alignment::Vertical>,
+ ) -> Self {
+ self.align_y = alignment.into();
+ self
+ }
+
+ /// Sets the [`Wrapping`] strategy of the [`Rich`] text.
+ pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
+ self.wrapping = wrapping;
+ self
+ }
+
+ /// Sets the message that will be produced when a link of the [`Rich`] text
+ /// is clicked.
+ pub fn on_link_click(
+ mut self,
+ on_link_clicked: impl Fn(Link) -> Message + 'a,
+ ) -> Self {
+ self.on_link_click = 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
+ where
+ Theme::Class<'a>: From<StyleFn<'a, Theme>>,
+ {
+ self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
+ self
+ }
+
+ /// Sets the default style class of the [`Rich`] text.
+ #[must_use]
+ pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
+ self.class = class.into();
+ self
+ }
+}
+
+impl<'a, Link, Message, Theme, Renderer> Default
+ for Rich<'a, Link, Message, Theme, Renderer>
+where
+ Link: Clone + 'a,
+ Theme: Catalog,
+ Renderer: core::text::Renderer<Paragraph = Paragraph, Font = Font>,
+ Renderer::Font: 'a,
+{
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+struct State<Link> {
+ spans: Vec<Span<'static, Link, Font>>,
+ span_pressed: Option<usize>,
+ paragraph: Paragraph,
+ is_hovered: bool,
+ selection: Selection,
+ is_dragging: bool,
+ last_click: Option<mouse::Click>,
+ keyboard_modifiers: keyboard::Modifiers,
+}
+
+impl<Link> State<Link> {
+ fn grapheme_line_and_index(&self, point: Point) -> Option<(usize, usize)> {
+ let cursor = self.paragraph.buffer().hit(point.x, point.y)?;
+ let value = self.paragraph.buffer().lines[cursor.line].text();
+
+ Some((
+ cursor.line,
+ unicode_segmentation::UnicodeSegmentation::graphemes(
+ &value[..cursor.index.min(value.len())],
+ true,
+ )
+ .count(),
+ ))
+ }
+
+ fn selection_end_points(&self) -> [Point; 2] {
+ let Selection { start, end, .. } = self.selection;
+
+ let start_position = self
+ .paragraph
+ .grapheme_position(start.line, start.index)
+ .unwrap_or(Point::ORIGIN);
+
+ let end_position = self
+ .paragraph
+ .grapheme_position(end.line, end.index)
+ .unwrap_or(Point::ORIGIN);
+
+ [start_position, end_position]
+ }
+}
+
+impl<Link, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
+ for Rich<'_, Link, Message, Theme, Renderer>
+where
+ Link: Clone + 'static,
+ Theme: Catalog,
+ Renderer: core::text::Renderer<Paragraph = Paragraph, Font = Font>,
+{
+ fn tag(&self) -> tree::Tag {
+ tree::Tag::of::<State<Link>>()
+ }
+
+ fn state(&self) -> tree::State {
+ tree::State::new(State::<Link> {
+ spans: Vec::new(),
+ span_pressed: None,
+ paragraph: Paragraph::default(),
+ is_hovered: false,
+ selection: Selection::default(),
+ is_dragging: false,
+ last_click: None,
+ keyboard_modifiers: keyboard::Modifiers::default(),
+ })
+ }
+
+ fn size(&self) -> Size<Length> {
+ Size {
+ width: self.width,
+ height: self.height,
+ }
+ }
+
+ fn layout(
+ &mut self,
+ tree: &mut Tree,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ ) -> layout::Node {
+ layout(
+ tree.state.downcast_mut::<State<Link>>(),
+ renderer,
+ limits,
+ self.width,
+ self.height,
+ self.spans.as_ref().as_ref(),
+ self.line_height,
+ self.size,
+ self.font,
+ self.align_x,
+ self.align_y,
+ self.wrapping,
+ )
+ }
+
+ fn draw(
+ &self,
+ tree: &Tree,
+ renderer: &mut Renderer,
+ theme: &Theme,
+ defaults: &renderer::Style,
+ layout: Layout<'_>,
+ _cursor: mouse::Cursor,
+ viewport: &Rectangle,
+ ) {
+ if !layout.bounds().intersects(viewport) {
+ return;
+ }
+
+ let state = tree.state.downcast_ref::<State<Link>>();
+
+ let style = theme.style(&self.class);
+
+ for (index, span) in self.spans.as_ref().as_ref().iter().enumerate() {
+ let is_hovered_link = self.on_link_click.is_some()
+ && Some(index) == self.hovered_link;
+
+ if span.highlight.is_some()
+ || span.underline
+ || span.strikethrough
+ || is_hovered_link
+ {
+ let translation = layout.position() - Point::ORIGIN;
+ let regions = state.paragraph.span_bounds(index);
+
+ if let Some(highlight) = span.highlight {
+ for bounds in &regions {
+ let bounds = Rectangle::new(
+ bounds.position()
+ - Vector::new(
+ span.padding.left,
+ span.padding.top,
+ ),
+ bounds.size()
+ + Size::new(
+ span.padding.horizontal(),
+ span.padding.vertical(),
+ ),
+ );
+
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: bounds + translation,
+ border: highlight.border,
+ ..Default::default()
+ },
+ highlight.background,
+ );
+ }
+ }
+
+ if span.underline || span.strikethrough || is_hovered_link {
+ let size = span
+ .size
+ .or(self.size)
+ .unwrap_or(renderer.default_size());
+
+ let line_height = span
+ .line_height
+ .unwrap_or(self.line_height)
+ .to_absolute(size);
+
+ let color = span
+ .color
+ .or(style.color)
+ .unwrap_or(defaults.text_color);
+
+ let baseline = translation
+ + Vector::new(
+ 0.0,
+ size.0 + (line_height.0 - size.0) / 2.0,
+ );
+
+ if span.underline || is_hovered_link {
+ for bounds in &regions {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: Rectangle::new(
+ bounds.position() + baseline
+ - Vector::new(0.0, size.0 * 0.08),
+ Size::new(bounds.width, 1.0),
+ ),
+ ..Default::default()
+ },
+ color,
+ );
+ }
+ }
+
+ if span.strikethrough {
+ for bounds in &regions {
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: Rectangle::new(
+ bounds.position() + baseline
+ - Vector::new(0.0, size.0 / 2.0),
+ Size::new(bounds.width, 1.0),
+ ),
+ ..Default::default()
+ },
+ color,
+ );
+ }
+ }
+ }
+ }
+ }
+
+ if !state.selection.is_empty() {
+ let bounds = layout.bounds();
+
+ let [start, end] = state
+ .selection_end_points()
+ .map(|pos| pos + Vector::new(bounds.x, bounds.y));
+
+ let line_height = self
+ .line_height
+ .to_absolute(
+ self.size.unwrap_or_else(|| renderer.default_size()),
+ )
+ .0;
+
+ let baseline_y = bounds.y
+ + ((start.y - bounds.y) / line_height).floor() * line_height;
+
+ // The correct code, uncomment when glyphs report a correct `y` value.
+ //
+ // let height = end.y - baseline_y - 0.5;
+ // let rows = (height / line_height).ceil() as usize;
+ //
+ // Temporary solution
+ let rows =
+ state.selection.end.line - state.selection.start.line + 1;
+
+ for row in 0..rows {
+ let (x, width) = if row == 0 {
+ (
+ start.x,
+ if rows == 1 {
+ end.x.min(bounds.x + bounds.width) - start.x
+ } else {
+ bounds.x + bounds.width - start.x
+ },
+ )
+ } else if row == rows - 1 {
+ (bounds.x, end.x - bounds.x)
+ } else {
+ (bounds.x, bounds.width)
+ };
+
+ let y = baseline_y + row as f32 * line_height;
+
+ renderer.fill_quad(
+ renderer::Quad {
+ bounds: Rectangle {
+ x,
+ y,
+ width,
+ height: line_height,
+ },
+ snap: true,
+ ..Default::default()
+ },
+ style.selection,
+ );
+ }
+ }
+
+ crate::text::draw(
+ renderer,
+ defaults,
+ layout.bounds(),
+ &state.paragraph,
+ style,
+ viewport,
+ );
+ }
+
+ fn update(
+ &mut self,
+ tree: &mut Tree,
+ event: &Event,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ _renderer: &Renderer,
+ clipboard: &mut dyn Clipboard,
+ shell: &mut Shell<'_, Message>,
+ viewport: &Rectangle,
+ ) {
+ let state = tree.state.downcast_mut::<State<Link>>();
+
+ let bounds = layout.bounds();
+ let click_position = cursor.position_in(bounds);
+
+ if viewport.intersection(&bounds).is_none()
+ && state.selection == Selection::default()
+ && !state.is_dragging
+ {
+ return;
+ }
+
+ let link_was_hovered = self.hovered_link.is_some();
+ let was_hovered = state.is_hovered;
+ let selection_before = state.selection;
+
+ state.is_hovered = click_position.is_some();
+
+ if let Some(position) = click_position {
+ self.hovered_link =
+ state.paragraph.hit_span(position).and_then(|span| {
+ if self.spans.as_ref().as_ref().get(span)?.link.is_some() {
+ Some(span)
+ } else {
+ None
+ }
+ });
+ } else {
+ self.hovered_link = None;
+ }
+
+ match event {
+ Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
+ | Event::Touch(touch::Event::FingerPressed { .. }) => {
+ if self.hovered_link.is_some() {
+ state.span_pressed = self.hovered_link;
+ shell.capture_event();
+ }
+
+ if let Some(position) = click_position {
+ let click = mouse::Click::new(
+ position,
+ mouse::Button::Left,
+ state.last_click,
+ );
+
+ let (line, index) = state
+ .grapheme_line_and_index(position)
+ .unwrap_or((0, 0));
+
+ match click.kind() {
+ mouse::click::Kind::Single => {
+ let new_end = SelectionEnd { line, index };
+
+ if state.keyboard_modifiers.shift() {
+ state.selection.change_selection(new_end);
+ state.is_dragging = true;
+ } else if state.span_pressed.is_none() {
+ state.selection.select_range(new_end, new_end);
+ state.is_dragging = true;
+ }
+ }
+ mouse::click::Kind::Double => {
+ state.selection.select_word(
+ line,
+ index,
+ &state.paragraph,
+ );
+ state.is_dragging = false;
+ }
+ mouse::click::Kind::Triple => {
+ state.selection.select_line(line, &state.paragraph);
+ state.is_dragging = false;
+ }
+ }
+ } else {
+ state.selection = Selection::default();
+ }
+ }
+ Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
+ | Event::Touch(touch::Event::FingerLifted { .. })
+ | Event::Touch(touch::Event::FingerLost { .. }) => {
+ state.is_dragging = false;
+ if !matches!(
+ event,
+ Event::Touch(touch::Event::FingerLost { .. })
+ ) && state.selection.is_empty()
+ {
+ match state.span_pressed {
+ Some(span) if Some(span) == self.hovered_link => {
+ if let Some((link, on_link_clicked)) = self
+ .spans
+ .as_ref()
+ .as_ref()
+ .get(span)
+ .and_then(|span| span.link.clone())
+ .zip(self.on_link_click.as_deref())
+ {
+ shell.publish(on_link_clicked(link));
+ }
+ }
+ _ => {}
+ }
+
+ state.span_pressed = None;
+ }
+ }
+ Event::Mouse(mouse::Event::CursorMoved { .. })
+ | Event::Touch(touch::Event::FingerMoved { .. }) => {
+ if let Some(position) = click_position
+ && state.is_dragging
+ {
+ let (line, index) = state
+ .grapheme_line_and_index(position)
+ .unwrap_or((0, 0));
+
+ let new_end = SelectionEnd { line, index };
+
+ state.selection.change_selection(new_end);
+ }
+ }
+ Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
+ match key.as_ref() {
+ keyboard::Key::Character("c")
+ if state.keyboard_modifiers.command()
+ && !state.selection.is_empty() =>
+ {
+ clipboard.write(
+ clipboard::Kind::Standard,
+ state.selection.text(&state.paragraph),
+ );
+
+ shell.capture_event();
+ }
+ keyboard::Key::Character("a")
+ if state.keyboard_modifiers.command()
+ && state.selection != Selection::default() =>
+ {
+ state.selection.select_all(&state.paragraph);
+
+ shell.capture_event();
+ }
+ keyboard::Key::Named(key::Named::Home)
+ if state.keyboard_modifiers.shift()
+ && state.selection != Selection::default() =>
+ {
+ if state.keyboard_modifiers.jump() {
+ state.selection.select_beginning();
+ } else {
+ state.selection.select_line_beginning();
+ }
+
+ shell.capture_event();
+ }
+ keyboard::Key::Named(key::Named::End)
+ if state.keyboard_modifiers.shift()
+ && state.selection != Selection::default() =>
+ {
+ if state.keyboard_modifiers.jump() {
+ state.selection.select_end(&state.paragraph);
+ } else {
+ state.selection.select_line_end(&state.paragraph);
+ }
+
+ shell.capture_event();
+ }
+ keyboard::Key::Named(key::Named::ArrowLeft)
+ if state.keyboard_modifiers.shift()
+ && state.selection != Selection::default() =>
+ {
+ if state.keyboard_modifiers.macos_command() {
+ state.selection.select_line_beginning();
+ } else if state.keyboard_modifiers.jump() {
+ state
+ .selection
+ .select_left_by_words(&state.paragraph);
+ } else {
+ state.selection.select_left(&state.paragraph);
+ }
+
+ shell.capture_event();
+ }
+ keyboard::Key::Named(key::Named::ArrowRight)
+ if state.keyboard_modifiers.shift()
+ && state.selection != Selection::default() =>
+ {
+ if state.keyboard_modifiers.macos_command() {
+ state.selection.select_line_end(&state.paragraph);
+ } else if state.keyboard_modifiers.jump() {
+ state
+ .selection
+ .select_right_by_words(&state.paragraph);
+ } else {
+ state.selection.select_right(&state.paragraph);
+ }
+
+ shell.capture_event();
+ }
+ keyboard::Key::Named(key::Named::ArrowUp)
+ if state.keyboard_modifiers.shift()
+ && state.selection != Selection::default() =>
+ {
+ if state.keyboard_modifiers.macos_command() {
+ state.selection.select_beginning();
+ } else if state.keyboard_modifiers.jump() {
+ state.selection.select_line_beginning();
+ } else {
+ state.selection.select_up(&state.paragraph);
+ }
+
+ shell.capture_event();
+ }
+ keyboard::Key::Named(key::Named::ArrowDown)
+ if state.keyboard_modifiers.shift()
+ && state.selection != Selection::default() =>
+ {
+ if state.keyboard_modifiers.macos_command() {
+ state.selection.select_end(&state.paragraph);
+ } else if state.keyboard_modifiers.jump() {
+ state.selection.select_line_end(&state.paragraph);
+ } else {
+ state.selection.select_down(&state.paragraph);
+ }
+
+ shell.capture_event();
+ }
+ keyboard::Key::Named(key::Named::Escape) => {
+ state.is_dragging = false;
+ state.selection = Selection::default();
+
+ state.keyboard_modifiers =
+ keyboard::Modifiers::default();
+
+ if state.selection != selection_before {
+ shell.capture_event();
+ }
+ }
+ _ => {}
+ }
+ }
+ Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
+ state.keyboard_modifiers = *modifiers;
+ }
+ _ => {}
+ }
+
+ if state.is_hovered != was_hovered
+ || state.selection != selection_before
+ || self.hovered_link.is_some() != link_was_hovered
+ {
+ shell.request_redraw();
+ }
+ }
+
+ fn mouse_interaction(
+ &self,
+ _tree: &Tree,
+ layout: Layout<'_>,
+ cursor: mouse::Cursor,
+ _viewport: &Rectangle,
+ _renderer: &Renderer,
+ ) -> mouse::Interaction {
+ if self.hovered_link.is_some() {
+ mouse::Interaction::Pointer
+ } else if cursor.is_over(layout.bounds()) {
+ mouse::Interaction::Text
+ } else {
+ mouse::Interaction::None
+ }
+ }
+}
+
+#[allow(clippy::too_many_arguments)]
+fn layout<Link, Renderer>(
+ state: &mut State<Link>,
+ renderer: &Renderer,
+ limits: &layout::Limits,
+ width: Length,
+ height: Length,
+ spans: &[Span<'_, Link, Renderer::Font>],
+ line_height: LineHeight,
+ size: Option<Pixels>,
+ font: Option<Renderer::Font>,
+ align_x: Alignment,
+ align_y: alignment::Vertical,
+ wrapping: Wrapping,
+) -> layout::Node
+where
+ Link: Clone,
+ Renderer: core::text::Renderer<Paragraph = Paragraph, Font = Font>,
+{
+ layout::sized(limits, width, height, |limits| {
+ let bounds = limits.max();
+
+ let size = size.unwrap_or_else(|| renderer.default_size());
+ let font = font.unwrap_or_else(|| renderer.default_font());
+
+ let text_with_spans = || core::Text {
+ content: spans,
+ bounds,
+ size,
+ line_height,
+ font,
+ align_x,
+ align_y,
+ shaping: Shaping::Advanced,
+ wrapping,
+ };
+
+ if state.spans != spans {
+ state.paragraph =
+ Renderer::Paragraph::with_spans(text_with_spans());
+ state.spans = spans.iter().cloned().map(Span::to_static).collect();
+ } else {
+ match state.paragraph.compare(core::Text {
+ content: (),
+ bounds,
+ size,
+ line_height,
+ font,
+ align_x,
+ align_y,
+ shaping: Shaping::Advanced,
+ wrapping,
+ }) {
+ core::text::Difference::None => {}
+ core::text::Difference::Bounds => {
+ state.paragraph.resize(bounds);
+ }
+ core::text::Difference::Shape => {
+ state.paragraph =
+ Renderer::Paragraph::with_spans(text_with_spans());
+ }
+ }
+ }
+
+ state.paragraph.min_bounds()
+ })
+}
+
+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,
+ Renderer: core::text::Renderer<Paragraph = Paragraph, Font = Font>,
+ Renderer::Font: 'a,
+{
+ fn from_iter<T: IntoIterator<Item = Span<'a, Link, Renderer::Font>>>(
+ spans: T,
+ ) -> Self {
+ Self::with_spans(spans.into_iter().collect::<Vec<_>>())
+ }
+}
+
+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<Paragraph = Paragraph, Font = Font> + 'a,
+{
+ fn from(
+ text: Rich<'a, Link, Message, Theme, Renderer>,
+ ) -> Element<'a, Message, Theme, Renderer> {
+ Element::new(text)
+ }
+}