mod grapheme;
mod lines;
use std::borrow::Cow;
use std::ops::RangeBounds;
use std::rc::Rc;
use web_sys::CanvasRenderingContext2d;
use piet::kurbo::{Point, Rect, Size};
use piet::{
util, Color, Error, FontFamily, HitTestPoint, HitTestPosition, LineMetric, Text, TextAttribute,
TextLayout, TextLayoutBuilder, TextStorage,
};
use unicode_segmentation::UnicodeSegmentation;
use self::grapheme::{get_grapheme_boundaries, point_x_in_grapheme};
use crate::WebText;
#[derive(Clone)]
pub struct WebFont {
family: FontFamily,
weight: u32,
style: FontStyle,
size: f64,
}
#[derive(Clone)]
pub struct WebTextLayout {
ctx: CanvasRenderingContext2d,
pub(crate) font: WebFont,
pub(crate) text: Rc<dyn TextStorage>,
pub(crate) line_metrics: Vec<LineMetric>,
size: Size,
color: Color,
}
pub struct WebTextLayoutBuilder {
ctx: CanvasRenderingContext2d,
text: Rc<dyn TextStorage>,
width: f64,
defaults: util::LayoutDefaults,
}
#[derive(Clone)]
enum FontStyle {
Normal,
Italic,
#[allow(dead_code)]
Oblique(Option<f64>),
}
impl Text for WebText {
type TextLayout = WebTextLayout;
type TextLayoutBuilder = WebTextLayoutBuilder;
fn font_family(&mut self, family_name: &str) -> Option<FontFamily> {
Some(FontFamily::new_unchecked(family_name))
}
fn load_font(&mut self, _data: &[u8]) -> Result<FontFamily, Error> {
Err(Error::MissingFeature)
}
fn new_text_layout(&mut self, text: impl TextStorage) -> Self::TextLayoutBuilder {
WebTextLayoutBuilder {
ctx: self.ctx.clone(),
text: Rc::new(text),
width: f64::INFINITY,
defaults: Default::default(),
}
}
}
impl WebFont {
fn new(family: FontFamily) -> Self {
WebFont {
family,
style: FontStyle::Normal,
size: piet::util::DEFAULT_FONT_SIZE,
weight: 400,
}
}
fn with_style(mut self, style: piet::FontStyle) -> Self {
let style = if style == piet::FontStyle::Italic {
FontStyle::Italic
} else {
FontStyle::Normal
};
self.style = style;
self
}
fn with_weight(mut self, weight: piet::FontWeight) -> Self {
self.weight = weight.to_raw() as u32;
self
}
fn with_size(mut self, size: f64) -> Self {
self.size = size;
self
}
pub(crate) fn get_font_string(&self) -> String {
let style_str = match self.style {
FontStyle::Normal => Cow::from("normal"),
FontStyle::Italic => Cow::from("italic"),
FontStyle::Oblique(None) => Cow::from("italic"),
FontStyle::Oblique(Some(angle)) => Cow::from(format!("oblique {}deg", angle)),
};
format!(
"{} {} {}px \"{}\"",
style_str,
self.weight,
self.size,
self.family.name()
)
}
}
impl TextLayoutBuilder for WebTextLayoutBuilder {
type Out = WebTextLayout;
fn max_width(mut self, width: f64) -> Self {
self.width = width;
self
}
fn alignment(self, _alignment: piet::TextAlignment) -> Self {
web_sys::console::log_1(&"TextLayout alignment unsupported on web".into());
self
}
fn default_attribute(mut self, attribute: impl Into<TextAttribute>) -> Self {
self.defaults.set(attribute);
self
}
fn range_attribute(
self,
_range: impl RangeBounds<usize>,
_attribute: impl Into<TextAttribute>,
) -> Self {
web_sys::console::log_1(&"Text attributes not yet implemented for web".into());
self
}
fn build(self) -> Result<Self::Out, Error> {
let font = WebFont::new(self.defaults.font)
.with_size(self.defaults.font_size)
.with_weight(self.defaults.weight)
.with_style(self.defaults.style);
let mut layout = WebTextLayout {
ctx: self.ctx,
font,
text: self.text,
line_metrics: Vec::new(),
size: Size::ZERO,
color: self.defaults.fg_color,
};
layout.update_width(self.width)?;
Ok(layout)
}
}
impl TextLayout for WebTextLayout {
fn size(&self) -> Size {
self.size
}
fn image_bounds(&self) -> Rect {
self.size.to_rect()
}
fn text(&self) -> &str {
&self.text
}
fn line_text(&self, line_number: usize) -> Option<&str> {
self.line_metrics
.get(line_number)
.map(|lm| &self.text[lm.start_offset..lm.end_offset])
}
fn line_metric(&self, line_number: usize) -> Option<LineMetric> {
self.line_metrics.get(line_number).cloned()
}
fn line_count(&self) -> usize {
self.line_metrics.len()
}
fn hit_test_point(&self, point: Point) -> HitTestPoint {
self.ctx.set_font(&self.font.get_font_string());
if self.text.is_empty() {
return HitTestPoint::default();
}
let first_baseline = self.line_metrics.get(0).map(|l| l.baseline).unwrap_or(0.0);
let mut is_y_inside = true;
if point.y < -1.0 * first_baseline {
is_y_inside = false
};
let mut lm = self
.line_metrics
.iter()
.skip_while(|l| l.y_offset + l.height < point.y);
let lm = lm
.next()
.or_else(|| {
is_y_inside = false;
self.line_metrics.last()
})
.cloned()
.unwrap_or_else(|| {
is_y_inside = false;
Default::default()
});
let line = &self.text[lm.start_offset..lm.end_offset];
let mut htp = hit_test_line_point(&self.ctx, line, point);
htp.idx += lm.start_offset;
if !is_y_inside {
htp.is_inside = false;
}
htp
}
fn hit_test_text_position(&self, idx: usize) -> HitTestPosition {
self.ctx.set_font(&self.font.get_font_string());
let idx = idx.min(self.text.len());
assert!(self.text.is_char_boundary(idx));
let line_num = util::line_number_for_position(&self.line_metrics, idx);
let lm = self.line_metrics.get(line_num).cloned().unwrap();
let y_pos = lm.y_offset + lm.baseline;
let line = &self.text[lm.range()];
let line_position = idx - lm.start_offset;
let x_pos = hit_test_line_position(&self.ctx, line, line_position);
HitTestPosition::new(Point::new(x_pos, y_pos), line_num)
}
}
impl WebTextLayout {
pub(crate) fn size(&self) -> Size {
self.size
}
pub(crate) fn color(&self) -> &Color {
&self.color
}
fn update_width(&mut self, new_width: impl Into<Option<f64>>) -> Result<(), Error> {
self.ctx.set_font(&self.font.get_font_string());
let new_width = new_width.into().unwrap_or(std::f64::INFINITY);
let line_metrics =
lines::calculate_line_metrics(&self.text, &self.ctx, new_width, self.font.size);
let max_width = line_metrics
.iter()
.map(|lm| text_width(&self.text[lm.start_offset..lm.end_offset], &self.ctx))
.fold(0., f64::max);
let height = line_metrics
.last()
.map(|l| l.y_offset + l.height)
.unwrap_or_default();
self.line_metrics = line_metrics;
self.size = Size::new(max_width, height);
Ok(())
}
}
fn hit_test_line_point(ctx: &CanvasRenderingContext2d, text: &str, point: Point) -> HitTestPoint {
if text.is_empty() {
return HitTestPoint::default();
}
let end = UnicodeSegmentation::graphemes(text, true).count() - 1;
let end_bounds = match get_grapheme_boundaries(ctx, text, end) {
Some(bounds) => bounds,
None => return HitTestPoint::default(),
};
let start = 0;
let start_bounds = match get_grapheme_boundaries(ctx, text, start) {
Some(bounds) => bounds,
None => return HitTestPoint::default(),
};
if point.x > end_bounds.trailing {
return HitTestPoint::new(text.len(), false);
}
if point.x <= start_bounds.leading {
return HitTestPoint::default();
}
if let Some(hit) = point_x_in_grapheme(point.x, &start_bounds) {
return hit;
}
if let Some(hit) = point_x_in_grapheme(point.x, &end_bounds) {
return hit;
}
let mut left = start;
let mut right = end;
loop {
let middle = left + ((right - left) / 2);
let grapheme_bounds = match get_grapheme_boundaries(ctx, text, middle) {
Some(bounds) => bounds,
None => return HitTestPoint::default(),
};
if let Some(hit) = point_x_in_grapheme(point.x, &grapheme_bounds) {
return hit;
}
if point.x < grapheme_bounds.leading {
right = middle;
} else if point.x > grapheme_bounds.trailing {
left = middle + 1;
} else {
unreachable!("hit_test_point conditional is exhaustive");
}
}
}
fn hit_test_line_position(ctx: &CanvasRenderingContext2d, text: &str, idx: usize) -> f64 {
let text_len = text.len();
if idx == 0 {
return 0.0;
}
if idx as usize >= text_len {
return text_width(text, ctx);
}
let grapheme_indices = UnicodeSegmentation::grapheme_indices(text, true)
.take_while(|(byte_idx, _s)| idx >= *byte_idx);
let text_end = grapheme_indices
.last()
.map(|(idx, _)| idx)
.unwrap_or(text_len);
text_width(&text[..text_end], ctx)
}
pub(crate) fn text_width(text: &str, ctx: &CanvasRenderingContext2d) -> f64 {
ctx.measure_text(text)
.map(|m| m.width())
.expect("Text measurement failed")
}
#[cfg(test)]
pub(crate) mod test {
use piet::kurbo::Point;
use piet::{Text, TextLayout, TextLayoutBuilder};
use wasm_bindgen_test::*;
use web_sys::{console, window, HtmlCanvasElement};
use crate::*;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
fn setup_ctx() -> (Window, CanvasRenderingContext2d) {
let window = window().unwrap();
let document = window.document().unwrap();
let canvas = document
.create_element("canvas")
.unwrap()
.dyn_into::<HtmlCanvasElement>()
.unwrap();
let context = canvas
.get_context("2d")
.unwrap()
.unwrap()
.dyn_into::<web_sys::CanvasRenderingContext2d>()
.unwrap();
let dpr = window.device_pixel_ratio();
canvas.set_width((canvas.offset_width() as f64 * dpr) as u32);
canvas.set_height((canvas.offset_height() as f64 * dpr) as u32);
let _ = context.scale(dpr, dpr);
(window, context)
}
fn assert_close_to(x: f64, target: f64, tolerance: f64) {
let min = target - tolerance;
let max = target + tolerance;
println!("x: {}, target: {}", x, target);
assert!(x <= max && x >= min);
}
#[wasm_bindgen_test]
pub fn test_hit_test_text_position_basic() {
let (_window, context) = setup_ctx();
let mut text_layout = WebText::new(context);
let input = "piet text!";
let font = text_layout.font_family("sans-serif").unwrap();
let layout = text_layout
.new_text_layout(&input[0..4])
.font(font.clone(), 12.0)
.build()
.unwrap();
let piet_width = layout.size().width;
let layout = text_layout
.new_text_layout(&input[0..3])
.font(font.clone(), 12.0)
.build()
.unwrap();
let pie_width = layout.size().width;
let layout = text_layout
.new_text_layout(&input[0..2])
.font(font.clone(), 12.0)
.build()
.unwrap();
let pi_width = layout.size().width;
let layout = text_layout
.new_text_layout(&input[0..1])
.font(font.clone(), 12.0)
.build()
.unwrap();
let p_width = layout.size().width;
let layout = text_layout
.new_text_layout("")
.font(font.clone(), 12.0)
.build()
.unwrap();
let null_width = layout.size().width;
let full_layout = text_layout
.new_text_layout(input)
.font(font, 12.0)
.build()
.unwrap();
let full_width = full_layout.size().width;
assert_close_to(
full_layout.hit_test_text_position(4).point.x,
piet_width,
3.0,
);
assert_close_to(
full_layout.hit_test_text_position(3).point.x,
pie_width,
3.0,
);
assert_close_to(full_layout.hit_test_text_position(2).point.x, pi_width, 3.0);
assert_close_to(full_layout.hit_test_text_position(1).point.x, p_width, 3.0);
assert_close_to(
full_layout.hit_test_text_position(0).point.x,
null_width,
3.0,
);
assert_close_to(
full_layout.hit_test_text_position(10).point.x,
full_width,
3.0,
);
assert_close_to(
full_layout.hit_test_text_position(11).point.x,
full_width,
3.0,
);
}
#[wasm_bindgen_test]
pub fn test_hit_test_text_position_complex_0() {
let (_window, context) = setup_ctx();
let mut text_layout = WebText::new(context);
let input = "é";
assert_eq!(input.len(), 2);
let font = text_layout.font_family("sans-serif").unwrap();
let layout = text_layout
.new_text_layout(input)
.font(font, 12.0)
.build()
.unwrap();
assert_close_to(layout.hit_test_text_position(0).point.x, 0.0, 3.0);
assert_close_to(
layout.hit_test_text_position(2).point.x,
layout.size().width,
3.0,
);
assert_close_to(layout.hit_test_text_position(1).point.x, 0.0, 3.0);
let input = "\u{0023}\u{FE0F}\u{20E3}";
assert_eq!(input.len(), 7);
assert_eq!(input.chars().count(), 3);
let font = text_layout.font_family("sans-serif").unwrap();
let layout = text_layout
.new_text_layout(input)
.font(font, 12.0)
.build()
.unwrap();
assert_close_to(layout.hit_test_text_position(0).point.x, 0.0, 3.0);
assert_close_to(
layout.hit_test_text_position(7).point.x,
layout.size().width,
3.0,
);
assert_close_to(layout.hit_test_text_position(1).point.x, 0.0, 3.0);
}
#[wasm_bindgen_test]
pub fn test_hit_test_text_position_complex_1() {
let (_window, context) = setup_ctx();
let mut text_layout = WebText::new(context);
let input = "é\u{0023}\u{FE0F}\u{20E3}1\u{1D407}";
assert_eq!(input.len(), 14);
let font = text_layout.font_family("sans-serif").unwrap();
let layout = text_layout
.new_text_layout(input)
.font(font.clone(), 12.0)
.build()
.unwrap();
let test_layout_0 = text_layout
.new_text_layout(&input[0..2])
.font(font.clone(), 12.0)
.build()
.unwrap();
let test_layout_1 = text_layout
.new_text_layout(&input[0..9])
.font(font.clone(), 12.0)
.build()
.unwrap();
let test_layout_2 = text_layout
.new_text_layout(&input[0..10])
.font(font, 12.0)
.build()
.unwrap();
assert_close_to(layout.hit_test_text_position(0).point.x, 0.0, 3.0);
assert_close_to(
layout.hit_test_text_position(2).point.x,
test_layout_0.size().width,
3.0,
);
assert_close_to(
layout.hit_test_text_position(9).point.x,
test_layout_1.size().width,
3.0,
);
assert_close_to(
layout.hit_test_text_position(10).point.x,
test_layout_2.size().width,
3.0,
);
assert_close_to(
layout.hit_test_text_position(14).point.x,
layout.size().width,
3.0,
);
assert_close_to(layout.hit_test_text_position(1).point.x, 0.0, 3.0);
assert_close_to(
layout.hit_test_text_position(3).point.x,
test_layout_0.size().width,
3.0,
);
assert_close_to(
layout.hit_test_text_position(6).point.x,
test_layout_0.size().width,
3.0,
);
}
#[wasm_bindgen_test]
pub fn test_hit_test_point_basic_0() {
let (_window, context) = setup_ctx();
let mut text_layout = WebText::new(context);
let font = text_layout.font_family("sans-serif").unwrap();
let layout = text_layout
.new_text_layout("piet text!")
.font(font, 16.0)
.build()
.unwrap();
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));
assert_eq!(pt.idx, 4);
let pt = layout.hit_test_point(Point::new(23.0, 0.0));
assert_eq!(pt.idx, 4);
let pt = layout.hit_test_point(Point::new(25.0, 0.0));
assert_eq!(pt.idx, 4);
let pt = layout.hit_test_point(Point::new(26.0, 0.0));
assert_eq!(pt.idx, 5);
let pt = layout.hit_test_point(Point::new(27.0, 0.0));
assert_eq!(pt.idx, 5);
let pt = layout.hit_test_point(Point::new(28.0, 0.0));
assert_eq!(pt.idx, 5);
console::log_1(&format!("layout_width: {:?}", layout.size().width).into());
let pt = layout.hit_test_point(Point::new(55.0, 0.0));
assert_eq!(pt.idx, 10);
assert_eq!(pt.is_inside, true);
let pt = layout.hit_test_point(Point::new(58.0, 0.0));
assert_eq!(pt.idx, 10);
assert_eq!(pt.is_inside, false);
let pt = layout.hit_test_point(Point::new(-1.0, 0.0));
assert_eq!(pt.idx, 0);
assert_eq!(pt.is_inside, false);
}
#[wasm_bindgen_test]
pub fn test_hit_test_point_basic_1() {
let (_window, context) = setup_ctx();
let mut text_layout = WebText::new(context);
let font = text_layout.font_family("sans-serif").unwrap();
let layout = text_layout
.new_text_layout("t")
.font(font.clone(), 16.0)
.build()
.unwrap();
println!("text pos 1: {:?}", layout.hit_test_text_position(1));
let pt = layout.hit_test_point(Point::new(1.0, 0.0));
assert_eq!(pt.idx, 0);
let layout = text_layout
.new_text_layout("te")
.font(font, 16.0)
.build()
.unwrap();
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));
assert_eq!(pt.idx, 0);
let pt = layout.hit_test_point(Point::new(4.0, 0.0));
assert_eq!(pt.idx, 1);
let pt = layout.hit_test_point(Point::new(6.0, 0.0));
assert_eq!(pt.idx, 1);
let pt = layout.hit_test_point(Point::new(11.0, 0.0));
assert_eq!(pt.idx, 2);
}
#[wasm_bindgen_test]
pub fn test_hit_test_point_complex_0() {
let (_window, context) = setup_ctx();
let mut text_layout = WebText::new(context);
let input = "é\u{0023}\u{FE0F}\u{20E3}1\u{1D407}";
let font = text_layout
.font_family("sans-serif")
.unwrap();
let layout = text_layout
.new_text_layout(input)
.font(font, 13.0)
.build()
.unwrap();
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));
assert_eq!(pt.idx, 0);
let pt = layout.hit_test_point(Point::new(4.0, 0.0));
assert_eq!(pt.idx, 2);
let pt = layout.hit_test_point(Point::new(7.0, 0.0));
assert_eq!(pt.idx, 2);
let pt = layout.hit_test_point(Point::new(10.0, 0.0));
assert_eq!(pt.idx, 2);
let pt = layout.hit_test_point(Point::new(14.0, 0.0));
assert_eq!(pt.idx, 9);
let pt = layout.hit_test_point(Point::new(18.0, 0.0));
assert_eq!(pt.idx, 9);
let pt = layout.hit_test_point(Point::new(23.0, 0.0));
assert_eq!(pt.idx, 9);
let pt = layout.hit_test_point(Point::new(26.0, 0.0));
assert_eq!(pt.idx, 10);
let pt = layout.hit_test_point(Point::new(29.0, 0.0));
assert_eq!(pt.idx, 10);
let pt = layout.hit_test_point(Point::new(32.0, 0.0));
assert_eq!(pt.idx, 10);
let pt = layout.hit_test_point(Point::new(35.5, 0.0));
assert_eq!(pt.idx, 14);
let pt = layout.hit_test_point(Point::new(38.0, 0.0));
assert_eq!(pt.idx, 14);
let pt = layout.hit_test_point(Point::new(40.0, 0.0));
assert_eq!(pt.idx, 14);
}
#[wasm_bindgen_test]
pub fn test_hit_test_point_complex_1() {
let (_window, context) = setup_ctx();
let mut text_layout = WebText::new(context);
let input = "tßßypi";
let font = text_layout.font_family("sans-serif").unwrap();
let layout = text_layout
.new_text_layout(input)
.font(font, 14.0)
.build()
.unwrap();
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));
assert_eq!(pt.idx, 6);
}
#[wasm_bindgen_test]
fn test_multiline_hit_test_text_position_basic() {
let (_window, context) = setup_ctx();
let mut text_layout = WebText::new(context);
let input = "piet text!";
let font = text_layout
.font_family("sans-serif")
.unwrap();
let layout = text_layout
.new_text_layout(&input[0..3])
.font(font.clone(), 15.0)
.max_width(30.0)
.build()
.unwrap();
let pie_width = layout.size().width;
let layout = text_layout
.new_text_layout(&input[0..4])
.font(font.clone(), 15.0)
.max_width(25.0)
.build()
.unwrap();
let piet_width = layout.size().width;
let layout = text_layout
.new_text_layout(&input[0..5])
.font(font.clone(), 15.0)
.max_width(30.0)
.build()
.unwrap();
let piet_space_width = layout.size().width;
let layout = text_layout
.new_text_layout(&input[6..10])
.font(font.clone(), 15.0)
.max_width(25.0)
.build()
.unwrap();
let text_width = layout.size().width;
let layout = text_layout
.new_text_layout(&input[6..9])
.font(font.clone(), 15.0)
.max_width(25.0)
.build()
.unwrap();
let tex_width = layout.size().width;
let layout = text_layout
.new_text_layout(&input[6..8])
.font(font.clone(), 15.0)
.max_width(25.0)
.build()
.unwrap();
let te_width = layout.size().width;
let layout = text_layout
.new_text_layout(&input[6..7])
.font(font.clone(), 15.0)
.max_width(25.0)
.build()
.unwrap();
let t_width = layout.size().width;
let full_layout = text_layout
.new_text_layout(input)
.font(font, 15.0)
.max_width(25.0)
.build()
.unwrap();
println!("lm: {:#?}", full_layout.line_metrics);
println!("layout width: {:#?}", full_layout.size().width);
println!("'pie': {}", pie_width);
println!("'piet': {}", piet_width);
println!("'piet ': {}", piet_space_width);
println!("'text': {}", text_width);
println!("'tex': {}", tex_width);
println!("'te': {}", te_width);
println!("'t': {}", t_width);
let line_zero_baseline = 0.0;
let line_one_baseline = full_layout.line_metric(1).unwrap().height;
assert_close_to(
full_layout.hit_test_text_position(10).point.x,
text_width,
3.0,
);
assert_close_to(
full_layout.hit_test_text_position(9).point.x,
tex_width,
3.0,
);
assert_close_to(full_layout.hit_test_text_position(8).point.x, te_width, 3.0);
assert_close_to(full_layout.hit_test_text_position(7).point.x, t_width, 3.0);
assert_close_to(full_layout.hit_test_text_position(6).point.x, 0.0, 3.0);
assert_close_to(
full_layout.hit_test_text_position(3).point.x,
pie_width,
3.0,
);
assert_close_to(
full_layout.hit_test_text_position(5).point.x,
piet_space_width,
3.0,
);
assert_close_to(
full_layout.hit_test_text_position(10).point.y,
line_one_baseline,
3.0,
);
assert_close_to(
full_layout.hit_test_text_position(9).point.y,
line_one_baseline,
3.0,
);
assert_close_to(
full_layout.hit_test_text_position(8).point.y,
line_one_baseline,
3.0,
);
assert_close_to(
full_layout.hit_test_text_position(7).point.y,
line_one_baseline,
3.0,
);
assert_close_to(
full_layout.hit_test_text_position(6).point.y,
line_one_baseline,
3.0,
);
assert_close_to(
full_layout.hit_test_text_position(5).point.y,
line_zero_baseline,
3.0,
);
assert_close_to(
full_layout.hit_test_text_position(4).point.y,
line_zero_baseline,
3.0,
);
}
#[wasm_bindgen_test]
fn test_multiline_hit_test_point_basic() {
let input = "piet text most best";
let (_window, context) = setup_ctx();
let mut text = WebText::new(context);
let font = text.font_family("sans-serif").unwrap();
let layout = text
.new_text_layout(input)
.font(font.clone(), 14.0)
.max_width(30.0)
.build()
.unwrap();
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());
console::log_1(&format!("lm 1: {:?}", layout.line_metric(1)).into());
console::log_1(&format!("lm 2: {:?}", layout.line_metric(2)).into());
console::log_1(&format!("lm 3: {:?}", layout.line_metric(3)).into());
let pt = layout.hit_test_point(Point::new(1.0, -1.0));
assert_eq!(pt.idx, 0);
assert_eq!(pt.is_inside, true);
let pt = layout.hit_test_point(Point::new(1.0, 00.0));
assert_eq!(pt.idx, 0);
let pt = layout.hit_test_point(Point::new(1.0, 04.0));
assert_eq!(pt.idx, 5);
let pt = layout.hit_test_point(Point::new(1.0, 21.0));
assert_eq!(pt.idx, 10);
let pt = layout.hit_test_point(Point::new(1.0, 38.0));
assert_eq!(pt.idx, 15);
let best_layout = text
.new_text_layout("best")
.font(font.clone(), 14.0)
.build()
.unwrap();
console::log_1(&format!("layout width: {:#?}", best_layout.size().width).into());
let pt = layout.hit_test_point(Point::new(1.0, 55.0));
assert_eq!(pt.idx, 15);
assert_eq!(pt.is_inside, false);
let pt = layout.hit_test_point(Point::new(25.0, 55.0));
assert_eq!(pt.idx, 19);
assert_eq!(pt.is_inside, false);
let pt = layout.hit_test_point(Point::new(27.0, 55.0));
assert_eq!(pt.idx, 19);
assert_eq!(pt.is_inside, false);
let piet_layout = text
.new_text_layout("piet ")
.font(font, 14.0)
.build()
.unwrap();
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);
assert_eq!(pt.is_inside, false);
let pt = layout.hit_test_point(Point::new(25.0, -14.0));
assert_eq!(pt.idx, 5);
assert_eq!(pt.is_inside, false);
let pt = layout.hit_test_point(Point::new(27.0, -14.0));
assert_eq!(pt.idx, 5);
assert_eq!(pt.is_inside, false);
}
}