use crate::text::Text;
use cosmic_text as ct;
use ct::{Buffer, LayoutRunIter};
use piet::kurbo::{Point, Rect, Size, Vec2};
use piet::TextStorage;
use swash::scale::image::Image as SwashImage;
use swash::scale::outline::Outline as SwashOutline;
use swash::scale::{ScaleContext, StrikeWith};
use swash::zeno;
use std::cell::Cell;
use std::cmp;
use std::collections::hash_map::{Entry, HashMap};
use std::fmt;
use std::rc::Rc;
#[derive(Clone)]
pub struct TextLayout {
text_buffer: Rc<BufferWrapper>,
}
impl fmt::Debug for TextLayout {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TextLayout")
.field("string", &self.text_buffer.string.as_str())
.field("glyph_size", &self.text_buffer.glyph_size)
.finish_non_exhaustive()
}
}
struct BufferWrapper {
string: Box<dyn TextStorage>,
glyph_size: i32,
buffer: Option<Buffer>,
run_metrics: Vec<piet::LineMetric>,
ink_rectangle: Rect,
logical_size: Cell<Option<Size>>,
handle: Text,
}
impl BufferWrapper {
fn buffer(&self) -> &Buffer {
self.buffer.as_ref().unwrap()
}
}
impl Drop for BufferWrapper {
fn drop(&mut self) {
let mut buffer = self.buffer.take().unwrap();
buffer.lines.clear();
let old_lines = self.handle.take_buffer();
if old_lines.capacity() > buffer.lines.capacity() {
self.handle.set_buffer(old_lines);
} else {
self.handle.set_buffer(buffer.lines);
}
}
}
impl TextLayout {
pub(crate) fn new(
text: Text,
buffer: Buffer,
string: Box<dyn TextStorage>,
glyph_size: i32,
font_system: &mut ct::FontSystem,
) -> Self {
let span = trace_span!("TextLayout::new", string = %string.as_str());
let _guard = span.enter();
let run_metrics = buffer
.layout_runs()
.map(|run| RunMetrics::new(run, glyph_size as f64))
.map(|RunMetrics { line_metric }| line_metric)
.collect();
let mut ink_context = text.borrow_ink();
let mut missing_bbox_count = 0;
let bounding_boxes = buffer
.layout_runs()
.flat_map(|run| {
let run_y = run.line_y;
run.glyphs.iter().map(move |glyph| (glyph, run_y))
})
.filter_map(|(glyph, run_y)| {
let physical = glyph.physical((0., 0.), 1.);
let offset = Vec2::new(
physical.x as f64 + physical.cache_key.x_bin.as_float() as f64,
run_y as f64 + physical.y as f64 + physical.cache_key.y_bin.as_float() as f64,
);
match ink_context.bounding_box(&physical, font_system) {
Some(mut rect) => {
rect = rect + offset;
Some(rect)
}
None => {
missing_bbox_count += 1;
None
}
}
});
let ink_rectangle = bounding_rectangle(bounding_boxes);
if missing_bbox_count > 0 {
warn!("Missing {} bounding boxes", missing_bbox_count);
}
drop(ink_context);
Self {
text_buffer: Rc::new(BufferWrapper {
string,
glyph_size,
buffer: Some(buffer),
run_metrics,
handle: text,
ink_rectangle,
logical_size: Cell::new(None),
}),
}
}
pub fn buffer(&self) -> &Buffer {
self.text_buffer.buffer()
}
pub fn layout_runs(&self) -> LayoutRunIter<'_> {
self.buffer().layout_runs()
}
}
impl piet::TextLayout for TextLayout {
fn size(&self) -> Size {
if let Some(size) = self.text_buffer.logical_size.get() {
return size;
}
let mut size = Size::new(f64::MIN, f64::MIN);
for run in self.layout_runs() {
let max = |a: f32, b: f64| {
let a: f64 = a.into();
if a < b {
b
} else {
a
}
};
size.width = max(run.line_w, size.width);
size.height = max(run.line_y, size.height);
}
self.text_buffer.logical_size.set(Some(size));
size
}
fn trailing_whitespace_width(&self) -> f64 {
self.size().width
}
fn image_bounds(&self) -> Rect {
self.text_buffer.ink_rectangle
}
fn text(&self) -> &str {
&self.text_buffer.string
}
fn line_text(&self, line_number: usize) -> Option<&str> {
self.buffer()
.layout_runs()
.nth(line_number)
.map(|run| run.text)
}
fn line_metric(&self, line_number: usize) -> Option<piet::LineMetric> {
self.text_buffer.run_metrics.get(line_number).cloned()
}
fn line_count(&self) -> usize {
self.buffer().layout_runs().count()
}
fn hit_test_point(&self, point: Point) -> piet::HitTestPoint {
let mut htp = piet::HitTestPoint::default();
let (x, y) = point.into();
if let Some(cursor) = self.buffer().hit(x as f32, y as f32) {
htp.idx = cursor.index;
htp.is_inside = true;
return htp;
}
let mut ink_context = self.text_buffer.handle.borrow_ink();
let mut font_system_guard = match self.text_buffer.handle.borrow_font_system() {
Some(system) => system,
None => {
warn!("Tried to borrow font system to calculate better hit test point, but it was already borrowed.");
htp.idx = 0;
htp.is_inside = false;
return htp;
}
};
let font_system = &mut font_system_guard
.get()
.expect("For a TextLayout to exist, the font system must have already been initialized")
.system;
let mut closest_distance = f64::MAX;
for (glyph, physical_glyph) in self.layout_runs().flat_map(|run| {
let run_y = run.line_y;
run.glyphs
.iter()
.map(move |glyph| (glyph, glyph.physical((0., run_y), 1.)))
}) {
let bounding_box = match ink_context.bounding_box(&physical_glyph, font_system) {
Some(bbox) => bbox,
None => continue,
};
if bounding_box.contains(point) {
htp.idx = glyph.start;
htp.is_inside = false;
return htp;
}
let midpoint = bounding_box.center();
let distance = midpoint.distance(point);
if distance < closest_distance {
closest_distance = distance;
htp.idx = glyph.start;
}
}
htp.is_inside = false;
htp
}
fn hit_test_text_position(&self, idx: usize) -> piet::HitTestPosition {
let mut lines_and_glyphs = self.layout_runs().enumerate().flat_map(|(line, run)| {
run.glyphs.iter().map(move |glyph| {
(
line,
{
let physical = glyph.physical((0.0, 0.0), 1.0);
let x = physical.x as f64;
let y = run.line_y as f64
+ physical.y as f64
+ self.text_buffer.glyph_size as f64;
Point::new(x, y)
},
glyph.start..glyph.end,
)
})
});
let (line, point, _) = match lines_and_glyphs.find(|(_, _, range)| range.contains(&idx)) {
Some(x) => x,
None => {
return piet::HitTestPosition::default();
}
};
let mut htp = piet::HitTestPosition::default();
htp.point = point;
htp.line = line;
htp
}
}
fn bounding_rectangle(rects: impl IntoIterator<Item = Rect>) -> Rect {
let mut iter = rects.into_iter();
let mut sum_rect = match iter.next() {
Some(rect) => rect,
None => return Rect::ZERO,
};
for rect in iter {
if rect.x0 < sum_rect.x0 {
sum_rect.x0 = rect.x0;
}
if rect.y0 < sum_rect.y0 {
sum_rect.y0 = rect.y0;
}
if rect.x1 > sum_rect.x1 {
sum_rect.x1 = rect.x1;
}
if rect.y1 > sum_rect.y1 {
sum_rect.y1 = rect.y1;
}
}
sum_rect
}
struct RunMetrics {
line_metric: piet::LineMetric,
}
impl RunMetrics {
fn new(run: ct::LayoutRun<'_>, glyph_size: f64) -> RunMetrics {
let (start_offset, end_offset) = run.glyphs.iter().fold((0, 0), |(start, end), glyph| {
(cmp::min(start, glyph.start), cmp::max(end, glyph.end))
});
let y_offset = run.line_top.into();
let baseline = run.line_y as f64 - run.line_top as f64;
RunMetrics {
line_metric: piet::LineMetric {
start_offset,
end_offset,
trailing_whitespace: 0, y_offset,
height: glyph_size as _,
baseline,
},
}
}
}
pub(crate) struct InkRectangleState {
scaler: ScaleContext,
bbox_cache: HashMap<ct::CacheKey, Option<Rect>>,
swash_image: SwashImage,
swash_outline: SwashOutline,
}
impl InkRectangleState {
pub(crate) fn new() -> Self {
Self {
scaler: ScaleContext::new(),
bbox_cache: HashMap::new(),
swash_image: SwashImage::new(),
swash_outline: SwashOutline::new(),
}
}
fn bounding_box(
&mut self,
glyph: &ct::PhysicalGlyph,
system: &mut ct::FontSystem,
) -> Option<Rect> {
let entry = match self.bbox_cache.entry(glyph.cache_key) {
Entry::Occupied(o) => return *o.into_mut(),
Entry::Vacant(v) => v,
};
let mut bbox = None;
if let Some(font) = system.get_font(glyph.cache_key.font_id) {
let mut scaler = self
.scaler
.builder(font.as_swash())
.size(f32::from_bits(glyph.cache_key.font_size_bits))
.build();
self.swash_outline.clear();
if scaler.scale_outline_into(glyph.cache_key.glyph_id, &mut self.swash_outline) {
bbox = Some(cvt_bounds(self.swash_outline.bounds()));
} else {
self.swash_image.clear();
if scaler.scale_bitmap_into(
glyph.cache_key.glyph_id,
StrikeWith::BestFit,
&mut self.swash_image,
) {
bbox = Some(cvt_placement(self.swash_image.placement));
}
}
}
*entry.insert(bbox)
}
}
fn cvt_placement(placement: zeno::Placement) -> Rect {
Rect::new(
placement.left.into(),
-placement.top as f64,
placement.left as f64 + placement.width as f64,
-placement.top as f64 + placement.height as f64,
)
}
fn cvt_bounds(mut bounds: zeno::Bounds) -> Rect {
bounds.min.y *= -1.0;
bounds.max.y *= -1.0;
Rect::from_points(cvt_point(bounds.min), cvt_point(bounds.max))
}
fn cvt_point(point: zeno::Point) -> Point {
Point::new(point.x.into(), point.y.into())
}