Merge pull request #670 from twitchyliquid64/text_backend

Refactor textual hit testing into a `renderer::Backend` method
This commit is contained in:
Héctor Ramón 2021-08-26 14:53:15 +07:00 committed by GitHub
commit 6821114cae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 335 additions and 69 deletions

View file

@ -1,4 +1,4 @@
//! Reuse basic keyboard types.
//! Listen to keyboard events.
mod event;
mod hotkey;
mod key_code;

View file

@ -17,6 +17,7 @@
pub mod keyboard;
pub mod menu;
pub mod mouse;
pub mod text;
mod align;
mod background;

View file

@ -1,4 +1,4 @@
//! Reuse basic mouse types.
//! Handle mouse events.
mod button;
mod event;
mod interaction;

29
core/src/text.rs Normal file
View file

@ -0,0 +1,29 @@
//! Draw and interact with text.
use crate::Vector;
/// The result of hit testing on text.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Hit {
/// The point was within the bounds of the returned character index.
CharOffset(usize),
/// The provided point was not within the bounds of a glyph. The index
/// of the character with the closest centeroid position is returned,
/// as well as its delta.
NearestCharOffset(usize, Vector),
}
impl Hit {
/// Computes the cursor position corresponding to this [`HitTestResult`] .
pub fn cursor(&self) -> usize {
match self {
Self::CharOffset(i) => *i,
Self::NearestCharOffset(i, delta) => {
if delta.x > f32::EPSILON {
i + 1
} else {
*i
}
}
}
}
}

View file

@ -2,6 +2,7 @@ use crate::quad;
use crate::text;
use crate::triangle;
use crate::{Settings, Transformation, Viewport};
use iced_graphics::backend;
use iced_graphics::font;
use iced_graphics::Layer;
@ -211,6 +212,25 @@ impl backend::Text for Backend {
) -> (f32, f32) {
self.text_pipeline.measure(contents, size, font, bounds)
}
fn hit_test(
&self,
contents: &str,
size: f32,
font: Font,
bounds: Size,
point: iced_native::Point,
nearest_only: bool,
) -> text::Hit {
self.text_pipeline.hit_test(
contents,
size,
font,
bounds,
point,
nearest_only,
)
}
}
#[cfg(feature = "image")]

View file

@ -1,8 +1,12 @@
use crate::Transformation;
use glow_glyph::ab_glyph;
use iced_graphics::font;
use glow_glyph::ab_glyph;
use std::{cell::RefCell, collections::HashMap};
pub use iced_native::text::Hit;
#[derive(Debug)]
pub struct Pipeline {
draw_brush: RefCell<glow_glyph::GlyphBrush>,
@ -109,6 +113,94 @@ impl Pipeline {
}
}
pub fn hit_test(
&self,
content: &str,
size: f32,
font: iced_native::Font,
bounds: iced_native::Size,
point: iced_native::Point,
nearest_only: bool,
) -> Hit {
use glow_glyph::GlyphCruncher;
let glow_glyph::FontId(font_id) = self.find_font(font);
let section = glow_glyph::Section {
bounds: (bounds.width, bounds.height),
text: vec![glow_glyph::Text {
text: content,
scale: size.into(),
font_id: glow_glyph::FontId(font_id),
extra: glow_glyph::Extra::default(),
}],
..Default::default()
};
let mut mb = self.measure_brush.borrow_mut();
// The underlying type is FontArc, so clones are cheap.
use ab_glyph::{Font, ScaleFont};
let font = mb.fonts()[font_id].clone().into_scaled(size);
// Implements an iterator over the glyph bounding boxes.
let bounds = mb.glyphs(section).map(
|glow_glyph::SectionGlyph {
byte_index, glyph, ..
}| {
(
*byte_index,
iced_native::Rectangle::new(
iced_native::Point::new(
glyph.position.x - font.h_side_bearing(glyph.id),
glyph.position.y - font.ascent(),
),
iced_native::Size::new(
font.h_advance(glyph.id),
font.ascent() - font.descent(),
),
),
)
},
);
// Implements computation of the character index based on the byte index
// within the input string.
let char_index = |byte_index| {
let mut b_count = 0;
for (i, utf8_len) in
content.chars().map(|c| c.len_utf8()).enumerate()
{
if byte_index < (b_count + utf8_len) {
return i;
}
b_count += utf8_len;
}
return byte_index;
};
if !nearest_only {
for (idx, bounds) in bounds.clone() {
if bounds.contains(point) {
return Hit::CharOffset(char_index(idx));
}
}
}
let (idx, nearest) = bounds.fold(
(0usize, iced_native::Point::ORIGIN),
|acc: (usize, iced_native::Point), (idx, bounds)| {
if bounds.center().distance(point) < acc.1.distance(point) {
(idx, bounds.center())
} else {
acc
}
},
);
Hit::NearestCharOffset(char_index(idx), (point - nearest).into())
}
pub fn trim_measurement_cache(&mut self) {
// TODO: We should probably use a `GlyphCalculator` for this. However,
// it uses a lifetimed `GlyphCalculatorGuard` with side-effects on drop.

View file

@ -1,7 +1,8 @@
//! Write a graphics backend.
use iced_native::image;
use iced_native::svg;
use iced_native::{Font, Size};
use iced_native::text;
use iced_native::{Font, Point, Size};
/// The graphics backend of a [`Renderer`].
///
@ -43,6 +44,23 @@ pub trait Text {
font: Font,
bounds: Size,
) -> (f32, f32);
/// Tests whether the provided point is within the boundaries of [`Text`]
/// laid out with the given parameters, returning information about
/// the nearest character.
///
/// If nearest_only is true, the hit test does not consider whether the
/// the point is interior to any glyph bounds, returning only the character
/// with the nearest centeroid.
fn hit_test(
&self,
contents: &str,
size: f32,
font: Font,
bounds: Size,
point: Point,
nearest_only: bool,
) -> text::Hit;
}
/// A graphics backend that supports image rendering.

