1mod grapheme;
7mod lines;
8
9use std::borrow::Cow;
10use std::fmt;
11use std::ops::RangeBounds;
12use std::rc::Rc;
13
14use web_sys::CanvasRenderingContext2d;
15
16use piet::kurbo::{Point, Rect, Size};
17
18use piet::{
19 Color, Error, FontFamily, HitTestPoint, HitTestPosition, LineMetric, Text, TextAttribute,
20 TextLayout, TextLayoutBuilder, TextStorage, util,
21};
22use unicode_segmentation::UnicodeSegmentation;
23
24use self::grapheme::{get_grapheme_boundaries, point_x_in_grapheme};
25use crate::WebText;
26
27#[derive(Clone)]
28pub struct WebFont {
29 family: FontFamily,
30 weight: u32,
31 style: FontStyle,
32 size: f64,
33}
34
35#[derive(Clone)]
36pub struct WebTextLayout {
37 ctx: CanvasRenderingContext2d,
38 pub(crate) font: WebFont,
39 pub(crate) text: Rc<dyn TextStorage>,
40
41 pub(crate) line_metrics: Vec<LineMetric>,
43 size: Size,
44 trailing_ws_width: f64,
45 color: Color,
46}
47
48pub struct WebTextLayoutBuilder {
49 ctx: CanvasRenderingContext2d,
50 text: Rc<dyn TextStorage>,
51 width: f64,
52 defaults: util::LayoutDefaults,
53}
54
55#[derive(Clone)]
57enum FontStyle {
58 Normal,
59 Italic,
60 #[allow(dead_code)] Oblique(Option<f64>),
62}
63
64impl Text for WebText {
65 type TextLayout = WebTextLayout;
66 type TextLayoutBuilder = WebTextLayoutBuilder;
67
68 fn font_family(&mut self, family_name: &str) -> Option<FontFamily> {
69 Some(FontFamily::new_unchecked(family_name))
70 }
71
72 fn load_font(&mut self, _data: &[u8]) -> Result<FontFamily, Error> {
73 Err(Error::Unimplemented)
74 }
75
76 fn new_text_layout(&mut self, text: impl TextStorage) -> Self::TextLayoutBuilder {
77 WebTextLayoutBuilder {
78 ctx: self.ctx.clone(),
81 text: Rc::new(text),
82 width: f64::INFINITY,
83 defaults: Default::default(),
84 }
85 }
86}
87
88impl fmt::Debug for WebText {
89 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
90 f.debug_struct("WebText").finish()
91 }
92}
93
94impl WebFont {
95 fn new(family: FontFamily) -> Self {
96 WebFont {
97 family,
98 style: FontStyle::Normal,
99 size: piet::util::DEFAULT_FONT_SIZE,
100 weight: 400,
101 }
102 }
103
104 fn with_style(mut self, style: piet::FontStyle) -> Self {
105 let style = if style == piet::FontStyle::Italic {
106 FontStyle::Italic
107 } else {
108 FontStyle::Normal
109 };
110
111 self.style = style;
112 self
113 }
114
115 fn with_weight(mut self, weight: piet::FontWeight) -> Self {
116 self.weight = weight.to_raw() as u32;
117 self
118 }
119
120 fn with_size(mut self, size: f64) -> Self {
121 self.size = size;
122 self
123 }
124
125 pub(crate) fn get_font_string(&self) -> String {
126 let style_str = match self.style {
127 FontStyle::Normal => Cow::from("normal"),
128 FontStyle::Italic => Cow::from("italic"),
129 FontStyle::Oblique(None) => Cow::from("italic"),
130 FontStyle::Oblique(Some(angle)) => Cow::from(format!("oblique {angle}deg")),
131 };
132 format!(
133 "{} {} {}px \"{}\"",
134 style_str,
135 self.weight,
136 self.size,
137 self.family.name()
138 )
139 }
140}
141
142impl TextLayoutBuilder for WebTextLayoutBuilder {
143 type Out = WebTextLayout;
144
145 fn max_width(mut self, width: f64) -> Self {
146 self.width = width;
147 self
148 }
149
150 fn alignment(self, _alignment: piet::TextAlignment) -> Self {
151 web_sys::console::log_1(&"TextLayout alignment unsupported on web".into());
152 self
153 }
154
155 fn default_attribute(mut self, attribute: impl Into<TextAttribute>) -> Self {
156 self.defaults.set(attribute);
157 self
158 }
159
160 fn range_attribute(
161 self,
162 _range: impl RangeBounds<usize>,
163 _attribute: impl Into<TextAttribute>,
164 ) -> Self {
165 web_sys::console::log_1(&"Text attributes not yet implemented for web".into());
166 self
167 }
168
169 fn build(self) -> Result<Self::Out, Error> {
170 let font = WebFont::new(self.defaults.font)
171 .with_size(self.defaults.font_size)
172 .with_weight(self.defaults.weight)
173 .with_style(self.defaults.style);
174
175 let mut layout = WebTextLayout {
176 ctx: self.ctx,
177 font,
178 text: self.text,
179 line_metrics: Vec::new(),
180 size: Size::ZERO,
181 trailing_ws_width: 0.0,
182 color: self.defaults.fg_color,
183 };
184
185 layout.update_width(self.width);
186 Ok(layout)
187 }
188}
189
190impl fmt::Debug for WebTextLayoutBuilder {
191 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
192 f.debug_struct("WebTextLayoutBuilder").finish()
193 }
194}
195
196impl TextLayout for WebTextLayout {
197 fn size(&self) -> Size {
198 self.size
199 }
200
201 fn trailing_whitespace_width(&self) -> f64 {
202 self.trailing_ws_width
203 }
204
205 fn image_bounds(&self) -> Rect {
206 self.size.to_rect()
208 }
209
210 fn text(&self) -> &str {
211 &self.text
212 }
213
214 fn line_text(&self, line_number: usize) -> Option<&str> {
215 self.line_metrics
216 .get(line_number)
217 .map(|lm| &self.text[lm.start_offset..lm.end_offset])
218 }
219
220 fn line_metric(&self, line_number: usize) -> Option<LineMetric> {
221 self.line_metrics.get(line_number).cloned()
222 }
223
224 fn line_count(&self) -> usize {
225 self.line_metrics.len()
226 }
227
228 fn hit_test_point(&self, point: Point) -> HitTestPoint {
229 self.ctx.set_font(&self.font.get_font_string());
230 if self.text.is_empty() {
235 return HitTestPoint::default();
236 }
237
238 let first_baseline = self.line_metrics.first().map(|l| l.baseline).unwrap_or(0.0);
242
243 let mut is_y_inside = true;
246 if point.y < -first_baseline {
247 is_y_inside = false
248 };
249
250 let mut lm = self
251 .line_metrics
252 .iter()
253 .skip_while(|l| l.y_offset + l.height < point.y);
254 let lm = lm
255 .next()
256 .or_else(|| {
257 is_y_inside = false;
259 self.line_metrics.last()
260 })
261 .cloned()
262 .unwrap_or_else(|| {
263 is_y_inside = false;
264 Default::default()
265 });
266
267 let line = &self.text[lm.start_offset..lm.end_offset];
270
271 let mut htp = hit_test_line_point(&self.ctx, line, point);
272 htp.idx += lm.start_offset;
273
274 if !is_y_inside {
275 htp.is_inside = false;
276 }
277
278 htp
279 }
280
281 fn hit_test_text_position(&self, idx: usize) -> HitTestPosition {
282 self.ctx.set_font(&self.font.get_font_string());
283 let idx = idx.min(self.text.len());
284 assert!(self.text.is_char_boundary(idx));
285 let line_num = util::line_number_for_position(&self.line_metrics, idx);
287 let lm = self.line_metrics.get(line_num).cloned().unwrap();
288
289 let y_pos = lm.y_offset + lm.baseline;
290 let line = &self.text[lm.range()];
293 let line_position = idx - lm.start_offset;
294
295 let x_pos = hit_test_line_position(&self.ctx, line, line_position);
296 HitTestPosition::new(Point::new(x_pos, y_pos), line_num)
297 }
298}
299
300impl fmt::Debug for WebTextLayout {
301 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
302 f.debug_struct("WebTextLayout").finish()
303 }
304}
305
306impl WebTextLayout {
307 pub(crate) fn size(&self) -> Size {
308 self.size
309 }
310
311 pub(crate) fn color(&self) -> Color {
312 self.color
313 }
314
315 fn update_width(&mut self, new_width: impl Into<Option<f64>>) {
316 self.ctx.set_font(&self.font.get_font_string());
319 let new_width = new_width.into().unwrap_or(f64::INFINITY);
320 let mut line_metrics =
321 lines::calculate_line_metrics(&self.text, &self.ctx, new_width, self.font.size);
322
323 if self.text.is_empty() {
324 line_metrics.push(LineMetric {
325 baseline: self.font.size * 0.2,
326 height: self.font.size * 1.2,
327 ..Default::default()
328 })
329 } else if util::trailing_nlf(&self.text).is_some() {
330 assert!(!line_metrics.is_empty());
331 let newline_eof = line_metrics
332 .last()
333 .map(|lm| LineMetric {
334 start_offset: self.text.len(),
335 end_offset: self.text.len(),
336 height: lm.height,
337 baseline: lm.baseline,
338 y_offset: lm.y_offset + lm.height,
339 trailing_whitespace: 0,
340 })
341 .unwrap();
342 line_metrics.push(newline_eof);
343 }
344
345 let (width, ws_width) = line_metrics
346 .iter()
347 .map(|lm| {
348 let full_width = text_width(&self.text[lm.range()], &self.ctx);
349 let non_ws_width = if lm.trailing_whitespace > 0 {
350 let non_ws_range = lm.start_offset..lm.end_offset - lm.trailing_whitespace;
351 text_width(&self.text[non_ws_range], &self.ctx)
352 } else {
353 full_width
354 };
355 (non_ws_width, full_width)
356 })
357 .fold((0.0, 0.0), |a: (f64, f64), b| (a.0.max(b.0), a.1.max(b.1)));
358
359 let height = line_metrics
360 .last()
361 .map(|l| l.y_offset + l.height)
362 .unwrap_or_default();
363 self.line_metrics = line_metrics;
364 self.trailing_ws_width = ws_width;
365 self.size = Size::new(width, height);
366 }
367}
368
369fn hit_test_line_point(ctx: &CanvasRenderingContext2d, text: &str, point: Point) -> HitTestPoint {
372 if text.is_empty() {
374 return HitTestPoint::default();
375 }
376
377 let end = UnicodeSegmentation::graphemes(text, true).count() - 1;
380 let end_bounds = match get_grapheme_boundaries(ctx, text, end) {
381 Some(bounds) => bounds,
382 None => return HitTestPoint::default(),
383 };
384
385 let start = 0;
386 let start_bounds = match get_grapheme_boundaries(ctx, text, start) {
387 Some(bounds) => bounds,
388 None => return HitTestPoint::default(),
389 };
390
391 if point.x > end_bounds.trailing {
393 return HitTestPoint::new(text.len(), false);
394 }
395
396 if point.x <= start_bounds.leading {
397 return HitTestPoint::default();
398 }
399
400 if let Some(hit) = point_x_in_grapheme(point.x, &start_bounds) {
402 return hit;
403 }
404 if let Some(hit) = point_x_in_grapheme(point.x, &end_bounds) {
405 return hit;
406 }
407
408 let mut left = start;
411 let mut right = end;
412 loop {
413 let middle = left + ((right - left) / 2);
415
416 let grapheme_bounds = match get_grapheme_boundaries(ctx, text, middle) {
417 Some(bounds) => bounds,
418 None => return HitTestPoint::default(),
419 };
420
421 if let Some(hit) = point_x_in_grapheme(point.x, &grapheme_bounds) {
422 return hit;
423 }
424
425 if point.x < grapheme_bounds.leading {
428 right = middle;
429 } else if point.x > grapheme_bounds.trailing {
430 left = middle + 1;
431 } else {
432 unreachable!("hit_test_point conditional is exhaustive");
433 }
434 }
435}
436
437fn hit_test_line_position(ctx: &CanvasRenderingContext2d, text: &str, idx: usize) -> f64 {
441 let text_len = text.len();
444
445 if idx == 0 {
446 return 0.0;
447 }
448
449 if idx >= text_len {
450 return text_width(text, ctx);
451 }
452
453 let grapheme_indices = UnicodeSegmentation::grapheme_indices(text, true)
458 .take_while(|(byte_idx, _s)| idx >= *byte_idx);
459
460 let text_end = grapheme_indices
461 .last()
462 .map(|(idx, _)| idx)
463 .unwrap_or(text_len);
464 text_width(&text[..text_end], ctx)
465}
466
467pub(crate) fn text_width(text: &str, ctx: &CanvasRenderingContext2d) -> f64 {
468 ctx.measure_text(text)
469 .map(|m| m.width())
470 .expect("Text measurement failed")
471}
472
473#[cfg(test)]
477pub(crate) mod test {
478 use piet::kurbo::Point;
479 use piet::{Text, TextLayout, TextLayoutBuilder};
480 use wasm_bindgen_test::*;
481 use web_sys::{HtmlCanvasElement, console, window};
482
483 use crate::*;
484
485 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
486
487 fn setup_ctx() -> (Window, CanvasRenderingContext2d) {
488 let window = window().unwrap();
489 let document = window.document().unwrap();
490
491 let canvas = document
492 .create_element("canvas")
493 .unwrap()
494 .dyn_into::<HtmlCanvasElement>()
495 .unwrap();
496 let context = canvas
497 .get_context("2d")
498 .unwrap()
499 .unwrap()
500 .dyn_into::<web_sys::CanvasRenderingContext2d>()
501 .unwrap();
502
503 let dpr = window.device_pixel_ratio();
504 canvas.set_width((canvas.offset_width() as f64 * dpr) as u32);
505 canvas.set_height((canvas.offset_height() as f64 * dpr) as u32);
506 let _ = context.scale(dpr, dpr);
507
508 (window, context)
509 }
510
511 fn assert_close_to(x: f64, target: f64, tolerance: f64) {
515 let min = target - tolerance;
516 let max = target + tolerance;
517 println!("x: {x}, target: {target}");
518 assert!(x <= max && x >= min);
519 }
520
521 #[wasm_bindgen_test]
522 pub fn test_hit_test_text_position_basic() {
523 let (_window, context) = setup_ctx();
524 let mut text_layout = WebText::new(context);
525
526 let input = "piet text!";
527 let font = text_layout.font_family("sans-serif").unwrap();
528
529 let layout = text_layout
530 .new_text_layout(&input[0..4])
531 .font(font.clone(), 12.0)
532 .build()
533 .unwrap();
534 let piet_width = layout.size().width;
535
536 let layout = text_layout
537 .new_text_layout(&input[0..3])
538 .font(font.clone(), 12.0)
539 .build()
540 .unwrap();
541 let pie_width = layout.size().width;
542
543 let layout = text_layout
544 .new_text_layout(&input[0..2])
545 .font(font.clone(), 12.0)
546 .build()
547 .unwrap();
548 let pi_width = layout.size().width;
549
550 let layout = text_layout
551 .new_text_layout(&input[0..1])
552 .font(font.clone(), 12.0)
553 .build()
554 .unwrap();
555 let p_width = layout.size().width;
556
557 let layout = text_layout
558 .new_text_layout("")
559 .font(font.clone(), 12.0)
560 .build()
561 .unwrap();
562 let null_width = layout.size().width;
563
564 let full_layout = text_layout
565 .new_text_layout(input)
566 .font(font, 12.0)
567 .build()
568 .unwrap();
569 let full_width = full_layout.size().width;
570
571 assert_close_to(
572 full_layout.hit_test_text_position(4).point.x,
573 piet_width,
574 3.0,
575 );
576 assert_close_to(
577 full_layout.hit_test_text_position(3).point.x,
578 pie_width,
579 3.0,
580 );
581 assert_close_to(full_layout.hit_test_text_position(2).point.x, pi_width, 3.0);
582 assert_close_to(full_layout.hit_test_text_position(1).point.x, p_width, 3.0);
583 assert_close_to(
584 full_layout.hit_test_text_position(0).point.x,
585 null_width,
586 3.0,
587 );
588 assert_close_to(
589 full_layout.hit_test_text_position(10).point.x,
590 full_width,
591 3.0,
592 );
593 assert_close_to(
594 full_layout.hit_test_text_position(11).point.x,
595 full_width,
596 3.0,
597 );
598 }
599
600 #[wasm_bindgen_test]
601 pub fn test_hit_test_text_position_complex_0() {
602 let (_window, context) = setup_ctx();
603 let mut text_layout = WebText::new(context);
604
605 let input = "é";
606 assert_eq!(input.len(), 2);
607
608 let font = text_layout.font_family("sans-serif").unwrap();
609 let layout = text_layout
610 .new_text_layout(input)
611 .font(font, 12.0)
612 .build()
613 .unwrap();
614
615 assert_close_to(layout.hit_test_text_position(0).point.x, 0.0, 3.0);
616 assert_close_to(
617 layout.hit_test_text_position(2).point.x,
618 layout.size().width,
619 3.0,
620 );
621
622 assert_close_to(layout.hit_test_text_position(1).point.x, 0.0, 3.0);
627
628 let input = "\u{0023}\u{FE0F}\u{20E3}"; assert_eq!(input.len(), 7);
641 assert_eq!(input.chars().count(), 3);
642
643 let font = text_layout.font_family("sans-serif").unwrap();
644 let layout = text_layout
645 .new_text_layout(input)
646 .font(font, 12.0)
647 .build()
648 .unwrap();
649
650 assert_close_to(layout.hit_test_text_position(0).point.x, 0.0, 3.0);
651 assert_close_to(
652 layout.hit_test_text_position(7).point.x,
653 layout.size().width,
654 3.0,
655 );
656
657 assert_close_to(layout.hit_test_text_position(1).point.x, 0.0, 3.0);
659 }
660
661 #[wasm_bindgen_test]
662 pub fn test_hit_test_text_position_complex_1() {
663 let (_window, context) = setup_ctx();
664 let mut text_layout = WebText::new(context);
665
666 let input = "é\u{0023}\u{FE0F}\u{20E3}1\u{1D407}"; assert_eq!(input.len(), 14);
673
674 let font = text_layout.font_family("sans-serif").unwrap();
675 let layout = text_layout
676 .new_text_layout(input)
677 .font(font.clone(), 12.0)
678 .build()
679 .unwrap();
680
681 let test_layout_0 = text_layout
682 .new_text_layout(&input[0..2])
683 .font(font.clone(), 12.0)
684 .build()
685 .unwrap();
686 let test_layout_1 = text_layout
687 .new_text_layout(&input[0..9])
688 .font(font.clone(), 12.0)
689 .build()
690 .unwrap();
691 let test_layout_2 = text_layout
692 .new_text_layout(&input[0..10])
693 .font(font, 12.0)
694 .build()
695 .unwrap();
696
697 assert_close_to(layout.hit_test_text_position(0).point.x, 0.0, 3.0);
699 assert_close_to(
700 layout.hit_test_text_position(2).point.x,
701 test_layout_0.size().width,
702 3.0,
703 );
704 assert_close_to(
705 layout.hit_test_text_position(9).point.x,
706 test_layout_1.size().width,
707 3.0,
708 );
709 assert_close_to(
710 layout.hit_test_text_position(10).point.x,
711 test_layout_2.size().width,
712 3.0,
713 );
714 assert_close_to(
715 layout.hit_test_text_position(14).point.x,
716 layout.size().width,
717 3.0,
718 );
719
720 assert_close_to(layout.hit_test_text_position(1).point.x, 0.0, 3.0);
723 assert_close_to(
724 layout.hit_test_text_position(3).point.x,
725 test_layout_0.size().width,
726 3.0,
727 );
728 assert_close_to(
729 layout.hit_test_text_position(6).point.x,
730 test_layout_0.size().width,
731 3.0,
732 );
733 }
734
735 #[wasm_bindgen_test]
737 pub fn test_hit_test_point_basic_0() {
738 let (_window, context) = setup_ctx();
739 let mut text_layout = WebText::new(context);
740
741 let font = text_layout.font_family("sans-serif").unwrap();
742 let layout = text_layout
743 .new_text_layout("piet text!")
744 .font(font, 16.0)
745 .build()
746 .unwrap();
747 console::log_1(&format!("text pos 4: {:?}", layout.hit_test_text_position(4)).into()); console::log_1(&format!("text pos 5: {:?}", layout.hit_test_text_position(5)).into()); let pt = layout.hit_test_point(Point::new(22.5, 0.0));
753 assert_eq!(pt.idx, 4);
754 let pt = layout.hit_test_point(Point::new(23.0, 0.0));
755 assert_eq!(pt.idx, 4);
756 let pt = layout.hit_test_point(Point::new(25.0, 0.0));
757 assert_eq!(pt.idx, 4);
758 let pt = layout.hit_test_point(Point::new(26.0, 0.0));
759 assert_eq!(pt.idx, 5);
760 let pt = layout.hit_test_point(Point::new(27.0, 0.0));
761 assert_eq!(pt.idx, 5);
762 let pt = layout.hit_test_point(Point::new(28.0, 0.0));
763 assert_eq!(pt.idx, 5);
764
765 console::log_1(&format!("layout_width: {:?}", layout.size().width).into()); let pt = layout.hit_test_point(Point::new(55.0, 0.0));
769 assert_eq!(pt.idx, 10); assert!(pt.is_inside);
771
772 let pt = layout.hit_test_point(Point::new(58.0, 0.0));
773 assert_eq!(pt.idx, 10); assert!(!pt.is_inside);
775
776 let pt = layout.hit_test_point(Point::new(-1.0, 0.0));
777 assert_eq!(pt.idx, 0); assert!(!pt.is_inside);
779 }
780
781 #[wasm_bindgen_test]
783 pub fn test_hit_test_point_basic_1() {
784 let (_window, context) = setup_ctx();
785 let mut text_layout = WebText::new(context);
786
787 let font = text_layout.font_family("sans-serif").unwrap();
789 let layout = text_layout
790 .new_text_layout("t")
791 .font(font.clone(), 16.0)
792 .build()
793 .unwrap();
794 println!("text pos 1: {:?}", layout.hit_test_text_position(1)); let pt = layout.hit_test_point(Point::new(1.0, 0.0));
798 assert_eq!(pt.idx, 0);
799
800 let layout = text_layout
801 .new_text_layout("te")
802 .font(font, 16.0)
803 .build()
804 .unwrap();
805 println!("text pos 1: {:?}", layout.hit_test_text_position(1)); println!("text pos 2: {:?}", layout.hit_test_text_position(2)); let pt = layout.hit_test_point(Point::new(1.0, 0.0));
809 assert_eq!(pt.idx, 0);
810 let pt = layout.hit_test_point(Point::new(4.0, 0.0));
811 assert_eq!(pt.idx, 1);
812 let pt = layout.hit_test_point(Point::new(6.0, 0.0));
813 assert_eq!(pt.idx, 1);
814 let pt = layout.hit_test_point(Point::new(11.0, 0.0));
815 assert_eq!(pt.idx, 2);
816 }
817
818 #[wasm_bindgen_test]
820 pub fn test_hit_test_point_complex_0() {
821 let (_window, context) = setup_ctx();
822 let mut text_layout = WebText::new(context);
823
824 let input = "é\u{0023}\u{FE0F}\u{20E3}1\u{1D407}"; let font = text_layout
832 .font_family("sans-serif") .unwrap();
834 let layout = text_layout
835 .new_text_layout(input)
836 .font(font, 13.0)
837 .build()
838 .unwrap();
839 console::log_1(&format!("text pos 2: {:?}", layout.hit_test_text_position(2)).into()); console::log_1(&format!("text pos 9: {:?}", layout.hit_test_text_position(9)).into()); console::log_1(&format!("text pos 10: {:?}", layout.hit_test_text_position(10)).into()); console::log_1(&format!("text pos 14: {:?}", layout.hit_test_text_position(14)).into()); let pt = layout.hit_test_point(Point::new(2.0, 0.0));
845 assert_eq!(pt.idx, 0);
846 let pt = layout.hit_test_point(Point::new(4.0, 0.0));
847 assert_eq!(pt.idx, 2);
848 let pt = layout.hit_test_point(Point::new(7.0, 0.0));
849 assert_eq!(pt.idx, 2);
850 let pt = layout.hit_test_point(Point::new(10.0, 0.0));
851 assert_eq!(pt.idx, 2);
852 let pt = layout.hit_test_point(Point::new(14.0, 0.0));
853 assert_eq!(pt.idx, 9);
854 let pt = layout.hit_test_point(Point::new(18.0, 0.0));
855 assert_eq!(pt.idx, 9);
856 let pt = layout.hit_test_point(Point::new(23.0, 0.0));
857 assert_eq!(pt.idx, 9);
858 let pt = layout.hit_test_point(Point::new(26.0, 0.0));
859 assert_eq!(pt.idx, 10);
860 let pt = layout.hit_test_point(Point::new(29.0, 0.0));
861 assert_eq!(pt.idx, 10);
862 let pt = layout.hit_test_point(Point::new(32.0, 0.0));
863 assert_eq!(pt.idx, 10);
864 let pt = layout.hit_test_point(Point::new(35.5, 0.0));
865 assert_eq!(pt.idx, 14);
866 let pt = layout.hit_test_point(Point::new(38.0, 0.0));
867 assert_eq!(pt.idx, 14);
868 let pt = layout.hit_test_point(Point::new(40.0, 0.0));
869 assert_eq!(pt.idx, 14);
870 }
871
872 #[wasm_bindgen_test]
874 pub fn test_hit_test_point_complex_1() {
875 let (_window, context) = setup_ctx();
876 let mut text_layout = WebText::new(context);
877
878 let input = "tßßypi";
883
884 let font = text_layout.font_family("sans-serif").unwrap();
885 let layout = text_layout
886 .new_text_layout(input)
887 .font(font, 14.0)
888 .build()
889 .unwrap();
890 console::log_1(&format!("text pos 0: {:?}", layout.hit_test_text_position(0)).into()); console::log_1(&format!("text pos 1: {:?}", layout.hit_test_text_position(1)).into()); console::log_1(&format!("text pos 2: {:?}", layout.hit_test_text_position(2)).into()); console::log_1(&format!("text pos 3: {:?}", layout.hit_test_text_position(3)).into()); console::log_1(&format!("text pos 4: {:?}", layout.hit_test_text_position(4)).into()); console::log_1(&format!("text pos 5: {:?}", layout.hit_test_text_position(5)).into()); console::log_1(&format!("text pos 6: {:?}", layout.hit_test_text_position(6)).into()); console::log_1(&format!("text pos 7: {:?}", layout.hit_test_text_position(7)).into()); console::log_1(&format!("text pos 8: {:?}", layout.hit_test_text_position(8)).into()); let pt = layout.hit_test_point(Point::new(27.0, 0.0));
901 assert_eq!(pt.idx, 6);
902 }
903
904 #[wasm_bindgen_test]
905 fn test_multiline_hit_test_text_position_basic() {
906 let (_window, context) = setup_ctx();
907 let mut text_layout = WebText::new(context);
908
909 let input = "piet text!";
910 let font = text_layout
911 .font_family("sans-serif") .unwrap();
913
914 let layout = text_layout
915 .new_text_layout(&input[0..3])
916 .font(font.clone(), 15.0)
917 .max_width(30.0)
918 .build()
919 .unwrap();
920 let pie_width = layout.size().width;
921
922 let layout = text_layout
923 .new_text_layout(&input[0..4])
924 .font(font.clone(), 15.0)
925 .max_width(25.0)
926 .build()
927 .unwrap();
928 let piet_width = layout.size().width;
929
930 let layout = text_layout
931 .new_text_layout(&input[0..5])
932 .font(font.clone(), 15.0)
933 .max_width(30.0)
934 .build()
935 .unwrap();
936 let piet_space_width = layout.size().width;
937
938 let layout = text_layout
940 .new_text_layout(&input[6..10])
941 .font(font.clone(), 15.0)
942 .max_width(25.0)
943 .build()
944 .unwrap();
945 let text_width = layout.size().width;
946
947 let layout = text_layout
948 .new_text_layout(&input[6..9])
949 .font(font.clone(), 15.0)
950 .max_width(25.0)
951 .build()
952 .unwrap();
953 let tex_width = layout.size().width;
954
955 let layout = text_layout
956 .new_text_layout(&input[6..8])
957 .font(font.clone(), 15.0)
958 .max_width(25.0)
959 .build()
960 .unwrap();
961 let te_width = layout.size().width;
962
963 let layout = text_layout
964 .new_text_layout(&input[6..7])
965 .font(font.clone(), 15.0)
966 .max_width(25.0)
967 .build()
968 .unwrap();
969 let t_width = layout.size().width;
970
971 let full_layout = text_layout
972 .new_text_layout(input)
973 .font(font, 15.0)
974 .max_width(25.0)
975 .build()
976 .unwrap();
977
978 println!("lm: {:#?}", full_layout.line_metrics);
979 println!("layout width: {:#?}", full_layout.size().width);
980
981 println!("'pie': {pie_width}");
982 println!("'piet': {piet_width}");
983 println!("'piet ': {piet_space_width}");
984 println!("'text': {text_width}");
985 println!("'tex': {tex_width}");
986 println!("'te': {te_width}");
987 println!("'t': {t_width}");
988
989 let line_zero_baseline = 0.0;
991 let line_one_baseline = full_layout.line_metric(1).unwrap().height;
992
993 assert_close_to(
995 full_layout.hit_test_text_position(10).point.x,
996 text_width,
997 3.0,
998 );
999 assert_close_to(
1000 full_layout.hit_test_text_position(9).point.x,
1001 tex_width,
1002 3.0,
1003 );
1004 assert_close_to(full_layout.hit_test_text_position(8).point.x, te_width, 3.0);
1005 assert_close_to(full_layout.hit_test_text_position(7).point.x, t_width, 3.0);
1006 assert_close_to(full_layout.hit_test_text_position(6).point.x, 0.0, 3.0);
1008
1009 assert_close_to(
1010 full_layout.hit_test_text_position(3).point.x,
1011 pie_width,
1012 3.0,
1013 );
1014
1015 assert_close_to(
1017 full_layout.hit_test_text_position(5).point.x,
1018 piet_space_width,
1019 3.0,
1020 );
1021
1022 assert_close_to(
1024 full_layout.hit_test_text_position(10).point.y,
1025 line_one_baseline,
1026 3.0,
1027 );
1028 assert_close_to(
1029 full_layout.hit_test_text_position(9).point.y,
1030 line_one_baseline,
1031 3.0,
1032 );
1033 assert_close_to(
1034 full_layout.hit_test_text_position(8).point.y,
1035 line_one_baseline,
1036 3.0,
1037 );
1038 assert_close_to(
1039 full_layout.hit_test_text_position(7).point.y,
1040 line_one_baseline,
1041 3.0,
1042 );
1043 assert_close_to(
1044 full_layout.hit_test_text_position(6).point.y,
1045 line_one_baseline,
1046 3.0,
1047 );
1048
1049 assert_close_to(
1051 full_layout.hit_test_text_position(5).point.y,
1052 line_zero_baseline,
1053 3.0,
1054 );
1055 assert_close_to(
1056 full_layout.hit_test_text_position(4).point.y,
1057 line_zero_baseline,
1058 3.0,
1059 );
1060 }
1061
1062 #[wasm_bindgen_test]
1064 fn test_multiline_hit_test_point_basic() {
1065 let input = "piet text most best";
1066
1067 let (_window, context) = setup_ctx();
1068 let mut text = WebText::new(context);
1069
1070 let font = text.font_family("sans-serif").unwrap();
1071 let layout = text
1074 .new_text_layout(input)
1075 .font(font.clone(), 14.0)
1076 .max_width(30.0)
1077 .build()
1078 .unwrap();
1079 console::log_1(&format!("text pos 01: {:?}", layout.hit_test_text_position(0)).into()); console::log_1(&format!("text pos 06: {:?}", layout.hit_test_text_position(5)).into()); console::log_1(&format!("text pos 11: {:?}", layout.hit_test_text_position(10)).into()); console::log_1(&format!("text pos 16: {:?}", layout.hit_test_text_position(15)).into()); console::log_1(&format!("lm 0: {:?}", layout.line_metric(0)).into());
1084 console::log_1(&format!("lm 1: {:?}", layout.line_metric(1)).into());
1085 console::log_1(&format!("lm 2: {:?}", layout.line_metric(2)).into());
1086 console::log_1(&format!("lm 3: {:?}", layout.line_metric(3)).into());
1087
1088 let pt = layout.hit_test_point(Point::new(1.0, -1.0));
1090 assert_eq!(pt.idx, 0);
1091 assert!(pt.is_inside);
1092 let pt = layout.hit_test_point(Point::new(1.0, 00.0));
1093 assert_eq!(pt.idx, 0);
1094 let pt = layout.hit_test_point(Point::new(1.0, 04.0));
1095 assert_eq!(pt.idx, 5);
1096 let pt = layout.hit_test_point(Point::new(1.0, 21.0));
1097 assert_eq!(pt.idx, 10);
1098 let pt = layout.hit_test_point(Point::new(1.0, 38.0));
1099 assert_eq!(pt.idx, 15);
1100
1101 let best_layout = text
1103 .new_text_layout("best")
1104 .font(font.clone(), 14.0)
1105 .build()
1106 .unwrap();
1107 console::log_1(&format!("layout width: {:#?}", best_layout.size().width).into()); let pt = layout.hit_test_point(Point::new(1.0, 55.0));
1110 assert_eq!(pt.idx, 15);
1111 assert!(!pt.is_inside);
1112
1113 let pt = layout.hit_test_point(Point::new(25.0, 55.0));
1114 assert_eq!(pt.idx, 19);
1115 assert!(!pt.is_inside);
1116
1117 let pt = layout.hit_test_point(Point::new(27.0, 55.0));
1118 assert_eq!(pt.idx, 19);
1119 assert!(!pt.is_inside);
1120
1121 let piet_layout = text
1123 .new_text_layout("piet ")
1124 .font(font, 14.0)
1125 .build()
1126 .unwrap();
1127 console::log_1(&format!("layout width: {:#?}", piet_layout.size().width).into()); let pt = layout.hit_test_point(Point::new(1.0, -14.0)); assert_eq!(pt.idx, 0);
1131 assert!(!pt.is_inside);
1132
1133 let pt = layout.hit_test_point(Point::new(25.0, -14.0)); assert_eq!(pt.idx, 5);
1135 assert!(!pt.is_inside);
1136
1137 let pt = layout.hit_test_point(Point::new(27.0, -14.0)); assert_eq!(pt.idx, 5);
1139 assert!(!pt.is_inside);
1140 }
1141}