use super::*;
use crate::{ParagraphRequest, PlatformParagraphLayout, PlatformParagraphShape};
#[derive(Clone, Default)]
pub(super) struct ParagraphIndex {
shapes: usize,
layouts: usize,
}
#[derive(Default)]
pub(super) struct ParagraphFrameCache {
shapes: FxHashMap<Arc<CacheKey>, Arc<dyn PlatformParagraphShape>>,
layouts: FxHashMap<Arc<CacheKey>, Arc<dyn PlatformParagraphLayout>>,
used_shapes: Vec<Arc<CacheKey>>,
used_layouts: Vec<Arc<CacheKey>>,
}
impl ParagraphFrameCache {
pub fn index(&self) -> ParagraphIndex {
ParagraphIndex {
shapes: self.used_shapes.len(),
layouts: self.used_layouts.len(),
}
}
pub fn truncate(&mut self, index: ParagraphIndex) {
self.used_shapes.truncate(index.shapes);
self.used_layouts.truncate(index.layouts);
}
pub fn clear(&mut self) {
self.shapes.clear();
self.layouts.clear();
self.used_shapes.clear();
self.used_layouts.clear();
}
pub fn reuse_from(&mut self, previous: &mut Self, range: Range<ParagraphIndex>) {
for key in &previous.used_shapes[range.start.shapes..range.end.shapes] {
if let Some((key, shape)) = previous.shapes.remove_entry(key) {
self.shapes.insert(key, shape);
}
self.used_shapes.push(key.clone());
}
for key in &previous.used_layouts[range.start.layouts..range.end.layouts] {
if let Some((key, layout)) = previous.layouts.remove_entry(key) {
self.layouts.insert(key, layout);
}
self.used_layouts.push(key.clone());
}
}
}
impl LineLayoutCache {
pub fn layout_paragraph(
&self,
text: SharedString,
font_size: Pixels,
runs: &[FontRun],
width: Pixels,
) -> anyhow::Result<Arc<dyn PlatformParagraphLayout>> {
anyhow::ensure!(
f32::from(font_size).is_finite() && font_size > px(0.0),
"invalid font size"
);
anyhow::ensure!(
f32::from(width).is_finite() && width >= px(0.0),
"invalid paragraph width"
);
let mut end = 0usize;
for run in runs {
end = end
.checked_add(run.len)
.ok_or_else(|| anyhow::anyhow!("font run overflow"))?;
anyhow::ensure!(
text.is_char_boundary(end),
"invalid paragraph font run range"
);
}
anyhow::ensure!(
runs.is_empty() || end == text.len(),
"font runs must cover paragraph"
);
let shape_ref = &CacheKeyRef {
text: text.as_ref(),
font_size,
runs,
wrap_width: None,
force_width: None,
} as &dyn AsCacheKeyRef;
let layout_ref = &CacheKeyRef {
text: text.as_ref(),
font_size,
runs,
wrap_width: Some(width),
force_width: None,
} as &dyn AsCacheKeyRef;
loop {
let generation = self.clear_if_font_generation_changed();
let (shape, layout) = {
let current = self.current_frame.read();
let previous = self.previous_frame.lock();
let shape = current
.paragraphs
.shapes
.get_key_value(shape_ref)
.map(|(key, value)| (key.clone(), value.clone()))
.or_else(|| {
previous
.paragraphs
.shapes
.get_key_value(shape_ref)
.map(|(key, value)| (key.clone(), value.clone()))
});
let layout = current
.paragraphs
.layouts
.get_key_value(layout_ref)
.map(|(key, value)| (key.clone(), value.clone()))
.or_else(|| {
previous
.paragraphs
.layouts
.get_key_value(layout_ref)
.map(|(key, value)| (key.clone(), value.clone()))
});
(shape, layout)
};
let (shape_key, shape) = match shape {
Some(entry) => entry,
None => {
let shape = self
.platform_text_system
.shape_paragraph(ParagraphRequest {
text: text.as_ref(),
font_size,
runs,
})?;
(
Arc::new(CacheKey {
text: text.clone(),
font_size,
runs: SmallVec::from(runs),
wrap_width: None,
force_width: None,
}),
shape,
)
}
};
let (layout_key, layout) = match layout {
Some(entry) => entry,
None => {
let layout = shape.layout(width)?;
(
Arc::new(CacheKey {
text: text.clone(),
font_size,
runs: SmallVec::from(runs),
wrap_width: Some(width),
force_width: None,
}),
layout,
)
}
};
let mut current = self.current_frame.write();
if self.font_generation.load(Ordering::Acquire) != generation
|| self.cached_font_generation.load(Ordering::Acquire) != generation
{
continue;
}
current
.paragraphs
.shapes
.entry(shape_key.clone())
.or_insert(shape);
let layout = current
.paragraphs
.layouts
.entry(layout_key.clone())
.or_insert(layout)
.clone();
current.paragraphs.used_shapes.push(shape_key.clone());
current.paragraphs.used_layouts.push(layout_key.clone());
return Ok(layout);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Bounds, NoopTextSystem, ParagraphPosition, Point, Size, VisualDirection, size};
struct Probe;
impl PlatformParagraphShape for Probe {
fn layout(&self, width: Pixels) -> anyhow::Result<Arc<dyn PlatformParagraphLayout>> {
anyhow::ensure!(width != px(200.0), "unsupported probe reflow");
Ok(Arc::new(Probe))
}
}
impl PlatformParagraphLayout for Probe {
fn source(&self) -> &str {
"probe"
}
fn font_size(&self) -> Pixels {
px(16.0)
}
fn size(&self) -> Size<Pixels> {
size(px(100.0), px(20.0))
}
fn runs(&self) -> &[crate::ShapedRun] {
&[]
}
fn hit_test(&self, _: Point<Pixels>) -> anyhow::Result<ParagraphPosition> {
anyhow::bail!("unused")
}
fn caret_bounds(&self, _: ParagraphPosition) -> anyhow::Result<Bounds<Pixels>> {
anyhow::bail!("unused")
}
fn move_visual(
&self,
_: ParagraphPosition,
_: VisualDirection,
) -> anyhow::Result<ParagraphPosition> {
anyhow::bail!("unused")
}
fn selection_bounds(
&self,
_: ParagraphPosition,
_: ParagraphPosition,
) -> anyhow::Result<Vec<Bounds<Pixels>>> {
anyhow::bail!("unused")
}
}
#[test]
fn paragraph_frame_ranges_retain_both_levels_and_obey_generation() {
let generation = Arc::new(AtomicUsize::new(0));
let cache = LineLayoutCache::new(Arc::new(NoopTextSystem), generation.clone());
let start = cache.layout_index();
let shape_key = Arc::new(CacheKey {
text: "probe".into(),
font_size: px(16.0),
runs: SmallVec::new(),
wrap_width: None,
force_width: None,
});
let layout_key = Arc::new(CacheKey {
text: "probe".into(),
font_size: px(16.0),
runs: SmallVec::new(),
wrap_width: Some(px(100.0)),
force_width: None,
});
let layout: Arc<dyn PlatformParagraphLayout> = Arc::new(Probe);
{
let mut frame = cache.current_frame.write();
frame
.paragraphs
.shapes
.insert(shape_key.clone(), Arc::new(Probe));
frame
.paragraphs
.layouts
.insert(layout_key.clone(), layout.clone());
frame.paragraphs.used_shapes.push(shape_key.clone());
frame.paragraphs.used_layouts.push(layout_key.clone());
}
let end = cache.layout_index();
cache.truncate_layouts(start.clone());
assert_eq!(cache.current_frame.read().paragraphs.index().layouts, 0);
cache
.current_frame
.write()
.paragraphs
.used_shapes
.push(shape_key);
cache
.current_frame
.write()
.paragraphs
.used_layouts
.push(layout_key);
cache.finish_frame();
cache.reuse_layouts(start.clone()..end.clone());
let cached = cache
.layout_paragraph("probe".into(), px(16.0), &[], px(100.0))
.unwrap();
assert!(Arc::ptr_eq(&layout, &cached));
cache.finish_frame();
let cached = cache
.layout_paragraph("probe".into(), px(16.0), &[], px(100.0))
.unwrap();
assert!(Arc::ptr_eq(&layout, &cached));
generation.fetch_add(1, Ordering::AcqRel);
cache.reuse_layouts(start..end); assert!(cache.current_frame.read().paragraphs.layouts.is_empty());
assert!(
cache
.layout_paragraph("probe".into(), px(16.0), &[], px(100.0))
.is_err()
);
}
#[test]
fn failed_reflow_preserves_previous_frame_shaping() {
let cache = LineLayoutCache::new(Arc::new(NoopTextSystem), Arc::new(AtomicUsize::new(0)));
let key = Arc::new(CacheKey {
text: "probe".into(),
font_size: px(16.0),
runs: SmallVec::new(),
wrap_width: None,
force_width: None,
});
cache
.current_frame
.write()
.paragraphs
.shapes
.insert(key, Arc::new(Probe));
cache.finish_frame();
assert!(
cache
.layout_paragraph("probe".into(), px(16.0), &[], px(200.0))
.is_err()
);
assert!(
cache
.layout_paragraph("probe".into(), px(16.0), &[], px(100.0))
.is_ok()
);
}
#[test]
fn reflow_runs_without_locks_and_does_not_publish_across_font_registration() {
struct ChangingShape {
cache: std::sync::Weak<LineLayoutCache>,
generation: Arc<AtomicUsize>,
}
impl PlatformParagraphShape for ChangingShape {
fn layout(&self, _: Pixels) -> anyhow::Result<Arc<dyn PlatformParagraphLayout>> {
let cache = self.cache.upgrade().unwrap();
assert!(cache.current_frame.try_write().is_some());
assert!(cache.previous_frame.try_lock().is_some());
self.generation.fetch_add(1, Ordering::AcqRel);
Ok(Arc::new(Probe))
}
}
let generation = Arc::new(AtomicUsize::new(0));
let cache = Arc::new(LineLayoutCache::new(
Arc::new(NoopTextSystem),
generation.clone(),
));
let key = Arc::new(CacheKey {
text: "probe".into(),
font_size: px(16.0),
runs: SmallVec::new(),
wrap_width: None,
force_width: None,
});
cache.current_frame.write().paragraphs.shapes.insert(
key,
Arc::new(ChangingShape {
cache: Arc::downgrade(&cache),
generation,
}),
);
assert!(
cache
.layout_paragraph("probe".into(), px(16.0), &[], px(100.0))
.is_err()
);
assert!(cache.current_frame.read().paragraphs.layouts.is_empty());
assert!(cache.previous_frame.lock().paragraphs.layouts.is_empty());
}
}