Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rich_text and markdown widgets #2508

Merged
merged 7 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ svg = ["iced_widget/svg"]
canvas = ["iced_widget/canvas"]
# Enables the `QRCode` widget
qr_code = ["iced_widget/qr_code"]
# Enables the `markdown` widget
markdown = ["iced_widget/markdown"]
# Enables lazy widgets
lazy = ["iced_widget/lazy"]
# Enables a debug view in native platforms (press F12)
Expand All @@ -51,7 +53,7 @@ web-colors = ["iced_renderer/web-colors"]
# Enables the WebGL backend, replacing WebGPU
webgl = ["iced_renderer/webgl"]
# Enables the syntax `highlighter` module
highlighter = ["iced_highlighter"]
highlighter = ["iced_highlighter", "iced_widget/highlighter"]
# Enables experimental multi-window support.
multi-window = ["iced_winit/multi-window"]
# Enables the advanced module
Expand Down Expand Up @@ -155,6 +157,7 @@ num-traits = "0.2"
once_cell = "1.0"
ouroboros = "0.18"
palette = "0.7"
pulldown-cmark = "0.11"
qrcode = { version = "0.13", default-features = false }
raw-window-handle = "0.6"
resvg = "0.42"
Expand Down
7 changes: 6 additions & 1 deletion core/src/renderer/null.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,14 @@ impl text::Paragraph for () {

fn with_text(_text: Text<&str>) -> Self {}

fn with_spans(
_text: Text<&[text::Span<'_, Self::Font>], Self::Font>,
) -> Self {
}

fn resize(&mut self, _new_bounds: Size) {}

fn compare(&self, _text: Text<&str>) -> text::Difference {
fn compare(&self, _text: Text<()>) -> text::Difference {
text::Difference::None
}

Expand Down
163 changes: 161 additions & 2 deletions core/src/text.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
//! Draw and interact with text.
mod paragraph;

pub mod editor;
pub mod highlighter;
pub mod paragraph;

pub use editor::Editor;
pub use highlighter::Highlighter;
Expand All @@ -11,6 +10,7 @@ pub use paragraph::Paragraph;
use crate::alignment;
use crate::{Color, Pixels, Point, Rectangle, Size};

use std::borrow::Cow;
use std::hash::{Hash, Hasher};

/// A paragraph.
Expand Down Expand Up @@ -221,3 +221,162 @@ pub trait Renderer: crate::Renderer {
clip_bounds: Rectangle,
);
}

/// A span of text.
#[derive(Debug, Clone, PartialEq)]
pub struct Span<'a, Font = crate::Font> {
/// The [`Fragment`] of text.
pub text: Fragment<'a>,
/// The size of the [`Span`] in [`Pixels`].
pub size: Option<Pixels>,
/// The [`LineHeight`] of the [`Span`].
pub line_height: Option<LineHeight>,
/// The font of the [`Span`].
pub font: Option<Font>,
/// The [`Color`] of the [`Span`].
pub color: Option<Color>,
hecrj marked this conversation as resolved.
Show resolved Hide resolved
}

impl<'a, Font> Span<'a, Font> {
/// Creates a new [`Span`] of text with the given text fragment.
pub fn new(fragment: impl IntoFragment<'a>) -> Self {
Self {
text: fragment.into_fragment(),
size: None,
line_height: None,
font: None,
color: None,
}
}

/// Sets the size of the [`Span`].
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
self.size = Some(size.into());
self
}

/// Sets the [`LineHeight`] of the [`Span`].
pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
self.line_height = Some(line_height.into());
self
}

/// Sets the font of the [`Span`].
pub fn font(mut self, font: impl Into<Font>) -> Self {
self.font = Some(font.into());
self
}

/// Sets the font of the [`Span`], if any.
pub fn font_maybe(mut self, font: Option<impl Into<Font>>) -> Self {
self.font = font.map(Into::into);
self
}

/// Sets the [`Color`] of the [`Span`].
pub fn color(mut self, color: impl Into<Color>) -> Self {
self.color = Some(color.into());
self
}

/// Sets the [`Color`] of the [`Span`], if any.
pub fn color_maybe(mut self, color: Option<impl Into<Color>>) -> Self {
self.color = color.map(Into::into);
self
}

/// Turns the [`Span`] into a static one.
pub fn to_static(self) -> Span<'static, Font> {
Span {
text: Cow::Owned(self.text.into_owned()),
size: self.size,
line_height: self.line_height,
font: self.font,
color: self.color,
}
}
}

impl<'a, Font> From<&'a str> for Span<'a, Font> {
fn from(value: &'a str) -> Self {
Span::new(value)
}
}

/// A fragment of [`Text`].
///
/// This is just an alias to a string that may be either
/// borrowed or owned.
pub type Fragment<'a> = Cow<'a, str>;

/// A trait for converting a value to some text [`Fragment`].
pub trait IntoFragment<'a> {
/// Converts the value to some text [`Fragment`].
fn into_fragment(self) -> Fragment<'a>;
}

impl<'a> IntoFragment<'a> for Fragment<'a> {
fn into_fragment(self) -> Fragment<'a> {
self
}
}

impl<'a, 'b> IntoFragment<'a> for &'a Fragment<'b> {
fn into_fragment(self) -> Fragment<'a> {
Fragment::Borrowed(self)
}
}

