use crate::{
Bounds, FontId, GlyphId, InlineBidiScope, InlineBoxRequest, InlineLayoutRequest,
InlineTextMetrics, InlineTextStyle, InlineVisualLine, Pixels, PlatformTextSystem, Point,
SharedString, Size, StrikethroughStyle, TextLayoutOptions, TextLayoutRequest, TextRun,
UnderlineStyle, VerticalAlign,
};
use collections::FxHashMap;
use palette::Hsla;
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
use smallvec::SmallVec;
use std::{
borrow::Borrow,
hash::{Hash, Hasher},
ops::Range,
sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
};
#[derive(Clone, Debug)]
pub struct LineLayout {
pub font_size: Pixels,
pub width: Pixels,
pub ascent: Pixels,
pub descent: Pixels,
pub visual_lines: SmallVec<[VisualLine; 1]>,
pub paint_fragments: Vec<PaintFragment>,
pub len: usize,
pub platform_layout: Arc<dyn PlatformTextLayout>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct PositionedInlineBox {
pub id: u64,
pub line_index: usize,
pub bounds: Bounds<Pixels>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct InlineRangeGeometry {
pub bounds: Bounds<Pixels>,
pub visual_line_index: usize,
}
#[derive(Clone, Debug)]
pub struct InlineLayout {
pub layout: Arc<LineLayout>,
pub lines: Vec<InlineVisualLine>,
pub boxes: Vec<PositionedInlineBox>,
pub alignment_offset: Pixels,
pub size: Size<Pixels>,
}
fn base_inline_line_bounds(metrics: InlineTextMetrics, line_height: Pixels) -> (Pixels, Pixels) {
let half_leading = ((line_height - metrics.ascent - metrics.descent) / 2.).max(Pixels::ZERO);
(
-metrics.ascent - half_leading,
metrics.descent + half_leading,
)
}
fn expand_inline_line_for_box(
top: &mut Pixels,
bottom: &mut Pixels,
top_box_height: &mut Pixels,
bottom_box_height: &mut Pixels,
height: Pixels,
metrics: InlineTextMetrics,
align: VerticalAlign,
) {
match align {
VerticalAlign::Baseline => *top = (*top).min(-height),
VerticalAlign::Middle => {
let middle = -metrics.x_height / 2.;
*top = (*top).min(middle - height / 2.);
*bottom = (*bottom).max(middle + height / 2.);
}
VerticalAlign::Top => *top_box_height = (*top_box_height).max(height),
VerticalAlign::Bottom => *bottom_box_height = (*bottom_box_height).max(height),
}
}
fn aligned_inline_box_y(
line: InlineVisualLine,
metrics: InlineTextMetrics,
height: Pixels,
align: VerticalAlign,
) -> Pixels {
match align {
VerticalAlign::Baseline => line.baseline - height,
VerticalAlign::Middle => line.baseline - metrics.x_height / 2. - height / 2.,
VerticalAlign::Top => Pixels::ZERO,
VerticalAlign::Bottom => line.size.height - height,
}
}
pub fn align_inline_boxes(
lines: &mut [InlineVisualLine],
boxes: &mut [PositionedInlineBox],
size: &mut Size<Pixels>,
requests: &[InlineBoxRequest],
line_metrics: &[InlineTextMetrics],
text_bounds: &[(Pixels, Pixels)],
fallback_metrics: InlineTextMetrics,
line_height: Pixels,
) {
let request_alignments = requests
.iter()
.map(|request| (request.id, request.vertical_align))
.collect::<FxHashMap<_, _>>();
let mut boxes_by_line: FxHashMap<_, Vec<_>> = FxHashMap::default();
for inline_box in boxes
.iter_mut()
.filter(|inline_box| inline_box.line_index < lines.len())
{
let align = request_alignments
.get(&inline_box.id)
.copied()
.unwrap_or(VerticalAlign::Baseline);
boxes_by_line
.entry(inline_box.line_index)
.or_default()
.push((inline_box, align));
}
let mut line_y = Pixels::ZERO;
for (line_index, line) in lines.iter_mut().enumerate() {
let line_boxes = boxes_by_line.remove(&line_index).unwrap_or_default();
let metrics = line_metrics
.get(line_index)
.copied()
.unwrap_or(fallback_metrics);
let (mut top, mut bottom) = text_bounds
.get(line_index)
.copied()
.unwrap_or_else(|| base_inline_line_bounds(metrics, line_height));
let mut top_box_height = Pixels::ZERO;
let mut bottom_box_height = Pixels::ZERO;
for (inline_box, align) in &line_boxes {
expand_inline_line_for_box(
&mut top,
&mut bottom,
&mut top_box_height,
&mut bottom_box_height,
inline_box.bounds.size.height,
metrics,
*align,
);
}
bottom = bottom.max(top + top_box_height);
top = top.min(bottom - bottom_box_height);
line.origin.y = line_y;
line.size.height = bottom - top;
line.baseline = -top;
for (inline_box, align) in line_boxes {
inline_box.bounds.origin.y =
line_y + aligned_inline_box_y(*line, metrics, inline_box.bounds.size.height, align);
}
line_y += line.size.height;
}
size.height = line_y;
}
pub trait PlatformTextLayout: Send + Sync + std::fmt::Debug {
fn len(&self) -> usize;
fn line_count(&self) -> usize;
fn size(&self) -> Size<Pixels>;
fn byte_index_from_pixel_point(
&self,
pixel_point: Point<Pixels>,
line_height: Pixels,
) -> Result<usize, usize>;
fn caret_from_pixel_point(
&self,
pixel_point: Point<Pixels>,
line_height: Pixels,
) -> Result<CaretPosition, CaretPosition>;
fn caret_bounds(&self, caret: CaretPosition, line_height: Pixels) -> Option<Bounds<Pixels>>;
fn normalized_caret(&self, caret: CaretPosition) -> CaretPosition;
fn adjacent_visual_caret(
&self,
caret: CaretPosition,
direction: VisualDirection,
) -> Option<CaretPosition>;
fn selection_bounds(
&self,
byte_range: Range<usize>,
line_height: Pixels,
) -> Vec<Bounds<Pixels>>;
fn inline_geometry(&self, range: Range<usize>) -> Option<Vec<InlineRangeGeometry>> {
if range.is_empty() {
return None;
}
let line_height = self.size().height / self.line_count().max(1) as f32;
Some(
self.selection_bounds(range, line_height)
.into_iter()
.map(|bounds| InlineRangeGeometry {
visual_line_index: (bounds.origin.y / line_height) as usize,
bounds,
})
.collect(),
)
}
fn inline_geometry_for_ranges(&self, ranges: &[Range<usize>]) -> Vec<Vec<InlineRangeGeometry>> {
ranges
.iter()
.map(|range| self.inline_geometry(range.clone()).unwrap_or_default())
.collect()
}
fn logical_cluster_before(&self, caret: CaretPosition) -> Option<Range<usize>>;
fn logical_cluster_after(&self, caret: CaretPosition) -> Option<Range<usize>>;
fn caret_movement(
&self,
caret: CaretPosition,
movement: TextMovement,
vertical_navigation_x: Option<Pixels>,
) -> CaretMovement;
fn selection_from_pixel_point(
&self,
pixel_point: Point<Pixels>,
line_height: Pixels,
kind: TextSelectionKind,
) -> Range<usize>;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum VisualDirection {
Left,
Right,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TextDirection {
Left,
Right,
Up,
Down,
Start,
End,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TextBoundary {
Cluster,
Word,
VisualLine,
HardLine,
Document,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TextMovement {
pub direction: TextDirection,
pub boundary: TextBoundary,
}
impl TextDirection {
pub const fn with_boundary(self, boundary: TextBoundary) -> TextMovement {
TextMovement {
direction: self,
boundary,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TextSelectionKind {
Word,
VisualLine,
HardLine,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct VisualLine {
pub text_range: Range<usize>,
pub paint_fragment_range: Range<usize>,
pub advance_width: Pixels,
pub offset: Pixels,
pub direction: crate::ResolvedDirection,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct PaintStyle {
pub color: Hsla,
pub background_color: Option<Hsla>,
pub underline: Option<UnderlineStyle>,
pub strikethrough: Option<StrikethroughStyle>,
}
impl From<&TextRun> for PaintStyle {
fn from(run: &TextRun) -> Self {
Self {
color: run.color,
background_color: run.background_color,
underline: run.underline,
strikethrough: run.strikethrough,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct PaintFragment {
pub source_run: usize,
pub font_id: FontId,
pub font_size: Pixels,
pub glyphs: Arc<[ShapedGlyph]>,
pub x_range: Range<Pixels>,
pub style: PaintStyle,
pub underline_offset: Option<Pixels>,
pub strikethrough_offset: Option<Pixels>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ShapedGlyph {
pub id: GlyphId,
pub position: Point<Pixels>,
pub is_emoji: bool,
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub enum CaretAffinity {
#[default]
Downstream,
Upstream,
}
static HARD_LINE_BREAK_CHARACTERS: phf::Set<char> = phf::phf_set! {
'\n',
'\r',
'\u{001c}',
'\u{001d}',
'\u{001e}',
'\u{0085}',
'\u{2028}',
'\u{2029}',
};
pub fn is_paragraph_separator(character: char) -> bool {
character != '\u{2028}' && HARD_LINE_BREAK_CHARACTERS.contains(&character)
}
impl CaretAffinity {
pub fn for_inserted_text(text: &str) -> Self {
Self::from(text)
}
}
impl From<&str> for CaretAffinity {
fn from(text: &str) -> Self {
let Some(last_character) = text.chars().next_back() else {
return Self::Downstream;
};
if HARD_LINE_BREAK_CHARACTERS.contains(&last_character) {
return Self::Downstream;
}
Self::Upstream
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub struct CaretPosition {
pub index: usize,
pub affinity: CaretAffinity,
}
impl From<(usize, CaretAffinity)> for CaretPosition {
fn from((index, affinity): (usize, CaretAffinity)) -> Self {
Self { index, affinity }
}
}
impl CaretPosition {
pub const fn attached_to_next_cluster(index: usize) -> Self {
Self {
index,
affinity: CaretAffinity::Downstream,
}
}
pub const fn attached_to_previous_cluster(index: usize) -> Self {
Self {
index,
affinity: CaretAffinity::Upstream,
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct CaretSelection {
pub anchor: CaretPosition,
pub caret: CaretPosition,
}
impl From<usize> for CaretSelection {
fn from(index: usize) -> Self {
CaretPosition::attached_to_next_cluster(index).into()
}
}
impl From<CaretPosition> for CaretSelection {
fn from(caret: CaretPosition) -> Self {
Self {
anchor: caret,
caret,
}
}
}
impl From<Range<usize>> for CaretSelection {
fn from(range: Range<usize>) -> Self {
Self {
anchor: CaretPosition::attached_to_next_cluster(range.end),
caret: CaretPosition::attached_to_next_cluster(range.start),
}
}
}
impl CaretSelection {
pub fn is_empty(&self) -> bool {
self.anchor.index == self.caret.index
}
pub fn endpoint_ordering(self) -> std::cmp::Ordering {
self.caret.index.cmp(&self.anchor.index)
}
pub fn byte_range(self) -> Range<usize> {
self.anchor.index.min(self.caret.index)..self.anchor.index.max(self.caret.index)
}
pub fn min(mut self, max_idx: usize) -> Self {
self.caret.index = self.caret.index.min(max_idx);
self.anchor.index = self.anchor.index.min(max_idx);
self
}
pub fn with_caret(self, caret: CaretPosition) -> Self {
Self {
anchor: self.anchor,
caret,
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct CaretMovement<T = CaretPosition> {
pub result: T,
pub vertical_navigation_x: Option<Pixels>,
}
pub type CaretSelectionMovement = CaretMovement<CaretSelection>;
impl CaretMovement<CaretSelection> {
pub fn move_or_select_to(self, caret: CaretPosition, extend: bool) -> Self {
let result = if extend {
self.result.with_caret(caret)
} else {
caret.into()
};
Self {
result,
vertical_navigation_x: None,
}
}
}
#[derive(Debug)]
pub struct ShapedTextLayout {
pub layout: Arc<LineLayout>,
pub wrap_width: Option<Pixels>,
}
impl ShapedTextLayout {
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.layout.platform_layout.len()
}
pub fn width(&self) -> Pixels {
self.wrap_width
.unwrap_or(Pixels::MAX)
.min(self.layout.width)
}
pub fn size(&self, line_height: Pixels) -> Size<Pixels> {
Size {
width: self.width(),
height: line_height * self.line_count(),
}
}
pub fn line_count(&self) -> usize {
self.layout.platform_layout.line_count()
}
pub fn ascent(&self) -> Pixels {
self.layout.ascent
}
pub fn descent(&self) -> Pixels {
self.layout.descent
}
pub fn visual_lines(&self) -> &[VisualLine] {
&self.layout.visual_lines
}
pub fn fragments_for_line(&self, line: &VisualLine) -> &[PaintFragment] {
&self.layout.paint_fragments[line.paint_fragment_range.clone()]
}
pub fn font_size(&self) -> Pixels {
self.layout.font_size
}
pub fn byte_index_for_pixel_point(
&self,
pixel_point: Point<Pixels>,
line_height: Pixels,
) -> Result<usize, usize> {
self.layout
.platform_layout
.byte_index_from_pixel_point(pixel_point, line_height)
}
pub fn closest_byte_index_for_pixel_point(
&self,
pixel_point: Point<Pixels>,
line_height: Pixels,
) -> Result<usize, usize> {
self.closest_caret_for_pixel_point(pixel_point, line_height)
.map(|caret| caret.index)
.map_err(|caret| caret.index)
}
pub fn closest_caret_for_pixel_point(
&self,
pixel_point: Point<Pixels>,
line_height: Pixels,
) -> Result<CaretPosition, CaretPosition> {
self.layout
.platform_layout
.caret_from_pixel_point(pixel_point, line_height)
}
pub fn visual_position_for_byte_index(
&self,
byte_index: usize,
line_height: Pixels,
) -> Option<Point<Pixels>> {
if byte_index > self.len() {
return None;
}
self.visual_position_for_caret(
CaretPosition::attached_to_next_cluster(byte_index),
line_height,
)
}
pub fn visual_position_for_caret(
&self,
caret: CaretPosition,
line_height: Pixels,
) -> Option<Point<Pixels>> {
self.layout
.platform_layout
.caret_bounds(caret, line_height)
.map(|bounds| bounds.origin)
}
pub fn normalized_caret(&self, caret: CaretPosition) -> CaretPosition {
self.layout.platform_layout.normalized_caret(caret)
}
pub fn adjacent_visual_caret(
&self,
caret: CaretPosition,
direction: VisualDirection,
) -> Option<CaretPosition> {
self.layout
.platform_layout
.adjacent_visual_caret(caret, direction)
}
pub fn logical_cluster_before(&self, caret: CaretPosition) -> Option<Range<usize>> {
self.layout.platform_layout.logical_cluster_before(caret)
}
pub fn logical_cluster_after(&self, caret: CaretPosition) -> Option<Range<usize>> {
self.layout.platform_layout.logical_cluster_after(caret)
}
pub fn caret_movement(
&self,
caret: CaretPosition,
movement: TextMovement,
vertical_navigation_x: Option<Pixels>,
) -> CaretMovement {
self.layout
.platform_layout
.caret_movement(caret, movement, vertical_navigation_x)
}
pub fn selection_movement(
&self,
selection: CaretSelection,
movement: TextMovement,
extend: bool,
vertical_navigation_x: Option<Pixels>,
line_height: Pixels,
) -> CaretSelectionMovement {
let forward = movement.direction == TextDirection::Right;
let horizontal = matches!(
movement.direction,
TextDirection::Left | TextDirection::Right
) && matches!(
movement.boundary,
TextBoundary::Cluster | TextBoundary::Word
);
if !extend && !selection.is_empty() && horizontal {
let caret_visual_position =
self.visual_position_for_caret(selection.caret, line_height);
let anchor_visual_position =
self.visual_position_for_caret(selection.anchor, line_height);
let (visual_start, visual_end) = caret_visual_position
.zip(anchor_visual_position)
.map(|(caret_visual_position, anchor_visual_position)| {
if (caret_visual_position.y, caret_visual_position.x)
<= (anchor_visual_position.y, anchor_visual_position.x)
{
(selection.caret, selection.anchor)
} else {
(selection.anchor, selection.caret)
}
})
.unwrap_or_else(|| {
if selection.caret.index <= selection.anchor.index {
(selection.caret, selection.anchor)
} else {
(selection.anchor, selection.caret)
}
});
let caret = if forward { visual_end } else { visual_start };
return CaretSelectionMovement {
result: caret.into(),
vertical_navigation_x: None,
};
}
let CaretMovement {
result: caret,
vertical_navigation_x,
} = self.caret_movement(selection.caret, movement, vertical_navigation_x);
CaretSelectionMovement {
result: if extend {
selection.with_caret(caret)
} else {
caret.into()
},
vertical_navigation_x,
}
}
pub fn selection_from_pixel_point(
&self,
pixel_point: Point<Pixels>,
line_height: Pixels,
kind: TextSelectionKind,
) -> Range<usize> {
self.layout
.platform_layout
.selection_from_pixel_point(pixel_point, line_height, kind)
}
pub fn selection_bounds(
&self,
byte_range: Range<usize>,
line_height: Pixels,
) -> SmallVec<[Bounds<Pixels>; 4]> {
let mut result = SmallVec::new();
if byte_range.is_empty() {
return result;
}
result.extend(
self.layout
.platform_layout
.selection_bounds(byte_range, line_height),
);
result
}
}
impl std::ops::Deref for ShapedTextLayout {
type Target = LineLayout;
fn deref(&self) -> &Self::Target {
&self.layout
}
}
pub(crate) struct LineLayoutCache {
previous_frame: Mutex<FrameCache>,
current_frame: RwLock<FrameCache>,
platform_text_system: Arc<dyn PlatformTextSystem>,
font_generation: AtomicU64,
}
#[derive(Default)]
struct FrameCache {
lines: FrameLayouts<CacheKey, LineLayout>,
shaped_texts: FrameLayouts<CacheKey, ShapedTextLayout>,
inline_layouts: FrameLayouts<InlineCacheKey, InlineLayout>,
}
struct FrameLayouts<Key, Value> {
entries: FxHashMap<Arc<Key>, Arc<Value>>,
used: Vec<Arc<Key>>,
}
impl<Key, Value> Default for FrameLayouts<Key, Value> {
fn default() -> Self {
Self {
entries: FxHashMap::default(),
used: Vec::new(),
}
}
}
impl<Key, Value> FrameLayouts<Key, Value>
where
Key: Eq + Hash,
{
fn get<Query>(&self, key: &Query) -> Option<&Arc<Value>>
where
Arc<Key>: Borrow<Query>,
Query: Eq + Hash + ?Sized,
{
self.entries.get(key)
}
fn remove_entry<Query>(&mut self, key: &Query) -> Option<(Arc<Key>, Arc<Value>)>
where
Arc<Key>: Borrow<Query>,
Query: Eq + Hash + ?Sized,
{
self.entries.remove_entry(key)
}
fn insert(&mut self, key: Arc<Key>, value: Arc<Value>) {
self.entries.insert(key.clone(), value);
self.used.push(key);
}
fn reuse(&mut self, previous: &mut Self, range: Range<usize>) {
let Some(keys) = previous.used.get(range) else {
return;
};
for key in keys {
if let Some(layout) = previous.entries.remove(key) {
self.entries.insert(key.clone(), layout);
}
self.used.push(key.clone());
}
}
fn clear(&mut self) {
self.entries.clear();
self.used.clear();
}
}
fn repaint_line_layout(layout: Arc<LineLayout>, runs: &[TextRun]) -> Arc<LineLayout> {
let paint_matches = layout.paint_fragments.iter().all(|fragment| {
runs.get(fragment.source_run)
.is_some_and(|run| fragment.style == PaintStyle::from(run))
});
if paint_matches {
return layout;
}
let mut repainted = (*layout).clone();
for fragment in &mut repainted.paint_fragments {
if let Some(run) = runs.get(fragment.source_run) {
fragment.style = PaintStyle::from(run);
}
}
Arc::new(repainted)
}
fn repaint_shaped_text_layout(
layout: Arc<ShapedTextLayout>,
runs: &[TextRun],
) -> Arc<ShapedTextLayout> {
let repainted = repaint_line_layout(layout.layout.clone(), runs);
if Arc::ptr_eq(&repainted, &layout.layout) {
return layout;
}
Arc::new(ShapedTextLayout {
layout: repainted,
wrap_width: layout.wrap_width,
})
}
#[derive(Clone, Default)]
pub(crate) struct LineLayoutIndex {
lines_index: usize,
shaped_texts_index: usize,
inline_layouts_index: usize,
}
impl LineLayoutIndex {
pub(crate) fn rebased(&self, from: &Self, to: &Self) -> Self {
let shift = |index: usize, from: usize, to: usize| index.saturating_sub(from) + to;
Self {
lines_index: shift(self.lines_index, from.lines_index, to.lines_index),
shaped_texts_index: shift(
self.shaped_texts_index,
from.shaped_texts_index,
to.shaped_texts_index,
),
inline_layouts_index: shift(
self.inline_layouts_index,
from.inline_layouts_index,
to.inline_layouts_index,
),
}
}
}
impl LineLayoutCache {
pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
let font_generation = platform_text_system.font_generation();
Self {
previous_frame: Mutex::default(),
current_frame: RwLock::default(),
platform_text_system,
font_generation: AtomicU64::new(font_generation),
}
}
pub fn layout_index(&self) -> LineLayoutIndex {
let frame = self.current_frame.read();
LineLayoutIndex {
lines_index: frame.lines.used.len(),
shaped_texts_index: frame.shaped_texts.used.len(),
inline_layouts_index: frame.inline_layouts.used.len(),
}
}
pub fn clear(&self) {
*self.previous_frame.lock() = FrameCache::default();
*self.current_frame.write() = FrameCache::default();
}
fn sync_font_generation(&self) {
let generation = self.platform_text_system.font_generation();
if self.font_generation.load(Ordering::Acquire) != generation {
self.clear();
self.font_generation.store(generation, Ordering::Release);
}
}
pub fn reuse_layouts(&self, range: Range<LineLayoutIndex>) {
let mut previous_frame = &mut *self.previous_frame.lock();
let mut current_frame = &mut *self.current_frame.write();
current_frame.lines.reuse(
&mut previous_frame.lines,
range.start.lines_index..range.end.lines_index,
);
current_frame.shaped_texts.reuse(
&mut previous_frame.shaped_texts,
range.start.shaped_texts_index..range.end.shaped_texts_index,
);
current_frame.inline_layouts.reuse(
&mut previous_frame.inline_layouts,
range.start.inline_layouts_index..range.end.inline_layouts_index,
);
}
pub fn truncate_layouts(&self, index: LineLayoutIndex) {
let mut current_frame = &mut *self.current_frame.write();
current_frame.lines.used.truncate(index.lines_index);
current_frame
.shaped_texts
.used
.truncate(index.shaped_texts_index);
current_frame
.inline_layouts
.used
.truncate(index.inline_layouts_index);
}
pub fn finish_frame(&self) {
let mut prev_frame = self.previous_frame.lock();
let mut curr_frame = self.current_frame.write();
std::mem::swap(&mut *prev_frame, &mut *curr_frame);
curr_frame.lines.clear();
curr_frame.shaped_texts.clear();
curr_frame.inline_layouts.clear();
}
pub fn layout_text_with_options<Text>(
&self,
text: Text,
font_size: Pixels,
runs: &[TextRun],
options: TextLayoutOptions,
) -> Arc<ShapedTextLayout>
where
Text: AsRef<str>,
SharedString: From<Text>,
{
self.sync_font_generation();
let wrap_width = options.wrap_width;
let shaping_runs = runs.iter().map(ShapingRun::from).collect::<SmallVec<_>>();
let key = &CacheKeyRef {
text: text.as_ref(),
font_size,
runs: &shaping_runs,
options,
} as &dyn AsCacheKeyRef;
let current_frame = self.current_frame.upgradable_read();
if let Some(layout) = current_frame.shaped_texts.get(key) {
return repaint_shaped_text_layout(layout.clone(), runs);
}
let previous_frame_entry = self.previous_frame.lock().shaped_texts.remove_entry(key);
if let Some((key, layout)) = previous_frame_entry {
let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
current_frame.shaped_texts.insert(key, layout.clone());
repaint_shaped_text_layout(layout, runs)
} else {
drop(current_frame);
let text = SharedString::from(text);
let document_layout = if options != TextLayoutOptions::default() || text.contains('\n')
{
Arc::new(self.platform_text_system.layout_text(TextLayoutRequest {
text: &text,
font_size,
runs,
options,
}))
} else {
self.layout_line::<&SharedString>(&text, font_size, runs)
};
let layout = Arc::new(ShapedTextLayout {
layout: document_layout,
wrap_width,
});
let key = Arc::new(CacheKey {
text,
font_size,
runs: shaping_runs,
options,
});
let mut current_frame = self.current_frame.write();
current_frame.shaped_texts.insert(key, layout.clone());
layout
}
}
pub fn layout_line<Text>(
&self,
text: Text,
font_size: Pixels,
runs: &[TextRun],
) -> Arc<LineLayout>
where
Text: AsRef<str>,
SharedString: From<Text>,
{
self.sync_font_generation();
let shaping_runs = runs.iter().map(ShapingRun::from).collect::<SmallVec<_>>();
let key = &CacheKeyRef {
text: text.as_ref(),
font_size,
runs: &shaping_runs,
options: TextLayoutOptions::default(),
} as &dyn AsCacheKeyRef;
let current_frame = self.current_frame.upgradable_read();
if let Some(layout) = current_frame.lines.get(key) {
return repaint_line_layout(layout.clone(), runs);
}
let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
if let Some((key, layout)) = self.previous_frame.lock().lines.remove_entry(key) {
current_frame.lines.insert(key, layout.clone());
repaint_line_layout(layout, runs)
} else {
let text = SharedString::from(text);
let layout = self.platform_text_system.layout_text(TextLayoutRequest {
text: &text,
font_size,
runs,
options: TextLayoutOptions::default(),
});
let key = Arc::new(CacheKey {
text,
font_size,
runs: shaping_runs,
options: TextLayoutOptions::default(),
});
let layout = Arc::new(layout);
current_frame.lines.insert(key, layout.clone());
layout
}
}
pub fn layout_inline(&self, request: InlineLayoutRequest<'_>) -> Arc<InlineLayout> {
self.sync_font_generation();
let key = &request as &dyn AsInlineCacheKeyRef;
let current_frame = self.current_frame.upgradable_read();
if let Some(layout) = current_frame.inline_layouts.get(key) {
return layout.clone();
}
let previous_frame_entry = self.previous_frame.lock().inline_layouts.remove_entry(key);
if let Some((key, layout)) = previous_frame_entry {
let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
current_frame.inline_layouts.insert(key, layout.clone());
return layout;
}
drop(current_frame);
let layout = Arc::new(self.platform_text_system.layout_inline(request));
let key = Arc::new(InlineCacheKey::from(request));
let mut current_frame = self.current_frame.write();
current_frame.inline_layouts.insert(key, layout.clone());
layout
}
}
trait AsInlineCacheKeyRef {
fn as_inline_cache_key_ref(&self) -> InlineLayoutRequest<'_>;
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct InlineCacheKey {
text: SharedString,
runs: SmallVec<[TextRun; 1]>,
text_styles: Vec<InlineTextStyle>,
boxes: Vec<InlineBoxRequest>,
font_size: Pixels,
line_height: Pixels,
text_metrics: InlineTextMetrics,
options: TextLayoutOptions,
bidi_scopes: Vec<InlineBidiScope>,
}
impl From<InlineLayoutRequest<'_>> for InlineCacheKey {
fn from(request: InlineLayoutRequest<'_>) -> Self {
Self {
text: SharedString::new(request.text),
runs: SmallVec::from(request.runs),
text_styles: request.text_styles.to_vec(),
boxes: request.boxes.to_vec(),
font_size: request.font_size,
line_height: request.line_height,
text_metrics: request.text_metrics,
options: request.options,
bidi_scopes: request.bidi_scopes.to_vec(),
}
}
}
impl AsInlineCacheKeyRef for InlineCacheKey {
fn as_inline_cache_key_ref(&self) -> InlineLayoutRequest<'_> {
InlineLayoutRequest {
text: &self.text,
runs: &self.runs,
text_styles: &self.text_styles,
boxes: &self.boxes,
font_size: self.font_size,
line_height: self.line_height,
text_metrics: self.text_metrics,
options: self.options,
bidi_scopes: &self.bidi_scopes,
}
}
}
impl AsInlineCacheKeyRef for InlineLayoutRequest<'_> {
fn as_inline_cache_key_ref(&self) -> InlineLayoutRequest<'_> {
*self
}
}
impl PartialEq for dyn AsInlineCacheKeyRef + '_ {
fn eq(&self, other: &dyn AsInlineCacheKeyRef) -> bool {
self.as_inline_cache_key_ref() == other.as_inline_cache_key_ref()
}
}
impl Eq for dyn AsInlineCacheKeyRef + '_ {}
impl Hash for dyn AsInlineCacheKeyRef + '_ {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_inline_cache_key_ref().hash(state);
}
}
impl<'a> Borrow<dyn AsInlineCacheKeyRef + 'a> for Arc<InlineCacheKey> {
fn borrow(&self) -> &(dyn AsInlineCacheKeyRef + 'a) {
self.as_ref() as &dyn AsInlineCacheKeyRef
}
}
trait AsCacheKeyRef {
fn as_cache_key_ref(&self) -> CacheKeyRef<'_>;
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct CacheKey {
text: SharedString,
font_size: Pixels,
runs: SmallVec<[ShapingRun; 1]>,
options: TextLayoutOptions,
}
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
struct CacheKeyRef<'a> {
text: &'a str,
font_size: Pixels,
runs: &'a [ShapingRun],
options: TextLayoutOptions,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct ShapingRun {
len: usize,
font: crate::Font,
letter_spacing: Option<Pixels>,
}
impl From<&TextRun> for ShapingRun {
fn from(run: &TextRun) -> Self {
Self {
len: run.len,
font: run.font.clone(),
letter_spacing: run.letter_spacing,
}
}
}
impl PartialEq for dyn AsCacheKeyRef + '_ {
fn eq(&self, other: &dyn AsCacheKeyRef) -> bool {
self.as_cache_key_ref() == other.as_cache_key_ref()
}
}
impl Eq for dyn AsCacheKeyRef + '_ {}
impl Hash for dyn AsCacheKeyRef + '_ {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_cache_key_ref().hash(state)
}
}
impl AsCacheKeyRef for CacheKey {
fn as_cache_key_ref(&self) -> CacheKeyRef<'_> {
CacheKeyRef {
text: &self.text,
font_size: self.font_size,
runs: self.runs.as_slice(),
options: self.options,
}
}
}
impl<'a> Borrow<dyn AsCacheKeyRef + 'a> for Arc<CacheKey> {
fn borrow(&self) -> &(dyn AsCacheKeyRef + 'a) {
self.as_ref() as &dyn AsCacheKeyRef
}
}
impl AsCacheKeyRef for CacheKeyRef<'_> {
fn as_cache_key_ref(&self) -> CacheKeyRef<'_> {
*self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{point, px, size};
#[test]
fn caret_selection_endpoint_ordering_uses_byte_indices() {
for (caret, anchor, expected) in [
(
CaretPosition::attached_to_next_cluster(2),
CaretPosition::attached_to_next_cluster(5),
std::cmp::Ordering::Less,
),
(
CaretPosition::attached_to_next_cluster(5),
CaretPosition::attached_to_previous_cluster(5),
std::cmp::Ordering::Equal,
),
(
CaretPosition::attached_to_previous_cluster(8),
CaretPosition::attached_to_next_cluster(5),
std::cmp::Ordering::Greater,
),
] {
let selection = CaretSelection { anchor, caret };
assert_eq!(selection.endpoint_ordering(), expected);
}
}
#[test]
fn paragraph_separators_match_unicode_bidi_class_b() {
for character in [
'\n', '\r', '\u{001c}', '\u{001d}', '\u{001e}', '\u{0085}', '\u{2029}',
] {
assert!(is_paragraph_separator(character));
}
for character in [' ', '\u{2028}'] {
assert!(!is_paragraph_separator(character));
}
}
#[test]
fn inserted_text_affinity_tracks_trailing_paragraph_separators() {
for (text, expected) in [
("", CaretAffinity::Downstream),
("ordinary text", CaretAffinity::Upstream),
("\n", CaretAffinity::Downstream),
("\r", CaretAffinity::Downstream),
("\u{001c}", CaretAffinity::Downstream),
("\u{001d}", CaretAffinity::Downstream),
("\u{001e}", CaretAffinity::Downstream),
("\u{0085}", CaretAffinity::Downstream),
("\u{2028}", CaretAffinity::Downstream),
("\u{2029}", CaretAffinity::Downstream),
("\nordinary text", CaretAffinity::Upstream),
] {
assert_eq!(CaretAffinity::from(text), expected, "From({text:?})");
assert_eq!(
CaretAffinity::for_inserted_text(text),
expected,
"for_inserted_text({text:?})"
);
}
}
#[test]
fn inline_box_alignment_preserves_unsorted_boxes_and_empty_lines() {
let mut lines = [InlineVisualLine::default(); 3];
let mut boxes =
[(20, 2, 14.), (10, 0, 18.), (90, 3, 100.)].map(|(id, line_index, height)| {
PositionedInlineBox {
id,
line_index,
bounds: Bounds::new(point(px(7.), px(99.)), size(px(5.), px(height))),
}
});
let mut layout_size = size(px(100.), px(999.));
let original_lines = lines;
let original_boxes = boxes;
let requests = [InlineBoxRequest {
id: 20,
index: 0,
size: boxes[0].bounds.size,
vertical_align: VerticalAlign::Top,
}];
let metrics = InlineTextMetrics {
ascent: px(8.),
descent: px(2.),
x_height: px(4.),
};
align_inline_boxes(
&mut lines,
&mut boxes,
&mut layout_size,
&requests,
&[metrics],
&[(px(-7.), px(3.)), (px(-11.), px(4.))],
metrics,
px(12.),
);
for ((line, mut expected_line), (origin_y, height, baseline)) in lines
.iter()
.zip(original_lines)
.zip([(0., 21., 18.), (21., 15., 11.), (36., 14., 9.)])
{
expected_line.origin.y = px(origin_y);
expected_line.size.height = px(height);
expected_line.baseline = px(baseline);
assert_eq!(*line, expected_line);
}
for ((inline_box, mut expected_box), origin_y) in
boxes.iter().zip(original_boxes).zip([36., 0., 99.])
{
expected_box.bounds.origin.y = px(origin_y);
assert_eq!(*inline_box, expected_box);
}
assert_eq!(layout_size, size(px(100.), px(50.)));
}
}