piet_direct2d/
text.rs

1// Copyright 2019 the Piet Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Text functionality for Piet direct2d backend
5
6mod lines;
7
8use std::cell::{Cell, RefCell};
9use std::convert::TryInto;
10use std::fmt;
11use std::ops::{Range, RangeBounds};
12use std::rc::Rc;
13use std::slice;
14use std::sync::Arc;
15
16pub use dwrite::DwriteFactory;
17use dwrote::{CustomFontCollectionLoaderImpl, FontCollection, FontFile};
18use winapi::um::d2d1::D2D1_DRAW_TEXT_OPTIONS_NONE;
19use wio::wide::ToWide;
20
21use piet::kurbo::{Insets, Point, Rect, Size};
22use piet::util;
23use piet::{
24    Color, Error, FontFamily, HitTestPoint, HitTestPosition, LineMetric, RenderContext, Text,
25    TextAlignment, TextAttribute, TextLayout, TextLayoutBuilder, TextStorage,
26};
27
28use crate::D2DRenderContext;
29use crate::conv;
30use crate::dwrite::{self, TextFormat, Utf16Range};
31
32#[derive(Clone)]
33pub struct D2DText {
34    dwrite: DwriteFactory,
35    loaded_fonts: D2DLoadedFonts,
36}
37
38/// The set of loaded fonts, shared between `D2DText` instances.
39///
40/// If you are using piet in an application context, you should create
41/// one of these objects and use it anytime you create a `D2DText`.
42#[derive(Clone)]
43pub struct D2DLoadedFonts {
44    inner: Rc<RefCell<LoadedFontsInner>>,
45}
46
47impl Default for D2DLoadedFonts {
48    fn default() -> Self {
49        D2DLoadedFonts {
50            inner: Rc::new(RefCell::new(LoadedFontsInner::default())),
51        }
52    }
53}
54
55#[derive(Default)]
56struct LoadedFontsInner {
57    files: Vec<FontFile>,
58    // - multiple files can have the same family name, so we don't want this to be a set.
59    // - we assume a small number of custom fonts will be loaded; if that isn't true we
60    // should use a set or something.
61    names: Vec<FontFamily>,
62    collection: Option<FontCollection>,
63}
64
65#[derive(Clone)]
66pub struct D2DTextLayout {
67    text: Rc<dyn TextStorage>,
68    // currently calculated on build
69    line_metrics: Rc<[LineMetric]>,
70    size: Size,
71    trailing_ws_width: f64,
72    /// insets that, when applied to our layout rect, generates our inking/image rect.
73    inking_insets: Insets,
74    // this is in a refcell because we need to mutate it to set colors on first draw
75    layout: Rc<RefCell<dwrite::TextLayout>>,
76    // these two are used when the layout is empty, so we can still correctly
77    // draw the cursor
78    default_line_height: f64,
79    default_baseline: f64,
80    // colors are only added to the layout lazily, because we need access to d2d::DeviceContext
81    // in order to generate the brushes.
82    colors: Rc<[(Utf16Range, Color)]>,
83    needs_to_set_colors: Cell<bool>,
84}
85
86pub struct D2DTextLayoutBuilder {
87    text: Rc<dyn TextStorage>,
88    layout: Result<dwrite::TextLayout, Error>,
89    len_utf16: usize,
90    loaded_fonts: D2DLoadedFonts,
91    default_font: FontFamily,
92    default_font_size: f64,
93    colors: Vec<(Utf16Range, Color)>,
94    // just used to assert api is used as expected
95    last_range_start_pos: usize,
96}
97
98impl D2DText {
99    /// Create a new text factory.
100    ///
101    /// The `loaded_fonts` object is optional; if you pass `None` we will create a
102    /// new instance for you.
103    pub fn new_with_shared_fonts(
104        dwrite: DwriteFactory,
105        loaded_fonts: Option<D2DLoadedFonts>,
106    ) -> D2DText {
107        D2DText {
108            dwrite,
109            loaded_fonts: loaded_fonts.unwrap_or_default(),
110        }
111    }
112
113    #[cfg(test)]
114    pub fn new_for_test() -> D2DText {
115        let dwrite = DwriteFactory::new().unwrap();
116        D2DText::new_with_shared_fonts(dwrite, None)
117    }
118}
119
120impl fmt::Debug for D2DText {
121    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
122        f.debug_struct("D2DText").finish()
123    }
124}
125
126impl Text for D2DText {
127    type TextLayoutBuilder = D2DTextLayoutBuilder;
128    type TextLayout = D2DTextLayout;
129
130    fn font_family(&mut self, family_name: &str) -> Option<FontFamily> {
131        self.loaded_fonts
132            .inner
133            .borrow()
134            .get(family_name)
135            .or_else(|| {
136                self.dwrite
137                    .system_font_collection()
138                    .ok()
139                    .and_then(|fonts| fonts.font_family(family_name))
140            })
141    }
142
143    fn load_font(&mut self, data: &[u8]) -> Result<FontFamily, Error> {
144        self.loaded_fonts.inner.borrow_mut().add(data)
145    }
146
147    fn new_text_layout(&mut self, text: impl TextStorage) -> Self::TextLayoutBuilder {
148        let text = Rc::new(text);
149        let width = f32::INFINITY;
150        let wide_str = ToWide::to_wide(&text.as_str());
151        let is_rtl = util::first_strong_rtl(text.as_str());
152        let layout = TextFormat::new(&self.dwrite, [], util::DEFAULT_FONT_SIZE as f32, is_rtl)
153            .and_then(|format| dwrite::TextLayout::new(&self.dwrite, format, width, &wide_str))
154            .map_err(Into::into);
155
156        D2DTextLayoutBuilder {
157            layout,
158            text,
159            len_utf16: wide_str.len(),
160            colors: Vec::new(),
161            loaded_fonts: self.loaded_fonts.clone(),
162            default_font: FontFamily::default(),
163            default_font_size: piet::util::DEFAULT_FONT_SIZE,
164            last_range_start_pos: 0,
165        }
166    }
167}
168
169impl TextLayoutBuilder for D2DTextLayoutBuilder {
170    type Out = D2DTextLayout;
171
172    fn max_width(mut self, width: f64) -> Self {
173        let width = width.max(0.0);
174        let result = match self.layout.as_mut() {
175            Ok(layout) => layout.set_max_width(width),
176            Err(_) => Ok(()),
177        };
178        if let Err(err) = result {
179            self.layout = Err(err.into());
180        }
181        self
182    }
183
184    fn alignment(mut self, alignment: TextAlignment) -> Self {
185        if let Ok(layout) = self.layout.as_mut() {
186            layout.set_alignment(alignment);
187        }
188        self
189    }
190
191    fn default_attribute(mut self, attribute: impl Into<TextAttribute>) -> Self {
192        debug_assert!(
193            self.last_range_start_pos == 0,
194            "default attributes must be added before range attributes"
195        );
196        let attribute = attribute.into();
197        match &attribute {
198            TextAttribute::FontFamily(font) => self.default_font = font.clone(),
199            TextAttribute::FontSize(size) => self.default_font_size = *size,
200            _ => (),
201        }
202        self.add_attribute_shared(attribute, None);
203        self
204    }
205
206    fn range_attribute(
207        mut self,
208        range: impl RangeBounds<usize>,
209        attribute: impl Into<TextAttribute>,
210    ) -> Self {
211        let range = util::resolve_range(range, self.text.len());
212        let attribute = attribute.into();
213
214        debug_assert!(
215            range.start >= self.last_range_start_pos,
216            "attributes must be added in non-decreasing start order"
217        );
218        self.last_range_start_pos = range.start;
219        self.add_attribute_shared(attribute, Some(range));
220        self
221    }
222
223    fn build(self) -> Result<Self::Out, Error> {
224        let (default_line_height, default_baseline) =
225            self.get_default_line_height_and_baseline()?;
226        let layout = self.layout?;
227
228        let mut layout = D2DTextLayout {
229            text: self.text,
230            colors: self.colors.into(),
231            needs_to_set_colors: Cell::new(true),
232            line_metrics: Rc::new([]),
233            layout: Rc::new(RefCell::new(layout)),
234            size: Size::ZERO,
235            trailing_ws_width: 0.0,
236            inking_insets: Insets::ZERO,
237            default_line_height,
238            default_baseline,
239        };
240        layout.rebuild_metrics();
241        Ok(layout)
242    }
243}
244
245impl fmt::Debug for D2DTextLayoutBuilder {
246    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
247        f.debug_struct("D2DTextLayoutBuilder").finish()
248    }
249}
250
251impl D2DTextLayoutBuilder {
252    /// used for both range and default attributes
253    fn add_attribute_shared(&mut self, attr: TextAttribute, range: Option<Range<usize>>) {
254        if let Ok(layout) = self.layout.as_mut() {
255            let utf16_range = match range {
256                Some(range) => {
257                    let start = util::count_utf16(&self.text[..range.start]);
258                    let len = if range.end == self.text.len() {
259                        self.len_utf16
260                    } else {
261                        util::count_utf16(&self.text[range])
262                    };
263                    Utf16Range::new(start, len)
264                }
265                None => Utf16Range::new(0, self.len_utf16),
266            };
267
268            match attr {
269                TextAttribute::FontFamily(font) => {
270                    let is_custom = self.loaded_fonts.inner.borrow().contains(&font);
271                    if is_custom {
272                        let mut loaded = self.loaded_fonts.inner.borrow_mut();
273                        layout.set_font_collection(utf16_range, loaded.collection());
274                    } else if !self.loaded_fonts.inner.borrow().is_empty() {
275                        // if we are using custom fonts we also need to set the collection
276                        // back to the system collection explicitly as needed
277                        layout.set_font_collection(utf16_range, &FontCollection::system());
278                    }
279                    let family_name = resolve_family_name(&font);
280                    layout.set_font_family(utf16_range, family_name);
281                }
282                TextAttribute::FontSize(size) => layout.set_size(utf16_range, size as f32),
283                TextAttribute::Weight(weight) => layout.set_weight(utf16_range, weight),
284                TextAttribute::Style(style) => layout.set_style(utf16_range, style),
285                TextAttribute::Underline(flag) => layout.set_underline(utf16_range, flag),
286                TextAttribute::Strikethrough(flag) => layout.set_strikethrough(utf16_range, flag),
287                TextAttribute::TextColor(color) => self.colors.push((utf16_range, color)),
288            }
289        }
290    }
291
292    fn get_default_line_height_and_baseline(&self) -> Result<(f64, f64), Error> {
293        let family_name = resolve_family_name(&self.default_font);
294        let is_custom = self
295            .loaded_fonts
296            .inner
297            .borrow()
298            .contains(&self.default_font);
299        let family = if is_custom {
300            let mut loaded = self.loaded_fonts.inner.borrow_mut();
301            loaded.collection().font_family_by_name(family_name)
302        } else {
303            FontCollection::system().font_family_by_name(family_name)
304        };
305
306        let family = match family {
307            Ok(Some(family)) => family,
308            // absolute fallback; use font size as line height
309            Ok(None) => return Ok((self.default_font_size, self.default_font_size * 0.8)),
310            Err(_) => return Err(Error::FontLoadingFailed),
311        };
312
313        let font = family
314            .first_matching_font(
315                dwrote::FontWeight::Regular,
316                dwrote::FontStretch::Normal,
317                dwrote::FontStyle::Normal,
318            )
319            .map_err(|_| Error::FontLoadingFailed)?;
320        let metrics = font.metrics().metrics0();
321        let ascent = metrics.ascent as f64;
322        let vert_metrics = ascent + metrics.descent as f64 + metrics.lineGap as f64;
323        let vert_fraction = vert_metrics / metrics.designUnitsPerEm as f64;
324        let ascent_fraction = ascent / metrics.designUnitsPerEm as f64;
325
326        let line_height = self.default_font_size * vert_fraction;
327        let baseline = self.default_font_size * ascent_fraction;
328
329        Ok((line_height, baseline))
330    }
331}
332
333impl fmt::Debug for D2DTextLayout {
334    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
335        f.debug_struct("D2DTextLayout").finish()
336    }
337}
338
339impl TextLayout for D2DTextLayout {
340    fn size(&self) -> Size {
341        self.size
342    }
343
344    fn trailing_whitespace_width(&self) -> f64 {
345        self.trailing_ws_width
346    }
347
348    fn image_bounds(&self) -> Rect {
349        self.size.to_rect() + self.inking_insets
350    }
351
352    fn text(&self) -> &str {
353        &self.text
354    }
355
356    fn line_text(&self, line_number: usize) -> Option<&str> {
357        self.line_metrics
358            .get(line_number)
359            .map(|lm| &self.text[lm.range()])
360    }
361
362    fn line_metric(&self, line_number: usize) -> Option<LineMetric> {
363        if line_number == 0 && self.text.is_empty() {
364            Some(LineMetric {
365                baseline: self.default_baseline,
366                height: self.default_line_height,
367                ..Default::default()
368            })
369        } else {
370            self.line_metrics.get(line_number).cloned()
371        }
372    }
373
374    fn line_count(&self) -> usize {
375        self.line_metrics.len()
376    }
377
378    fn hit_test_point(&self, point: Point) -> HitTestPoint {
379        // lossy from f64 to f32, but shouldn't have too much impact
380        let htp = self
381            .layout
382            .borrow()
383            .hit_test_point(point.x as f32, point.y as f32);
384
385        // Round up to next grapheme cluster boundary if DirectWrite
386        // reports a trailing hit.
387        let text_position_16 = if htp.is_trailing_hit {
388            htp.metrics.text_position + htp.metrics.length
389        } else {
390            htp.metrics.text_position
391        } as usize;
392
393        // Convert text position from utf-16 code units to utf-8 code units.
394        let text_position =
395            util::count_until_utf16(&self.text, text_position_16).unwrap_or(self.text.len());
396
397        HitTestPoint::new(text_position, htp.is_inside)
398    }
399
400    // Can panic if text position is not at a code point boundary, or if it's out of bounds.
401    fn hit_test_text_position(&self, idx: usize) -> HitTestPosition {
402        let idx = idx.min(self.text.len());
403        assert!(self.text.is_char_boundary(idx));
404
405        if self.text.is_empty() {
406            return HitTestPosition::new(Point::new(0., self.default_baseline), 0);
407        }
408        // Note: DirectWrite will just return the line width if text position is
409        // out of bounds. This is what want for piet; return line width for the last text position
410        // (equal to line.len()). This is basically returning line width for the last cursor
411        // position.
412
413        let trailing = false;
414        let idx_16 = util::count_utf16(&self.text[..idx]);
415        let line = util::line_number_for_position(&self.line_metrics, idx);
416        // Maximum string length on Windows is 32bits; nothing we can do here.
417        let idx_16: u32 = idx_16.try_into().unwrap();
418
419        let mut hit_point = self
420            .layout
421            .borrow()
422            .hit_test_text_position(idx_16, trailing)
423            .map(|hit| Point::new(hit.point_x as f64, hit.point_y as f64))
424            // if DWrite fails we just return 0, 0
425            .unwrap_or_default();
426        // Raw reported point is top of glyph run box; move to baseline.
427        if let Some(metric) = self.line_metrics.get(line) {
428            hit_point.y = metric.y_offset + metric.baseline;
429        }
430        HitTestPosition::new(hit_point, line)
431    }
432}
433
434impl D2DTextLayout {
435    // must be called after build and after updating the width
436    fn rebuild_metrics(&mut self) {
437        let line_metrics = lines::fetch_line_metrics(&self.text, &self.layout.borrow());
438        let text_metrics = self.layout.borrow().get_metrics();
439        let overhang = self.layout.borrow().get_overhang_metrics();
440
441        let size = Size::new(text_metrics.width as f64, text_metrics.height as f64);
442        let overhang_width = text_metrics.layoutWidth as f64 + overhang.x1;
443        let overhang_height = text_metrics.layoutHeight as f64 + overhang.y1;
444
445        let inking_insets = Insets::new(
446            overhang.x0,
447            overhang.y0,
448            overhang_width - size.width,
449            overhang_height - size.height,
450        );
451
452        self.size = size;
453        self.trailing_ws_width = text_metrics.widthIncludingTrailingWhitespace as f64;
454        if self.text.is_empty() {
455            self.size.height = self.default_line_height;
456        }
457        self.line_metrics = line_metrics.into();
458        self.inking_insets = inking_insets;
459    }
460
461    pub fn draw(&self, pos: Point, ctx: &mut D2DRenderContext) {
462        if !self.text.is_empty() {
463            self.resolve_colors_if_needed(ctx);
464            let pos = conv::to_point2f(pos);
465            let black_brush = ctx.solid_brush(Color::BLACK);
466            let text_options = D2D1_DRAW_TEXT_OPTIONS_NONE;
467            ctx.rt
468                .draw_text_layout(pos, &self.layout.borrow(), &black_brush, text_options);
469        }
470    }
471
472    fn resolve_colors_if_needed(&self, ctx: &mut D2DRenderContext) {
473        if self.needs_to_set_colors.replace(false) {
474            for (range, color) in self.colors.as_ref() {
475                let brush = ctx.solid_brush(*color);
476                self.layout.borrow_mut().set_foreground_brush(*range, brush)
477            }
478        }
479    }
480}
481
482//  this is not especially robust, but all of these are preinstalled on win 7+
483fn resolve_family_name(family: &FontFamily) -> &str {
484    match family {
485        f if f == &FontFamily::SYSTEM_UI || f == &FontFamily::SANS_SERIF => "Segoe UI",
486        f if f == &FontFamily::SERIF => "Times New Roman",
487        f if f == &FontFamily::MONOSPACE => "Consolas",
488        other => other.name(),
489    }
490}
491
492impl LoadedFontsInner {
493    fn add(&mut self, font_data: &[u8]) -> Result<FontFamily, Error> {
494        let font_data: Arc<Vec<u8>> = Arc::new(font_data.to_owned());
495        let font_file = FontFile::new_from_buffer(font_data).ok_or(Error::FontLoadingFailed)?;
496        let collection_loader = CustomFontCollectionLoaderImpl::new(slice::from_ref(&font_file));
497        let collection = FontCollection::from_loader(collection_loader);
498        let mut families = collection.families_iter();
499        let first_fam_name = families
500            .next()
501            .and_then(|f| f.family_name().ok())
502            .ok_or(Error::FontLoadingFailed)?;
503        // just being defensive:
504        let remaining_family_names = families
505            .map(|f| f.family_name())
506            .collect::<Result<Vec<String>, i32>>()
507            .map_err(|_| Error::FontLoadingFailed)?;
508        if remaining_family_names
509            .into_iter()
510            .any(|n| n != first_fam_name)
511        {
512            eprintln!("loaded font contains multiple family names");
513        }
514
515        let fam_name = FontFamily::new_unchecked(first_fam_name);
516        self.files.push(font_file);
517        self.names.push(fam_name.clone());
518        Ok(fam_name)
519    }
520
521    fn get(&self, family_name: &str) -> Option<FontFamily> {
522        self.names
523            .iter()
524            .find(|fam| fam.name() == family_name)
525            .cloned()
526    }
527
528    fn contains(&self, family: &FontFamily) -> bool {
529        self.names.contains(family)
530    }
531
532    fn is_empty(&self) -> bool {
533        self.files.is_empty()
534    }
535
536    fn collection(&mut self) -> &FontCollection {
537        if self.collection.is_none() {
538            let loader = CustomFontCollectionLoaderImpl::new(self.files.as_slice());
539            let collection = FontCollection::from_loader(loader);
540            self.collection = Some(collection);
541        }
542        self.collection.as_ref().unwrap()
543    }
544}
545
546#[cfg(test)]
547mod test {
548    use super::*;
549
550    macro_rules! assert_close {
551        ($val:expr, $target:expr, $tolerance:expr) => {{
552            let min = $target - $tolerance;
553            let max = $target + $tolerance;
554            if $val < min || $val > max {
555                panic!(
556                    "value {} outside target {} with tolerance {}",
557                    $val, $target, $tolerance
558                );
559            }
560        }};
561
562        ($val:expr, $target:expr, $tolerance:expr,) => {{ assert_close!($val, $target, $tolerance) }};
563    }
564
565    #[test]
566    fn layout_size() {
567        let a_font = FontFamily::new_unchecked("Segoe UI");
568        let mut factory = D2DText::new_for_test();
569        let empty_layout = factory
570            .new_text_layout("")
571            .font(a_font.clone(), 22.0)
572            .build()
573            .unwrap();
574        let layout = factory
575            .new_text_layout("hello")
576            .font(a_font, 22.0)
577            .build()
578            .unwrap();
579
580        assert_close!(empty_layout.size().height, layout.size().height, 1e-6);
581    }
582
583    #[test]
584    #[allow(clippy::float_cmp)]
585    fn hit_test_empty_string() {
586        let a_font = FontFamily::new_unchecked("Segoe UI");
587        let layout = D2DText::new_for_test()
588            .new_text_layout("")
589            .font(a_font, 12.0)
590            .build()
591            .unwrap();
592        let pt = layout.hit_test_point(Point::new(0.0, 0.0));
593        assert_eq!(pt.idx, 0);
594        let pos = layout.hit_test_text_position(0);
595        assert_eq!(pos.point.x, 0.0);
596        assert_close!(pos.point.y, 10.0, 3.0);
597        let line = layout.line_metric(0).unwrap();
598        assert_close!(line.height, 14.0, 3.0);
599    }
600
601    #[test]
602    fn newline_text() {
603        let layout = D2DText::new_for_test()
604            .new_text_layout("A\nB")
605            .build()
606            .unwrap();
607        assert_eq!(layout.line_count(), 2);
608        assert_eq!(layout.line_text(0), Some("A\n"));
609        assert_eq!(layout.line_text(1), Some("B"));
610    }
611
612    #[test]
613    fn test_hit_test_text_position_basic() {
614        let mut text_layout = D2DText::new_for_test();
615
616        let input = "piet text!";
617        let font = text_layout.font_family("Segoe UI").unwrap();
618
619        let layout = text_layout
620            .new_text_layout(&input[0..4])
621            .font(font.clone(), 12.0)
622            .build()
623            .unwrap();
624        let piet_width = layout.size().width;
625
626        let layout = text_layout
627            .new_text_layout(&input[0..3])
628            .font(font.clone(), 12.0)
629            .build()
630            .unwrap();
631        let pie_width = layout.size().width;
632
633        let layout = text_layout
634            .new_text_layout(&input[0..2])
635            .font(font.clone(), 12.0)
636            .build()
637            .unwrap();
638        let pi_width = layout.size().width;
639
640        let layout = text_layout
641            .new_text_layout(&input[0..1])
642            .font(font.clone(), 12.0)
643            .build()
644            .unwrap();
645        let p_width = layout.size().width;
646
647        let layout = text_layout
648            .new_text_layout("")
649            .font(font.clone(), 12.0)
650            .build()
651            .unwrap();
652        let null_width = layout.size().width;
653
654        let full_layout = text_layout
655            .new_text_layout(input)
656            .font(font, 12.0)
657            .build()
658            .unwrap();
659        let full_width = full_layout.size().width;
660
661        assert_close!(
662            full_layout.hit_test_text_position(4).point.x,
663            piet_width,
664            3.0,
665        );
666        assert_close!(
667            full_layout.hit_test_text_position(3).point.x,
668            pie_width,
669            3.0,
670        );
671        assert_close!(full_layout.hit_test_text_position(2).point.x, pi_width, 3.0,);
672        assert_close!(full_layout.hit_test_text_position(1).point.x, p_width, 3.0,);
673        assert_close!(
674            full_layout.hit_test_text_position(0).point.x,
675            null_width,
676            3.0,
677        );
678        assert_close!(
679            full_layout.hit_test_text_position(10).point.x,
680            full_width,
681            3.0,
682        );
683    }
684
685    #[test]
686    fn test_hit_test_text_position_complex_0() {
687        let mut text_layout = D2DText::new_for_test();
688
689        let input = "é";
690        assert_eq!(input.len(), 2);
691
692        let font = text_layout.font_family("Segoe UI").unwrap();
693        let layout = text_layout
694            .new_text_layout(input)
695            .font(font, 12.0)
696            .build()
697            .unwrap();
698
699        assert_close!(layout.hit_test_text_position(0).point.x, 0.0, 3.0);
700        assert_close!(
701            layout.hit_test_text_position(2).point.x,
702            layout.size().width,
703            3.0,
704        );
705
706        // unicode segmentation is wrong on this one for now.
707        //let input = "🤦\u{1f3fc}\u{200d}\u{2642}\u{fe0f}";
708
709        //let mut text_layout = D2DText::new();
710        //let font = text_layout.new_font_by_name("sans-serif", 12.0).build();
711        //let layout = text_layout.new_text_layout(&font, input, None).build();
712
713        //assert_eq!(input.graphemes(true).count(), 1);
714        //assert_eq!(layout.hit_test_text_position(0, true).map(|p| p.point_x), Some(layout.size().width));
715        //assert_eq!(input.len(), 17);
716
717        let input = "\u{0023}\u{FE0F}\u{20E3}"; // #️⃣
718        assert_eq!(input.len(), 7);
719        assert_eq!(input.chars().count(), 3);
720
721        let mut text_layout = D2DText::new_for_test();
722
723        let font = text_layout.font_family("Segoe UI").unwrap();
724        let layout = text_layout
725            .new_text_layout(input)
726            .font(font, 12.0)
727            .build()
728            .unwrap();
729
730        assert_close!(layout.hit_test_text_position(0).point.x, 0.0, 3.0);
731        assert_close!(
732            layout.hit_test_text_position(7).point.x,
733            layout.size().width,
734            3.0,
735        );
736
737        // note code unit not at grapheme boundary
738        assert_close!(layout.hit_test_text_position(1).point.x, 0.0, 3.0);
739    }
740
741    #[test]
742    fn test_hit_test_text_position_complex_1() {
743        let mut text_layout = D2DText::new_for_test();
744
745        // Notes on this input:
746        // 6 code points
747        // 7 utf-16 code units (1/1/1/1/1/2)
748        // 14 utf-8 code units (2/1/3/3/1/4)
749        // 4 graphemes
750        let input = "é\u{0023}\u{FE0F}\u{20E3}1\u{1D407}"; // #️⃣,, 𝐇
751        assert_eq!(input.len(), 14);
752
753        let font = text_layout.font_family("Segoe UI").unwrap();
754        let layout = text_layout
755            .new_text_layout(input)
756            .font(font, 12.0)
757            .build()
758            .unwrap();
759
760        let test_layout_0 = text_layout.new_text_layout(&input[0..2]).build().unwrap();
761        let test_layout_1 = text_layout.new_text_layout(&input[0..9]).build().unwrap();
762        let test_layout_2 = text_layout.new_text_layout(&input[0..10]).build().unwrap();
763
764        // Note: text position is in terms of utf8 code units
765        assert_close!(layout.hit_test_text_position(0).point.x, 0.0, 3.0);
766        assert_close!(
767            layout.hit_test_text_position(2).point.x,
768            test_layout_0.size().width,
769            3.0,
770        );
771        assert_close!(
772            layout.hit_test_text_position(9).point.x,
773            test_layout_1.size().width,
774            3.0,
775        );
776        assert_close!(
777            layout.hit_test_text_position(10).point.x,
778            test_layout_2.size().width,
779            3.0,
780        );
781        assert_close!(
782            layout.hit_test_text_position(14).point.x,
783            layout.size().width,
784            3.0,
785        );
786
787        // Code point boundaries, but not grapheme boundaries.
788        // Width should stay at the current grapheme boundary.
789        assert_close!(
790            layout.hit_test_text_position(3).point.x,
791            test_layout_0.size().width,
792            3.0,
793        );
794        assert_close!(
795            layout.hit_test_text_position(6).point.x,
796            test_layout_0.size().width,
797            3.0,
798        );
799    }
800
801    #[test]
802    fn test_hit_test_point_basic() {
803        let mut text_layout = D2DText::new_for_test();
804
805        let font = text_layout.font_family("Segoe UI").unwrap();
806        let layout = text_layout
807            .new_text_layout("piet text!")
808            .font(font, 12.0)
809            .build()
810            .unwrap();
811        println!("text pos 4: {:?}", layout.hit_test_text_position(4)); // 20.302734375
812        println!("text pos 5: {:?}", layout.hit_test_text_position(5)); // 23.58984375
813        println!("text pos 6: {:?}", layout.hit_test_text_position(6)); // 23.58984375
814
815        // test hit test point
816        // all inside
817        let pt = layout.hit_test_point(Point::new(21.0, 0.0));
818        assert_eq!(pt.idx, 4);
819        let pt = layout.hit_test_point(Point::new(22.0, 0.0));
820        assert_eq!(pt.idx, 5);
821        let pt = layout.hit_test_point(Point::new(23.0, 0.0));
822        assert_eq!(pt.idx, 5);
823        let pt = layout.hit_test_point(Point::new(24.0, 0.0));
824        assert_eq!(pt.idx, 5);
825        let pt = layout.hit_test_point(Point::new(25.0, 0.0));
826        assert_eq!(pt.idx, 5);
827
828        // outside
829        println!("layout_width: {:?}", layout.size().width); // 46.916015625
830
831        let pt = layout.hit_test_point(Point::new(48.0, 0.0));
832        assert_eq!(pt.idx, 10); // last text position
833        assert!(!pt.is_inside);
834
835        let pt = layout.hit_test_point(Point::new(-1.0, 0.0));
836        assert_eq!(pt.idx, 0); // first text position
837        assert!(!pt.is_inside);
838    }
839
840    #[test]
841    fn test_hit_test_point_complex() {
842        let mut text_layout = D2DText::new_for_test();
843
844        // Notes on this input:
845        // 6 code points
846        // 7 utf-16 code units (1/1/1/1/1/2)
847        // 14 utf-8 code units (2/1/3/3/1/4)
848        // 4 graphemes
849        let input = "é\u{0023}\u{FE0F}\u{20E3}1\u{1D407}"; // #️⃣,, 𝐇
850        let font = text_layout.font_family("Segoe UI").unwrap();
851        let layout = text_layout
852            .new_text_layout(input)
853            .font(font, 12.0)
854            .build()
855            .unwrap();
856        println!("text pos 2: {:?}", layout.hit_test_text_position(2)); // 6.275390625
857        println!("text pos 9: {:?}", layout.hit_test_text_position(9)); // 18.0
858        println!("text pos 10: {:?}", layout.hit_test_text_position(10)); // 24.46875
859        println!("text pos 14: {:?}", layout.hit_test_text_position(14)); // 33.3046875, line width
860
861        let pt = layout.hit_test_point(Point::new(2.0, 0.0));
862        assert_eq!(pt.idx, 0);
863        let pt = layout.hit_test_point(Point::new(4.0, 0.0));
864        assert_eq!(pt.idx, 2);
865        let pt = layout.hit_test_point(Point::new(7.0, 0.0));
866        assert_eq!(pt.idx, 2);
867        let pt = layout.hit_test_point(Point::new(10.0, 0.0));
868        assert_eq!(pt.idx, 2);
869        let pt = layout.hit_test_point(Point::new(14.0, 0.0));
870        assert_eq!(pt.idx, 9);
871        let pt = layout.hit_test_point(Point::new(18.0, 0.0));
872        assert_eq!(pt.idx, 9);
873        let pt = layout.hit_test_point(Point::new(19.0, 0.0));
874        assert_eq!(pt.idx, 9);
875        let pt = layout.hit_test_point(Point::new(23.0, 0.0));
876        assert_eq!(pt.idx, 10);
877        let pt = layout.hit_test_point(Point::new(25.0, 0.0));
878        assert_eq!(pt.idx, 10);
879        let pt = layout.hit_test_point(Point::new(32.0, 0.0));
880        assert_eq!(pt.idx, 14);
881        let pt = layout.hit_test_point(Point::new(35.0, 0.0));
882        assert_eq!(pt.idx, 14);
883    }
884
885    #[test]
886    fn test_basic_multiline() {
887        let input = "piet text most best";
888        let width_small = 30.0;
889
890        let mut text_layout = D2DText::new_for_test();
891        let font = text_layout.font_family("Segoe UI").unwrap();
892        let layout = text_layout
893            .new_text_layout(input)
894            .max_width(width_small)
895            .font(font, 12.0)
896            .build()
897            .unwrap();
898
899        assert_eq!(layout.line_count(), 4);
900        assert_eq!(layout.line_text(0), Some("piet "));
901        assert_eq!(layout.line_text(1), Some("text "));
902        assert_eq!(layout.line_text(2), Some("most "));
903        assert_eq!(layout.line_text(3), Some("best"));
904        assert_eq!(layout.line_text(4), None);
905    }
906
907    // NOTE be careful, windows will break lines at the sub-word level!
908    #[test]
909    fn test_multiline_hit_test_text_position_basic() {
910        let mut text_layout = D2DText::new_for_test();
911
912        let input = "piet  text!";
913        let font = text_layout.font_family("Segoe UI").unwrap();
914
915        let layout = text_layout
916            .new_text_layout(&input[0..4])
917            .font(font.clone(), 15.0)
918            .max_width(30.0)
919            .build()
920            .unwrap();
921        let piet_width = layout.size().width;
922
923        let layout = text_layout
924            .new_text_layout(&input[0..3])
925            .font(font.clone(), 15.0)
926            .max_width(30.0)
927            .build()
928            .unwrap();
929        let pie_width = layout.size().width;
930
931        let layout = text_layout.new_text_layout(&input[0..5]).build().unwrap();
932        let piet_space_width = layout.size().width;
933
934        // "text" should be on second line
935        let layout = text_layout
936            .new_text_layout(&input[6..10])
937            .font(font.clone(), 15.0)
938            .max_width(30.0)
939            .build()
940            .unwrap();
941        let text_width = layout.size().width;
942
943        let layout = text_layout
944            .new_text_layout(&input[6..9])
945            .font(font.clone(), 15.0)
946            .max_width(30.0)
947            .build()
948            .unwrap();
949        let tex_width = layout.size().width;
950
951        let layout = text_layout
952            .new_text_layout(&input[6..8])
953            .max_width(30.0)
954            .build()
955            .unwrap();
956        let te_width = layout.size().width;
957
958        let layout = text_layout
959            .new_text_layout(&input[6..7])
960            .font(font.clone(), 15.0)
961            .max_width(30.0)
962            .build()
963            .unwrap();
964        let t_width = layout.size().width;
965
966        let full_layout = text_layout
967            .new_text_layout(input)
968            .font(font, 15.0)
969            .max_width(30.0)
970            .build()
971            .unwrap();
972        println!("lm: {:#?}", full_layout.line_metrics);
973        println!("layout width: {:#?}", full_layout.size().width);
974
975        println!("'pie': {pie_width}");
976        println!("'piet': {piet_width}");
977        println!("'piet ': {piet_space_width}");
978        println!("'text': {text_width}");
979        println!("'tex': {tex_width}");
980        println!("'te': {te_width}");
981        println!("'t': {t_width}");
982
983        // NOTE these heights are representative of baseline-to-baseline measures
984        let line_zero_metric = full_layout.line_metric(0).unwrap();
985        let line_one_metric = full_layout.line_metric(1).unwrap();
986        let line_zero_baseline = line_zero_metric.y_offset + line_zero_metric.baseline;
987        let line_one_baseline = line_one_metric.y_offset + line_one_metric.baseline;
988
989        // these just test the x position of text positions on the second line
990        assert_close!(
991            full_layout.hit_test_text_position(10).point.x,
992            text_width,
993            3.0,
994        );
995        assert_close!(
996            full_layout.hit_test_text_position(9).point.x,
997            tex_width,
998            3.0,
999        );
1000        assert_close!(full_layout.hit_test_text_position(8).point.x, te_width, 3.0,);
1001        assert_close!(full_layout.hit_test_text_position(7).point.x, t_width, 3.0,);
1002        // This should be beginning of second line
1003        assert_close!(full_layout.hit_test_text_position(6).point.x, 0.0, 3.0,);
1004
1005        assert_close!(
1006            full_layout.hit_test_text_position(3).point.x,
1007            pie_width,
1008            3.0,
1009        );
1010
1011        // This tests that hit-testing trailing whitespace can return points
1012        // outside of the layout's reported width.
1013        assert!(full_layout.hit_test_text_position(5).point.x > piet_space_width + 3.0,);
1014
1015        // These test y position of text positions on line 1 (0-index)
1016        assert_close!(
1017            full_layout.hit_test_text_position(10).point.y,
1018            line_one_baseline,
1019            3.0,
1020        );
1021        assert_close!(
1022            full_layout.hit_test_text_position(9).point.y,
1023            line_one_baseline,
1024            3.0,
1025        );
1026        assert_close!(
1027            full_layout.hit_test_text_position(8).point.y,
1028            line_one_baseline,
1029            3.0,
1030        );
1031        assert_close!(
1032            full_layout.hit_test_text_position(7).point.y,
1033            line_one_baseline,
1034            3.0,
1035        );
1036        assert_close!(
1037            full_layout.hit_test_text_position(6).point.y,
1038            line_one_baseline,
1039            3.0,
1040        );
1041
1042        // this tests y position of 0 line
1043        assert_close!(
1044            full_layout.hit_test_text_position(5).point.y,
1045            line_zero_baseline,
1046            3.0,
1047        );
1048        assert_close!(
1049            full_layout.hit_test_text_position(4).point.y,
1050            line_zero_baseline,
1051            3.0,
1052        );
1053    }
1054
1055    #[test]
1056    // very basic testing that multiline works
1057    fn test_multiline_hit_test_point_basic() {
1058        let input = "piet text most best";
1059
1060        let mut text = D2DText::new_for_test();
1061
1062        let font = text.font_family("Segoe UI").unwrap();
1063        // this should break into four lines
1064        let layout = text
1065            .new_text_layout(input)
1066            .font(font, 12.0)
1067            .max_width(30.0)
1068            .build()
1069            .unwrap();
1070        println!("{}", layout.line_metric(0).unwrap().baseline); // 12.94...
1071        println!("text pos 01: {:?}", layout.hit_test_text_position(0)); // (0.0, 12.94)
1072        println!("text pos 06: {:?}", layout.hit_test_text_position(5)); // (0.0, 28.91...)
1073        println!("text pos 11: {:?}", layout.hit_test_text_position(10)); // (0.0, 44.87...)
1074        println!("text pos 16: {:?}", layout.hit_test_text_position(15)); // (0.0, 60.83...)
1075
1076        let pt = layout.hit_test_point(Point::new(1.0, -13.0)); // under
1077        assert_eq!(pt.idx, 0);
1078        assert!(!pt.is_inside);
1079        let pt = layout.hit_test_point(Point::new(1.0, 1.0));
1080        assert_eq!(pt.idx, 0);
1081        assert!(pt.is_inside);
1082        let pt = layout.hit_test_point(Point::new(1.0, 00.0));
1083        assert_eq!(pt.idx, 0);
1084        let pt = layout.hit_test_point(Point::new(1.0, 20.0));
1085        assert_eq!(pt.idx, 5);
1086        let pt = layout.hit_test_point(Point::new(1.0, 36.0));
1087        assert_eq!(pt.idx, 10);
1088        let pt = layout.hit_test_point(Point::new(1.0, 54.0));
1089        assert_eq!(pt.idx, 15);
1090
1091        // over on y axis, but x still affects the text position
1092        let best_layout = text.new_text_layout("best").build().unwrap();
1093        println!("layout width: {:#?}", best_layout.size().width); // 22.48...
1094
1095        let pt = layout.hit_test_point(Point::new(1.0, 68.0));
1096        assert_eq!(pt.idx, 15);
1097        assert!(!pt.is_inside);
1098
1099        let pt = layout.hit_test_point(Point::new(22.0, 68.0));
1100        assert_eq!(pt.idx, 19);
1101        assert!(!pt.is_inside);
1102
1103        let pt = layout.hit_test_point(Point::new(24.0, 68.0));
1104        assert_eq!(pt.idx, 19);
1105        assert!(!pt.is_inside);
1106
1107        // under
1108        let piet_layout = text.new_text_layout("piet ").build().unwrap();
1109        println!("layout width: {:#?}", piet_layout.size().width); // 23.58...
1110
1111        let pt = layout.hit_test_point(Point::new(1.0, -14.0)); // under
1112        assert_eq!(pt.idx, 0);
1113        assert!(!pt.is_inside);
1114
1115        let pt = layout.hit_test_point(Point::new(23.0, -14.0)); // under
1116        assert_eq!(pt.idx, 5);
1117        assert!(!pt.is_inside);
1118
1119        let pt = layout.hit_test_point(Point::new(27.0, -14.0)); // under
1120        assert_eq!(pt.idx, 5);
1121        assert!(!pt.is_inside);
1122    }
1123
1124    #[test]
1125    fn missing_font_is_missing() {
1126        let mut text = D2DText::new_for_test();
1127        assert!(text.font_family("A Quite Unlikely Font Ñame").is_none());
1128    }
1129}