use crate::{LayoutPoint, LayoutRect, LayoutSize, LayoutUnit};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LineMetric {
pub start_index: usize,
pub end_index: usize,
pub baseline: LayoutUnit,
pub height: LayoutUnit,
pub width: LayoutUnit,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct RichTextInlineBox {
pub id: u64,
pub x: LayoutUnit,
pub y: LayoutUnit,
pub width: LayoutUnit,
pub height: LayoutUnit,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct ParagraphCluster {
pub start_index: usize,
pub end_index: usize,
pub line_index: usize,
pub rect: LayoutRect,
pub is_rtl: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct ParagraphGlyph {
pub id: u32,
pub style_index: usize,
pub cluster_index: usize,
pub position: LayoutPoint,
pub advance: LayoutUnit,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct ParagraphSelectionBox {
pub start_index: usize,
pub end_index: usize,
pub line_index: usize,
pub rect: LayoutRect,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct ParagraphCaretStop {
pub index: usize,
pub upstream: bool,
pub position: LayoutPoint,
pub height: LayoutUnit,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ResolvedParagraphLayout {
pub constraint_width: Option<LayoutUnit>,
pub size: LayoutSize,
pub lines: Vec<LineMetric>,
pub inline_boxes: Vec<RichTextInlineBox>,
pub clusters: Vec<ParagraphCluster>,
pub glyphs: Vec<ParagraphGlyph>,
pub caret_stops: Vec<ParagraphCaretStop>,
pub selection_boxes: Vec<ParagraphSelectionBox>,
}
impl ResolvedParagraphLayout {
pub fn empty(constraint_width: Option<LayoutUnit>) -> Self {
Self {
constraint_width,
size: LayoutSize::ZERO,
lines: Vec::new(),
inline_boxes: Vec::new(),
clusters: Vec::new(),
glyphs: Vec::new(),
caret_stops: Vec::new(),
selection_boxes: Vec::new(),
}
}
pub fn hit_test(&self, point: LayoutPoint) -> usize {
let Some(cluster) = self.clusters.iter().min_by(|left, right| {
distance_to_rect(point, left.rect).total_cmp(&distance_to_rect(point, right.rect))
}) else {
return 0;
};
let after_midpoint = point.x >= cluster.rect.x() + cluster.rect.width() * 0.5;
match (cluster.is_rtl, after_midpoint) {
(false, false) | (true, true) => cluster.start_index,
(false, true) | (true, false) => cluster.end_index,
}
}
pub fn caret(&self, index: usize, upstream: bool) -> Option<ParagraphCaretStop> {
self.caret_stops
.iter()
.find(|stop| stop.index == index && stop.upstream == upstream)
.copied()
.or_else(|| {
self.caret_stops
.iter()
.filter(|stop| stop.upstream == upstream)
.min_by_key(|stop| stop.index.abs_diff(index))
.copied()
})
}
pub fn selection_rects(&self, start: usize, end: usize) -> Vec<LayoutRect> {
let range = start.min(end)..start.max(end);
let mut selected = self
.selection_boxes
.iter()
.filter(|selection| {
selection.start_index < range.end && selection.end_index > range.start
})
.copied()
.collect::<Vec<_>>();
selected.sort_by(|left, right| {
left.line_index
.cmp(&right.line_index)
.then_with(|| left.rect.x().total_cmp(&right.rect.x()))
});
let mut rects: Vec<LayoutRect> = Vec::new();
for selection in selected {
if let Some(last) = rects.last_mut() {
let same_line = (last.y() - selection.rect.y()).abs() < 0.5
&& (last.height() - selection.rect.height()).abs() < 0.5;
if same_line && selection.rect.x() <= last.right() + 0.5 {
let right = last.right().max(selection.rect.right());
last.size.width = right - last.x();
continue;
}
}
rects.push(selection.rect);
}
rects
}
}
fn distance_to_rect(point: LayoutPoint, rect: LayoutRect) -> LayoutUnit {
let dx = if point.x < rect.x() {
rect.x() - point.x
} else if point.x > rect.right() {
point.x - rect.right()
} else {
0.0
};
let dy = if point.y < rect.y() {
rect.y() - point.y
} else if point.y > rect.bottom() {
point.y - rect.bottom()
} else {
0.0
};
dx * dx + dy * dy
}
#[derive(Debug, Clone, PartialEq)]
pub struct RichTextLayoutInfo {
pub width: LayoutUnit,
pub height: LayoutUnit,
pub inline_boxes: Vec<RichTextInlineBox>,
}
impl From<ResolvedParagraphLayout> for RichTextLayoutInfo {
fn from(layout: ResolvedParagraphLayout) -> Self {
Self {
width: layout.size.width,
height: layout.size.height,
inline_boxes: layout.inline_boxes,
}
}
}