summaryrefslogtreecommitdiffstats
path: root/graphics
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón Jiménez <hector0193@gmail.com>2023-08-30 04:31:21 +0200
committerLibravatar Héctor Ramón Jiménez <hector0193@gmail.com>2023-08-30 04:31:21 +0200
commited3454301e663a7cb7d73cd56b57b188f4d14a2f (patch)
tree8118d1305c2eba3a1b45d04634cd0e8d050fc0fa /graphics
parentc9bd48704dd9679c033dd0b8588e2744a3df44a0 (diff)
downloadiced-ed3454301e663a7cb7d73cd56b57b188f4d14a2f.tar.gz
iced-ed3454301e663a7cb7d73cd56b57b188f4d14a2f.tar.bz2
iced-ed3454301e663a7cb7d73cd56b57b188f4d14a2f.zip
Implement explicit text caching in the widget state tree
Diffstat (limited to '')
-rw-r--r--graphics/Cargo.toml11
-rw-r--r--graphics/fonts/Iced-Icons.ttf (renamed from tiny_skia/fonts/Iced-Icons.ttf)bin5108 -> 5108 bytes
-rw-r--r--graphics/src/backend.rs69
-rw-r--r--graphics/src/damage.rs26
-rw-r--r--graphics/src/geometry/text.rs6
-rw-r--r--graphics/src/lib.rs3
-rw-r--r--graphics/src/primitive.rs14
-rw-r--r--graphics/src/renderer.rs125
-rw-r--r--graphics/src/text.rs113
-rw-r--r--graphics/src/text/cache.rs120
-rw-r--r--graphics/src/text/paragraph.rs246
11 files changed, 596 insertions, 137 deletions
diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml
index ca7bf61a..442eb007 100644
--- a/graphics/Cargo.toml
+++ b/graphics/Cargo.toml
@@ -23,6 +23,9 @@ log = "0.4"
raw-window-handle = "0.5"
thiserror = "1.0"
bitflags = "1.2"
+cosmic-text = "0.9"
+rustc-hash = "1.1"
+unicode-segmentation = "1.6"
[dependencies.bytemuck]
version = "1.4"
@@ -32,6 +35,14 @@ features = ["derive"]
version = "0.10"
path = "../core"
+[dependencies.twox-hash]
+version = "1.6"
+default-features = false
+
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies.twox-hash]
+version = "1.6.1"
+features = ["std"]
+
[dependencies.image]
version = "0.24"
optional = true
diff --git a/tiny_skia/fonts/Iced-Icons.ttf b/graphics/fonts/Iced-Icons.ttf
index e3273141..e3273141 100644
--- a/tiny_skia/fonts/Iced-Icons.ttf
+++ b/graphics/fonts/Iced-Icons.ttf
Binary files differ
diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs
index 59e95bf8..6774b9ca 100644
--- a/graphics/src/backend.rs
+++ b/graphics/src/backend.rs
@@ -1,8 +1,8 @@
//! Write a graphics backend.
-use iced_core::image;
-use iced_core::svg;
-use iced_core::text;
-use iced_core::{Font, Point, Size};
+use crate::core::image;
+use crate::core::svg;
+use crate::core::Size;
+use crate::text;
use std::borrow::Cow;
@@ -12,70 +12,15 @@ use std::borrow::Cow;
pub trait Backend {
/// The custom kind of primitives this [`Backend`] supports.
type Primitive;
-
- /// Trims the measurements cache.
- ///
- /// This method is currently necessary to properly trim the text cache in
- /// `iced_wgpu` and `iced_glow` because of limitations in the text rendering
- /// pipeline. It will be removed in the future.
- fn trim_measurements(&mut self) {}
}
/// A graphics backend that supports text rendering.
pub trait Text {
- /// The icon font of the backend.
- const ICON_FONT: Font;
-
- /// The `char` representing a ✔ icon in the [`ICON_FONT`].
- ///
- /// [`ICON_FONT`]: Self::ICON_FONT
- const CHECKMARK_ICON: char;
-
- /// The `char` representing a ▼ icon in the built-in [`ICON_FONT`].
- ///
- /// [`ICON_FONT`]: Self::ICON_FONT
- const ARROW_DOWN_ICON: char;
-
- /// Returns the default [`Font`].
- fn default_font(&self) -> Font;
-
- /// Returns the default size of text.
- fn default_size(&self) -> f32;
-
- /// Measures the text contents with the given size and font,
- /// returning the size of a laid out paragraph that fits in the provided
- /// bounds.
- fn measure(
- &self,
- contents: &str,
- size: f32,
- line_height: text::LineHeight,
- font: Font,
- bounds: Size,
- shaping: text::Shaping,
- ) -> Size;
-
- /// Tests whether the provided point is within the boundaries of [`Text`]
- /// laid out with the given parameters, returning information about
- /// the nearest character.
- ///
- /// If nearest_only is true, the hit test does not consider whether the
- /// the point is interior to any glyph bounds, returning only the character
- /// with the nearest centeroid.
- fn hit_test(
- &self,
- contents: &str,
- size: f32,
- line_height: text::LineHeight,
- font: Font,
- bounds: Size,
- shaping: text::Shaping,
- point: Point,
- nearest_only: bool,
- ) -> Option<text::Hit>;
-
/// Loads a [`Font`] from its bytes.
fn load_font(&mut self, font: Cow<'static, [u8]>);
+
+ /// Returns the [`cosmic_text::FontSystem`] of the [`Backend`].
+ fn font_system(&self) -> &text::FontSystem;
}
/// A graphics backend that supports image rendering.
diff --git a/graphics/src/damage.rs b/graphics/src/damage.rs
index 2f29956e..3276c2d4 100644
--- a/graphics/src/damage.rs
+++ b/graphics/src/damage.rs
@@ -40,6 +40,32 @@ impl<T: Damage> Damage for Primitive<T> {
bounds.expand(1.5)
}
+ Self::Paragraph {
+ paragraph,
+ position,
+ ..
+ } => {
+ let mut bounds =
+ Rectangle::new(*position, paragraph.min_bounds);
+
+ bounds.x = match paragraph.horizontal_alignment {
+ alignment::Horizontal::Left => bounds.x,
+ alignment::Horizontal::Center => {
+ bounds.x - bounds.width / 2.0
+ }
+ alignment::Horizontal::Right => bounds.x - bounds.width,
+ };
+
+ bounds.y = match paragraph.vertical_alignment {
+ alignment::Vertical::Top => bounds.y,
+ alignment::Vertical::Center => {
+ bounds.y - bounds.height / 2.0
+ }
+ alignment::Vertical::Bottom => bounds.y - bounds.height,
+ };
+
+ bounds.expand(1.5)
+ }
Self::Quad { bounds, .. }
| Self::Image { bounds, .. }
| Self::Svg { bounds, .. } => bounds.expand(1.0),
diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs
index c584f3cd..0bf7ec97 100644
--- a/graphics/src/geometry/text.rs
+++ b/graphics/src/geometry/text.rs
@@ -1,6 +1,6 @@
use crate::core::alignment;
use crate::core::text::{LineHeight, Shaping};
-use crate::core::{Color, Font, Point};
+use crate::core::{Color, Font, Pixels, Point};
/// A bunch of text that can be drawn to a canvas
#[derive(Debug, Clone)]
@@ -19,7 +19,7 @@ pub struct Text {
/// The color of the text
pub color: Color,
/// The size of the text
- pub size: f32,
+ pub size: Pixels,
/// The line height of the text.
pub line_height: LineHeight,
/// The font of the text
@@ -38,7 +38,7 @@ impl Default for Text {
content: String::new(),
position: Point::ORIGIN,
color: Color::BLACK,
- size: 16.0,
+ size: Pixels(16.0),
line_height: LineHeight::Relative(1.2),
font: Font::default(),
horizontal_alignment: alignment::Horizontal::Left,
diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs
index af374a2f..902eb5b0 100644
--- a/graphics/src/lib.rs
+++ b/graphics/src/lib.rs
@@ -9,7 +9,7 @@
)]
#![deny(
missing_debug_implementations,
- missing_docs,
+ //missing_docs,
unsafe_code,
unused_results,
clippy::extra_unused_lifetimes,
@@ -34,6 +34,7 @@ pub mod damage;
pub mod gradient;
pub mod mesh;
pub mod renderer;
+pub mod text;
#[cfg(feature = "geometry")]
pub mod geometry;
diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs
index 7592a410..cdc8923e 100644
--- a/graphics/src/primitive.rs
+++ b/graphics/src/primitive.rs
@@ -3,7 +3,8 @@ use crate::core::alignment;
use crate::core::image;
use crate::core::svg;
use crate::core::text;
-use crate::core::{Background, Color, Font, Rectangle, Vector};
+use crate::core::{Background, Color, Font, Pixels, Point, Rectangle, Vector};
+use crate::text::paragraph;
use std::sync::Arc;
@@ -19,7 +20,7 @@ pub enum Primitive<T> {
/// The color of the text
color: Color,
/// The size of the text in logical pixels
- size: f32,
+ size: Pixels,
/// The line height of the text
line_height: text::LineHeight,
/// The font of the text
@@ -31,6 +32,15 @@ pub enum Primitive<T> {
/// The shaping strategy of the text.
shaping: text::Shaping,
},
+ /// A paragraph primitive
+ Paragraph {
+ /// The [`Paragraph`].
+ paragraph: paragraph::Weak,
+ /// The position of the [`Paragraph`].
+ position: Point,
+ /// The color of the [`Paragraph`].
+ color: Color,
+ },
/// A quad primitive
Quad {
/// The bounds of the quad
diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs
index c0cec60a..f93f4a6d 100644
--- a/graphics/src/renderer.rs
+++ b/graphics/src/renderer.rs
@@ -1,15 +1,15 @@
//! Create a renderer from a [`Backend`].
use crate::backend::{self, Backend};
-use crate::Primitive;
-
-use iced_core::image;
-use iced_core::layout;
-use iced_core::renderer;
-use iced_core::svg;
-use iced_core::text::{self, Text};
-use iced_core::{
- Background, Color, Element, Font, Point, Rectangle, Size, Vector,
+use crate::core;
+use crate::core::image;
+use crate::core::renderer;
+use crate::core::svg;
+use crate::core::text::Text;
+use crate::core::{
+ Background, Color, Font, Pixels, Point, Rectangle, Size, Vector,
};
+use crate::text;
+use crate::Primitive;
use std::borrow::Cow;
use std::marker::PhantomData;
@@ -18,15 +18,23 @@ use std::marker::PhantomData;
#[derive(Debug)]
pub struct Renderer<B: Backend, Theme> {
backend: B,
+ default_font: Font,
+ default_text_size: Pixels,
primitives: Vec<Primitive<B::Primitive>>,
theme: PhantomData<Theme>,
}
impl<B: Backend, T> Renderer<B, T> {
/// Creates a new [`Renderer`] from the given [`Backend`].
- pub fn new(backend: B) -> Self {
+ pub fn new(
+ backend: B,
+ default_font: Font,
+ default_text_size: Pixels,
+ ) -> Self {
Self {
backend,
+ default_font,
+ default_text_size,
primitives: Vec::new(),
theme: PhantomData,
}
@@ -88,16 +96,6 @@ impl<B: Backend, T> Renderer<B, T> {
impl<B: Backend, T> iced_core::Renderer for Renderer<B, T> {
type Theme = T;
- fn layout<Message>(
- &mut self,
- element: &Element<'_, Message, Self>,
- limits: &layout::Limits,
- ) -> layout::Node {
- self.backend.trim_measurements();
-
- element.as_widget().layout(self, limits)
- }
-
fn with_layer(&mut self, bounds: Rectangle, f: impl FnOnce(&mut Self)) {
let current = self.start_layer();
@@ -137,77 +135,66 @@ impl<B: Backend, T> iced_core::Renderer for Renderer<B, T> {
}
}
-impl<B, T> text::Renderer for Renderer<B, T>
+impl<B, T> core::text::Renderer for Renderer<B, T>
where
B: Backend + backend::Text,
{
type Font = Font;
+ type Paragraph = text::Paragraph;
- const ICON_FONT: Font = B::ICON_FONT;
- const CHECKMARK_ICON: char = B::CHECKMARK_ICON;
- const ARROW_DOWN_ICON: char = B::ARROW_DOWN_ICON;
+ const ICON_FONT: Font = Font::with_name("Iced-Icons");
+ const CHECKMARK_ICON: char = '\u{f00c}';
+ const ARROW_DOWN_ICON: char = '\u{e800}';
fn default_font(&self) -> Self::Font {
- self.backend().default_font()
+ self.default_font
}
- fn default_size(&self) -> f32 {
- self.backend().default_size()
+ fn default_size(&self) -> Pixels {
+ self.default_text_size
}
- fn measure(
- &self,
- content: &str,
- size: f32,
- line_height: text::LineHeight,
- font: Font,
- bounds: Size,
- shaping: text::Shaping,
- ) -> Size {
- self.backend().measure(
- content,
- size,
- line_height,
- font,
- bounds,
- shaping,
- )
+ fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
+ self.backend.load_font(bytes);
+ }
+
+ fn create_paragraph(&self, text: Text<'_, Self::Font>) -> text::Paragraph {
+ text::Paragraph::with_text(text, self.backend.font_system())
}
- fn hit_test(
+ fn resize_paragraph(
&self,
- content: &str,
- size: f32,
- line_height: text::LineHeight,
- font: Font,
- bounds: Size,
- shaping: text::Shaping,
- point: Point,
- nearest_only: bool,
- ) -> Option<text::Hit> {
- self.backend().hit_test(
- content,
- size,
- line_height,
- font,
- bounds,
- shaping,
- point,
- nearest_only,
- )
+ paragraph: &mut Self::Paragraph,
+ new_bounds: Size,
+ ) {
+ paragraph.resize(new_bounds, self.backend.font_system());
}
- fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
- self.backend.load_font(bytes);
+ fn fill_paragraph(
+ &mut self,
+ paragraph: &Self::Paragraph,
+ position: Point,
+ color: Color,
+ ) {
+ self.primitives.push(Primitive::Paragraph {
+ paragraph: paragraph.downgrade(),
+ position,
+ color,
+ });
}
- fn fill_text(&mut self, text: Text<'_, Self::Font>) {
+ fn fill_text(
+ &mut self,
+ text: Text<'_, Self::Font>,
+ position: Point,
+ color: Color,
+ ) {
self.primitives.push(Primitive::Text {
content: text.content.to_string(),
- bounds: text.bounds,
+ bounds: Rectangle::new(position, text.bounds),
size: text.size,
line_height: text.line_height,
- color: text.color,
+ color,
font: text.font,
horizontal_alignment: text.horizontal_alignment,
vertical_alignment: text.vertical_alignment,
diff --git a/graphics/src/text.rs b/graphics/src/text.rs
new file mode 100644
index 00000000..bbe9d7cb
--- /dev/null
+++ b/graphics/src/text.rs
@@ -0,0 +1,113 @@
+pub mod cache;
+pub mod paragraph;
+
+pub use cache::Cache;
+pub use paragraph::Paragraph;
+
+pub use cosmic_text;
+
+use crate::core::font::{self, Font};
+use crate::core::text::Shaping;
+use crate::core::Size;
+
+use std::sync::{self, Arc, RwLock};
+
+#[allow(missing_debug_implementations)]
+pub struct FontSystem(RwLock<cosmic_text::FontSystem>);
+
+impl FontSystem {
+ pub fn new() -> Self {
+ FontSystem(RwLock::new(cosmic_text::FontSystem::new_with_fonts(
+ [cosmic_text::fontdb::Source::Binary(Arc::new(
+ include_bytes!("../fonts/Iced-Icons.ttf").as_slice(),
+ ))]
+ .into_iter(),
+ )))
+ }
+
+ pub fn get_mut(&mut self) -> &mut cosmic_text::FontSystem {
+ self.0.get_mut().expect("Lock font system")
+ }
+
+ pub fn write(&self) -> sync::RwLockWriteGuard<'_, cosmic_text::FontSystem> {
+ self.0.write().expect("Write font system")
+ }
+}
+
+impl Default for FontSystem {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+pub fn measure(buffer: &cosmic_text::Buffer) -> Size {
+ let (width, total_lines) = buffer
+ .layout_runs()
+ .fold((0.0, 0usize), |(width, total_lines), run| {
+ (run.line_w.max(width), total_lines + 1)
+ });
+
+ Size::new(width, total_lines as f32 * buffer.metrics().line_height)
+}
+
+pub fn to_attributes(font: Font) -> cosmic_text::Attrs<'static> {
+ cosmic_text::Attrs::new()
+ .family(to_family(font.family))
+ .weight(to_weight(font.weight))
+ .stretch(to_stretch(font.stretch))
+ .style(to_style(font.style))
+}
+
+fn to_family(family: font::Family) -> cosmic_text::Family<'static> {
+ match family {
+ font::Family::Name(name) => cosmic_text::Family::Name(name),
+ font::Family::SansSerif => cosmic_text::Family::SansSerif,
+ font::Family::Serif => cosmic_text::Family::Serif,
+ font::Family::Cursive => cosmic_text::Family::Cursive,
+ font::Family::Fantasy => cosmic_text::Family::Fantasy,
+ font::Family::Monospace => cosmic_text::Family::Monospace,
+ }
+}
+
+fn to_weight(weight: font::Weight) -> cosmic_text::Weight {
+ match weight {
+ font::Weight::Thin => cosmic_text::Weight::THIN,
+ font::Weight::ExtraLight => cosmic_text::Weight::EXTRA_LIGHT,
+ font::Weight::Light => cosmic_text::Weight::LIGHT,
+ font::Weight::Normal => cosmic_text::Weight::NORMAL,
+ font::Weight::Medium => cosmic_text::Weight::MEDIUM,
+ font::Weight::Semibold => cosmic_text::Weight::SEMIBOLD,
+ font::Weight::Bold => cosmic_text::Weight::BOLD,
+ font::Weight::ExtraBold => cosmic_text::Weight::EXTRA_BOLD,
+ font::Weight::Black => cosmic_text::Weight::BLACK,
+ }
+}
+
+fn to_stretch(stretch: font::Stretch) -> cosmic_text::Stretch {
+ match stretch {
+ font::Stretch::UltraCondensed => cosmic_text::Stretch::UltraCondensed,
+ font::Stretch::ExtraCondensed => cosmic_text::Stretch::ExtraCondensed,
+ font::Stretch::Condensed => cosmic_text::Stretch::Condensed,
+ font::Stretch::SemiCondensed => cosmic_text::Stretch::SemiCondensed,
+ font::Stretch::Normal => cosmic_text::Stretch::Normal,
+ font::Stretch::SemiExpanded => cosmic_text::Stretch::SemiExpanded,
+ font::Stretch::Expanded => cosmic_text::Stretch::Expanded,
+ font::Stretch::ExtraExpanded => cosmic_text::Stretch::ExtraExpanded,
+ font::Stretch::UltraExpanded => cosmic_text::Stretch::UltraExpanded,
+ }
+}
+
+fn to_style(style: font::Style) -> cosmic_text::Style {
+ match style {
+ font::Style::Normal => cosmic_text::Style::Normal,
+ font::Style::Italic => cosmic_text::Style::Italic,
+ font::Style::Oblique => cosmic_text::Style::Oblique,
+ }
+}
+
+pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping {
+ match shaping {
+ Shaping::Basic => cosmic_text::Shaping::Basic,
+ Shaping::Advanced => cosmic_text::Shaping::Advanced,
+ }
+}
diff --git a/graphics/src/text/cache.rs b/graphics/src/text/cache.rs
new file mode 100644
index 00000000..8aea6715
--- /dev/null
+++ b/graphics/src/text/cache.rs
@@ -0,0 +1,120 @@
+use crate::core::{Font, Size};
+use crate::text;
+
+use rustc_hash::{FxHashMap, FxHashSet};
+use std::collections::hash_map;
+use std::hash::{BuildHasher, Hash, Hasher};
+
+#[allow(missing_debug_implementations)]
+#[derive(Default)]
+pub struct Cache {
+ entries: FxHashMap<KeyHash, cosmic_text::Buffer>,
+ aliases: FxHashMap<KeyHash, KeyHash>,
+ recently_used: FxHashSet<KeyHash>,
+ hasher: HashBuilder,
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+type HashBuilder = twox_hash::RandomXxHashBuilder64;
+
+#[cfg(target_arch = "wasm32")]
+type HashBuilder = std::hash::BuildHasherDefault<twox_hash::XxHash64>;
+
+impl Cache {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn get(&self, key: &KeyHash) -> Option<&cosmic_text::Buffer> {
+ self.entries.get(key)
+ }
+
+ pub fn allocate(
+ &mut self,
+ font_system: &mut cosmic_text::FontSystem,
+ key: Key<'_>,
+ ) -> (KeyHash, &mut cosmic_text::Buffer) {
+ let hash = key.hash(self.hasher.build_hasher());
+
+ if let Some(hash) = self.aliases.get(&hash) {
+ let _ = self.recently_used.insert(*hash);
+
+ return (*hash, self.entries.get_mut(hash).unwrap());
+ }
+
+ if let hash_map::Entry::Vacant(entry) = self.entries.entry(hash) {
+ let metrics = cosmic_text::Metrics::new(key.size, key.line_height);
+ let mut buffer = cosmic_text::Buffer::new(font_system, metrics);
+
+ buffer.set_size(
+ font_system,
+ key.bounds.width,
+ key.bounds.height.max(key.line_height),
+ );
+ buffer.set_text(
+ font_system,
+ key.content,
+ text::to_attributes(key.font),
+ text::to_shaping(key.shaping),
+ );
+
+ let bounds = text::measure(&buffer);
+ let _ = entry.insert(buffer);
+
+ for bounds in [
+ bounds,
+ Size {
+ width: key.bounds.width,
+ ..bounds
+ },
+ ] {
+ if key.bounds != bounds {
+ let _ = self.aliases.insert(
+ Key { bounds, ..key }.hash(self.hasher.build_hasher()),
+ hash,
+ );
+ }
+ }
+ }
+
+ let _ = self.recently_used.insert(hash);
+
+ (hash, self.entries.get_mut(&hash).unwrap())
+ }
+
+ pub fn trim(&mut self) {
+ self.entries
+ .retain(|key, _| self.recently_used.contains(key));
+
+ self.aliases
+ .retain(|_, value| self.recently_used.contains(value));
+
+ self.recently_used.clear();
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct Key<'a> {
+ pub content: &'a str,
+ pub size: f32,
+ pub line_height: f32,
+ pub font: Font,
+ pub bounds: Size,
+ pub shaping: text::Shaping,
+}
+
+impl Key<'_> {
+ fn hash<H: Hasher>(self, mut hasher: H) -> KeyHash {
+ self.content.hash(&mut hasher);
+ self.size.to_bits().hash(&mut hasher);
+ self.line_height.to_bits().hash(&mut hasher);
+ self.font.hash(&mut hasher);
+ self.bounds.width.to_bits().hash(&mut hasher);
+ self.bounds.height.to_bits().hash(&mut hasher);
+ self.shaping.hash(&mut hasher);
+
+ hasher.finish()
+ }
+}
+
+pub type KeyHash = u64;
diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs
new file mode 100644
index 00000000..7b70376a
--- /dev/null
+++ b/graphics/src/text/paragraph.rs
@@ -0,0 +1,246 @@
+use crate::core;
+use crate::core::alignment;
+use crate::core::text::{Hit, LineHeight, Shaping, Text};
+use crate::core::{Font, Pixels, Point, Size};
+use crate::text::{self, FontSystem};
+
+use std::fmt;
+use std::sync::{self, Arc};
+
+#[derive(Clone, PartialEq, Default)]
+pub struct Paragraph(Arc<Internal>);
+
+struct Internal {
+ buffer: cosmic_text::Buffer,
+ content: String, // TODO: Reuse from `buffer` (?)
+ font: Font,
+ shaping: Shaping,
+ horizontal_alignment: alignment::Horizontal,
+ vertical_alignment: alignment::Vertical,
+ bounds: Size,
+ min_bounds: Size,
+}
+
+impl Paragraph {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn with_text(text: Text<'_, Font>, font_system: &FontSystem) -> Self {
+ let mut font_system = font_system.write();
+
+ let mut buffer = cosmic_text::Buffer::new(
+ &mut font_system,
+ cosmic_text::Metrics::new(
+ text.size.into(),
+ text.line_height.to_absolute(text.size).into(),
+ ),
+ );
+
+ buffer.set_size(
+ &mut font_system,
+ text.bounds.width,
+ text.bounds.height,
+ );
+
+ buffer.set_text(
+ &mut font_system,
+ text.content,
+ text::to_attributes(text.font),
+ text::to_shaping(text.shaping),
+ );
+
+ let min_bounds = text::measure(&buffer);
+
+ Self(Arc::new(Internal {
+ buffer,
+ content: text.content.to_owned(),
+ font: text.font,
+ horizontal_alignment: text.horizontal_alignment,
+ vertical_alignment: text.vertical_alignment,
+ shaping: text.shaping,
+ bounds: text.bounds,
+ min_bounds,
+ }))
+ }
+
+ pub fn buffer(&self) -> &cosmic_text::Buffer {
+ &self.0.buffer
+ }
+
+ pub fn downgrade(&self) -> Weak {
+ Weak {
+ raw: Arc::downgrade(&self.0),
+ min_bounds: self.0.min_bounds,
+ horizontal_alignment: self.0.horizontal_alignment,
+ vertical_alignment: self.0.vertical_alignment,
+ }
+ }
+
+ pub fn resize(&mut self, new_bounds: Size, font_system: &FontSystem) {
+ if let Some(internal) = Arc::get_mut(&mut self.0) {
+ // If there is no strong reference holding on to the paragraph, we
+ // resize the buffer in-place
+ internal.buffer.set_size(
+ &mut font_system.write(),
+ new_bounds.width,
+ new_bounds.height,
+ );
+
+ internal.bounds = new_bounds;
+ internal.min_bounds = text::measure(&internal.buffer);
+ } else {
+ let metrics = self.0.buffer.metrics();
+
+ // If there is a strong reference somewhere, we recompute the buffer
+ // from scratch
+ *self = Self::with_text(
+ Text {
+ content: &self.0.content,
+ bounds: self.0.bounds,
+ size: Pixels(metrics.font_size),
+ line_height: LineHeight::Absolute(Pixels(
+ metrics.line_height,
+ )),
+ font: self.0.font,
+ horizontal_alignment: self.0.horizontal_alignment,
+ vertical_alignment: self.0.vertical_alignment,
+ shaping: self.0.shaping,
+ },
+ font_system,
+ );
+ }
+ }
+}
+
+impl core::text::Paragraph for Paragraph {
+ type Font = Font;
+
+ fn content(&self) -> &str {
+ &self.0.content
+ }
+
+ fn text_size(&self) -> Pixels {
+ Pixels(self.0.buffer.metrics().font_size)
+ }
+
+ fn line_height(&self) -> LineHeight {
+ LineHeight::Absolute(Pixels(self.0.buffer.metrics().line_height))
+ }
+
+ fn font(&self) -> Font {
+ self.0.font
+ }
+
+ fn shaping(&self) -> Shaping {
+ self.0.shaping
+ }
+
+ fn horizontal_alignment(&self) -> alignment::Horizontal {
+ self.0.horizontal_alignment
+ }
+
+ fn vertical_alignment(&self) -> alignment::Vertical {
+ self.0.vertical_alignment
+ }
+
+ fn bounds(&self) -> Size {
+ self.0.bounds
+ }
+
+ fn min_bounds(&self) -> Size {
+ self.0.min_bounds
+ }
+
+ fn hit_test(&self, point: Point) -> Option<Hit> {
+ let cursor = self.0.buffer.hit(point.x, point.y)?;
+
+ Some(Hit::CharOffset(cursor.index))
+ }
+
+ fn grapheme_position(&self, line: usize, index: usize) -> Option<Point> {
+ let run = self.0.buffer.layout_runs().nth(line)?;
+
+ // TODO: Index represents a grapheme, not a glyph
+ let glyph = run.glyphs.get(index).or_else(|| run.glyphs.last())?;
+
+ let advance_last = if index == run.glyphs.len() {
+ glyph.w
+ } else {
+ 0.0
+ };
+
+ Some(Point::new(
+ glyph.x + glyph.x_offset * glyph.font_size + advance_last,
+ glyph.y - glyph.y_offset * glyph.font_size,
+ ))
+ }
+}
+
+impl PartialEq for Internal {
+ fn eq(&self, other: &Self) -> bool {
+ self.content == other.content
+ && self.font == other.font
+ && self.shaping == other.shaping
+ && self.horizontal_alignment == other.horizontal_alignment
+ && self.vertical_alignment == other.vertical_alignment
+ && self.bounds == other.bounds
+ && self.min_bounds == other.min_bounds
+ && self.buffer.metrics() == other.buffer.metrics()
+ }
+}
+
+impl Default for Internal {
+ fn default() -> Self {
+ Self {
+ buffer: cosmic_text::Buffer::new_empty(cosmic_text::Metrics {
+ font_size: 1.0,
+ line_height: 1.0,
+ }),
+ content: String::new(),
+ font: Font::default(),
+ shaping: Shaping::default(),
+ horizontal_alignment: alignment::Horizontal::Left,
+ vertical_alignment: alignment::Vertical::Top,
+ bounds: Size::ZERO,
+ min_bounds: Size::ZERO,
+ }
+ }
+}
+
+impl fmt::Debug for Paragraph {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("Paragraph")
+ .field("content", &self.0.content)
+ .field("font", &self.0.font)
+ .field("shaping", &self.0.shaping)
+ .field("horizontal_alignment", &self.0.horizontal_alignment)
+ .field("vertical_alignment", &self.0.vertical_alignment)
+ .field("bounds", &self.0.bounds)
+ .field("min_bounds", &self.0.min_bounds)
+ .finish()
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct Weak {
+ raw: sync::Weak<Internal>,
+ pub min_bounds: Size,
+ pub horizontal_alignment: alignment::Horizontal,
+ pub vertical_alignment: alignment::Vertical,
+}
+
+impl Weak {
+ pub fn upgrade(&self) -> Option<Paragraph> {
+ self.raw.upgrade().map(Paragraph)
+ }
+}
+
+impl PartialEq for Weak {
+ fn eq(&self, other: &Self) -> bool {
+ match (self.raw.upgrade(), other.raw.upgrade()) {
+ (Some(p1), Some(p2)) => p1 == p2,
+ _ => false,
+ }
+ }
+}