1mod 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#[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 names: Vec<FontFamily>,
62 collection: Option<FontCollection>,
63}
64
65#[derive(Clone)]
66pub struct D2DTextLayout {
67 text: Rc<dyn TextStorage>,
68 line_metrics: Rc<[LineMetric]>,
70 size: Size,
71 trailing_ws_width: f64,
72 inking_insets: Insets,
74 layout: Rc<RefCell<dwrite::TextLayout>>,
76 default_line_height: f64,
79 default_baseline: f64,
80 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 last_range_start_pos: usize,
96}
97
98impl D2DText {
99 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 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 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 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 let htp = self
381 .layout
382 .borrow()
383 .hit_test_point(point.x as f32, point.y as f32);
384
385 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 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 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 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 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 .unwrap_or_default();
426 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 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
482fn 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 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 let input = "\u{0023}\u{FE0F}\u{20E3}"; 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 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 let input = "é\u{0023}\u{FE0F}\u{20E3}1\u{1D407}"; 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 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 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)); println!("text pos 5: {:?}", layout.hit_test_text_position(5)); println!("text pos 6: {:?}", layout.hit_test_text_position(6)); 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 println!("layout_width: {:?}", layout.size().width); let pt = layout.hit_test_point(Point::new(48.0, 0.0));
832 assert_eq!(pt.idx, 10); assert!(!pt.is_inside);
834
835 let pt = layout.hit_test_point(Point::new(-1.0, 0.0));
836 assert_eq!(pt.idx, 0); 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 let input = "é\u{0023}\u{FE0F}\u{20E3}1\u{1D407}"; 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)); println!("text pos 9: {:?}", layout.hit_test_text_position(9)); println!("text pos 10: {:?}", layout.hit_test_text_position(10)); println!("text pos 14: {:?}", layout.hit_test_text_position(14)); 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 #[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 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 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 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 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 assert!(full_layout.hit_test_text_position(5).point.x > piet_space_width + 3.0,);
1014
1015 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 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 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 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); println!("text pos 01: {:?}", layout.hit_test_text_position(0)); println!("text pos 06: {:?}", layout.hit_test_text_position(5)); println!("text pos 11: {:?}", layout.hit_test_text_position(10)); println!("text pos 16: {:?}", layout.hit_test_text_position(15)); let pt = layout.hit_test_point(Point::new(1.0, -13.0)); 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 let best_layout = text.new_text_layout("best").build().unwrap();
1093 println!("layout width: {:#?}", best_layout.size().width); 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 let piet_layout = text.new_text_layout("piet ").build().unwrap();
1109 println!("layout width: {:#?}", piet_layout.size().width); let pt = layout.hit_test_point(Point::new(1.0, -14.0)); assert_eq!(pt.idx, 0);
1113 assert!(!pt.is_inside);
1114
1115 let pt = layout.hit_test_point(Point::new(23.0, -14.0)); assert_eq!(pt.idx, 5);
1117 assert!(!pt.is_inside);
1118
1119 let pt = layout.hit_test_point(Point::new(27.0, -14.0)); 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}