impl<'a> IntoFragment<'a> for &'a str {
fn into_fragment(self) -> Fragment<'a> {
Fragment::Borrowed(self)
}
}

impl<'a> IntoFragment<'a> for &'a String {
fn into_fragment(self) -> Fragment<'a> {
Fragment::Borrowed(self.as_str())
}
}

impl<'a> IntoFragment<'a> for String {
fn into_fragment(self) -> Fragment<'a> {
Fragment::Owned(self)
}
}

macro_rules! into_fragment {
($type:ty) => {
impl<'a> IntoFragment<'a> for $type {
fn into_fragment(self) -> Fragment<'a> {
Fragment::Owned(self.to_string())
}
}

impl<'a> IntoFragment<'a> for &$type {
fn into_fragment(self) -> Fragment<'a> {
Fragment::Owned(self.to_string())
}
}
};
}

into_fragment!(char);
into_fragment!(bool);

into_fragment!(u8);
into_fragment!(u16);
into_fragment!(u32);
into_fragment!(u64);
into_fragment!(u128);
into_fragment!(usize);

into_fragment!(i8);
into_fragment!(i16);
into_fragment!(i32);
into_fragment!(i64);
into_fragment!(i128);
into_fragment!(isize);

into_fragment!(f32);
into_fragment!(f64);
91 changes: 78 additions & 13 deletions core/src/text/paragraph.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Draw paragraphs.
use crate::alignment;
use crate::text::{Difference, Hit, Text};
use crate::text::{Difference, Hit, Span, Text};
use crate::{Point, Size};

/// A text paragraph.
Expand All @@ -10,12 +11,15 @@ pub trait Paragraph: Sized + Default {
/// Creates a new [`Paragraph`] laid out with the given [`Text`].
fn with_text(text: Text<&str, Self::Font>) -> Self;

/// Creates a new [`Paragraph`] laid out with the given [`Text`].
fn with_spans(text: Text<&[Span<'_, Self::Font>], Self::Font>) -> Self;

/// Lays out the [`Paragraph`] with some new boundaries.
fn resize(&mut self, new_bounds: Size);

/// Compares the [`Paragraph`] with some desired [`Text`] and returns the
/// [`Difference`].
fn compare(&self, text: Text<&str, Self::Font>) -> Difference;
fn compare(&self, text: Text<(), Self::Font>) -> Difference;

/// Returns the horizontal alignment of the [`Paragraph`].
fn horizontal_alignment(&self) -> alignment::Horizontal;
Expand All @@ -34,26 +38,87 @@ pub trait Paragraph: Sized + Default {
/// Returns the distance to the given grapheme index in the [`Paragraph`].
fn grapheme_position(&self, line: usize, index: usize) -> Option<Point>;

/// Updates the [`Paragraph`] to match the given [`Text`], if needed.
fn update(&mut self, text: Text<&str, Self::Font>) {
match self.compare(text) {
/// Returns the minimum width that can fit the contents of the [`Paragraph`].
fn min_width(&self) -> f32 {
self.min_bounds().width
}

/// Returns the minimum height that can fit the contents of the [`Paragraph`].
fn min_height(&self) -> f32 {
self.min_bounds().height
}
}

/// A [`Paragraph`] of plain text.
#[derive(Debug, Clone, Default)]
pub struct Plain<P: Paragraph> {
raw: P,
content: String,
}

impl<P: Paragraph> Plain<P> {
/// Creates a new [`Plain`] paragraph.
pub fn new(text: Text<&str, P::Font>) -> Self {
let content = text.content.to_owned();

Self {
raw: P::with_text(text),
content,
}
}

/// Updates the plain [`Paragraph`] to match the given [`Text`], if needed.
pub fn update(&mut self, text: Text<&str, P::Font>) {
if self.content != text.content {
text.content.clone_into(&mut self.content);
self.raw = P::with_text(text);
return;
}

match self.raw.compare(Text {
content: (),
bounds: text.bounds,
size: text.size,
line_height: text.line_height,
font: text.font,
horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment,
shaping: text.shaping,
}) {
Difference::None => {}
Difference::Bounds => {
self.resize(text.bounds);
self.raw.resize(text.bounds);
}
Difference::Shape => {
*self = Self::with_text(text);
self.raw = P::with_text(text);
}
}
}

/// Returns the minimum width that can fit the contents of the [`Paragraph`].
fn min_width(&self) -> f32 {
self.min_bounds().width
/// Returns the horizontal alignment of the [`Paragraph`].
pub fn horizontal_alignment(&self) -> alignment::Horizontal {
self.raw.horizontal_alignment()
}

/// Returns the minimum height that can fit the contents of the [`Paragraph`].
fn min_height(&self) -> f32 {
self.min_bounds().height
/// Returns the vertical alignment of the [`Paragraph`].
pub fn vertical_alignment(&self) -> alignment::Vertical {
self.raw.vertical_alignment()
}

/// Returns the minimum boundaries that can fit the contents of the
/// [`Paragraph`].
pub fn min_bounds(&self) -> Size {
self.raw.min_bounds()
}

/// Returns the minimum width that can fit the contents of the
/// [`Paragraph`].
pub fn min_width(&self) -> f32 {
self.raw.min_width()
}

/// Returns the cached [`Paragraph`].
pub fn raw(&self) -> &P {
&self.raw
}
}
Loading
Loading