diff options
-rw-r--r-- | core/src/keyboard.rs | 2 | ||||
-rw-r--r-- | core/src/lib.rs | 1 | ||||
-rw-r--r-- | core/src/mouse.rs | 2 | ||||
-rw-r--r-- | core/src/text.rs | 29 | ||||
-rw-r--r-- | glow/src/backend.rs | 20 | ||||
-rw-r--r-- | glow/src/text.rs | 94 | ||||
-rw-r--r-- | graphics/src/backend.rs | 20 | ||||
-rw-r--r-- | graphics/src/widget/text.rs | 21 | ||||
-rw-r--r-- | native/src/renderer/null.rs | 14 | ||||
-rw-r--r-- | native/src/widget/text.rs | 19 | ||||
-rw-r--r-- | native/src/widget/text_input.rs | 70 | ||||
-rw-r--r-- | wgpu/src/backend.rs | 20 | ||||
-rw-r--r-- | wgpu/src/text.rs | 92 |
13 files changed, 335 insertions, 69 deletions
diff --git a/core/src/keyboard.rs b/core/src/keyboard.rs index cb64701a..6827a4db 100644 --- a/core/src/keyboard.rs +++ b/core/src/keyboard.rs @@ -1,4 +1,4 @@ -//! Reuse basic keyboard types. +//! Listen to keyboard events. mod event; mod hotkey; mod key_code; diff --git a/core/src/lib.rs b/core/src/lib.rs index c4288158..a0decdab 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -17,6 +17,7 @@ pub mod keyboard; pub mod menu; pub mod mouse; +pub mod text; mod align; mod background; diff --git a/core/src/mouse.rs b/core/src/mouse.rs index 25ce6ac3..48214f65 100644 --- a/core/src/mouse.rs +++ b/core/src/mouse.rs @@ -1,4 +1,4 @@ -//! Reuse basic mouse types. +//! Handle mouse events. mod button; mod event; mod interaction; diff --git a/core/src/text.rs b/core/src/text.rs new file mode 100644 index 00000000..ded22eef --- /dev/null +++ b/core/src/text.rs @@ -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 + } + } + } + } +} diff --git a/glow/src/backend.rs b/glow/src/backend.rs index 1680fc00..37c0ac9d 100644 --- a/glow/src/backend.rs +++ b/glow/src/backend.rs @@ -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")] diff --git a/glow/src/text.rs b/glow/src/text.rs index a4c39dfe..d6915d92 100644 --- a/glow/src/text.rs +++ b/glow/src/text.rs @@ -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. diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index ed1b9e08..656949c5 100644 --- a/graphics/src/backend.rs +++ b/graphics/src/backend.rs @@ -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. diff --git a/graphics/src/widget/text.rs b/graphics/src/widget/text.rs index 7e22e680..c235f254 100644 --- a/graphics/src/widget/text.rs +++ b/graphics/src/widget/text.rs @@ -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, diff --git a/native/src/renderer/null.rs b/native/src/renderer/null.rs index bb57c163..b1a26c41 100644 --- a/native/src/renderer/null.rs +++ b/native/src/renderer/null.rs @@ -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, diff --git a/native/src/widget/text.rs b/native/src/widget/text.rs index 6cc18e6c..adf6a74f 100644 --- a/native/src/widget/text.rs +++ b/native/src/widget/text.rs @@ -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: diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index bb64d5b7..f1a7a1a0 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -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; diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs index 4f34045b..b31bf92c 100644 --- a/wgpu/src/backend.rs +++ b/wgpu/src/backend.rs @@ -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")] diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 2b5b94c9..ee49ee4b 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -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. |