piet_cosmic_text/
text_layout.rs

1// SPDX-License-Identifier: LGPL-3.0-or-later OR MPL-2.0
2// This file is a part of `piet-cosmic-text`.
3//
4// `piet-cosmic-text` is free software: you can redistribute it and/or modify it under the
5// terms of either:
6//
7// * GNU Lesser General Public License as published by the Free Software Foundation, either
8//   version 3 of the License, or (at your option) any later version.
9// * Mozilla Public License as published by the Mozilla Foundation, version 2.
10//
11// `piet-cosmic-text` is distributed in the hope that it will be useful, but WITHOUT ANY
12// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
13// PURPOSE. See the GNU Lesser General Public License or the Mozilla Public License for more
14// details.
15//
16// You should have received a copy of the GNU Lesser General Public License and the Mozilla
17// Public License along with `piet-cosmic-text`. If not, see <https://www.gnu.org/licenses/>.
18
19use crate::text::Text;
20
21use cosmic_text as ct;
22use ct::{Buffer, LayoutRunIter};
23
24use piet::kurbo::{Point, Rect, Size, Vec2};
25use piet::TextStorage;
26
27use swash::scale::image::Image as SwashImage;
28use swash::scale::outline::Outline as SwashOutline;
29use swash::scale::{ScaleContext, StrikeWith};
30use swash::zeno;
31
32use std::cell::Cell;
33use std::cmp;
34use std::collections::hash_map::{Entry, HashMap};
35use std::fmt;
36use std::rc::Rc;
37
38/// A text layout.
39#[derive(Clone)]
40pub struct TextLayout {
41    /// The text buffer.
42    text_buffer: Rc<BufferWrapper>,
43}
44
45impl fmt::Debug for TextLayout {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        f.debug_struct("TextLayout")
48            .field("string", &self.text_buffer.string.as_str())
49            .field("glyph_size", &self.text_buffer.glyph_size)
50            .finish_non_exhaustive()
51    }
52}
53
54struct BufferWrapper {
55    /// The original string.
56    string: Box<dyn TextStorage>,
57
58    /// The size of the glyph in pixels.
59    glyph_size: i32,
60
61    /// The original buffer.
62    buffer: Option<Buffer>,
63
64    /// Run metrics.
65    run_metrics: Vec<piet::LineMetric>,
66
67    /// Ink rectangle for the buffer.
68    ink_rectangle: Rect,
69
70    /// Logical extent for the buffer.
71    logical_size: Cell<Option<Size>>,
72
73    /// The text handle.
74    handle: Text,
75}
76
77impl BufferWrapper {
78    fn buffer(&self) -> &Buffer {
79        self.buffer.as_ref().unwrap()
80    }
81}
82
83impl Drop for BufferWrapper {
84    fn drop(&mut self) {
85        let mut buffer = self.buffer.take().unwrap();
86        buffer.lines.clear();
87        let old_lines = self.handle.take_buffer();
88
89        // Use whichever buffer has the most lines.
90        if old_lines.capacity() > buffer.lines.capacity() {
91            self.handle.set_buffer(old_lines);
92        } else {
93            self.handle.set_buffer(buffer.lines);
94        }
95    }
96}
97
98impl TextLayout {
99    /// Create a new `TextLayout`.
100    pub(crate) fn new(
101        text: Text,
102        buffer: Buffer,
103        string: Box<dyn TextStorage>,
104        glyph_size: i32,
105        font_system: &mut ct::FontSystem,
106    ) -> Self {
107        let span = trace_span!("TextLayout::new", string = %string.as_str());
108        let _guard = span.enter();
109
110        // Figure out the metrics.
111        let run_metrics = buffer
112            .layout_runs()
113            .map(|run| RunMetrics::new(run, glyph_size as f64))
114            .map(|RunMetrics { line_metric }| line_metric)
115            .collect();
116
117        // Scale up the buffers to get a good idea of the ink rectangle.
118        let mut ink_context = text.borrow_ink();
119        let mut missing_bbox_count = 0;
120
121        let bounding_boxes = buffer
122            .layout_runs()
123            .flat_map(|run| {
124                let run_y = run.line_y;
125                run.glyphs.iter().map(move |glyph| (glyph, run_y))
126            })
127            .filter_map(|(glyph, run_y)| {
128                let physical = glyph.physical((0., 0.), 1.);
129                let offset = Vec2::new(
130                    physical.x as f64 + physical.cache_key.x_bin.as_float() as f64,
131                    run_y as f64 + physical.y as f64 + physical.cache_key.y_bin.as_float() as f64,
132                );
133
134                // Figure out the bounding box.
135                match ink_context.bounding_box(&physical, font_system) {
136                    Some(mut rect) => {
137                        rect = rect + offset;
138                        Some(rect)
139                    }
140
141                    None => {
142                        missing_bbox_count += 1;
143                        None
144                    }
145                }
146            });
147        let ink_rectangle = bounding_rectangle(bounding_boxes);
148
149        if missing_bbox_count > 0 {
150            warn!("Missing {} bounding boxes", missing_bbox_count);
151        }
152
153        drop(ink_context);
154
155        Self {
156            text_buffer: Rc::new(BufferWrapper {
157                string,
158                glyph_size,
159                buffer: Some(buffer),
160                run_metrics,
161                handle: text,
162                ink_rectangle,
163                logical_size: Cell::new(None),
164            }),
165        }
166    }
167
168    /// Get a reference to the inner `Buffer`.
169    pub fn buffer(&self) -> &Buffer {
170        self.text_buffer.buffer()
171    }
172
173    /// Get an iterator over the layout runs.
174    pub fn layout_runs(&self) -> LayoutRunIter<'_> {
175        self.buffer().layout_runs()
176    }
177}
178
179impl piet::TextLayout for TextLayout {
180    fn size(&self) -> Size {
181        if let Some(size) = self.text_buffer.logical_size.get() {
182            return size;
183        }
184
185        let mut size = Size::new(f64::MIN, f64::MIN);
186
187        for run in self.layout_runs() {
188            let max = |a: f32, b: f64| {
189                let a: f64 = a.into();
190                if a < b {
191                    b
192                } else {
193                    a
194                }
195            };
196
197            size.width = max(run.line_w, size.width);
198            size.height = max(run.line_y, size.height);
199        }
200
201        self.text_buffer.logical_size.set(Some(size));
202
203        size
204    }
205
206    fn trailing_whitespace_width(&self) -> f64 {
207        // TODO: This doesn't matter I think.
208        self.size().width
209    }
210
211    fn image_bounds(&self) -> Rect {
212        self.text_buffer.ink_rectangle
213    }
214
215    fn text(&self) -> &str {
216        &self.text_buffer.string
217    }
218
219    fn line_text(&self, line_number: usize) -> Option<&str> {
220        self.buffer()
221            .layout_runs()
222            .nth(line_number)
223            .map(|run| run.text)
224    }
225
226    fn line_metric(&self, line_number: usize) -> Option<piet::LineMetric> {
227        self.text_buffer.run_metrics.get(line_number).cloned()
228    }
229
230    fn line_count(&self) -> usize {
231        self.buffer().layout_runs().count()
232    }
233
234    fn hit_test_point(&self, point: Point) -> piet::HitTestPoint {
235        let mut htp = piet::HitTestPoint::default();
236        let (x, y) = point.into();
237
238        if let Some(cursor) = self.buffer().hit(x as f32, y as f32) {
239            htp.idx = cursor.index;
240            htp.is_inside = true;
241            return htp;
242        }
243
244        let mut ink_context = self.text_buffer.handle.borrow_ink();
245        let mut font_system_guard = match self.text_buffer.handle.borrow_font_system() {
246            Some(system) => system,
247            None => {
248                warn!("Tried to borrow font system to calculate better hit test point, but it was already borrowed.");
249                htp.idx = 0;
250                htp.is_inside = false;
251                return htp;
252            }
253        };
254        let font_system = &mut font_system_guard
255            .get()
256            .expect("For a TextLayout to exist, the font system must have already been initialized")
257            .system;
258
259        // Look for the glyph with the closest distance to the point.
260        let mut closest_distance = f64::MAX;
261
262        for (glyph, physical_glyph) in self.layout_runs().flat_map(|run| {
263            let run_y = run.line_y;
264            run.glyphs
265                .iter()
266                .map(move |glyph| (glyph, glyph.physical((0., run_y), 1.)))
267        }) {
268            let bounding_box = match ink_context.bounding_box(&physical_glyph, font_system) {
269                Some(bbox) => bbox,
270                None => continue,
271            };
272
273            // If the point is inside of the bounding box, this is definitely it.
274            if bounding_box.contains(point) {
275                htp.idx = glyph.start;
276                htp.is_inside = false;
277                return htp;
278            }
279
280            // Otherwise, find the distance from the midpoint.
281            let midpoint = bounding_box.center();
282            let distance = midpoint.distance(point);
283            if distance < closest_distance {
284                closest_distance = distance;
285                htp.idx = glyph.start;
286            }
287        }
288
289        // If we didn't find anything, just return the closest index.
290        htp.is_inside = false;
291        htp
292    }
293
294    fn hit_test_text_position(&self, idx: usize) -> piet::HitTestPosition {
295        // Iterator over glyphs and their assorted lines.
296        let mut lines_and_glyphs = self.layout_runs().enumerate().flat_map(|(line, run)| {
297            run.glyphs.iter().map(move |glyph| {
298                (
299                    line,
300                    {
301                        // Get the point.
302                        let physical = glyph.physical((0.0, 0.0), 1.0);
303                        let x = physical.x as f64;
304                        let y = run.line_y as f64
305                            + physical.y as f64
306                            + self.text_buffer.glyph_size as f64;
307
308                        Point::new(x, y)
309                    },
310                    glyph.start..glyph.end,
311                )
312            })
313        });
314
315        let (line, point, _) = match lines_and_glyphs.find(|(_, _, range)| range.contains(&idx)) {
316            Some(x) => x,
317            None => {
318                // TODO: What are you supposed to do here?
319                return piet::HitTestPosition::default();
320            }
321        };
322
323        let mut htp = piet::HitTestPosition::default();
324        htp.point = point;
325        htp.line = line;
326        htp
327    }
328}
329
330fn bounding_rectangle(rects: impl IntoIterator<Item = Rect>) -> Rect {
331    let mut iter = rects.into_iter();
332    let mut sum_rect = match iter.next() {
333        Some(rect) => rect,
334        None => return Rect::ZERO,
335    };
336
337    for rect in iter {
338        if rect.x0 < sum_rect.x0 {
339            sum_rect.x0 = rect.x0;
340        }
341        if rect.y0 < sum_rect.y0 {
342            sum_rect.y0 = rect.y0;
343        }
344        if rect.x1 > sum_rect.x1 {
345            sum_rect.x1 = rect.x1;
346        }
347        if rect.y1 > sum_rect.y1 {
348            sum_rect.y1 = rect.y1;
349        }
350    }
351
352    sum_rect
353}
354
355/// Line metrics associated with a layout run.
356struct RunMetrics {
357    /// The `piet` line metrics.
358    line_metric: piet::LineMetric,
359}
360
361impl RunMetrics {
362    fn new(run: ct::LayoutRun<'_>, glyph_size: f64) -> RunMetrics {
363        let (start_offset, end_offset) = run.glyphs.iter().fold((0, 0), |(start, end), glyph| {
364            (cmp::min(start, glyph.start), cmp::max(end, glyph.end))
365        });
366
367        let y_offset = run.line_top.into();
368        let baseline = run.line_y as f64 - run.line_top as f64;
369
370        RunMetrics {
371            line_metric: piet::LineMetric {
372                start_offset,
373                end_offset,
374                trailing_whitespace: 0, // TODO
375                y_offset,
376                height: glyph_size as _,
377                baseline,
378            },
379        }
380    }
381}
382
383/// State for calculating the ink rectangle.
384pub(crate) struct InkRectangleState {
385    /// The swash scaling context.
386    scaler: ScaleContext,
387
388    /// Cache between fonts, glyphs and their bounding boxes.
389    bbox_cache: HashMap<ct::CacheKey, Option<Rect>>,
390
391    /// Swash image buffer.
392    swash_image: SwashImage,
393
394    /// Swash outline buffer.
395    swash_outline: SwashOutline,
396}
397
398impl InkRectangleState {
399    pub(crate) fn new() -> Self {
400        Self {
401            scaler: ScaleContext::new(),
402            bbox_cache: HashMap::new(),
403            swash_image: SwashImage::new(),
404            swash_outline: SwashOutline::new(),
405        }
406    }
407
408    /// Get the bounding box for a glyph.
409    fn bounding_box(
410        &mut self,
411        glyph: &ct::PhysicalGlyph,
412        system: &mut ct::FontSystem,
413    ) -> Option<Rect> {
414        // If we already have the bounding box here, return it.
415        let entry = match self.bbox_cache.entry(glyph.cache_key) {
416            Entry::Occupied(o) => return *o.into_mut(),
417            Entry::Vacant(v) => v,
418        };
419
420        let mut bbox = None;
421
422        // Find the font.
423        if let Some(font) = system.get_font(glyph.cache_key.font_id) {
424            // Create a scaler for this font.
425            let mut scaler = self
426                .scaler
427                .builder(font.as_swash())
428                .size(f32::from_bits(glyph.cache_key.font_size_bits))
429                .build();
430
431            // See if we can get an outline.
432            self.swash_outline.clear();
433            if scaler.scale_outline_into(glyph.cache_key.glyph_id, &mut self.swash_outline) {
434                bbox = Some(cvt_bounds(self.swash_outline.bounds()));
435            } else {
436                // See if we can get a bitmap.
437                self.swash_image.clear();
438                if scaler.scale_bitmap_into(
439                    glyph.cache_key.glyph_id,
440                    StrikeWith::BestFit,
441                    &mut self.swash_image,
442                ) {
443                    bbox = Some(cvt_placement(self.swash_image.placement));
444                }
445            }
446        }
447
448        // Cache the result.
449        *entry.insert(bbox)
450    }
451}
452
453fn cvt_placement(placement: zeno::Placement) -> Rect {
454    Rect::new(
455        placement.left.into(),
456        -placement.top as f64,
457        placement.left as f64 + placement.width as f64,
458        -placement.top as f64 + placement.height as f64,
459    )
460}
461
462fn cvt_bounds(mut bounds: zeno::Bounds) -> Rect {
463    bounds.min.y *= -1.0;
464    bounds.max.y *= -1.0;
465    Rect::from_points(cvt_point(bounds.min), cvt_point(bounds.max))
466}
467
468fn cvt_point(point: zeno::Point) -> Point {
469    Point::new(point.x.into(), point.y.into())
470}