use std::ops::Range;
use crate::shaping::run::ShapedRun;
use crate::shaping::shaper::TextDirection;
use crate::types::CursorAffinity;
#[derive(Clone, Copy, Debug)]
pub(crate) struct CaretStop {
pub offset: usize,
pub x: f32,
pub trailing: bool,
}
fn pick_by_affinity<'a>(
stops: &[&'a CaretStop],
affinity: CursorAffinity,
) -> Option<&'a CaretStop> {
if stops.len() < 2 {
return stops.first().copied();
}
let want_trailing = affinity == CursorAffinity::Downstream;
stops
.iter()
.find(|s| s.trailing == want_trailing)
.or_else(|| stops.first())
.copied()
}
#[derive(Clone)]
pub struct LayoutLine {
pub runs: Vec<PositionedRun>,
pub y: f32,
pub ascent: f32,
pub descent: f32,
pub leading: f32,
pub line_height: f32,
pub width: f32,
pub char_range: Range<usize>,
}
impl LayoutLine {
pub(crate) fn cluster_end(&self, cluster: usize) -> usize {
let mut best: Option<usize> = None;
for run in &self.runs {
for g in &run.shaped_run.glyphs {
let c = g.cluster as usize;
if c > cluster {
best = Some(best.map_or(c, |b| b.min(c)));
}
}
}
best.unwrap_or(self.char_range.end)
}
pub fn x_for_offset(&self, offset: usize) -> f32 {
self.x_for_offset_with_affinity(offset, CursorAffinity::default())
}
pub fn x_for_offset_with_affinity(&self, offset: usize, affinity: CursorAffinity) -> f32 {
let stops = self.caret_stops();
if stops.is_empty() {
return 0.0;
}
let exact: Vec<&CaretStop> = stops.iter().filter(|s| s.offset == offset).collect();
if let Some(stop) = pick_by_affinity(&exact, affinity) {
return stop.x;
}
stops
.iter()
.min_by_key(|s| s.offset.abs_diff(offset))
.map(|s| s.x)
.unwrap_or(0.0)
}
pub fn is_direction_boundary(&self, offset: usize) -> bool {
let stops = self.caret_stops();
let mut xs = stops.iter().filter(|s| s.offset == offset).map(|s| s.x);
let Some(first) = xs.next() else {
return false;
};
xs.any(|x| x != first)
}
pub(crate) fn caret_stops(&self) -> Vec<CaretStop> {
let mut stops: Vec<CaretStop> = Vec::new();
for run in &self.runs {
let glyphs = &run.shaped_run.glyphs;
if glyphs.is_empty() {
continue;
}
let mut gx = run.x;
if run.shaped_run.direction == TextDirection::RightToLeft {
stops.push(CaretStop {
offset: self.cluster_end(glyphs[0].cluster as usize),
x: gx,
trailing: true,
});
for g in glyphs {
gx += g.x_advance;
stops.push(CaretStop {
offset: g.cluster as usize,
x: gx,
trailing: false,
});
}
} else {
for g in glyphs {
stops.push(CaretStop {
offset: g.cluster as usize,
x: gx,
trailing: false,
});
gx += g.x_advance;
}
let last = glyphs.last().map(|g| g.cluster as usize).unwrap_or(0);
stops.push(CaretStop {
offset: self.cluster_end(last),
x: gx,
trailing: true,
});
}
}
stops
}
}
#[derive(Clone)]
pub struct PositionedRun {
pub shaped_run: ShapedRun,
pub x: f32,
pub decorations: RunDecorations,
}
#[derive(Clone, Debug, Default)]
pub struct RunDecorations {
pub underline_style: crate::types::UnderlineStyle,
pub overline: bool,
pub strikeout: bool,
pub is_link: bool,
pub foreground_color: Option<[f32; 4]>,
pub underline_color: Option<[f32; 4]>,
pub background_color: Option<[f32; 4]>,
pub anchor_href: Option<String>,
pub tooltip: Option<String>,
pub vertical_alignment: crate::types::VerticalAlignment,
}