View file

@ -4,7 +4,7 @@ use crate::{Primitive, Renderer};
use iced_native::mouse;
use iced_native::text;
use iced_native::{
Color, Font, HorizontalAlignment, Rectangle, Size, VerticalAlignment,
Color, Font, HorizontalAlignment, Point, Rectangle, Size, VerticalAlignment,
};
/// A paragraph of text.
@ -35,6 +35,25 @@ where
.measure(content, f32::from(size), font, bounds)
}
fn hit_test(
&self,
content: &str,
size: f32,
font: Font,
bounds: Size,
point: Point,
nearest_only: bool,
) -> text::Hit {
self.backend().hit_test(
content,
size,
font,
bounds,
point,
nearest_only,
)
}
fn draw(
&mut self,
defaults: &Self::Defaults,

View file

@ -2,7 +2,7 @@ use crate::{
button, checkbox, column, container, pane_grid, progress_bar, radio, row,
scrollable, slider, text, text_input, toggler, Color, Element, Font,
HorizontalAlignment, Layout, Padding, Point, Rectangle, Renderer, Size,
VerticalAlignment,
Vector, VerticalAlignment,
};
/// A renderer that does nothing.
@ -67,6 +67,18 @@ impl text::Renderer for Null {
(0.0, 20.0)
}
fn hit_test(
&self,
_contents: &str,
_size: f32,
_font: Self::Font,
_bounds: Size,
_point: Point,
_nearest_only: bool,
) -> text::Hit {
text::Hit::NearestCharOffset(0, Vector::new(0., 0.))
}
fn draw(
&mut self,
_defaults: &Self::Defaults,

View file

@ -4,6 +4,8 @@ use crate::{
Rectangle, Size, VerticalAlignment, Widget,
};
pub use iced_core::text::Hit;
use std::hash::Hash;
/// A paragraph of text.
@ -179,6 +181,23 @@ pub trait Renderer: crate::Renderer {
bounds: Size,
) -> (f32, f32);
/// Tests whether the provided point is within the boundaries of [`Text`]
/// laid out with the given parameters, returning information about
/// the nearest character.
///
/// If `nearest_only` is true, the hit test does not consider whether the
/// the point is interior to any glyph bounds, returning only the character
/// with the nearest centeroid.
fn hit_test(
&self,
contents: &str,
size: f32,
font: Self::Font,
bounds: Size,
point: Point,
nearest_only: bool,
) -> Hit;
/// Draws a [`Text`] fragment.
///
/// It receives:

View file

@ -707,15 +707,15 @@ pub trait Renderer: text::Renderer + Sized {
let offset = self.offset(text_bounds, font, size, &value, &state);
find_cursor_position(
self,
&value,
self.hit_test(
&value.to_string(),
size.into(),
font,
size,
x + offset,
0,
value.len(),
Size::INFINITY,
Point::new(x + offset, text_bounds.height / 2.0),
true,
)
.cursor()
}
}
@ -803,62 +803,6 @@ impl State {
}
}
// TODO: Reduce allocations
fn find_cursor_position<Renderer: self::Renderer>(
renderer: &Renderer,
value: &Value,
font: Renderer::Font,
size: u16,
target: f32,
start: usize,
end: usize,
) -> usize {
if start >= end {
if start == 0 {
return 0;
}
let prev = value.until(start - 1);
let next = value.until(start);
let prev_width = renderer.measure_value(&prev.to_string(), size, font);
let next_width = renderer.measure_value(&next.to_string(), size, font);
if next_width - target > target - prev_width {
return start - 1;
} else {
return start;
}
}
let index = (end - start) / 2;
let subvalue = value.until(start + index);
let width = renderer.measure_value(&subvalue.to_string(), size, font);
if width > target {
find_cursor_position(
renderer,
value,
font,
size,
target,
start,
start + index,
)
} else {
find_cursor_position(
renderer,
value,
font,
size,
target,
start + index + 1,
end,
)
}
}
mod platform {
use crate::keyboard;

View file

@ -2,6 +2,7 @@ use crate::quad;
use crate::text;
use crate::triangle;
use crate::{Settings, Transformation};
use iced_graphics::backend;
use iced_graphics::font;
use iced_graphics::layer::Layer;
@ -274,6 +275,25 @@ impl backend::Text for Backend {
) -> (f32, f32) {
self.text_pipeline.measure(contents, size, font, bounds)
}
fn hit_test(
&self,
contents: &str,
size: f32,
font: Font,
bounds: Size,
point: iced_native::Point,
nearest_only: bool,
) -> text::Hit {
self.text_pipeline.hit_test(
contents,
size,
font,
bounds,
point,
nearest_only,
)
}
}
#[cfg(feature = "image_rs")]

View file

@ -1,8 +1,12 @@
use crate::Transformation;
use iced_graphics::font;
use std::{cell::RefCell, collections::HashMap};
use wgpu_glyph::ab_glyph;
pub use iced_native::text::Hit;
#[derive(Debug)]
pub struct Pipeline {
draw_brush: RefCell<wgpu_glyph::GlyphBrush<()>>,
@ -117,6 +121,94 @@ impl Pipeline {
}
}
pub fn hit_test(
&self,
content: &str,
size: f32,
font: iced_native::Font,
bounds: iced_native::Size,
point: iced_native::Point,
nearest_only: bool,
) -> Hit {
use wgpu_glyph::GlyphCruncher;
let wgpu_glyph::FontId(font_id) = self.find_font(font);
let section = wgpu_glyph::Section {
bounds: (bounds.width, bounds.height),
text: vec![wgpu_glyph::Text {
text: content,
scale: size.into(),
font_id: wgpu_glyph::FontId(font_id),
extra: wgpu_glyph::Extra::default(),
}],
..Default::default()
};
let mut mb = self.measure_brush.borrow_mut();
// The underlying type is FontArc, so clones are cheap.
use wgpu_glyph::ab_glyph::{Font, ScaleFont};
let font = mb.fonts()[font_id].clone().into_scaled(size);
// Implements an iterator over the glyph bounding boxes.
let bounds = mb.glyphs(section).map(
|wgpu_glyph::SectionGlyph {
byte_index, glyph, ..
}| {
(
*byte_index,
iced_native::Rectangle::new(
iced_native::Point::new(
glyph.position.x - font.h_side_bearing(glyph.id),
glyph.position.y - font.ascent(),
),
iced_native::Size::new(
font.h_advance(glyph.id),
font.ascent() - font.descent(),
),
),
)
},
);
// Implements computation of the character index based on the byte index
// within the input string.
let char_index = |byte_index| {
let mut b_count = 0;
for (i, utf8_len) in
content.chars().map(|c| c.len_utf8()).enumerate()
{
if byte_index < (b_count + utf8_len) {
return i;
}
b_count += utf8_len;
}
return byte_index;
};
if !nearest_only {
for (idx, bounds) in bounds.clone() {
if bounds.contains(point) {
return Hit::CharOffset(char_index(idx));
}
}
}
let (idx, nearest) = bounds.fold(
(0usize, iced_native::Point::ORIGIN),
|acc: (usize, iced_native::Point), (idx, bounds)| {
if bounds.center().distance(point) < acc.1.distance(point) {
(idx, bounds.center())
} else {
acc
}
},
);
Hit::NearestCharOffset(char_index(idx), (point - nearest).into())
}
pub fn trim_measurement_cache(&mut self) {
// TODO: We should probably use a `GlyphCalculator` for this. However,
// it uses a lifetimed `GlyphCalculatorGuard` with side-effects on drop.