Merge pull request #2512 from iced-rs/feature/rich-text-links
Add `Link` support to `rich_text` widget
This commit is contained in:
commit
5443e4d828
9 changed files with 284 additions and 79 deletions
|
|
@ -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 () {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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_in_background(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)
|
||||||
|
|
|
||||||
|
|
@ -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,18 +122,8 @@ 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 = text::to_attributes(span.font.unwrap_or(text.font));
|
||||||
|
|
||||||
let attrs = if let Some(font) = span.font {
|
|
||||||
attrs
|
|
||||||
.family(text::to_family(font.family))
|
|
||||||
.weight(text::to_weight(font.weight))
|
|
||||||
.stretch(text::to_stretch(font.stretch))
|
|
||||||
.style(text::to_style(font.style))
|
|
||||||
} else {
|
|
||||||
text::to_attributes(text.font)
|
|
||||||
};
|
|
||||||
|
|
||||||
let attrs = match (span.size, span.line_height) {
|
let attrs = match (span.size, span.line_height) {
|
||||||
(None, None) => attrs,
|
(None, None) => attrs,
|
||||||
|
|
@ -156,7 +146,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 +221,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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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: 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(on_link))
|
||||||
.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(on_link).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)].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)]
|
||||||
.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(on_link),
|
||||||
.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))
|
||||||
|
|
|
||||||
|
|
@ -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: 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: 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,12 @@ 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(mut self, on_link: impl Fn(Link) -> Message + 'a) -> Self {
|
||||||
|
self.on_link = Some(Box::new(on_link));
|
||||||
|
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 +170,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 +191,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 +230,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 +255,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 +270,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.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) = self
|
||||||
|
.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.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| self.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 +377,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 +427,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 +445,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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue