Implement explicit text caching in the widget state tree
This commit is contained in:
parent
c9bd48704d
commit
ed3454301e
79 changed files with 1910 additions and 1705 deletions
120
graphics/src/text/cache.rs
Normal file
120
graphics/src/text/cache.rs
Normal file
|
|
@ -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;
|
||||
246
graphics/src/text/paragraph.rs
Normal file
246
graphics/src/text/paragraph.rs
Normal file
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue