mod grapheme;
mod lines;
use std::fmt;
use std::ops::RangeBounds;
use std::rc::Rc;
use cairo::{FontFace, FontOptions, FontSlant, FontWeight, Matrix, ScaledFont};
use piet::kurbo::{Point, Rect, Size};
use piet::{
util, Color, Error, FontFamily, FontStyle, HitTestPoint, HitTestPosition, LineMetric, Text,
TextAttribute, TextLayout, TextLayoutBuilder, TextStorage,
};
use unicode_segmentation::UnicodeSegmentation;
use self::grapheme::{get_grapheme_boundaries, point_x_in_grapheme};
#[derive(Clone)]
pub struct CairoText;
#[derive(Clone)]
struct CairoFont {
family: FontFamily,
}
#[derive(Clone)]
pub struct CairoTextLayout {
pub(crate) fg_color: Color,
size: Size,
trailing_ws_width: f64,
pub(crate) font: ScaledFont,
pub(crate) text: Rc<dyn TextStorage>,
pub(crate) line_metrics: Vec<LineMetric>,
}
pub struct CairoTextLayoutBuilder {
text: Rc<dyn TextStorage>,
defaults: util::LayoutDefaults,
width_constraint: f64,
}
impl CairoText {
#[allow(clippy::new_without_default)]
pub fn new() -> CairoText {
CairoText
}
}
impl Text for CairoText {
type TextLayout = CairoTextLayout;
type TextLayoutBuilder = CairoTextLayoutBuilder;
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::NotSupported)
}
fn new_text_layout(&mut self, text: impl TextStorage) -> Self::TextLayoutBuilder {
CairoTextLayoutBuilder {
defaults: util::LayoutDefaults::default(),
text: Rc::new(text),
width_constraint: f64::INFINITY,
}
}
}
impl fmt::Debug for CairoText {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("CairoText").finish()
}
}
impl CairoFont {
pub(crate) fn new(family: FontFamily) -> Self {
CairoFont { family }
}
#[cfg(test)]
pub(crate) fn resolve_simple(&self, size: f64) -> ScaledFont {
self.resolve(size, FontSlant::Normal, FontWeight::Normal)
}
pub(crate) fn resolve(&self, size: f64, slant: FontSlant, weight: FontWeight) -> ScaledFont {
let font_face = FontFace::toy_create(self.family.name(), slant, weight);
let font_matrix = scale_matrix(size);
let ctm = scale_matrix(1.0);
let options = FontOptions::default();
ScaledFont::new(&font_face, &font_matrix, &ctm, &options)
}
}
impl fmt::Debug for CairoFont {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("CairoFont").finish()
}
}
impl TextLayoutBuilder for CairoTextLayoutBuilder {
type Out = CairoTextLayout;
fn max_width(mut self, width: f64) -> Self {
self.width_constraint = width;
self
}
fn alignment(self, _alignment: piet::TextAlignment) -> Self {
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 {
self
}
fn build(self) -> Result<Self::Out, Error> {
let font = CairoFont::new(self.defaults.font.clone());
let size = self.defaults.font_size;
let weight = if self.defaults.weight.to_raw() <= piet::FontWeight::MEDIUM.to_raw() {
FontWeight::Normal
} else {
FontWeight::Bold
};
let slant = match self.defaults.style {
FontStyle::Italic => FontSlant::Italic,
FontStyle::Regular => FontSlant::Normal,
};
let scaled_font = font.resolve(size, slant, weight);
let mut layout = CairoTextLayout {
fg_color: self.defaults.fg_color,
font: scaled_font,
size: Size::ZERO,
trailing_ws_width: 0.0,
line_metrics: Vec::new(),
text: self.text,
};
layout.update_width(self.width_constraint)?;
Ok(layout)
}
}
impl fmt::Debug for CairoTextLayoutBuilder {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("CairoTextLayoutBuilder").finish()
}
}
impl TextLayout for CairoTextLayout {
fn size(&self) -> Size {
self.size
}
fn trailing_whitespace_width(&self) -> f64 {
self.trailing_ws_width
}
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.range()])
}
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 {
if self.text.is_empty() {
return HitTestPoint::default();
}
let height = self
.line_metrics
.last()
.map(|lm| lm.y_offset + lm.height)
.unwrap_or(0.0);
let (y_inside, lm) = if point.y < 0. {
(false, self.line_metrics.first().unwrap())
} else if point.y >= height {
(false, self.line_metrics.last().unwrap())
} else {
let line = self
.line_metrics
.iter()
.find(|l| point.y >= l.y_offset && point.y < l.y_offset + l.height)
.unwrap();
(true, line)
};
let line = &self.text[lm.range()];
let mut htp = hit_test_line_point(&self.font, line, point);
htp.idx += lm.start_offset;
if htp.idx == lm.end_offset {
htp.idx -= util::trailing_nlf(line).unwrap_or(0);
}
htp.is_inside &= y_inside;
htp
}
fn hit_test_text_position(&self, idx: usize) -> HitTestPosition {
let idx = idx.min(self.text.len());
assert!(self.text.is_char_boundary(idx));
if idx == 0 && self.text.is_empty() {
return HitTestPosition::new(Point::new(0., self.font.extents().ascent), 0);
}
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.font, line, line_position);
HitTestPosition::new(Point::new(x_pos, y_pos), line_num)
}
}
impl CairoTextLayout {
fn update_width(&mut self, new_width: impl Into<Option<f64>>) -> Result<(), Error> {
let new_width = new_width.into().unwrap_or(std::f64::INFINITY);
self.line_metrics = lines::calculate_line_metrics(&self.text, &self.font, new_width);
if self.text.is_empty() {
self.line_metrics.push(LineMetric {
baseline: self.font.extents().ascent,
height: self.font.extents().height,
..Default::default()
})
} else if util::trailing_nlf(&self.text).is_some() {
let newline_eof = self
.line_metrics
.last()
.map(|lm| LineMetric {
start_offset: self.text.len(),
end_offset: self.text.len(),
height: lm.height,
baseline: lm.baseline,
y_offset: lm.y_offset + lm.height,
trailing_whitespace: 0,
})
.unwrap();
self.line_metrics.push(newline_eof);
}
let (width, ws_width) = self
.line_metrics
.iter()
.map(|lm| {
let full_width = self.font.text_extents(&self.text[lm.range()]).x_advance;
let non_ws_width = if lm.trailing_whitespace > 0 {
let non_ws_range = lm.start_offset..lm.end_offset - lm.trailing_whitespace;
self.font.text_extents(&self.text[non_ws_range]).x_advance
} else {
full_width
};
(non_ws_width, full_width)
})
.fold((0.0, 0.0), |a: (f64, f64), b| (a.0.max(b.0), a.1.max(b.1)));
let height = self
.line_metrics
.last()
.map(|l| l.y_offset + l.height)
.unwrap_or_else(|| self.font.extents().height);
self.size = Size::new(width, height);
self.trailing_ws_width = ws_width;
Ok(())
}
}
impl fmt::Debug for CairoTextLayout {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("CairoTextLayout").finish()
}
}
fn hit_test_line_point(font: &ScaledFont, 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(font, text, end) {
Some(bounds) => bounds,
None => return HitTestPoint::default(),
};
let start = 0;
let start_bounds = match get_grapheme_boundaries(font, 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(font, 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(font: &ScaledFont, text: &str, text_position: usize) -> f64 {
let text_len = text.len();
if text_position == 0 {
return 0.0;
}
if text_position as usize >= text_len {
return font.text_extents(&text).x_advance;
}
let grapheme_indices = UnicodeSegmentation::grapheme_indices(text, true)
.take_while(|(byte_idx, _s)| text_position >= *byte_idx);
grapheme_indices
.last()
.map(|(idx, _)| font.text_extents(&text[..idx]).x_advance)
.unwrap_or_else(|| font.text_extents(&text).x_advance)
}
fn scale_matrix(scale: f64) -> Matrix {
Matrix {
xx: scale,
yx: 0.0,
xy: 0.0,
yy: scale,
x0: 0.0,
y0: 0.0,
}
}
#[cfg(test)]
mod test {
use super::*;
use piet::TextLayout;
macro_rules! assert_close {
($val:expr, $target:expr, $tolerance:expr) => {{
let min = $target - $tolerance;
let max = $target + $tolerance;
if $val < min || $val > max {
panic!(
"value {} outside target {} with tolerance {}",
$val, $target, $tolerance
);
}
}};
($val:expr, $target:expr, $tolerance:expr,) => {{
assert_close!($val, $target, $tolerance)
}};
}
#[test]
#[allow(clippy::float_cmp)]
fn hit_test_empty_string() {
let layout = CairoText::new().new_text_layout("").build().unwrap();
let pt = layout.hit_test_point(Point::new(0.0, 0.0));
assert_eq!(pt.idx, 0);
let pos = layout.hit_test_text_position(0);
assert_eq!(pos.point.x, 0.0);
assert_close!(pos.point.y, 10.0, 3.0);
let line = layout.line_metric(0).unwrap();
assert_close!(line.height, 12.0, 3.0);
}
#[test]
fn test_hit_test_text_position_basic() {
let mut text_layout = CairoText::new();
let input = "piet text!";
let layout = text_layout.new_text_layout(&input[0..4]).build().unwrap();
let piet_width = layout.size().width;
let layout = text_layout.new_text_layout(&input[0..3]).build().unwrap();
let pie_width = layout.size().width;
let layout = text_layout.new_text_layout(&input[0..2]).build().unwrap();
let pi_width = layout.size().width;
let layout = text_layout.new_text_layout(&input[0..1]).build().unwrap();
let p_width = layout.size().width;
let layout = text_layout.new_text_layout("").build().unwrap();
let null_width = layout.size().width;
let full_layout = text_layout.new_text_layout(input).build().unwrap();
let full_width = full_layout.size().width;
assert_close!(
full_layout.hit_test_text_position(4).point.x,
piet_width,
3.0,
);
assert_close!(
full_layout.hit_test_text_position(3).point.x,
pie_width,
3.0,
);
assert_close!(full_layout.hit_test_text_position(2).point.x, pi_width, 3.0,);
assert_close!(full_layout.hit_test_text_position(1).point.x, p_width, 3.0,);
assert_close!(
full_layout.hit_test_text_position(0).point.x,
null_width,
3.0,
);
assert_close!(
full_layout.hit_test_text_position(10).point.x,
full_width,
3.0,
);
assert_close!(
full_layout.hit_test_text_position(11).point.x,
full_width,
3.0,
);
}
#[test]
fn test_hit_test_text_position_complex_0() {
let input = "é";
assert_eq!(input.len(), 2);
let mut text_layout = CairoText::new();
let layout = text_layout.new_text_layout(input).build().unwrap();
assert_close!(layout.hit_test_text_position(0).point.x, 0.0, 3.0);
assert_close!(
layout.hit_test_text_position(2).point.x,
layout.size().width,
3.0,
);
let input = "\u{0023}\u{FE0F}\u{20E3}";
assert_eq!(input.len(), 7);
assert_eq!(input.chars().count(), 3);
let mut text_layout = CairoText::new();
let layout = text_layout.new_text_layout(input).build().unwrap();
assert_close!(layout.hit_test_text_position(0).point.x, 0.0, 3.0);
assert_close!(
layout.hit_test_text_position(7).point.x,
layout.size().width,
3.0,
);
}
#[test]
fn test_hit_test_text_position_complex_1() {
let input = "é\u{0023}\u{FE0F}\u{20E3}1\u{1D407}";
assert_eq!(input.len(), 14);
let mut text_layout = CairoText::new();
let layout = text_layout.new_text_layout(input).build().unwrap();
let test_layout_0 = text_layout.new_text_layout(&input[0..2]).build().unwrap();
let test_layout_1 = text_layout.new_text_layout(&input[0..9]).build().unwrap();
let test_layout_2 = text_layout.new_text_layout(&input[0..10]).build().unwrap();
assert_close!(layout.hit_test_text_position(0).point.x, 0.0, 3.0);
assert_close!(
layout.hit_test_text_position(2).point.x,
test_layout_0.size().width,
3.0,
);
assert_close!(
layout.hit_test_text_position(9).point.x,
test_layout_1.size().width,
3.0,
);
assert_close!(
layout.hit_test_text_position(10).point.x,
test_layout_2.size().width,
3.0,
);
assert_close!(
layout.hit_test_text_position(14).point.x,
layout.size().width,
3.0,
);
assert_close!(
layout.hit_test_text_position(3).point.x,
test_layout_0.size().width,
3.0,
);
assert_close!(
layout.hit_test_text_position(6).point.x,
test_layout_0.size().width,
3.0,
);
}
#[test]
#[cfg(target_os = "linux")]
fn test_hit_test_point_basic_0() {
let mut text_layout = CairoText::new();
let layout = text_layout.new_text_layout("piet text!").build().unwrap();
println!("text pos 4: {:?}", layout.hit_test_text_position(4));
println!("text pos 5: {:?}", layout.hit_test_text_position(5));
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, 5);
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);
println!("layout_width: {:?}", layout.size().width);
let pt = layout.hit_test_point(Point::new(56.0, 0.0));
assert_eq!(pt.idx, 10);
assert_eq!(pt.is_inside, true);
let pt = layout.hit_test_point(Point::new(57.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);
}
#[test]
#[cfg(target_os = "macos")]
fn test_hit_test_point_basic_0() {
let mut text_layout = CairoText::new();
let layout = text_layout.new_text_layout("piet text!").build().unwrap();
println!("text pos 4: {:?}", layout.hit_test_text_position(4));
println!("text pos 5: {:?}", layout.hit_test_text_position(5));
let pt = layout.hit_test_point(Point::new(19.0, 0.0));
assert_eq!(pt.idx, 4);
let pt = layout.hit_test_point(Point::new(20.0, 0.0));
assert_eq!(pt.idx, 4);
let pt = layout.hit_test_point(Point::new(21.0, 0.0));
assert_eq!(pt.idx, 4);
let pt = layout.hit_test_point(Point::new(22.0, 0.0));
assert_eq!(pt.idx, 5);
let pt = layout.hit_test_point(Point::new(23.0, 0.0));
assert_eq!(pt.idx, 5);
println!("layout_width: {:?}", layout.size().width);
let pt = layout.hit_test_point(Point::new(45.0, 0.0));
assert_eq!(pt.idx, 10);
assert_eq!(pt.is_inside, true);
let pt = layout.hit_test_point(Point::new(46.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);
}
#[test]
#[cfg(target_os = "linux")]
fn test_hit_test_point_basic_1() {
let mut text_layout = CairoText::new();
let layout = text_layout.new_text_layout("t").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").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);
}
#[test]
#[cfg(target_os = "macos")]
fn test_hit_test_point_basic_1() {
let mut text_layout = CairoText::new();
let layout = text_layout.new_text_layout("t").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").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);
}
#[test]
#[cfg(target_os = "linux")]
fn test_hit_test_point_complex_0() {
let input = "é\u{0023}\u{FE0F}\u{20E3}1\u{1D407}";
let mut text_layout = CairoText::new();
let layout = text_layout.new_text_layout(input).build().unwrap();
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, 2);
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, 9);
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);
}
#[test]
#[cfg(target_os = "macos")]
fn test_hit_test_point_complex_0() {
let input = "é\u{0023}\u{FE0F}\u{20E3}1\u{1D407}";
let mut text_layout = CairoText::new();
let layout = text_layout.new_text_layout(input).build().unwrap();
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));
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, 2);
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, 9);
let pt = layout.hit_test_point(Point::new(29.0, 0.0));
assert_eq!(pt.idx, 9);
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, 10);
let pt = layout.hit_test_point(Point::new(38.0, 0.0));
assert_eq!(pt.idx, 10);
let pt = layout.hit_test_point(Point::new(40.0, 0.0));
assert_eq!(pt.idx, 14);
let pt = layout.hit_test_point(Point::new(43.0, 0.0));
assert_eq!(pt.idx, 14);
}
#[test]
#[cfg(target_os = "linux")]
fn test_hit_test_point_complex_1() {
let input = "tßßypi";
let mut text_layout = CairoText::new();
let layout = text_layout.new_text_layout(input).build().unwrap();
println!("text pos 0: {:?}", layout.hit_test_text_position(0));
println!("text pos 1: {:?}", layout.hit_test_text_position(1));
println!("text pos 3: {:?}", layout.hit_test_text_position(3));
println!("text pos 5: {:?}", layout.hit_test_text_position(5));
println!("text pos 6: {:?}", layout.hit_test_text_position(6));
println!("text pos 7: {:?}", layout.hit_test_text_position(7));
println!("text pos 8: {:?}", layout.hit_test_text_position(8));
let pt = layout.hit_test_point(Point::new(27.0, 0.0));
assert_eq!(pt.idx, 6);
}
#[test]
#[cfg(target_os = "macos")]
fn test_hit_test_point_complex_1() {
let input = "tßßypi";
let mut text_layout = CairoText::new();
let layout = text_layout.new_text_layout(input).build().unwrap();
println!("text pos 0: {:?}", layout.hit_test_text_position(0));
println!("text pos 1: {:?}", layout.hit_test_text_position(1));
println!("text pos 3: {:?}", layout.hit_test_text_position(3));
println!("text pos 5: {:?}", layout.hit_test_text_position(5));
println!("text pos 6: {:?}", layout.hit_test_text_position(6));
println!("text pos 7: {:?}", layout.hit_test_text_position(7));
println!("text pos 8: {:?}", layout.hit_test_text_position(8));
let pt = layout.hit_test_point(Point::new(27.0, 0.0));
assert_eq!(pt.idx, 6);
}
#[test]
#[cfg(target_os = "linux")]
fn test_multiline_hit_test_text_position_basic() {
let mut text_layout = CairoText::new();
let input = "piet text!";
let layout = text_layout.new_text_layout(&input[0..3]).build().unwrap();
let pie_width = layout.size().width;
let layout = text_layout
.new_text_layout(&input[0..4])
.max_width(25.0)
.build()
.unwrap();
let piet_width = layout.size().width;
let layout = text_layout
.new_text_layout(&input[0..5])
.max_width(30.)
.build()
.unwrap();
let piet_space_width = layout.size().width;
let layout = text_layout
.new_text_layout(&input[6..10])
.max_width(25.0)
.build()
.unwrap();
let text_width = layout.size().width;
let layout = text_layout
.new_text_layout(&input[6..9])
.max_width(25.0)
.build()
.unwrap();
let tex_width = layout.size().width;
let layout = text_layout
.new_text_layout(&input[6..8])
.max_width(25.0)
.build()
.unwrap();
let te_width = layout.size().width;
let layout = text_layout
.new_text_layout(&input[6..7])
.max_width(25.0)
.build()
.unwrap();
let t_width = layout.size().width;
let full_layout = text_layout
.new_text_layout(input)
.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 = full_layout
.line_metric(0)
.map(|l| l.y_offset + l.baseline)
.unwrap();
let line_one_baseline = full_layout
.line_metric(1)
.map(|l| l.y_offset + l.baseline)
.unwrap();
assert_close!(
full_layout.hit_test_text_position(10).point.x,
text_width,
3.0,
);
assert_close!(
full_layout.hit_test_text_position(9).point.x,
tex_width,
3.0,
);
assert_close!(full_layout.hit_test_text_position(8).point.x, te_width, 3.0,);
assert_close!(full_layout.hit_test_text_position(7).point.x, t_width, 3.0,);
assert_close!(full_layout.hit_test_text_position(6).point.x, 0.0, 3.0,);
assert_close!(
full_layout.hit_test_text_position(3).point.x,
pie_width,
3.0,
);
assert_close!(
full_layout.hit_test_text_position(10).point.y,
line_one_baseline,
3.0,
);
assert_close!(
full_layout.hit_test_text_position(9).point.y,
line_one_baseline,
3.0,
);
assert_close!(
full_layout.hit_test_text_position(8).point.y,
line_one_baseline,
3.0,
);
assert_close!(
full_layout.hit_test_text_position(7).point.y,
line_one_baseline,
3.0,
);
assert_close!(
full_layout.hit_test_text_position(6).point.y,
line_one_baseline,
3.0,
);
assert_close!(
full_layout.hit_test_text_position(5).point.y,
line_zero_baseline,
3.0,
);
assert_close!(
full_layout.hit_test_text_position(4).point.y,
line_zero_baseline,
3.0,
);
}
#[test]
#[cfg(target_os = "macos")]
fn test_multiline_hit_test_text_position_basic() {
let mut text_layout = CairoText::new();
let input = "piet text!";
let font = text_layout
.font_family("Helvetica")
.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 = full_layout
.line_metric(0)
.map(|l| l.y_offset + l.baseline)
.unwrap();
let line_one_baseline = full_layout
.line_metric(1)
.map(|l| l.y_offset + l.baseline)
.unwrap();
assert_close!(
full_layout.hit_test_text_position(10).point.x,
text_width,
3.0,
);
assert_close!(
full_layout.hit_test_text_position(9).point.x,
tex_width,
3.0,
);
assert_close!(full_layout.hit_test_text_position(8).point.x, te_width, 3.0,);
assert_close!(full_layout.hit_test_text_position(7).point.x, t_width, 3.0,);
assert_close!(full_layout.hit_test_text_position(6).point.x, 0.0, 3.0,);
assert_close!(
full_layout.hit_test_text_position(3).point.x,
pie_width,
3.0,
);
assert_close!(
full_layout.hit_test_text_position(10).point.y,
line_one_baseline,
3.0,
);
assert_close!(
full_layout.hit_test_text_position(9).point.y,
line_one_baseline,
3.0,
);
assert_close!(
full_layout.hit_test_text_position(8).point.y,
line_one_baseline,
3.0,
);
assert_close!(
full_layout.hit_test_text_position(7).point.y,
line_one_baseline,
3.0,
);
assert_close!(
full_layout.hit_test_text_position(6).point.y,
line_one_baseline,
3.0,
);
assert_close!(
full_layout.hit_test_text_position(5).point.y,
line_zero_baseline,
3.0,
);
assert_close!(
full_layout.hit_test_text_position(4).point.y,
line_zero_baseline,
3.0,
);
}
#[test]
#[cfg(target_os = "linux")]
fn test_multiline_hit_test_point_basic() {
let input = "piet text most best";
let mut text = CairoText::new();
let layout = text.new_text_layout(input).max_width(30.0).build().unwrap();
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, -1.0));
assert_eq!(pt.idx, 0);
assert_eq!(pt.is_inside, false);
let pt = layout.hit_test_point(Point::new(1.0, 00.0));
assert_eq!(pt.idx, 0);
assert!(pt.is_inside);
let pt = layout.hit_test_point(Point::new(1.0, 14.0));
assert_eq!(pt.idx, 5);
let pt = layout.hit_test_point(Point::new(1.0, 28.0));
assert_eq!(pt.idx, 10);
let pt = layout.hit_test_point(Point::new(1.0, 44.0));
assert_eq!(pt.idx, 15);
let best_layout = text.new_text_layout("best").build().unwrap();
println!("layout width: {:#?}", best_layout.size().width);
let pt = layout.hit_test_point(Point::new(1.0, 56.0));
assert_eq!(pt.idx, 15);
assert_eq!(pt.is_inside, false);
let pt = layout.hit_test_point(Point::new(25.0, 56.0));
assert_eq!(pt.idx, 19);
assert_eq!(pt.is_inside, false);
let pt = layout.hit_test_point(Point::new(27.0, 56.0));
assert_eq!(pt.idx, 19);
assert_eq!(pt.is_inside, false);
let piet_layout = text.new_text_layout("piet ").build().unwrap();
println!("layout width: {:#?}", piet_layout.size().width);
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(26.0, -14.0));
assert_eq!(pt.idx, 5);
assert_eq!(pt.is_inside, false);
let pt = layout.hit_test_point(Point::new(28.0, -14.0));
assert_eq!(pt.idx, 5);
assert_eq!(pt.is_inside, false);
}
#[test]
#[cfg(target_os = "macos")]
fn test_multiline_hit_test_point_basic() {
let input = "piet text most best";
let mut text = CairoText::new();
let font = text.font_family("Helvetica").unwrap();
let layout = text
.new_text_layout(input)
.font(font.clone(), 13.0)
.max_width(30.0)
.build()
.unwrap();
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, -1.0));
assert_eq!(pt.idx, 0);
assert_eq!(pt.is_inside, false);
let pt = layout.hit_test_point(Point::new(1.0, 00.0));
assert_eq!(pt.idx, 0);
assert!(pt.is_inside);
let pt = layout.hit_test_point(Point::new(1.0, 12.));
assert_eq!(pt.idx, 0);
let pt = layout.hit_test_point(Point::new(1.0, 13.));
assert_eq!(pt.idx, 5);
let pt = layout.hit_test_point(Point::new(1.0, 26.0));
assert_eq!(pt.idx, 10);
let pt = layout.hit_test_point(Point::new(1.0, 39.0));
assert_eq!(pt.idx, 15);
assert!(pt.is_inside);
let best_layout = text
.new_text_layout("best")
.font(font.clone(), 13.0)
.build()
.unwrap();
println!("layout width: {:#?}", best_layout.size().width);
let pt = layout.hit_test_point(Point::new(1.0, 52.0));
assert_eq!(pt.idx, 15);
assert_eq!(pt.is_inside, false);
let pt = layout.hit_test_point(Point::new(25.0, 52.0));
assert_eq!(pt.idx, 19);
assert_eq!(pt.is_inside, false);
let pt = layout.hit_test_point(Point::new(27.0, 52.0));
assert_eq!(pt.idx, 19);
assert_eq!(pt.is_inside, false);
let piet_layout = text
.new_text_layout("piet ")
.font(font, 13.0)
.build()
.unwrap();
println!("layout width: {:#?}", piet_layout.size().width);
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);
}
}