Skip to content
Draft
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
3 changes: 1 addition & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,6 @@ useless_conversion = "deny"

[workspace.lints.rustdoc]
broken_intra_doc_links = "forbid"

[patch.crates-io]
cosmic-text = { git = "https://github.com/airstrike/cosmic-text", branch = "decouple-decoration-from-shaping" }
13 changes: 13 additions & 0 deletions core/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ use crate::{Background, Border, Color, Padding, Pixels, Point, Rectangle, Size};
use std::borrow::Cow;
use std::hash::{Hash, Hasher};

/// Which decoration line a rectangle represents, when a [`Paragraph`] hands
/// back decoration bounds. Lets a widget draw underlines and strikethroughs
/// in different colors, or opt out of a kind.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Decoration {
/// A line below the glyphs; two rectangles for a double underline.
Underline,
/// A line through the middle of the glyphs.
Strikethrough,
/// A line above the glyphs.
Overline,
}

/// A paragraph.
#[derive(Debug, Clone, Copy)]
pub struct Text<Content = String, Font = crate::Font> {
Expand Down
9 changes: 8 additions & 1 deletion core/src/text/paragraph.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Draw paragraphs.
use crate::alignment;
use crate::text::{
Alignment, Difference, Ellipsis, Hit, LineHeight, Shaping, Span, Text, Wrapping,
Alignment, Decoration, Difference, Ellipsis, Hit, LineHeight, Shaping, Span, Text, Wrapping,
};
use crate::{Pixels, Point, Rectangle, Size};

Expand Down Expand Up @@ -73,6 +73,13 @@ 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>;

/// The bounds of the given `decoration` line for the span at `index`,
/// one rectangle per line the span occupies. Empty by default.
fn decoration_bounds(&self, index: usize, decoration: Decoration) -> Vec<Rectangle> {
let _ = (index, decoration);
Vec::new()
}

/// Returns the minimum width that can fit the contents of the [`Paragraph`].
fn min_width(&self) -> f32 {
self.min_bounds().width
Expand Down
4 changes: 2 additions & 2 deletions examples/tour/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ publish = false

[dependencies]
iced.workspace = true
iced.features = ["image", "debug"]
iced.features = ["image", "debug", "tokio"]

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tracing-subscriber = "0.3"
open = "5"

[target.'cfg(target_arch = "wasm32")'.dependencies]
iced.workspace = true
iced.features = ["image", "debug", "webgl", "fira-sans"]
iced.features = ["image", "debug", "tokio", "webgl", "fira-sans"]

console_error_panic_hook = "0.1"
console_log = "1.0"
362 changes: 359 additions & 3 deletions examples/tour/src/main.rs

Large diffs are not rendered by default.

73 changes: 71 additions & 2 deletions graphics/src/text/paragraph.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
//! Draw paragraphs.
use crate::core;
use crate::core::alignment;
use crate::core::text::{Alignment, Ellipsis, Hit, LineHeight, Shaping, Span, Text, Wrapping};
use crate::core::text::{
Alignment, Decoration, Ellipsis, Hit, LineHeight, Shaping, Span, Text, Wrapping,
};
use crate::core::{Font, Pixels, Point, Rectangle, Size};
use crate::text;

Expand Down Expand Up @@ -177,7 +179,13 @@ impl core::text::Paragraph for Paragraph {
attrs
};

(span.text.as_ref(), attrs.metadata(i))
let mut attrs = attrs.metadata(i);
if span.underline || span.link.is_some() {
attrs.text_decoration.underline = cosmic_text::UnderlineStyle::Single;
}
attrs.text_decoration.strikethrough = span.strikethrough;

(span.text.as_ref(), attrs)
}),
&text::to_attributes(text.font),
cosmic_text::Shaping::Advanced,
Expand Down Expand Up @@ -426,6 +434,67 @@ impl core::text::Paragraph for Paragraph {
(glyph.y - glyph.y_offset * glyph.font_size) / self.0.hint_factor,
))
}

fn decoration_bounds(&self, index: usize, decoration: Decoration) -> Vec<Rectangle> {
let internal = self.internal();
let scale = 1.0 / self.0.hint_factor;
let mut bounds = Vec::new();

for run in internal.buffer.layout_runs() {
// Each span's glyphs carry their `Span` index as metadata (set in
// `with_spans`), mapping a decoration span to its `Span`.
for span in run.decorations.iter().filter(|span| {
run.glyphs
.get(span.glyph_range.start)
.is_some_and(|glyph| glyph.metadata == index)
}) {
let x_range = span.x_range(&run);
if x_range.is_empty() {
continue;
}

let decorations = span.data.text_decoration;
let font_size = span.font_size;
let mut line = |y: f32, thickness: f32| {
bounds.push(
Rectangle::new(
Point::new(x_range.start, y),
Size::new(x_range.end - x_range.start, thickness.max(1.0)),
) * scale,
);
};

match decoration {
Decoration::Underline
if decorations.underline != cosmic_text::UnderlineStyle::None =>
{
let metrics = span.data.underline_metrics;
let y = run.line_y - metrics.offset * font_size;
let thickness = metrics.thickness * font_size;
line(y, thickness);

if decorations.underline == cosmic_text::UnderlineStyle::Double {
line(y + 2.0 * thickness.max(1.0), thickness);
}
}
Decoration::Strikethrough if decorations.strikethrough => {
let metrics = span.data.strikethrough_metrics;
line(
run.line_y - metrics.offset * font_size,
metrics.thickness * font_size,
);
}
Decoration::Overline if decorations.overline => {
let y = (run.line_y - span.data.ascent * font_size).max(run.line_top);
line(y, span.data.underline_metrics.thickness * font_size);
}
_ => {}
}
}
}

bounds
}
}

impl Default for Paragraph {
Expand Down
52 changes: 18 additions & 34 deletions widget/src/text/rich.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::core::alignment;
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
use crate::core::text::{Paragraph, Span};
use crate::core::text::{Decoration, Paragraph, Span};
use crate::core::widget::text::{
self, Alignment, Catalog, Ellipsis, LineHeight, Shaping, Style, StyleFn, Wrapping,
};
Expand Down Expand Up @@ -268,7 +268,13 @@ where
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;
// Span and decoration bounds are in paragraph coordinates, so
// they need the same anchoring `fill_paragraph` applies.
let translation = layout.bounds().anchor(
state.paragraph.min_bounds(),
state.paragraph.align_x(),
state.paragraph.align_y(),
) - Point::ORIGIN;
let regions = state.paragraph.span_bounds(index);

if let Some(highlight) = span.highlight {
Expand All @@ -290,48 +296,26 @@ where
}

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 {
let mut draw = |decoration| {
for bounds in state.paragraph.decoration_bounds(index, decoration) {
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),
),
bounds: bounds + translation,
..Default::default()
},
color,
);
}
};

if span.underline || is_hovered_link {
draw(Decoration::Underline);
}

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,
);
}
draw(Decoration::Strikethrough);
}
}
}
Expand Down Expand Up @@ -361,7 +345,7 @@ where
return;
};

let was_hovered = self.hovered_link.is_some();
let previous = self.hovered_link;

if let Some(position) = cursor.position_in(layout.bounds()) {
let state = tree
Expand All @@ -379,7 +363,7 @@ where
self.hovered_link = None;
}

if was_hovered != self.hovered_link.is_some() {
if previous != self.hovered_link {
shell.request_redraw();
}

Expand Down
Loading