#![cfg(windows)]
mod conv;
pub mod error;
use crate::conv::{
affine_to_matrix3x2f, color_to_colorf, convert_stroke_style, gradient_stop_to_d2d,
rect_to_rectf, to_point2f,
};
use crate::error::WrapError;
use std::borrow::Cow;
use std::convert::TryInto;
use winapi::shared::basetsd::UINT32;
use winapi::um::dcommon::D2D_SIZE_U;
use dxgi::Format;
use direct2d::brush::gradient::linear::LinearGradientBrushBuilder;
use direct2d::brush::gradient::radial::RadialGradientBrushBuilder;
pub use direct2d::brush::GenericBrush;
use direct2d::brush::{Brush, SolidColorBrush};
use direct2d::enums::{
AlphaMode, BitmapInterpolationMode, DrawTextOptions, FigureBegin, FigureEnd, FillMode,
};
use direct2d::geometry::path::{FigureBuilder, GeometryBuilder};
use direct2d::geometry::Path;
use direct2d::image::Bitmap;
use direct2d::layer::Layer;
use direct2d::math::{BezierSegment, QuadBezierSegment, SizeU, Vector2F};
use direct2d::render_target::{GenericRenderTarget, RenderTarget};
use directwrite::text_format::TextFormatBuilder;
use directwrite::text_layout;
use directwrite::TextFormat;
use piet::kurbo::{Affine, PathEl, Point, Rect, Shape};
use piet::{
new_error, Color, Error, ErrorKind, FixedGradient, Font, FontBuilder, HitTestMetrics,
HitTestPoint, HitTestTextPosition, ImageFormat, InterpolationMode, IntoBrush, RenderContext,
StrokeStyle, Text, TextLayout, TextLayoutBuilder,
};
pub struct D2DRenderContext<'a> {
factory: &'a direct2d::Factory,
inner_text: D2DText<'a>,
rt: GenericRenderTarget,
ctx_stack: Vec<CtxState>,
err: Result<(), Error>,
}
pub struct D2DText<'a> {
dwrite: &'a directwrite::Factory,
}
pub struct D2DFont(TextFormat);
pub struct D2DFontBuilder<'a> {
builder: TextFormatBuilder<'a>,
name: String,
}
pub struct D2DTextLayout {
text: String,
layout: text_layout::TextLayout,
}
pub struct D2DTextLayoutBuilder<'a> {
builder: text_layout::TextLayoutBuilder<'a>,
format: TextFormat,
text: String,
}
#[derive(Default)]
struct CtxState {
transform: Affine,
n_layers_pop: usize,
}
impl<'b, 'a: 'b> D2DRenderContext<'a> {
pub fn new<RT: RenderTarget>(
factory: &'a direct2d::Factory,
dwrite: &'a directwrite::Factory,
rt: &'b mut RT,
) -> D2DRenderContext<'b> {
let inner_text = D2DText { dwrite };
D2DRenderContext {
factory,
inner_text: inner_text,
rt: rt.as_generic(),
ctx_stack: vec![CtxState::default()],
err: Ok(()),
}
}
fn pop_state(&mut self) {
let old_state = self.ctx_stack.pop().unwrap();
for _ in 0..old_state.n_layers_pop {
self.rt.pop_layer();
}
}
}
enum PathBuilder<'a> {
Geom(GeometryBuilder<'a>),
Fig(FigureBuilder<'a>),
}
impl<'a> PathBuilder<'a> {
fn finish_figure(self) -> GeometryBuilder<'a> {
match self {
PathBuilder::Geom(g) => g,
PathBuilder::Fig(f) => f.end(),
}
}
}
const BEZ_TOLERANCE: f64 = 1e-3;
fn path_from_shape(
d2d: &direct2d::Factory,
is_filled: bool,
shape: impl Shape,
fill_mode: FillMode,
) -> Result<Path, Error> {
let mut path = Path::create(d2d).wrap()?;
{
let mut g = path.open().wrap()?;
if fill_mode == FillMode::Winding {
g = g.fill_mode(fill_mode);
}
let mut builder = Some(PathBuilder::Geom(g));
let bez_path = shape.into_bez_path(BEZ_TOLERANCE);
let bez_elements = bez_path.elements();
for i in 0..bez_elements.len() {
let el = &bez_elements[i];
match el {
PathEl::MoveTo(p) => {
let mut is_closed = is_filled;
if !is_filled {
for close_el in &bez_elements[i + 1..] {
match close_el {
PathEl::MoveTo(_) => break,
PathEl::ClosePath => {
is_closed = true;
break;
}
_ => (),
}
}
}
if let Some(b) = builder.take() {
let g = b.finish_figure();
let begin = if is_filled {
FigureBegin::Filled
} else {
FigureBegin::Hollow
};
let end = if is_closed {
FigureEnd::Closed
} else {
FigureEnd::Open
};
let f = g.begin_figure(to_point2f(*p), begin, end);
builder = Some(PathBuilder::Fig(f));
}
}
PathEl::LineTo(p) => {
if let Some(PathBuilder::Fig(f)) = builder.take() {
let f = f.add_line(to_point2f(*p));
builder = Some(PathBuilder::Fig(f));
}
}
PathEl::QuadTo(p1, p2) => {
if let Some(PathBuilder::Fig(f)) = builder.take() {
let q = QuadBezierSegment::new(to_point2f(*p1), to_point2f(*p2));
let f = f.add_quadratic_bezier(&q);
builder = Some(PathBuilder::Fig(f));
}
}
PathEl::CurveTo(p1, p2, p3) => {
if let Some(PathBuilder::Fig(f)) = builder.take() {
let c =
BezierSegment::new(to_point2f(*p1), to_point2f(*p2), to_point2f(*p3));
let f = f.add_bezier(&c);
builder = Some(PathBuilder::Fig(f));
}
}
_ => (),
}
}
if let Some(b) = builder.take() {
let _ = b.finish_figure().close();
}
}
Ok(path)
}
impl<'a> RenderContext for D2DRenderContext<'a> {
type Brush = GenericBrush;
type Text = D2DText<'a>;
type TextLayout = D2DTextLayout;
type Image = Bitmap;
fn status(&mut self) -> Result<(), Error> {
std::mem::replace(&mut self.err, Ok(()))
}
fn clear(&mut self, color: Color) {
self.rt.clear(color.as_rgba_u32() >> 8);
}
fn solid_brush(&mut self, color: Color) -> GenericBrush {
SolidColorBrush::create(&self.rt)
.with_color(color_to_colorf(color))
.build()
.wrap()
.expect("error creating solid brush")
.to_generic()
}
fn gradient(&mut self, gradient: impl Into<FixedGradient>) -> Result<GenericBrush, Error> {
match gradient.into() {
FixedGradient::Linear(linear) => {
let mut builder = LinearGradientBrushBuilder::new(&self.rt)
.with_start(to_point2f(linear.start))
.with_end(to_point2f(linear.end));
for stop in &linear.stops {
builder = builder.with_stop(gradient_stop_to_d2d(stop));
}
let brush = builder.build().wrap()?;
Ok(brush.to_generic())
}
FixedGradient::Radial(radial) => {
let radius = radial.radius as f32;
let mut builder = RadialGradientBrushBuilder::new(&self.rt)
.with_center(to_point2f(radial.center))
.with_origin_offset(to_point2f(radial.origin_offset))
.with_radius(radius, radius);
for stop in &radial.stops {
builder = builder.with_stop(gradient_stop_to_d2d(stop));
}
let brush = builder.build().wrap()?;
Ok(brush.to_generic())
}
}
}
fn fill(&mut self, shape: impl Shape, brush: &impl IntoBrush<Self>) {
let brush = brush.make_brush(self, || shape.bounding_box());
match path_from_shape(self.factory, true, shape, FillMode::Winding) {
Ok(path) => self.rt.fill_geometry(&path, &*brush),
Err(e) => self.err = Err(e),
}
}
fn fill_even_odd(&mut self, shape: impl Shape, brush: &impl IntoBrush<Self>) {
let brush = brush.make_brush(self, || shape.bounding_box());
match path_from_shape(self.factory, true, shape, FillMode::Alternate) {
Ok(path) => self.rt.fill_geometry(&path, &*brush),
Err(e) => self.err = Err(e),
}
}
fn stroke(&mut self, shape: impl Shape, brush: &impl IntoBrush<Self>, width: f64) {
let brush = brush.make_brush(self, || shape.bounding_box());
let path = match path_from_shape(self.factory, false, shape, FillMode::Alternate) {
Ok(path) => path,
Err(e) => {
self.err = Err(e);
return;
}
};
let width = width as f32;
self.rt.draw_geometry(&path, &*brush, width, None);
}
fn stroke_styled(
&mut self,
shape: impl Shape,
brush: &impl IntoBrush<Self>,
width: f64,
style: &StrokeStyle,
) {
let brush = brush.make_brush(self, || shape.bounding_box());
let path = match path_from_shape(self.factory, false, shape, FillMode::Alternate) {
Ok(path) => path,
Err(e) => {
self.err = Err(e);
return;
}
};
let width = width as f32;
let style = convert_stroke_style(self.factory, style, width)
.expect("stroke style conversion failed");
self.rt.draw_geometry(&path, &*brush, width, Some(&style));
}
fn clip(&mut self, shape: impl Shape) {
let layer = match Layer::create(&mut self.rt, None).wrap() {
Ok(layer) => layer,
Err(e) => {
self.err = Err(e);
return;
}
};
let path = match path_from_shape(self.factory, true, shape, FillMode::Winding) {
Ok(path) => path,
Err(e) => {
self.err = Err(e);
return;
}
};
let _clone = path.clone();
self.rt.push_layer(&layer).with_mask(path).push();
self.ctx_stack.last_mut().unwrap().n_layers_pop += 1;
}
fn text(&mut self) -> &mut Self::Text {
&mut self.inner_text
}
fn draw_text(
&mut self,
layout: &Self::TextLayout,
pos: impl Into<Point>,
brush: &impl IntoBrush<Self>,
) {
let brush = brush.make_brush(self, || Rect::ZERO);
let mut line_metrics = Vec::with_capacity(1);
layout.layout.get_line_metrics(&mut line_metrics);
if line_metrics.is_empty() {
return;
}
let pos = to_point2f(pos.into());
let pos = pos - Vector2F::new(0.0, line_metrics[0].baseline());
let text_options = DrawTextOptions::NONE;
self.rt
.draw_text_layout(pos, &layout.layout, &*brush, text_options);
}
fn save(&mut self) -> Result<(), Error> {
let new_state = CtxState {
transform: self.current_transform(),
n_layers_pop: 0,
};
self.ctx_stack.push(new_state);
Ok(())
}
fn restore(&mut self) -> Result<(), Error> {
if self.ctx_stack.len() <= 1 {
return Err(new_error(ErrorKind::StackUnbalance));
}
self.pop_state();
self.rt
.set_transform(&affine_to_matrix3x2f(self.current_transform()));
Ok(())
}
fn finish(&mut self) -> Result<(), Error> {
if self.ctx_stack.len() != 1 {
return Err(new_error(ErrorKind::StackUnbalance));
}
self.pop_state();
std::mem::replace(&mut self.err, Ok(()))
}
fn transform(&mut self, transform: Affine) {
self.ctx_stack.last_mut().unwrap().transform *= transform;
self.rt
.set_transform(&affine_to_matrix3x2f(self.current_transform()));
}
fn current_transform(&self) -> Affine {
self.ctx_stack.last().unwrap().transform
}
fn make_image(
&mut self,
width: usize,
height: usize,
buf: &[u8],
format: ImageFormat,
) -> Result<Self::Image, Error> {
let alpha_mode = match format {
ImageFormat::Rgb => AlphaMode::Ignore,
ImageFormat::RgbaPremul | ImageFormat::RgbaSeparate => AlphaMode::Premultiplied,
_ => return Err(new_error(ErrorKind::NotSupported)),
};
let buf = match format {
ImageFormat::Rgb => {
let mut new_buf = vec![255; width * height * 4];
for i in 0..width * height {
new_buf[i * 4 + 0] = buf[i * 3 + 0];
new_buf[i * 4 + 1] = buf[i * 3 + 1];
new_buf[i * 4 + 2] = buf[i * 3 + 2];
}
Cow::from(new_buf)
}
ImageFormat::RgbaSeparate => {
let mut new_buf = vec![255; width * height * 4];
fn premul(x: u8, a: u8) -> u8 {
let y = (x as u16) * (a as u16);
((y + (y >> 8) + 0x80) >> 8) as u8
}
for i in 0..width * height {
let a = buf[i * 4 + 3];
new_buf[i * 4 + 0] = premul(buf[i * 4 + 0], a);
new_buf[i * 4 + 1] = premul(buf[i * 4 + 1], a);
new_buf[i * 4 + 2] = premul(buf[i * 4 + 2], a);
new_buf[i * 4 + 3] = a;
}
Cow::from(new_buf)
}
ImageFormat::RgbaPremul => Cow::from(buf),
_ => return Err(new_error(ErrorKind::NotSupported)),
};
Bitmap::create(&self.rt)
.with_raw_data(
SizeU(D2D_SIZE_U {
width: width as UINT32,
height: height as UINT32,
}),
&buf,
width as UINT32 * 4,
)
.with_format(Format::R8G8B8A8Unorm)
.with_alpha_mode(alpha_mode)
.build()
.wrap()
}
fn draw_image(
&mut self,
image: &Self::Image,
rect: impl Into<Rect>,
interp: InterpolationMode,
) {
let interp = match interp {
InterpolationMode::NearestNeighbor => BitmapInterpolationMode::NearestNeighbor,
InterpolationMode::Bilinear => BitmapInterpolationMode::Linear,
};
let src_size = image.get_size();
let src_rect = (0.0, 0.0, src_size.0.width, src_size.0.height);
self.rt
.draw_bitmap(&image, rect_to_rectf(rect.into()), 1.0, interp, src_rect);
}
}
impl<'a> IntoBrush<D2DRenderContext<'a>> for GenericBrush {
fn make_brush<'b>(
&'b self,
_piet: &mut D2DRenderContext,
_bbox: impl FnOnce() -> Rect,
) -> std::borrow::Cow<'b, GenericBrush> {
Cow::Borrowed(self)
}
}
impl<'a> D2DText<'a> {
pub fn new(dwrite: &'a directwrite::Factory) -> D2DText<'a> {
D2DText { dwrite }
}
}
impl<'a> Text for D2DText<'a> {
type FontBuilder = D2DFontBuilder<'a>;
type Font = D2DFont;
type TextLayoutBuilder = D2DTextLayoutBuilder<'a>;
type TextLayout = D2DTextLayout;
fn new_font_by_name(&mut self, name: &str, size: f64) -> Self::FontBuilder {
D2DFontBuilder {
builder: TextFormat::create(self.dwrite).with_size(size as f32),
name: name.to_owned(),
}
}
fn new_text_layout(&mut self, font: &Self::Font, text: &str) -> Self::TextLayoutBuilder {
D2DTextLayoutBuilder {
builder: text_layout::TextLayout::create(self.dwrite),
format: font.0.clone(),
text: text.to_owned(),
}
}
}
impl<'a> FontBuilder for D2DFontBuilder<'a> {
type Out = D2DFont;
fn build(self) -> Result<Self::Out, Error> {
Ok(D2DFont(
self.builder.with_family(&self.name).build().wrap()?,
))
}
}
impl Font for D2DFont {}
impl<'a> TextLayoutBuilder for D2DTextLayoutBuilder<'a> {
type Out = D2DTextLayout;
fn build(self) -> Result<Self::Out, Error> {
Ok(D2DTextLayout {
layout: self
.builder
.with_text(&self.text)
.with_font(&self.format)
.with_width(1e6)
.with_height(1e6)
.build()
.wrap()?,
text: self.text,
})
}
}
impl TextLayout for D2DTextLayout {
fn width(&self) -> f64 {
self.layout.get_metrics().width() as f64
}
fn hit_test_point(&self, point: Point) -> HitTestPoint {
let htp = self.layout.hit_test_point(point.x as f32, point.y as f32);
let text_position_16 = if htp.is_trailing_hit {
htp.metrics.text_position() + htp.metrics.length()
} else {
htp.metrics.text_position()
} as usize;
let text_position =
count_until_utf16(&self.text, text_position_16).unwrap_or(self.text.len());
HitTestPoint {
metrics: HitTestMetrics { text_position },
is_inside: htp.is_inside,
}
}
fn hit_test_text_position(&self, text_position: usize) -> Option<HitTestTextPosition> {
let idx_16 = count_utf16(&self.text[0..text_position]);
let idx_16 = idx_16.try_into().ok()?;
let trailing = true;
self.layout
.hit_test_text_position(idx_16, trailing)
.map(|http| {
HitTestTextPosition {
point: Point {
x: http.point_x as f64,
y: http.point_y as f64,
},
metrics: HitTestMetrics {
text_position: text_position,
},
}
})
}
}
pub(crate) fn count_utf16(s: &str) -> usize {
let mut utf16_count = 0;
for &b in s.as_bytes() {
if (b as i8) >= -0x40 {
utf16_count += 1;
}
if b >= 0xf0 {
utf16_count += 1;
}
}
utf16_count
}
pub(crate) fn count_until_utf16(s: &str, utf16_text_position: usize) -> Option<usize> {
let mut utf8_count = 0;
let mut utf16_count = 0;
for &b in s.as_bytes() {
if (b as i8) >= -0x40 {
utf16_count += 1;
}
if b >= 0xf0 {
utf16_count += 1;
}
if utf16_count > utf16_text_position {
return Some(utf8_count);
}
utf8_count += 1;
}
None
}
#[cfg(test)]
mod test {
use crate::*;
use piet::TextLayout;
fn assert_close_to(x: f64, target: f64, tolerance: f64) {
let min = target - tolerance;
let max = target + tolerance;
assert!(x <= max && x >= min);
}
#[test]
fn test_hit_test_text_position_basic() {
let dwrite = directwrite::factory::Factory::new().unwrap();
let mut text_layout = D2DText::new(&dwrite);
let input = "piet text!";
let font = text_layout
.new_font_by_name("sans-serif", 12.0)
.build()
.unwrap();
let layout = text_layout
.new_text_layout(&font, &input[0..4])
.build()
.unwrap();
let piet_width = layout.width();
let layout = text_layout
.new_text_layout(&font, &input[0..3])
.build()
.unwrap();
let pie_width = layout.width();
let layout = text_layout
.new_text_layout(&font, &input[0..2])
.build()
.unwrap();
let pi_width = layout.width();
let layout = text_layout
.new_text_layout(&font, &input[0..1])
.build()
.unwrap();
let p_width = layout.width();
let layout = text_layout.new_text_layout(&font, "").build().unwrap();
let null_width = layout.width();
let full_layout = text_layout.new_text_layout(&font, input).build().unwrap();
let full_width = full_layout.width();
assert_close_to(
full_layout.hit_test_text_position(4).unwrap().point.x as f64,
piet_width,
3.0,
);
assert_close_to(
full_layout.hit_test_text_position(3).unwrap().point.x as f64,
pie_width,
3.0,
);
assert_close_to(
full_layout.hit_test_text_position(2).unwrap().point.x as f64,
pi_width,
3.0,
);
assert_close_to(
full_layout.hit_test_text_position(1).unwrap().point.x as f64,
p_width,
3.0,
);
assert_close_to(
full_layout.hit_test_text_position(0).unwrap().point.x as f64,
null_width,
3.0,
);
assert_close_to(
full_layout.hit_test_text_position(10).unwrap().point.x as f64,
full_width,
3.0,
);
}
#[test]
fn test_hit_test_text_position_complex_0() {
let dwrite = directwrite::factory::Factory::new().unwrap();
let input = "é";
assert_eq!(input.len(), 2);
let mut text_layout = D2DText::new(&dwrite);
let font = text_layout
.new_font_by_name("sans-serif", 12.0)
.build()
.unwrap();
let layout = text_layout.new_text_layout(&font, input).build().unwrap();
assert_close_to(layout.hit_test_text_position(0).unwrap().point.x, 0.0, 3.0);
assert_close_to(
layout.hit_test_text_position(2).unwrap().point.x,
layout.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 = D2DText::new(&dwrite);
let font = text_layout
.new_font_by_name("sans-serif", 12.0)
.build()
.unwrap();
let layout = text_layout.new_text_layout(&font, input).build().unwrap();
assert_close_to(layout.hit_test_text_position(0).unwrap().point.x, 0.0, 3.0);
assert_close_to(
layout.hit_test_text_position(7).unwrap().point.x,
layout.width(),
3.0,
);
assert_close_to(layout.hit_test_text_position(1).unwrap().point.x, 0.0, 3.0);
assert_eq!(
layout
.hit_test_text_position(1)
.unwrap()
.metrics
.text_position,
1
);
}
#[test]
fn test_hit_test_text_position_complex_1() {
let dwrite = directwrite::factory::Factory::new().unwrap();
let input = "é\u{0023}\u{FE0F}\u{20E3}1\u{1D407}";
assert_eq!(input.len(), 14);
let mut text_layout = D2DText::new(&dwrite);
let font = text_layout
.new_font_by_name("sans-serif", 12.0)
.build()
.unwrap();
let layout = text_layout.new_text_layout(&font, input).build().unwrap();
let test_layout_0 = text_layout
.new_text_layout(&font, &input[0..2])
.build()
.unwrap();
let test_layout_1 = text_layout
.new_text_layout(&font, &input[0..9])
.build()
.unwrap();
let test_layout_2 = text_layout
.new_text_layout(&font, &input[0..10])
.build()
.unwrap();
assert_close_to(layout.hit_test_text_position(0).unwrap().point.x, 0.0, 3.0);
assert_close_to(
layout.hit_test_text_position(2).unwrap().point.x,
test_layout_0.width(),
3.0,
);
assert_close_to(
layout.hit_test_text_position(9).unwrap().point.x,
test_layout_1.width(),
3.0,
);
assert_close_to(
layout.hit_test_text_position(10).unwrap().point.x,
test_layout_2.width(),
3.0,
);
assert_close_to(
layout.hit_test_text_position(14).unwrap().point.x,
layout.width(),
3.0,
);
assert_close_to(
layout.hit_test_text_position(3).unwrap().point.x,
test_layout_0.width(),
3.0,
);
assert_eq!(
layout
.hit_test_text_position(3)
.unwrap()
.metrics
.text_position,
3
);
assert_close_to(
layout.hit_test_text_position(6).unwrap().point.x,
test_layout_0.width(),
3.0,
);
assert_eq!(
layout
.hit_test_text_position(6)
.unwrap()
.metrics
.text_position,
6
);
}
#[test]
fn test_hit_test_point_basic() {
let dwrite = directwrite::factory::Factory::new().unwrap();
let mut text_layout = D2DText::new(&dwrite);
let font = text_layout
.new_font_by_name("sans-serif", 12.0)
.build()
.unwrap();
let layout = text_layout
.new_text_layout(&font, "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(21.0, 0.0));
assert_eq!(pt.metrics.text_position, 4);
let pt = layout.hit_test_point(Point::new(22.0, 0.0));
assert_eq!(pt.metrics.text_position, 5);
let pt = layout.hit_test_point(Point::new(23.0, 0.0));
assert_eq!(pt.metrics.text_position, 5);
let pt = layout.hit_test_point(Point::new(24.0, 0.0));
assert_eq!(pt.metrics.text_position, 5);
let pt = layout.hit_test_point(Point::new(25.0, 0.0));
assert_eq!(pt.metrics.text_position, 5);
println!("layout_width: {:?}", layout.width());
let pt = layout.hit_test_point(Point::new(48.0, 0.0));
assert_eq!(pt.metrics.text_position, 10);
assert_eq!(pt.is_inside, false);
let pt = layout.hit_test_point(Point::new(-1.0, 0.0));
assert_eq!(pt.metrics.text_position, 0);
assert_eq!(pt.is_inside, false);
}
#[test]
fn test_hit_test_point_complex() {
let dwrite = directwrite::factory::Factory::new().unwrap();
let input = "é\u{0023}\u{FE0F}\u{20E3}1\u{1D407}";
let mut text_layout = D2DText::new(&dwrite);
let font = text_layout
.new_font_by_name("sans-serif", 12.0)
.build()
.unwrap();
let layout = text_layout.new_text_layout(&font, 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.metrics.text_position, 0);
let pt = layout.hit_test_point(Point::new(4.0, 0.0));
assert_eq!(pt.metrics.text_position, 2);
let pt = layout.hit_test_point(Point::new(7.0, 0.0));
assert_eq!(pt.metrics.text_position, 2);
let pt = layout.hit_test_point(Point::new(10.0, 0.0));
assert_eq!(pt.metrics.text_position, 2);
let pt = layout.hit_test_point(Point::new(14.0, 0.0));
assert_eq!(pt.metrics.text_position, 9);
let pt = layout.hit_test_point(Point::new(18.0, 0.0));
assert_eq!(pt.metrics.text_position, 9);
let pt = layout.hit_test_point(Point::new(19.0, 0.0));
assert_eq!(pt.metrics.text_position, 9);
let pt = layout.hit_test_point(Point::new(23.0, 0.0));
assert_eq!(pt.metrics.text_position, 10);
let pt = layout.hit_test_point(Point::new(25.0, 0.0));
assert_eq!(pt.metrics.text_position, 10);
let pt = layout.hit_test_point(Point::new(32.0, 0.0));
assert_eq!(pt.metrics.text_position, 14);
let pt = layout.hit_test_point(Point::new(35.0, 0.0));
assert_eq!(pt.metrics.text_position, 14);
}
#[test]
fn test_count_until_utf16() {
let input = "é\u{0023}\u{FE0F}\u{20E3}1";
assert_eq!(count_until_utf16(input, 0), Some(0));
assert_eq!(count_until_utf16(input, 1), Some(2));
assert_eq!(count_until_utf16(input, 2), Some(3));
assert_eq!(count_until_utf16(input, 3), Some(6));
assert_eq!(count_until_utf16(input, 4), Some(9));
assert_eq!(count_until_utf16(input, 5), None);
}
}