Move `text` into `reference` folder (#433)

This was imported in #241

See also [#masonry > text vs
text2](https://xi.zulipchat.com/#narrow/stream/317477-masonry/topic/text.20vs.20text2)

We do want to sort out text properly, see #337, so I'm keeping the
reference code around.
This commit is contained in:
Daniel McNab 2024-07-18 08:05:11 +01:00 committed by GitHub
parent 7a28babdd6
commit 732cfa8376
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 1201 additions and 1201 deletions

View File

@ -14,7 +14,7 @@ use crate::action::Action;
use crate::dpi::LogicalPosition;
use crate::promise::PromiseToken;
use crate::render_root::{RenderRootSignal, RenderRootState};
use crate::text2::TextBrush;
use crate::text::TextBrush;
use crate::text_helpers::{ImeChangeSignal, TextFieldRegistration};
use crate::tree_arena::TreeArenaTokenMut;
use crate::widget::{CursorChange, WidgetMut, WidgetState};

View File

@ -115,7 +115,7 @@ pub mod app_driver;
pub mod debug_logger;
pub mod debug_values;
pub mod event_loop_runner;
pub mod text2;
pub mod text;
mod tracing_backend;
mod tree_arena;

View File

@ -21,7 +21,7 @@ use crate::debug_logger::DebugLogger;
use crate::dpi::{LogicalPosition, LogicalSize, PhysicalSize};
use crate::event::{PointerEvent, TextEvent, WindowEvent};
use crate::kurbo::Point;
use crate::text2::TextBrush;
use crate::text::TextBrush;
use crate::tree_arena::TreeArena;
use crate::widget::{WidgetMut, WidgetRef, WidgetState};
use crate::{

View File

@ -5,17 +5,16 @@
use std::rc::Rc;
use kurbo::{Line, Point, Rect, Size};
use kurbo::{Affine, Line, Point, Rect, Size};
use parley::context::RangedBuilder;
use parley::fontique::{Style, Weight};
use parley::layout::{Alignment, Cursor};
use parley::style::{FontFamily, GenericFamily};
use parley::{Layout, LayoutContext};
use vello::peniko::{Brush, Color};
use parley::style::{Brush as BrushTrait, FontFamily, FontStack, GenericFamily, StyleProperty};
use parley::{FontContext, Layout, LayoutContext};
use vello::peniko::{self, Color, Gradient};
use vello::Scene;
use crate::PaintCtx;
use super::attribute::Link;
use super::font_descriptor::FontDescriptor;
use super::storage::TextStorage;
use super::{Link, TextStorage};
/// A component for displaying text on screen.
///
@ -35,20 +34,67 @@ use super::storage::TextStorage;
/// [`update`]: trait.Widget.html#tymethod.update
/// [`needs_rebuild_after_update`]: #method.needs_rebuild_after_update
/// [`rebuild_if_needed`]: #method.rebuild_if_needed
///
/// TODO: Update docs to mentionParley
#[derive(Clone)]
pub struct TextLayout<T> {
// TODO - remove Option
text: Option<T>,
font: FontDescriptor,
// when set, this will be used to override the size in he font descriptor.
// This provides an easy way to change only the font size, while still
// using a `FontDescriptor` in the `Env`.
text_size_override: Option<f32>,
text_color: Color,
layout: Option<Layout<Brush>>,
wrap_width: f64,
text: T,
// TODO: Find a way to let this use borrowed data
scale: f32,
brush: TextBrush,
font: FontStack<'static>,
text_size: f32,
weight: Weight,
style: Style,
alignment: Alignment,
max_advance: Option<f32>,
links: Rc<[(Rect, usize)]>,
needs_layout: bool,
needs_line_breaks: bool,
layout: Layout<TextBrush>,
scratch_scene: Scene,
}
/// A custom brush for `Parley`, enabling using Parley to pass-through
/// which glyphs are selected/highlighted
#[derive(Clone, Debug, PartialEq)]
pub enum TextBrush {
Normal(peniko::Brush),
Highlight {
text: peniko::Brush,
fill: peniko::Brush,
},
}
impl BrushTrait for TextBrush {}
impl From<peniko::Brush> for TextBrush {
fn from(value: peniko::Brush) -> Self {
Self::Normal(value)
}
}
impl From<Gradient> for TextBrush {
fn from(value: Gradient) -> Self {
Self::Normal(value.into())
}
}
impl From<Color> for TextBrush {
fn from(value: Color) -> Self {
Self::Normal(value.into())
}
}
// Parley requires their Brush implementations to implement Default
impl Default for TextBrush {
fn default() -> Self {
Self::Normal(Default::default())
}
}
/// Metrics describing the layout text.
@ -65,80 +111,129 @@ pub struct LayoutMetrics {
impl<T> TextLayout<T> {
/// Create a new `TextLayout` object.
///
/// You must set the text ([`set_text`]) before using this object.
///
/// [`set_text`]: #method.set_text
pub fn new() -> Self {
pub fn new(text: T, text_size: f32) -> Self {
TextLayout {
text: None,
font: FontDescriptor::new(FontFamily::Generic(GenericFamily::SystemUi)),
text_color: crate::theme::TEXT_COLOR,
text_size_override: None,
layout: None,
wrap_width: f64::INFINITY,
text,
scale: 1.0,
brush: crate::theme::TEXT_COLOR.into(),
font: FontStack::Single(FontFamily::Generic(GenericFamily::SansSerif)),
text_size,
weight: Weight::NORMAL,
style: Style::Normal,
max_advance: None,
alignment: Default::default(),
links: Rc::new([]),
needs_layout: true,
needs_line_breaks: true,
layout: Layout::new(),
scratch_scene: Scene::new(),
}
}
/// Set the default text color for this layout.
pub fn set_text_color(&mut self, color: Color) {
if color != self.text_color {
self.text_color = color;
self.layout = None;
/// Mark that the inner layout needs to be updated.
///
/// This should be used if your `T` has interior mutability
pub fn invalidate(&mut self) {
self.needs_layout = true;
self.needs_line_breaks = true;
}
/// Set the scaling factor
pub fn set_scale(&mut self, scale: f32) {
if scale != self.scale {
self.scale = scale;
self.invalidate();
}
}
/// Set the default font.
/// Set the default brush used for the layout.
///
/// The argument is a [`FontDescriptor`].
///
/// [`FontDescriptor`]: struct.FontDescriptor.html
pub fn set_font(&mut self, font: FontDescriptor) {
/// This is the non-layout impacting styling (primarily colour)
/// used when displaying the text
#[doc(alias = "set_color")]
pub fn set_brush(&mut self, brush: impl Into<TextBrush>) {
let brush = brush.into();
if brush != self.brush {
self.brush = brush;
self.invalidate();
}
}
/// Set the default font stack.
pub fn set_font(&mut self, font: FontStack<'static>) {
if font != self.font {
self.font = font;
self.layout = None;
self.text_size_override = None;
self.invalidate();
}
}
/// Set the font size.
///
/// This overrides the size in the [`FontDescriptor`] provided to [`set_font`].
///
/// [`set_font`]: #method.set_font.html
/// [`FontDescriptor`]: struct.FontDescriptor.html
#[doc(alias = "set_font_size")]
pub fn set_text_size(&mut self, size: f32) {
if Some(&size) != self.text_size_override.as_ref() {
self.text_size_override = Some(size);
self.layout = None;
if size != self.text_size {
self.text_size = size;
self.invalidate();
}
}
/// Set the font weight.
pub fn set_weight(&mut self, weight: Weight) {
if weight != self.weight {
self.weight = weight;
self.invalidate();
}
}
/// Set the font style.
pub fn set_style(&mut self, style: Style) {
if style != self.style {
self.style = style;
self.invalidate();
}
}
/// Set the [`Alignment`] for this layout.
pub fn set_text_alignment(&mut self, alignment: Alignment) {
if self.alignment != alignment {
self.alignment = alignment;
self.invalidate();
}
}
/// Set the width at which to wrap words.
///
/// You may pass `f64::INFINITY` to disable word wrapping
/// You may pass `None` to disable word wrapping
/// (the default behaviour).
pub fn set_wrap_width(&mut self, width: f64) {
let width = width.max(0.0);
// 1e-4 is an arbitrary small-enough value that we don't care to rewrap
if (width - self.wrap_width).abs() > 1e-4 {
self.wrap_width = width;
self.layout = None;
pub fn set_max_advance(&mut self, max_advance: Option<f32>) {
let max_advance = max_advance.map(|it| it.max(0.0));
if self.max_advance.is_some() != max_advance.is_some()
|| self
.max_advance
.zip(max_advance)
// 1e-4 is an arbitrary small-enough value that we don't care to rewrap
.map(|(old, new)| (old - new).abs() >= 1e-4)
.unwrap_or(false)
{
self.max_advance = max_advance;
self.needs_line_breaks = true;
}
}
/// Set the [`TextAlignment`] for this layout.
/// Returns `true` if this layout needs to be rebuilt.
///
/// [`TextAlignment`]: enum.TextAlignment.html
pub fn set_text_alignment(&mut self, alignment: Alignment) {
if self.alignment != alignment {
self.alignment = alignment;
self.layout = None;
}
/// This happens (for instance) after style attributes are modified.
///
/// This does not account for things like the text changing, handling that
/// is the responsibility of the user.
pub fn needs_rebuild(&self) -> bool {
self.needs_layout || self.needs_line_breaks
}
// TODO: What are the valid use cases for this, where we shouldn't use a run-specific check instead?
// /// Returns `true` if this layout's text appears to be right-to-left.
// ///
// /// See [`piet::util::first_strong_rtl`] for more information.
@ -150,99 +245,85 @@ impl<T> TextLayout<T> {
}
impl<T: TextStorage> TextLayout<T> {
/// Create a new `TextLayout` with the provided text.
///
/// This is useful when the text is not tied to application data.
pub fn from_text(text: impl Into<T>) -> Self {
let mut this = TextLayout::new();
this.set_text(text.into());
this
}
/// Returns `true` if this layout needs to be rebuilt.
///
/// This happens (for instance) after style attributes are modified.
///
/// This does not account for things like the text changing, handling that
/// is the responsibility of the user.
pub fn needs_rebuild(&self) -> bool {
self.layout.is_none()
#[track_caller]
fn assert_rebuilt(&self, method: &str) {
if self.needs_layout || self.needs_line_breaks {
debug_panic!(
"TextLayout::{method} called without rebuilding layout object. Text was '{}'",
self.text.as_str().chars().take(250).collect::<String>()
);
}
}
/// Set the text to display.
pub fn set_text(&mut self, text: T) {
if self.text.is_none() || !self.text.as_ref().unwrap().maybe_eq(&text) {
self.text = Some(text);
self.layout = None;
if !self.text.maybe_eq(&text) {
self.text = text;
self.invalidate();
}
}
/// Returns the [`TextStorage`] backing this layout, if it exists.
pub fn text(&self) -> Option<&T> {
self.text.as_ref()
pub fn text(&self) -> &T {
&self.text
}
/// Returns the length of the [`TextStorage`] backing this layout, if it exists.
pub fn text_len(&self) -> usize {
if let Some(text) = &self.text {
text.as_str().len()
} else {
0
}
/// Returns the [`TextStorage`] backing this layout, if it exists.
///
/// Invalidates the layout and so should only be used when definitely applying an edit
pub fn text_mut(&mut self) -> &mut T {
self.invalidate();
&mut self.text
}
/// Returns the inner Piet [`TextLayout`] type.
///
/// [`TextLayout`]: ./piet/trait.TextLayout.html
pub fn layout(&self) -> Option<&Layout<Brush>> {
self.layout.as_ref()
/// Returns the inner Parley [`Layout`] value.
pub fn layout(&self) -> &Layout<TextBrush> {
self.assert_rebuilt("layout");
&self.layout
}
/// The size of the laid-out text.
/// The size of the laid-out text, excluding any trailing whitespace.
///
/// This is not meaningful until [`rebuild_if_needed`] has been called.
///
/// [`rebuild_if_needed`]: #method.rebuild_if_needed
/// This is not meaningful until [`Self::rebuild`] has been called.
pub fn size(&self) -> Size {
self.layout
.as_ref()
.map(|layout| Size::new(layout.width().into(), layout.height().into()))
.unwrap_or_default()
self.assert_rebuilt("size");
Size::new(self.layout.width().into(), self.layout.height().into())
}
/// The size of the laid-out text, including any trailing whitespace.
///
/// This is not meaningful until [`Self::rebuild`] has been called.
pub fn full_size(&self) -> Size {
self.assert_rebuilt("full_size");
Size::new(self.layout.full_width().into(), self.layout.height().into())
}
/// Return the text's [`LayoutMetrics`].
///
/// This is not meaningful until [`rebuild_if_needed`] has been called.
///
/// [`rebuild_if_needed`]: #method.rebuild_if_needed
/// [`LayoutMetrics`]: struct.LayoutMetrics.html
/// This is not meaningful until [`Self::rebuild`] has been called.
pub fn layout_metrics(&self) -> LayoutMetrics {
debug_assert!(
self.layout.is_some(),
"TextLayout::layout_metrics called without rebuilding layout object. Text was '{}'",
self.text().as_ref().map(|s| s.as_str()).unwrap_or_default()
);
self.assert_rebuilt("layout_metrics");
if let Some(layout) = self.layout.as_ref() {
let first_baseline = layout.get(0).unwrap().metrics().baseline;
let size = Size::new(layout.width().into(), layout.height().into());
LayoutMetrics {
size,
first_baseline,
trailing_whitespace_width: layout.width(),
}
} else {
LayoutMetrics::default()
let first_baseline = self.layout.get(0).unwrap().metrics().baseline;
let size = Size::new(self.layout.width().into(), self.layout.height().into());
LayoutMetrics {
size,
first_baseline,
trailing_whitespace_width: self.layout.full_width(),
}
}
/// For a given `Point` (relative to this object's origin), returns index
/// into the underlying text of the nearest grapheme boundary.
pub fn text_position_for_point(&self, point: Point) -> usize {
self.layout
.as_ref()
.map(|layout| Cursor::from_point(layout, point.x as f32, point.y as f32).insert_point)
.unwrap_or_default()
///
/// This is not meaningful until [`Self::rebuild`] has been called.
pub fn cursor_for_point(&self, point: Point) -> Cursor {
self.assert_rebuilt("text_position_for_point");
// TODO: This is a mostly good first pass, but doesn't handle cursor positions in
// grapheme clusters within a parley cluster.
// We can also try
Cursor::from_point(&self.layout, point.x as f32, point.y as f32)
}
/// Given the utf-8 position of a character boundary in the underlying text,
@ -252,20 +333,38 @@ impl<T: TextStorage> TextLayout<T> {
/// # Panics
///
/// Panics if `text_pos` is not a character boundary.
pub fn point_for_text_position(&self, text_pos: usize) -> Point {
self.layout
.as_ref()
.map(|layout| {
let from_position = Cursor::from_position(layout, text_pos, /* TODO */ false);
///
/// This is not meaningful until [`Self::rebuild`] has been called.
pub fn cursor_for_text_position(&self, text_pos: usize) -> Cursor {
self.assert_rebuilt("cursor_for_text_position");
Point::new(
from_position.advance as f64,
(from_position.baseline + from_position.offset) as f64,
)
})
.unwrap_or_default()
// TODO: As a reminder, `is_leading` is not very useful to us; we don't know this ahead of time
// We're going to need to do quite a bit of remedial work on these
// e.g. to handle a inside a ligature made of multiple (unicode) grapheme clusters
// https://raphlinus.github.io/text/2020/10/26/text-layout.html#shaping-cluster
// But we're choosing to defer this work
// This also needs to handle affinity.
Cursor::from_position(&self.layout, text_pos, true)
}
/// Given the utf-8 position of a character boundary in the underlying text,
/// return the `Point` (relative to this object's origin) representing the
/// boundary of the containing grapheme.
///
/// # Panics
///
/// Panics if `text_pos` is not a character boundary.
///
/// This is not meaningful until [`Self::rebuild`] has been called.
pub fn point_for_text_position(&self, text_pos: usize) -> Point {
let cursor = self.cursor_for_text_position(text_pos);
Point::new(
cursor.advance as f64,
(cursor.baseline + cursor.offset) as f64,
)
}
// TODO: needed for text selection
// /// Given a utf-8 range in the underlying text, return a `Vec` of `Rect`s
// /// representing the nominal bounding boxes of the text in that range.
// ///
@ -273,30 +372,28 @@ impl<T: TextStorage> TextLayout<T> {
// ///
// /// Panics if the range start or end is not a character boundary.
// pub fn rects_for_range(&self, range: Range<usize>) -> Vec<Rect> {
// self.layout
// .as_ref()
// .map(|layout| layout.rects_for_range(range))
// .unwrap_or_default()
// self.layout.rects_for_range(range)
// }
/// Given the utf-8 position of a character boundary in the underlying text,
/// return a `Line` suitable for drawing a vertical cursor at that boundary.
///
/// This is not meaningful until [`Self::rebuild`] has been called.
// TODO: This is too simplistic. See https://raphlinus.github.io/text/2020/10/26/text-layout.html#shaping-cluster
// for example. This would break in a `fi` ligature
pub fn cursor_line_for_text_position(&self, text_pos: usize) -> Line {
self.layout
.as_ref()
.map(|layout| {
let from_position = Cursor::from_position(layout, text_pos, /* TODO */ false);
let from_position = self.cursor_for_text_position(text_pos);
let line_metrics = from_position.path.line(layout).unwrap().metrics();
let line = from_position.path.line(&self.layout).unwrap();
let line_metrics = line.metrics();
let p1 = (from_position.advance as f64, line_metrics.baseline as f64);
let p2 = (
from_position.advance as f64,
(line_metrics.baseline + line_metrics.size()) as f64,
);
Line::new(p1, p2)
})
.unwrap_or_else(|| Line::new(Point::ZERO, Point::ZERO))
let baseline = line_metrics.baseline + line_metrics.descent;
let p1 = (from_position.offset as f64, baseline as f64);
let p2 = (
from_position.offset as f64,
(baseline - line_metrics.size()) as f64,
);
Line::new(p1, p2)
}
/// Returns the [`Link`] at the provided point (relative to the layout's origin) if one exists.
@ -312,8 +409,7 @@ impl<T: TextStorage> TextLayout<T> {
.iter()
.rfind(|(hit_box, _)| hit_box.contains(pos))?;
let text = self.text()?;
text.links().get(*i)
self.text.links().get(*i)
}
/// Rebuild the inner layout as needed.
@ -324,94 +420,95 @@ impl<T: TextStorage> TextLayout<T> {
///
/// This method should be called whenever any of these things may have changed.
/// A simple way to ensure this is correct is to always call this method
/// as part of your widget's [`layout`] method.
/// as part of your widget's [`layout`][crate::Widget::layout] method.
pub fn rebuild(
&mut self,
font_ctx: &mut FontContext,
layout_ctx: &mut LayoutContext<TextBrush>,
) {
self.rebuild_with_attributes(font_ctx, layout_ctx, |builder| builder);
}
/// Rebuild the inner layout as needed, adding attributes to the underlying layout.
///
/// [`layout`]: trait.Widget.html#method.layout
pub fn rebuild_if_needed(&mut self, factory: &mut LayoutContext<Brush>) {
if let Some(text) = &self.text {
if self.layout.is_none() {
let font = self.font.clone();
let color = self.text_color;
let size_override = self.text_size_override;
/// See [`Self::rebuild`] for more information
pub fn rebuild_with_attributes(
&mut self,
font_ctx: &mut FontContext,
layout_ctx: &mut LayoutContext<TextBrush>,
attributes: impl for<'b> FnOnce(
RangedBuilder<'b, TextBrush, &'b str>,
) -> RangedBuilder<'b, TextBrush, &'b str>,
) {
if self.needs_layout {
self.needs_layout = false;
let descriptor = if let Some(size) = size_override {
font.with_size(size)
} else {
font
};
let mut builder = layout_ctx.ranged_builder(font_ctx, self.text.as_str(), self.scale);
builder.push_default(&StyleProperty::Brush(self.brush.clone()));
builder.push_default(&StyleProperty::FontSize(self.text_size));
builder.push_default(&StyleProperty::FontStack(self.font));
builder.push_default(&StyleProperty::FontWeight(self.weight));
builder.push_default(&StyleProperty::FontStyle(self.style));
// For more advanced features (e.g. variable font axes), these can be set in add_attributes
let builder = factory.ranged_builder(fcx, text, 1.0);
builder
.push_default(StyleProperty)
.new_text_layout(text.clone())
.max_width(self.wrap_width)
.alignment(self.alignment)
.font(descriptor.family.clone(), descriptor.size)
.default_attribute(descriptor.weight)
.default_attribute(descriptor.style);
// .default_attribute(TextAttribute::TextColor(color));
let layout = text.add_attributes(builder).build().unwrap();
let builder = self.text.add_attributes(builder);
let mut builder = attributes(builder);
builder.build_into(&mut self.layout);
self.links = text
.links()
.iter()
.enumerate()
.flat_map(|(i, link)| {
layout
.rects_for_range(link.range())
.into_iter()
.map(move |rect| (rect, i))
})
.collect();
self.needs_line_breaks = true;
}
if self.needs_line_breaks {
self.needs_line_breaks = false;
self.layout
.break_all_lines(self.max_advance, self.alignment);
self.layout = Some(layout);
}
// TODO:
// self.links = text
// .links()
// ...
}
}
/// Draw the layout at the provided `Point`.
/// Draw the layout at the provided `Point`.
///
/// The origin of the layout is the top-left corner.
/// The origin of the layout is the top-left corner.
///
/// You must call [`rebuild_if_needed`] at some point before you first
/// call this method.
///
/// [`rebuild_if_needed`]: #method.rebuild_if_needed
pub fn draw(&self, ctx: &mut PaintCtx, point: impl Into<Point>) {
debug_assert!(
self.layout.is_some(),
"TextLayout::draw called without rebuilding layout object. Text was '{}'",
self.text
.as_ref()
.map(|t| t.as_str())
.unwrap_or("layout is missing text")
/// You must call [`Self::rebuild`] at some point before you first
/// call this method.
pub fn draw(&mut self, scene: &mut Scene, point: impl Into<Point>) {
self.assert_rebuilt("draw");
// TODO: This translation doesn't seem great
let p: Point = point.into();
crate::text_helpers::render_text(
scene,
&mut self.scratch_scene,
Affine::translate((p.x, p.y)),
&self.layout,
);
if let Some(layout) = self.layout.as_ref() {
ctx.draw_text(layout, point);
}
}
}
impl<T> std::fmt::Debug for TextLayout<T> {
impl<T: TextStorage> std::fmt::Debug for TextLayout<T> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("TextLayout")
.field("text", &self.text.as_str().len())
.field("scale", &self.scale)
.field("brush", &self.brush)
.field("font", &self.font)
.field("text_size_override", &self.text_size_override)
.field("text_color", &self.text_color)
.field(
"layout",
if self.layout.is_some() {
&"Some"
} else {
&"None"
},
)
.field("text_size", &self.text_size)
.field("weight", &self.weight)
.field("style", &self.style)
.field("alignment", &self.alignment)
.field("wrap_width", &self.max_advance)
.field("outdated?", &self.needs_rebuild())
.field("width", &self.layout.width())
.field("height", &self.layout.height())
.finish()
}
}
impl<T: TextStorage> Default for TextLayout<T> {
impl<T: TextStorage + Default> Default for TextLayout<T> {
fn default() -> Self {
Self::new()
Self::new(Default::default(), crate::theme::TEXT_SIZE_NORMAL as f32)
}
}

View File

@ -1,20 +1,30 @@
// Copyright 2018 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0
//! Editing and displaying text.
//! Support for text display and rendering
//!
//! There are three kinds of text commonly needed:
//! 1) Entirely display text (e.g. a button)
//! 2) Selectable text (e.g. a paragraph of content)
//! 3) Editable text (e.g. a search bar)
//!
//! All of these have the same set of global styling options, and can contain rich text
// TODO
#![allow(clippy::all)]
mod store;
pub use store::{Link, TextStorage};
mod attribute;
mod backspace;
mod editable_text;
mod font_descriptor;
mod input_component;
mod layout;
mod movement;
mod rich_text;
mod shell_text;
mod storage;
mod util;
pub use layout::{LayoutMetrics, TextBrush, TextLayout};
mod selection;
pub use selection::{
len_utf8_from_first_byte, EditableTextCursor, Selectable, StringCursor, TextWithSelection,
};
// mod movement;
mod edit;
pub use edit::{EditableText, TextEditor};
mod backspace;
pub use backspace::offset_for_delete_backwards;

View File

@ -9,12 +9,7 @@ use unicode_segmentation::UnicodeSegmentation;
use crate::kurbo::Point;
use super::{
editable_text::EditableText,
layout::TextLayout,
shell_text::{Movement, Selection, VerticalMovement, WritingDirection},
storage::TextStorage,
};
use super::{layout::TextLayout, Selectable, TextStorage};
/// Compute the result of a [`Movement`] on a [`Selection`].
///
@ -23,48 +18,58 @@ use super::{
/// If `modify` is true, only the 'active' edge (the `end`) of the selection
/// should be changed; this is the case when the user moves with the shift
/// key pressed.
pub fn movement<T: EditableText + TextStorage>(
pub fn movement<T: Selectable + TextStorage>(
m: Movement,
s: Selection,
layout: &TextLayout<T>,
modify: bool,
) -> Selection {
let (text, layout) = match (layout.text(), layout.layout()) {
(Some(text), Some(layout)) => (text, layout),
_ => {
debug_assert!(false, "movement() called before layout rebuild");
return s;
if layout.needs_rebuild() {
debug_panic!("movement() called before layout rebuild");
return s;
}
let text = layout.text();
let parley_layout = layout.layout();
let writing_direction = || {
if layout
.cursor_for_text_position(s.active, s.active_affinity)
.is_rtl
{
WritingDirection::RightToLeft
} else {
WritingDirection::LeftToRight
}
};
// TODO
let writing_direction = WritingDirection::LeftToRight;
let (offset, h_pos) = match m {
Movement::Grapheme(d) if d.is_upstream_for_direction(writing_direction) => {
if s.is_caret() || modify {
text.prev_grapheme_offset(s.active)
.map(|off| (off, None))
.unwrap_or((0, s.h_pos))
Movement::Grapheme(d) => {
let direction = writing_direction();
if d.is_upstream_for_direction(direction) {
if s.is_caret() || modify {
text.prev_grapheme_offset(s.active)
.map(|off| (off, None))
.unwrap_or((0, s.h_pos))
} else {
(s.min(), None)
}
} else {
(s.min(), None)
}
}
Movement::Grapheme(_) => {
if s.is_caret() || modify {
text.next_grapheme_offset(s.active)
.map(|off| (off, None))
.unwrap_or((s.active, s.h_pos))
} else {
(s.max(), None)
if s.is_caret() || modify {
text.next_grapheme_offset(s.active)
.map(|off| (off, None))
.unwrap_or((s.active, s.h_pos))
} else {
(s.max(), None)
}
}
}
Movement::Vertical(VerticalMovement::LineUp) => {
let cur_pos = layout.hit_test_text_position(s.active);
let h_pos = s.h_pos.unwrap_or(cur_pos.point.x);
if cur_pos.line == 0 {
let cur_pos = layout.cursor_for_text_position(s.active, s.active_affinity);
let h_pos = s.h_pos.unwrap_or(cur_pos.advance);
if cur_pos.path.line_index == 0 {
(0, Some(h_pos))
} else {
let lm = layout.line_metric(cur_pos.line).unwrap();
let lm = cur_pos.path.line(&parley_layout).unwrap();
let point_above = Point::new(h_pos, cur_pos.point.y - lm.height);
let up_pos = layout.hit_test_point(point_above);
if up_pos.is_inside {
@ -110,21 +115,22 @@ pub fn movement<T: EditableText + TextStorage>(
};
(offset, None)
}
Movement::Word(d) if d.is_upstream_for_direction(writing_direction) => {
let offset = if s.is_caret() || modify {
text.prev_word_offset(s.active).unwrap_or(0)
Movement::Word(d) => {
if d.is_upstream_for_direction(writing_direction()) {
let offset = if s.is_caret() || modify {
text.prev_word_offset(s.active).unwrap_or(0)
} else {
s.min()
};
(offset, None)
} else {
s.min()
};
(offset, None)
}
Movement::Word(_) => {
let offset = if s.is_caret() || modify {
text.next_word_offset(s.active).unwrap_or(s.active)
} else {
s.max()
};
(offset, None)
let offset = if s.is_caret() || modify {
text.next_word_offset(s.active).unwrap_or(s.active)
} else {
s.max()
};
(offset, None)
}
}
// These two are not handled; they require knowledge of the size
@ -141,6 +147,164 @@ pub fn movement<T: EditableText + TextStorage>(
Selection::new(start, offset).with_h_pos(h_pos)
}
/// Indicates a movement that transforms a particular text position in a
/// document.
///
/// These movements transform only single indices — not selections.
///
/// You'll note that a lot of these operations are idempotent, but you can get
/// around this by first sending a `Grapheme` movement. If for instance, you
/// want a `ParagraphStart` that is not idempotent, you can first send
/// `Movement::Grapheme(Direction::Upstream)`, and then follow it with
/// `ParagraphStart`.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Movement {
/// A movement that stops when it reaches an extended grapheme cluster boundary.
///
/// This movement is achieved on most systems by pressing the left and right
/// arrow keys. For more information on grapheme clusters, see
/// [Unicode Text Segmentation](https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries).
Grapheme(Direction),
/// A movement that stops when it reaches a word boundary.
///
/// This movement is achieved on most systems by pressing the left and right
/// arrow keys while holding control. For more information on words, see
/// [Unicode Text Segmentation](https://unicode.org/reports/tr29/#Word_Boundaries).
Word(Direction),
/// A movement that stops when it reaches a soft line break.
///
/// This movement is achieved on macOS by pressing the left and right arrow
/// keys while holding command. `Line` should be idempotent: if the
/// position is already at the end of a soft-wrapped line, this movement
/// should never push it onto another soft-wrapped line.
///
/// In order to implement this properly, your text positions should remember
/// their affinity.
Line(Direction),
/// An upstream movement that stops when it reaches a hard line break.
///
/// `ParagraphStart` should be idempotent: if the position is already at the
/// start of a hard-wrapped line, this movement should never push it onto
/// the previous line.
ParagraphStart,
/// A downstream movement that stops when it reaches a hard line break.
///
/// `ParagraphEnd` should be idempotent: if the position is already at the
/// end of a hard-wrapped line, this movement should never push it onto the
/// next line.
ParagraphEnd,
/// A vertical movement, see `VerticalMovement` for more details.
Vertical(VerticalMovement),
}
/// Indicates a horizontal direction in the text.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Direction {
/// The direction visually to the left.
///
/// This may be byte-wise forwards or backwards in the document, depending
/// on the text direction around the position being moved.
Left,
/// The direction visually to the right.
///
/// This may be byte-wise forwards or backwards in the document, depending
/// on the text direction around the position being moved.
Right,
/// Byte-wise backwards in the document.
///
/// In a left-to-right context, this value is the same as `Left`.
Upstream,
/// Byte-wise forwards in the document.
///
/// In a left-to-right context, this value is the same as `Right`.
Downstream,
}
impl Direction {
/// Returns `true` if this direction is byte-wise backwards for
/// the provided [`WritingDirection`].
///
/// The provided direction *must not be* `WritingDirection::Natural`.
pub fn is_upstream_for_direction(self, direction: WritingDirection) -> bool {
// assert!(
// !matches!(direction, WritingDirection::Natural),
// "writing direction must be resolved"
// );
match self {
Direction::Upstream => true,
Direction::Downstream => false,
Direction::Left => matches!(direction, WritingDirection::LeftToRight),
Direction::Right => matches!(direction, WritingDirection::RightToLeft),
}
}
}
/// Distinguishes between two visually distinct locations with the same byte
/// index.
///
/// Sometimes, a byte location in a document has two visual locations. For
/// example, the end of a soft-wrapped line and the start of the subsequent line
/// have different visual locations (and we want to be able to place an input
/// caret in either place!) but the same byte-wise location. This also shows up
/// in bidirectional text contexts. Affinity allows us to disambiguate between
/// these two visual locations.
///
/// Note that in scenarios where soft line breaks interact with bidi text, this gets
/// more complicated.
#[derive(Copy, Clone, Debug, Hash, PartialEq)]
pub enum Affinity {
/// The position which has an apparent position "earlier" in the text.
/// For soft line breaks, this is the position at the end of the first line.
///
/// For positions in-between bidi contexts, this is the position which is
/// related to the "outgoing" text section. E.g. for the string "abcDEF" (rendered `abcFED`),
/// with the cursor at "abc|DEF" with upstream affinity, the cursor would be rendered at the
/// position `abc|DEF`
Upstream,
/// The position which has a higher apparent position in the text.
/// For soft line breaks, this is the position at the beginning of the second line.
///
/// For positions in-between bidi contexts, this is the position which is
/// related to the "incoming" text section. E.g. for the string "abcDEF" (rendered `abcFED`),
/// with the cursor at "abc|DEF" with downstream affinity, the cursor would be rendered at the
/// position `abcDEF|`
Downstream,
}
impl Affinity {
/// Convert into the `parley` form of "leading"
pub fn is_leading(&self) -> bool {
match self {
Affinity::Upstream => false,
Affinity::Downstream => true,
}
}
}
/// Indicates a horizontal direction for writing text.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum WritingDirection {
LeftToRight,
RightToLeft,
// /// Indicates writing direction should be automatically detected based on
// /// the text contents.
// See also `is_upstream_for_direction` if adding back in
// Natural,
}
/// Indicates a vertical movement in a text document.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum VerticalMovement {
LineUp,
LineDown,
PageUp,
PageDown,
DocumentStart,
DocumentEnd,
}
/// Given a position in some text, return the containing word boundaries.
///
/// The returned range may not necessary be a 'word'; for instance it could be

View File

@ -0,0 +1,417 @@
// Copyright 2018 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0
//! A type for laying out, drawing, and interacting with text.
use std::rc::Rc;
use kurbo::{Line, Point, Rect, Size};
use parley::layout::{Alignment, Cursor};
use parley::style::{FontFamily, GenericFamily};
use parley::{Layout, LayoutContext};
use vello::peniko::{Brush, Color};
use crate::PaintCtx;
use super::attribute::Link;
use super::font_descriptor::FontDescriptor;
use super::storage::TextStorage;
/// A component for displaying text on screen.
///
/// This is a type intended to be used by other widgets that display text.
/// It allows for the text itself as well as font and other styling information
/// to be set and modified. It wraps an inner layout object, and handles
/// invalidating and rebuilding it as required.
///
/// This object is not valid until the [`rebuild_if_needed`] method has been
/// called. You should generally do this in your widget's [`layout`] method.
/// Additionally, you should call [`needs_rebuild_after_update`]
/// as part of your widget's [`update`] method; if this returns `true`, you will need
/// to call [`rebuild_if_needed`] again, generally by scheduling another [`layout`]
/// pass.
///
/// [`layout`]: trait.Widget.html#tymethod.layout
/// [`update`]: trait.Widget.html#tymethod.update
/// [`needs_rebuild_after_update`]: #method.needs_rebuild_after_update
/// [`rebuild_if_needed`]: #method.rebuild_if_needed
#[derive(Clone)]
pub struct TextLayout<T> {
// TODO - remove Option
text: Option<T>,
font: FontDescriptor,
// when set, this will be used to override the size in he font descriptor.
// This provides an easy way to change only the font size, while still
// using a `FontDescriptor` in the `Env`.
text_size_override: Option<f32>,
text_color: Color,
layout: Option<Layout<Brush>>,
wrap_width: f64,
alignment: Alignment,
links: Rc<[(Rect, usize)]>,
}
/// Metrics describing the layout text.
#[derive(Debug, Clone, Copy, Default)]
pub struct LayoutMetrics {
/// The nominal size of the layout.
pub size: Size,
/// The distance from the nominal top of the layout to the first baseline.
pub first_baseline: f32,
/// The width of the layout, inclusive of trailing whitespace.
pub trailing_whitespace_width: f32,
//TODO: add inking_rect
}
impl<T> TextLayout<T> {
/// Create a new `TextLayout` object.
///
/// You must set the text ([`set_text`]) before using this object.
///
/// [`set_text`]: #method.set_text
pub fn new() -> Self {
TextLayout {
text: None,
font: FontDescriptor::new(FontFamily::Generic(GenericFamily::SystemUi)),
text_color: crate::theme::TEXT_COLOR,
text_size_override: None,
layout: None,
wrap_width: f64::INFINITY,
alignment: Default::default(),
links: Rc::new([]),
}
}
/// Set the default text color for this layout.
pub fn set_text_color(&mut self, color: Color) {
if color != self.text_color {
self.text_color = color;
self.layout = None;
}
}
/// Set the default font.
///
/// The argument is a [`FontDescriptor`].
///
/// [`FontDescriptor`]: struct.FontDescriptor.html
pub fn set_font(&mut self, font: FontDescriptor) {
if font != self.font {
self.font = font;
self.layout = None;
self.text_size_override = None;
}
}
/// Set the font size.
///
/// This overrides the size in the [`FontDescriptor`] provided to [`set_font`].
///
/// [`set_font`]: #method.set_font.html
/// [`FontDescriptor`]: struct.FontDescriptor.html
pub fn set_text_size(&mut self, size: f32) {
if Some(&size) != self.text_size_override.as_ref() {
self.text_size_override = Some(size);
self.layout = None;
}
}
/// Set the width at which to wrap words.
///
/// You may pass `f64::INFINITY` to disable word wrapping
/// (the default behaviour).
pub fn set_wrap_width(&mut self, width: f64) {
let width = width.max(0.0);
// 1e-4 is an arbitrary small-enough value that we don't care to rewrap
if (width - self.wrap_width).abs() > 1e-4 {
self.wrap_width = width;
self.layout = None;
}
}
/// Set the [`TextAlignment`] for this layout.
///
/// [`TextAlignment`]: enum.TextAlignment.html
pub fn set_text_alignment(&mut self, alignment: Alignment) {
if self.alignment != alignment {
self.alignment = alignment;
self.layout = None;
}
}
// /// Returns `true` if this layout's text appears to be right-to-left.
// ///
// /// See [`piet::util::first_strong_rtl`] for more information.
// ///
// /// [`piet::util::first_strong_rtl`]: crate::piet::util::first_strong_rtl
// pub fn text_is_rtl(&self) -> bool {
// self.text_is_rtl
// }
}
impl<T: TextStorage> TextLayout<T> {
/// Create a new `TextLayout` with the provided text.
///
/// This is useful when the text is not tied to application data.
pub fn from_text(text: impl Into<T>) -> Self {
let mut this = TextLayout::new();
this.set_text(text.into());
this
}
/// Returns `true` if this layout needs to be rebuilt.
///
/// This happens (for instance) after style attributes are modified.
///
/// This does not account for things like the text changing, handling that
/// is the responsibility of the user.
pub fn needs_rebuild(&self) -> bool {
self.layout.is_none()
}
/// Set the text to display.
pub fn set_text(&mut self, text: T) {
if self.text.is_none() || !self.text.as_ref().unwrap().maybe_eq(&text) {
self.text = Some(text);
self.layout = None;
}
}
/// Returns the [`TextStorage`] backing this layout, if it exists.
pub fn text(&self) -> Option<&T> {
self.text.as_ref()
}
/// Returns the length of the [`TextStorage`] backing this layout, if it exists.
pub fn text_len(&self) -> usize {
if let Some(text) = &self.text {
text.as_str().len()
} else {
0
}
}
/// Returns the inner Piet [`TextLayout`] type.
///
/// [`TextLayout`]: ./piet/trait.TextLayout.html
pub fn layout(&self) -> Option<&Layout<Brush>> {
self.layout.as_ref()
}
/// The size of the laid-out text.
///
/// This is not meaningful until [`rebuild_if_needed`] has been called.
///
/// [`rebuild_if_needed`]: #method.rebuild_if_needed
pub fn size(&self) -> Size {
self.layout
.as_ref()
.map(|layout| Size::new(layout.width().into(), layout.height().into()))
.unwrap_or_default()
}
/// Return the text's [`LayoutMetrics`].
///
/// This is not meaningful until [`rebuild_if_needed`] has been called.
///
/// [`rebuild_if_needed`]: #method.rebuild_if_needed
/// [`LayoutMetrics`]: struct.LayoutMetrics.html
pub fn layout_metrics(&self) -> LayoutMetrics {
debug_assert!(
self.layout.is_some(),
"TextLayout::layout_metrics called without rebuilding layout object. Text was '{}'",
self.text().as_ref().map(|s| s.as_str()).unwrap_or_default()
);
if let Some(layout) = self.layout.as_ref() {
let first_baseline = layout.get(0).unwrap().metrics().baseline;
let size = Size::new(layout.width().into(), layout.height().into());
LayoutMetrics {
size,
first_baseline,
trailing_whitespace_width: layout.width(),
}
} else {
LayoutMetrics::default()
}
}
/// For a given `Point` (relative to this object's origin), returns index
/// into the underlying text of the nearest grapheme boundary.
pub fn text_position_for_point(&self, point: Point) -> usize {
self.layout
.as_ref()
.map(|layout| Cursor::from_point(layout, point.x as f32, point.y as f32).insert_point)
.unwrap_or_default()
}
/// Given the utf-8 position of a character boundary in the underlying text,
/// return the `Point` (relative to this object's origin) representing the
/// boundary of the containing grapheme.
///
/// # Panics
///
/// Panics if `text_pos` is not a character boundary.
pub fn point_for_text_position(&self, text_pos: usize) -> Point {
self.layout
.as_ref()
.map(|layout| {
let from_position = Cursor::from_position(layout, text_pos, /* TODO */ false);
Point::new(
from_position.advance as f64,
(from_position.baseline + from_position.offset) as f64,
)
})
.unwrap_or_default()
}
// /// Given a utf-8 range in the underlying text, return a `Vec` of `Rect`s
// /// representing the nominal bounding boxes of the text in that range.
// ///
// /// # Panics
// ///
// /// Panics if the range start or end is not a character boundary.
// pub fn rects_for_range(&self, range: Range<usize>) -> Vec<Rect> {
// self.layout
// .as_ref()
// .map(|layout| layout.rects_for_range(range))
// .unwrap_or_default()
// }
/// Given the utf-8 position of a character boundary in the underlying text,
/// return a `Line` suitable for drawing a vertical cursor at that boundary.
pub fn cursor_line_for_text_position(&self, text_pos: usize) -> Line {
self.layout
.as_ref()
.map(|layout| {
let from_position = Cursor::from_position(layout, text_pos, /* TODO */ false);
let line_metrics = from_position.path.line(layout).unwrap().metrics();
let p1 = (from_position.advance as f64, line_metrics.baseline as f64);
let p2 = (
from_position.advance as f64,
(line_metrics.baseline + line_metrics.size()) as f64,
);
Line::new(p1, p2)
})
.unwrap_or_else(|| Line::new(Point::ZERO, Point::ZERO))
}
/// Returns the [`Link`] at the provided point (relative to the layout's origin) if one exists.
///
/// This can be used both for hit-testing (deciding whether to change the mouse cursor,
/// or performing some other action when hovering) as well as for retrieving a [`Link`]
/// on click.
///
/// [`Link`]: super::attribute::Link
pub fn link_for_pos(&self, pos: Point) -> Option<&Link> {
let (_, i) = self
.links
.iter()
.rfind(|(hit_box, _)| hit_box.contains(pos))?;
let text = self.text()?;
text.links().get(*i)
}
/// Rebuild the inner layout as needed.
///
/// This `TextLayout` object manages a lower-level layout object that may
/// need to be rebuilt in response to changes to the text or attributes
/// like the font.
///
/// This method should be called whenever any of these things may have changed.
/// A simple way to ensure this is correct is to always call this method
/// as part of your widget's [`layout`] method.
///
/// [`layout`]: trait.Widget.html#method.layout
pub fn rebuild_if_needed(&mut self, factory: &mut LayoutContext<Brush>) {
if let Some(text) = &self.text {
if self.layout.is_none() {
let font = self.font.clone();
let color = self.text_color;
let size_override = self.text_size_override;
let descriptor = if let Some(size) = size_override {
font.with_size(size)
} else {
font
};
let builder = factory.ranged_builder(fcx, text, 1.0);
builder
.push_default(StyleProperty)
.new_text_layout(text.clone())
.max_width(self.wrap_width)
.alignment(self.alignment)
.font(descriptor.family.clone(), descriptor.size)
.default_attribute(descriptor.weight)
.default_attribute(descriptor.style);
// .default_attribute(TextAttribute::TextColor(color));
let layout = text.add_attributes(builder).build().unwrap();
self.links = text
.links()
.iter()
.enumerate()
.flat_map(|(i, link)| {
layout
.rects_for_range(link.range())
.into_iter()
.map(move |rect| (rect, i))
})
.collect();
self.layout = Some(layout);
}
}
}
/// Draw the layout at the provided `Point`.
///
/// The origin of the layout is the top-left corner.
///
/// You must call [`rebuild_if_needed`] at some point before you first
/// call this method.
///
/// [`rebuild_if_needed`]: #method.rebuild_if_needed
pub fn draw(&self, ctx: &mut PaintCtx, point: impl Into<Point>) {
debug_assert!(
self.layout.is_some(),
"TextLayout::draw called without rebuilding layout object. Text was '{}'",
self.text
.as_ref()
.map(|t| t.as_str())
.unwrap_or("layout is missing text")
);
if let Some(layout) = self.layout.as_ref() {
ctx.draw_text(layout, point);
}
}
}
impl<T> std::fmt::Debug for TextLayout<T> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("TextLayout")
.field("font", &self.font)
.field("text_size_override", &self.text_size_override)
.field("text_color", &self.text_color)
.field(
"layout",
if self.layout.is_some() {
&"Some"
} else {
&"None"
},
)
.finish()
}
}
impl<T: TextStorage> Default for TextLayout<T> {
fn default() -> Self {
Self::new()
}
}

View File

@ -0,0 +1,20 @@
// Copyright 2018 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0
//! Editing and displaying text.
// TODO
#![allow(clippy::all)]
mod attribute;
mod backspace;
mod editable_text;
mod font_descriptor;
mod input_component;
mod layout;
mod movement;
mod rich_text;
mod shell_text;
mod storage;
mod util;

View File

@ -0,0 +1,193 @@
// Copyright 2018 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0
//! Text editing movements.
use std::ops::Range;
use unicode_segmentation::UnicodeSegmentation;
use crate::kurbo::Point;
use super::{
editable_text::EditableText,
layout::TextLayout,
shell_text::{Movement, Selection, VerticalMovement, WritingDirection},
storage::TextStorage,
};
/// Compute the result of a [`Movement`] on a [`Selection`].
///
/// returns a new selection representing the state after the movement.
///
/// If `modify` is true, only the 'active' edge (the `end`) of the selection
/// should be changed; this is the case when the user moves with the shift
/// key pressed.
pub fn movement<T: EditableText + TextStorage>(
m: Movement,
s: Selection,
layout: &TextLayout<T>,
modify: bool,
) -> Selection {
let (text, layout) = match (layout.text(), layout.layout()) {
(Some(text), Some(layout)) => (text, layout),
_ => {
debug_assert!(false, "movement() called before layout rebuild");
return s;
}
};
// TODO
let writing_direction = WritingDirection::LeftToRight;
let (offset, h_pos) = match m {
Movement::Grapheme(d) if d.is_upstream_for_direction(writing_direction) => {
if s.is_caret() || modify {
text.prev_grapheme_offset(s.active)
.map(|off| (off, None))
.unwrap_or((0, s.h_pos))
} else {
(s.min(), None)
}
}
Movement::Grapheme(_) => {
if s.is_caret() || modify {
text.next_grapheme_offset(s.active)
.map(|off| (off, None))
.unwrap_or((s.active, s.h_pos))
} else {
(s.max(), None)
}
}
Movement::Vertical(VerticalMovement::LineUp) => {
let cur_pos = layout.hit_test_text_position(s.active);
let h_pos = s.h_pos.unwrap_or(cur_pos.point.x);
if cur_pos.line == 0 {
(0, Some(h_pos))
} else {
let lm = layout.line_metric(cur_pos.line).unwrap();
let point_above = Point::new(h_pos, cur_pos.point.y - lm.height);
let up_pos = layout.hit_test_point(point_above);
if up_pos.is_inside {
(up_pos.idx, Some(h_pos))
} else {
// because we can't specify affinity, moving up when h_pos
// is wider than both the current line and the previous line
// can result in a cursor position at the visual start of the
// current line; so we handle this as a special-case.
let lm_prev = layout.line_metric(cur_pos.line.saturating_sub(1)).unwrap();
let up_pos = lm_prev.end_offset - lm_prev.trailing_whitespace;
(up_pos, Some(h_pos))
}
}
}
Movement::Vertical(VerticalMovement::LineDown) => {
let cur_pos = layout.hit_test_text_position(s.active);
let h_pos = s.h_pos.unwrap_or(cur_pos.point.x);
if cur_pos.line == layout.line_count() - 1 {
(text.len(), Some(h_pos))
} else {
let lm = layout.line_metric(cur_pos.line).unwrap();
// may not work correctly for point sizes below 1.0
let y_below = lm.y_offset + lm.height + 1.0;
let point_below = Point::new(h_pos, y_below);
let up_pos = layout.hit_test_point(point_below);
(up_pos.idx, Some(point_below.x))
}
}
Movement::Vertical(VerticalMovement::DocumentStart) => (0, None),
Movement::Vertical(VerticalMovement::DocumentEnd) => (text.len(), None),
Movement::ParagraphStart => (text.preceding_line_break(s.active), None),
Movement::ParagraphEnd => (text.next_line_break(s.active), None),
Movement::Line(d) => {
let hit = layout.hit_test_text_position(s.active);
let lm = layout.line_metric(hit.line).unwrap();
let offset = if d.is_upstream_for_direction(writing_direction) {
lm.start_offset
} else {
lm.end_offset - lm.trailing_whitespace
};
(offset, None)
}
Movement::Word(d) if d.is_upstream_for_direction(writing_direction) => {
let offset = if s.is_caret() || modify {
text.prev_word_offset(s.active).unwrap_or(0)
} else {
s.min()
};
(offset, None)
}
Movement::Word(_) => {
let offset = if s.is_caret() || modify {
text.next_word_offset(s.active).unwrap_or(s.active)
} else {
s.max()
};
(offset, None)
}
// These two are not handled; they require knowledge of the size
// of the viewport.
Movement::Vertical(VerticalMovement::PageDown)
| Movement::Vertical(VerticalMovement::PageUp) => (s.active, s.h_pos),
other => {
tracing::warn!("unhandled movement {:?}", other);
(s.anchor, s.h_pos)
}
};
let start = if modify { s.anchor } else { offset };
Selection::new(start, offset).with_h_pos(h_pos)
}
/// Given a position in some text, return the containing word boundaries.
///
/// The returned range may not necessary be a 'word'; for instance it could be
/// the sequence of whitespace between two words.
///
/// If the position is on a word boundary, that will be considered the start
/// of the range.
///
/// This uses Unicode word boundaries, as defined in [UAX#29].
///
/// [UAX#29]: http://www.unicode.org/reports/tr29/
pub(crate) fn word_range_for_pos(text: &str, pos: usize) -> Range<usize> {
text.split_word_bound_indices()
.map(|(ix, word)| ix..(ix + word.len()))
.find(|range| range.contains(&pos))
.unwrap_or(pos..pos)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn word_range_simple() {
assert_eq!(word_range_for_pos("hello world", 3), 0..5);
assert_eq!(word_range_for_pos("hello world", 8), 6..11);
}
#[test]
fn word_range_whitespace() {
assert_eq!(word_range_for_pos("hello world", 5), 5..6);
}
#[test]
fn word_range_rtl() {
let rtl = "مرحبا بالعالم";
assert_eq!(word_range_for_pos(rtl, 5), 0..10);
assert_eq!(word_range_for_pos(rtl, 16), 11..25);
assert_eq!(word_range_for_pos(rtl, 10), 10..11);
}
#[test]
fn word_range_mixed() {
let mixed = "hello مرحبا بالعالم world";
assert_eq!(word_range_for_pos(mixed, 3), 0..5);
assert_eq!(word_range_for_pos(mixed, 8), 6..16);
assert_eq!(word_range_for_pos(mixed, 19), 17..31);
assert_eq!(word_range_for_pos(mixed, 36), 32..37);
}
}

View File

@ -1,514 +0,0 @@
// Copyright 2018 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0
//! A type for laying out, drawing, and interacting with text.
use std::rc::Rc;
use kurbo::{Affine, Line, Point, Rect, Size};
use parley::context::RangedBuilder;
use parley::fontique::{Style, Weight};
use parley::layout::{Alignment, Cursor};
use parley::style::{Brush as BrushTrait, FontFamily, FontStack, GenericFamily, StyleProperty};
use parley::{FontContext, Layout, LayoutContext};
use vello::peniko::{self, Color, Gradient};
use vello::Scene;
use super::{Link, TextStorage};
/// A component for displaying text on screen.
///
/// This is a type intended to be used by other widgets that display text.
/// It allows for the text itself as well as font and other styling information
/// to be set and modified. It wraps an inner layout object, and handles
/// invalidating and rebuilding it as required.
///
/// This object is not valid until the [`rebuild_if_needed`] method has been
/// called. You should generally do this in your widget's [`layout`] method.
/// Additionally, you should call [`needs_rebuild_after_update`]
/// as part of your widget's [`update`] method; if this returns `true`, you will need
/// to call [`rebuild_if_needed`] again, generally by scheduling another [`layout`]
/// pass.
///
/// [`layout`]: trait.Widget.html#tymethod.layout
/// [`update`]: trait.Widget.html#tymethod.update
/// [`needs_rebuild_after_update`]: #method.needs_rebuild_after_update
/// [`rebuild_if_needed`]: #method.rebuild_if_needed
///
/// TODO: Update docs to mentionParley
#[derive(Clone)]
pub struct TextLayout<T> {
text: T,
// TODO: Find a way to let this use borrowed data
scale: f32,
brush: TextBrush,
font: FontStack<'static>,
text_size: f32,
weight: Weight,
style: Style,
alignment: Alignment,
max_advance: Option<f32>,
links: Rc<[(Rect, usize)]>,
needs_layout: bool,
needs_line_breaks: bool,
layout: Layout<TextBrush>,
scratch_scene: Scene,
}
/// A custom brush for `Parley`, enabling using Parley to pass-through
/// which glyphs are selected/highlighted
#[derive(Clone, Debug, PartialEq)]
pub enum TextBrush {
Normal(peniko::Brush),
Highlight {
text: peniko::Brush,
fill: peniko::Brush,
},
}
impl BrushTrait for TextBrush {}
impl From<peniko::Brush> for TextBrush {
fn from(value: peniko::Brush) -> Self {
Self::Normal(value)
}
}
impl From<Gradient> for TextBrush {
fn from(value: Gradient) -> Self {
Self::Normal(value.into())
}
}
impl From<Color> for TextBrush {
fn from(value: Color) -> Self {
Self::Normal(value.into())
}
}
// Parley requires their Brush implementations to implement Default
impl Default for TextBrush {
fn default() -> Self {
Self::Normal(Default::default())
}
}
/// Metrics describing the layout text.
#[derive(Debug, Clone, Copy, Default)]
pub struct LayoutMetrics {
/// The nominal size of the layout.
pub size: Size,
/// The distance from the nominal top of the layout to the first baseline.
pub first_baseline: f32,
/// The width of the layout, inclusive of trailing whitespace.
pub trailing_whitespace_width: f32,
//TODO: add inking_rect
}
impl<T> TextLayout<T> {
/// Create a new `TextLayout` object.
pub fn new(text: T, text_size: f32) -> Self {
TextLayout {
text,
scale: 1.0,
brush: crate::theme::TEXT_COLOR.into(),
font: FontStack::Single(FontFamily::Generic(GenericFamily::SansSerif)),
text_size,
weight: Weight::NORMAL,
style: Style::Normal,
max_advance: None,
alignment: Default::default(),
links: Rc::new([]),
needs_layout: true,
needs_line_breaks: true,
layout: Layout::new(),
scratch_scene: Scene::new(),
}
}
/// Mark that the inner layout needs to be updated.
///
/// This should be used if your `T` has interior mutability
pub fn invalidate(&mut self) {
self.needs_layout = true;
self.needs_line_breaks = true;
}
/// Set the scaling factor
pub fn set_scale(&mut self, scale: f32) {
if scale != self.scale {
self.scale = scale;
self.invalidate();
}
}
/// Set the default brush used for the layout.
///
/// This is the non-layout impacting styling (primarily colour)
/// used when displaying the text
#[doc(alias = "set_color")]
pub fn set_brush(&mut self, brush: impl Into<TextBrush>) {
let brush = brush.into();
if brush != self.brush {
self.brush = brush;
self.invalidate();
}
}
/// Set the default font stack.
pub fn set_font(&mut self, font: FontStack<'static>) {
if font != self.font {
self.font = font;
self.invalidate();
}
}
/// Set the font size.
#[doc(alias = "set_font_size")]
pub fn set_text_size(&mut self, size: f32) {
if size != self.text_size {
self.text_size = size;
self.invalidate();
}
}
/// Set the font weight.
pub fn set_weight(&mut self, weight: Weight) {
if weight != self.weight {
self.weight = weight;
self.invalidate();
}
}
/// Set the font style.
pub fn set_style(&mut self, style: Style) {
if style != self.style {
self.style = style;
self.invalidate();
}
}
/// Set the [`Alignment`] for this layout.
pub fn set_text_alignment(&mut self, alignment: Alignment) {
if self.alignment != alignment {
self.alignment = alignment;
self.invalidate();
}
}
/// Set the width at which to wrap words.
///
/// You may pass `None` to disable word wrapping
/// (the default behaviour).
pub fn set_max_advance(&mut self, max_advance: Option<f32>) {
let max_advance = max_advance.map(|it| it.max(0.0));
if self.max_advance.is_some() != max_advance.is_some()
|| self
.max_advance
.zip(max_advance)
// 1e-4 is an arbitrary small-enough value that we don't care to rewrap
.map(|(old, new)| (old - new).abs() >= 1e-4)
.unwrap_or(false)
{
self.max_advance = max_advance;
self.needs_line_breaks = true;
}
}
/// Returns `true` if this layout needs to be rebuilt.
///
/// This happens (for instance) after style attributes are modified.
///
/// This does not account for things like the text changing, handling that
/// is the responsibility of the user.
pub fn needs_rebuild(&self) -> bool {
self.needs_layout || self.needs_line_breaks
}
// TODO: What are the valid use cases for this, where we shouldn't use a run-specific check instead?
// /// Returns `true` if this layout's text appears to be right-to-left.
// ///
// /// See [`piet::util::first_strong_rtl`] for more information.
// ///
// /// [`piet::util::first_strong_rtl`]: crate::piet::util::first_strong_rtl
// pub fn text_is_rtl(&self) -> bool {
// self.text_is_rtl
// }
}
impl<T: TextStorage> TextLayout<T> {
#[track_caller]
fn assert_rebuilt(&self, method: &str) {
if self.needs_layout || self.needs_line_breaks {
debug_panic!(
"TextLayout::{method} called without rebuilding layout object. Text was '{}'",
self.text.as_str().chars().take(250).collect::<String>()
);
}
}
/// Set the text to display.
pub fn set_text(&mut self, text: T) {
if !self.text.maybe_eq(&text) {
self.text = text;
self.invalidate();
}
}
/// Returns the [`TextStorage`] backing this layout, if it exists.
pub fn text(&self) -> &T {
&self.text
}
/// Returns the [`TextStorage`] backing this layout, if it exists.
///
/// Invalidates the layout and so should only be used when definitely applying an edit
pub fn text_mut(&mut self) -> &mut T {
self.invalidate();
&mut self.text
}
/// Returns the inner Parley [`Layout`] value.
pub fn layout(&self) -> &Layout<TextBrush> {
self.assert_rebuilt("layout");
&self.layout
}
/// The size of the laid-out text, excluding any trailing whitespace.
///
/// This is not meaningful until [`Self::rebuild`] has been called.
pub fn size(&self) -> Size {
self.assert_rebuilt("size");
Size::new(self.layout.width().into(), self.layout.height().into())
}
/// The size of the laid-out text, including any trailing whitespace.
///
/// This is not meaningful until [`Self::rebuild`] has been called.
pub fn full_size(&self) -> Size {
self.assert_rebuilt("full_size");
Size::new(self.layout.full_width().into(), self.layout.height().into())
}
/// Return the text's [`LayoutMetrics`].
///
/// This is not meaningful until [`Self::rebuild`] has been called.
pub fn layout_metrics(&self) -> LayoutMetrics {
self.assert_rebuilt("layout_metrics");
let first_baseline = self.layout.get(0).unwrap().metrics().baseline;
let size = Size::new(self.layout.width().into(), self.layout.height().into());
LayoutMetrics {
size,
first_baseline,
trailing_whitespace_width: self.layout.full_width(),
}
}
/// For a given `Point` (relative to this object's origin), returns index
/// into the underlying text of the nearest grapheme boundary.
///
/// This is not meaningful until [`Self::rebuild`] has been called.
pub fn cursor_for_point(&self, point: Point) -> Cursor {
self.assert_rebuilt("text_position_for_point");
// TODO: This is a mostly good first pass, but doesn't handle cursor positions in
// grapheme clusters within a parley cluster.
// We can also try
Cursor::from_point(&self.layout, point.x as f32, point.y as f32)
}
/// Given the utf-8 position of a character boundary in the underlying text,
/// return the `Point` (relative to this object's origin) representing the
/// boundary of the containing grapheme.
///
/// # Panics
///
/// Panics if `text_pos` is not a character boundary.
///
/// This is not meaningful until [`Self::rebuild`] has been called.
pub fn cursor_for_text_position(&self, text_pos: usize) -> Cursor {
self.assert_rebuilt("cursor_for_text_position");
// TODO: As a reminder, `is_leading` is not very useful to us; we don't know this ahead of time
// We're going to need to do quite a bit of remedial work on these
// e.g. to handle a inside a ligature made of multiple (unicode) grapheme clusters
// https://raphlinus.github.io/text/2020/10/26/text-layout.html#shaping-cluster
// But we're choosing to defer this work
// This also needs to handle affinity.
Cursor::from_position(&self.layout, text_pos, true)
}
/// Given the utf-8 position of a character boundary in the underlying text,
/// return the `Point` (relative to this object's origin) representing the
/// boundary of the containing grapheme.
///
/// # Panics
///
/// Panics if `text_pos` is not a character boundary.
///
/// This is not meaningful until [`Self::rebuild`] has been called.
pub fn point_for_text_position(&self, text_pos: usize) -> Point {
let cursor = self.cursor_for_text_position(text_pos);
Point::new(
cursor.advance as f64,
(cursor.baseline + cursor.offset) as f64,
)
}
// TODO: needed for text selection
// /// Given a utf-8 range in the underlying text, return a `Vec` of `Rect`s
// /// representing the nominal bounding boxes of the text in that range.
// ///
// /// # Panics
// ///
// /// Panics if the range start or end is not a character boundary.
// pub fn rects_for_range(&self, range: Range<usize>) -> Vec<Rect> {
// self.layout.rects_for_range(range)
// }
/// Given the utf-8 position of a character boundary in the underlying text,
/// return a `Line` suitable for drawing a vertical cursor at that boundary.
///
/// This is not meaningful until [`Self::rebuild`] has been called.
// TODO: This is too simplistic. See https://raphlinus.github.io/text/2020/10/26/text-layout.html#shaping-cluster
// for example. This would break in a `fi` ligature
pub fn cursor_line_for_text_position(&self, text_pos: usize) -> Line {
let from_position = self.cursor_for_text_position(text_pos);
let line = from_position.path.line(&self.layout).unwrap();
let line_metrics = line.metrics();
let baseline = line_metrics.baseline + line_metrics.descent;
let p1 = (from_position.offset as f64, baseline as f64);
let p2 = (
from_position.offset as f64,
(baseline - line_metrics.size()) as f64,
);
Line::new(p1, p2)
}
/// Returns the [`Link`] at the provided point (relative to the layout's origin) if one exists.
///
/// This can be used both for hit-testing (deciding whether to change the mouse cursor,
/// or performing some other action when hovering) as well as for retrieving a [`Link`]
/// on click.
///
/// [`Link`]: super::attribute::Link
pub fn link_for_pos(&self, pos: Point) -> Option<&Link> {
let (_, i) = self
.links
.iter()
.rfind(|(hit_box, _)| hit_box.contains(pos))?;
self.text.links().get(*i)
}
/// Rebuild the inner layout as needed.
///
/// This `TextLayout` object manages a lower-level layout object that may
/// need to be rebuilt in response to changes to the text or attributes
/// like the font.
///
/// This method should be called whenever any of these things may have changed.
/// A simple way to ensure this is correct is to always call this method
/// as part of your widget's [`layout`][crate::Widget::layout] method.
pub fn rebuild(
&mut self,
font_ctx: &mut FontContext,
layout_ctx: &mut LayoutContext<TextBrush>,
) {
self.rebuild_with_attributes(font_ctx, layout_ctx, |builder| builder);
}
/// Rebuild the inner layout as needed, adding attributes to the underlying layout.
///
/// See [`Self::rebuild`] for more information
pub fn rebuild_with_attributes(
&mut self,
font_ctx: &mut FontContext,
layout_ctx: &mut LayoutContext<TextBrush>,
attributes: impl for<'b> FnOnce(
RangedBuilder<'b, TextBrush, &'b str>,
) -> RangedBuilder<'b, TextBrush, &'b str>,
) {
if self.needs_layout {
self.needs_layout = false;
let mut builder = layout_ctx.ranged_builder(font_ctx, self.text.as_str(), self.scale);
builder.push_default(&StyleProperty::Brush(self.brush.clone()));
builder.push_default(&StyleProperty::FontSize(self.text_size));
builder.push_default(&StyleProperty::FontStack(self.font));
builder.push_default(&StyleProperty::FontWeight(self.weight));
builder.push_default(&StyleProperty::FontStyle(self.style));
// For more advanced features (e.g. variable font axes), these can be set in add_attributes
let builder = self.text.add_attributes(builder);
let mut builder = attributes(builder);
builder.build_into(&mut self.layout);
self.needs_line_breaks = true;
}
if self.needs_line_breaks {
self.needs_line_breaks = false;
self.layout
.break_all_lines(self.max_advance, self.alignment);
// TODO:
// self.links = text
// .links()
// ...
}
}
/// Draw the layout at the provided `Point`.
///
/// The origin of the layout is the top-left corner.
///
/// You must call [`Self::rebuild`] at some point before you first
/// call this method.
pub fn draw(&mut self, scene: &mut Scene, point: impl Into<Point>) {
self.assert_rebuilt("draw");
// TODO: This translation doesn't seem great
let p: Point = point.into();
crate::text_helpers::render_text(
scene,
&mut self.scratch_scene,
Affine::translate((p.x, p.y)),
&self.layout,
);
}
}
impl<T: TextStorage> std::fmt::Debug for TextLayout<T> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("TextLayout")
.field("text", &self.text.as_str().len())
.field("scale", &self.scale)
.field("brush", &self.brush)
.field("font", &self.font)
.field("text_size", &self.text_size)
.field("weight", &self.weight)
.field("style", &self.style)
.field("alignment", &self.alignment)
.field("wrap_width", &self.max_advance)
.field("outdated?", &self.needs_rebuild())
.field("width", &self.layout.width())
.field("height", &self.layout.height())
.finish()
}
}
impl<T: TextStorage + Default> Default for TextLayout<T> {
fn default() -> Self {
Self::new(Default::default(), crate::theme::TEXT_SIZE_NORMAL as f32)
}
}

View File

@ -1,30 +0,0 @@
// Copyright 2018 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0
//! Support for text display and rendering
//!
//! There are three kinds of text commonly needed:
//! 1) Entirely display text (e.g. a button)
//! 2) Selectable text (e.g. a paragraph of content)
//! 3) Editable text (e.g. a search bar)
//!
//! All of these have the same set of global styling options, and can contain rich text
mod store;
pub use store::{Link, TextStorage};
mod layout;
pub use layout::{LayoutMetrics, TextBrush, TextLayout};
mod selection;
pub use selection::{
len_utf8_from_first_byte, EditableTextCursor, Selectable, StringCursor, TextWithSelection,
};
// mod movement;
mod edit;
pub use edit::{EditableText, TextEditor};
mod backspace;
pub use backspace::offset_for_delete_backwards;

View File

@ -1,357 +0,0 @@
// Copyright 2018 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0
//! Text editing movements.
use std::ops::Range;
use unicode_segmentation::UnicodeSegmentation;
use crate::kurbo::Point;
use super::{layout::TextLayout, Selectable, TextStorage};
/// Compute the result of a [`Movement`] on a [`Selection`].
///
/// returns a new selection representing the state after the movement.
///
/// If `modify` is true, only the 'active' edge (the `end`) of the selection
/// should be changed; this is the case when the user moves with the shift
/// key pressed.
pub fn movement<T: Selectable + TextStorage>(
m: Movement,
s: Selection,
layout: &TextLayout<T>,
modify: bool,
) -> Selection {
if layout.needs_rebuild() {
debug_panic!("movement() called before layout rebuild");
return s;
}
let text = layout.text();
let parley_layout = layout.layout();
let writing_direction = || {
if layout
.cursor_for_text_position(s.active, s.active_affinity)
.is_rtl
{
WritingDirection::RightToLeft
} else {
WritingDirection::LeftToRight
}
};
let (offset, h_pos) = match m {
Movement::Grapheme(d) => {
let direction = writing_direction();
if d.is_upstream_for_direction(direction) {
if s.is_caret() || modify {
text.prev_grapheme_offset(s.active)
.map(|off| (off, None))
.unwrap_or((0, s.h_pos))
} else {
(s.min(), None)
}
} else {
if s.is_caret() || modify {
text.next_grapheme_offset(s.active)
.map(|off| (off, None))
.unwrap_or((s.active, s.h_pos))
} else {
(s.max(), None)
}
}
}
Movement::Vertical(VerticalMovement::LineUp) => {
let cur_pos = layout.cursor_for_text_position(s.active, s.active_affinity);
let h_pos = s.h_pos.unwrap_or(cur_pos.advance);
if cur_pos.path.line_index == 0 {
(0, Some(h_pos))
} else {
let lm = cur_pos.path.line(&parley_layout).unwrap();
let point_above = Point::new(h_pos, cur_pos.point.y - lm.height);
let up_pos = layout.hit_test_point(point_above);
if up_pos.is_inside {
(up_pos.idx, Some(h_pos))
} else {
// because we can't specify affinity, moving up when h_pos
// is wider than both the current line and the previous line
// can result in a cursor position at the visual start of the
// current line; so we handle this as a special-case.
let lm_prev = layout.line_metric(cur_pos.line.saturating_sub(1)).unwrap();
let up_pos = lm_prev.end_offset - lm_prev.trailing_whitespace;
(up_pos, Some(h_pos))
}
}
}
Movement::Vertical(VerticalMovement::LineDown) => {
let cur_pos = layout.hit_test_text_position(s.active);
let h_pos = s.h_pos.unwrap_or(cur_pos.point.x);
if cur_pos.line == layout.line_count() - 1 {
(text.len(), Some(h_pos))
} else {
let lm = layout.line_metric(cur_pos.line).unwrap();
// may not work correctly for point sizes below 1.0
let y_below = lm.y_offset + lm.height + 1.0;
let point_below = Point::new(h_pos, y_below);
let up_pos = layout.hit_test_point(point_below);
(up_pos.idx, Some(point_below.x))
}
}
Movement::Vertical(VerticalMovement::DocumentStart) => (0, None),
Movement::Vertical(VerticalMovement::DocumentEnd) => (text.len(), None),
Movement::ParagraphStart => (text.preceding_line_break(s.active), None),
Movement::ParagraphEnd => (text.next_line_break(s.active), None),
Movement::Line(d) => {
let hit = layout.hit_test_text_position(s.active);
let lm = layout.line_metric(hit.line).unwrap();
let offset = if d.is_upstream_for_direction(writing_direction) {
lm.start_offset
} else {
lm.end_offset - lm.trailing_whitespace
};
(offset, None)
}
Movement::Word(d) => {
if d.is_upstream_for_direction(writing_direction()) {
let offset = if s.is_caret() || modify {
text.prev_word_offset(s.active).unwrap_or(0)
} else {
s.min()
};
(offset, None)
} else {
let offset = if s.is_caret() || modify {
text.next_word_offset(s.active).unwrap_or(s.active)
} else {
s.max()
};
(offset, None)
}
}
// These two are not handled; they require knowledge of the size
// of the viewport.
Movement::Vertical(VerticalMovement::PageDown)
| Movement::Vertical(VerticalMovement::PageUp) => (s.active, s.h_pos),
other => {
tracing::warn!("unhandled movement {:?}", other);
(s.anchor, s.h_pos)
}
};
let start = if modify { s.anchor } else { offset };
Selection::new(start, offset).with_h_pos(h_pos)
}
/// Indicates a movement that transforms a particular text position in a
/// document.
///
/// These movements transform only single indices — not selections.
///
/// You'll note that a lot of these operations are idempotent, but you can get
/// around this by first sending a `Grapheme` movement. If for instance, you
/// want a `ParagraphStart` that is not idempotent, you can first send
/// `Movement::Grapheme(Direction::Upstream)`, and then follow it with
/// `ParagraphStart`.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Movement {
/// A movement that stops when it reaches an extended grapheme cluster boundary.
///
/// This movement is achieved on most systems by pressing the left and right
/// arrow keys. For more information on grapheme clusters, see
/// [Unicode Text Segmentation](https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries).
Grapheme(Direction),
/// A movement that stops when it reaches a word boundary.
///
/// This movement is achieved on most systems by pressing the left and right
/// arrow keys while holding control. For more information on words, see
/// [Unicode Text Segmentation](https://unicode.org/reports/tr29/#Word_Boundaries).
Word(Direction),
/// A movement that stops when it reaches a soft line break.
///
/// This movement is achieved on macOS by pressing the left and right arrow
/// keys while holding command. `Line` should be idempotent: if the
/// position is already at the end of a soft-wrapped line, this movement
/// should never push it onto another soft-wrapped line.
///
/// In order to implement this properly, your text positions should remember
/// their affinity.
Line(Direction),
/// An upstream movement that stops when it reaches a hard line break.
///
/// `ParagraphStart` should be idempotent: if the position is already at the
/// start of a hard-wrapped line, this movement should never push it onto
/// the previous line.
ParagraphStart,
/// A downstream movement that stops when it reaches a hard line break.
///
/// `ParagraphEnd` should be idempotent: if the position is already at the
/// end of a hard-wrapped line, this movement should never push it onto the
/// next line.
ParagraphEnd,
/// A vertical movement, see `VerticalMovement` for more details.
Vertical(VerticalMovement),
}
/// Indicates a horizontal direction in the text.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Direction {
/// The direction visually to the left.
///
/// This may be byte-wise forwards or backwards in the document, depending
/// on the text direction around the position being moved.
Left,
/// The direction visually to the right.
///
/// This may be byte-wise forwards or backwards in the document, depending
/// on the text direction around the position being moved.
Right,
/// Byte-wise backwards in the document.
///
/// In a left-to-right context, this value is the same as `Left`.
Upstream,
/// Byte-wise forwards in the document.
///
/// In a left-to-right context, this value is the same as `Right`.
Downstream,
}
impl Direction {
/// Returns `true` if this direction is byte-wise backwards for
/// the provided [`WritingDirection`].
///
/// The provided direction *must not be* `WritingDirection::Natural`.
pub fn is_upstream_for_direction(self, direction: WritingDirection) -> bool {
// assert!(
// !matches!(direction, WritingDirection::Natural),
// "writing direction must be resolved"
// );
match self {
Direction::Upstream => true,
Direction::Downstream => false,
Direction::Left => matches!(direction, WritingDirection::LeftToRight),
Direction::Right => matches!(direction, WritingDirection::RightToLeft),
}
}
}
/// Distinguishes between two visually distinct locations with the same byte
/// index.
///
/// Sometimes, a byte location in a document has two visual locations. For
/// example, the end of a soft-wrapped line and the start of the subsequent line
/// have different visual locations (and we want to be able to place an input
/// caret in either place!) but the same byte-wise location. This also shows up
/// in bidirectional text contexts. Affinity allows us to disambiguate between
/// these two visual locations.
///
/// Note that in scenarios where soft line breaks interact with bidi text, this gets
/// more complicated.
#[derive(Copy, Clone, Debug, Hash, PartialEq)]
pub enum Affinity {
/// The position which has an apparent position "earlier" in the text.
/// For soft line breaks, this is the position at the end of the first line.
///
/// For positions in-between bidi contexts, this is the position which is
/// related to the "outgoing" text section. E.g. for the string "abcDEF" (rendered `abcFED`),
/// with the cursor at "abc|DEF" with upstream affinity, the cursor would be rendered at the
/// position `abc|DEF`
Upstream,
/// The position which has a higher apparent position in the text.
/// For soft line breaks, this is the position at the beginning of the second line.
///
/// For positions in-between bidi contexts, this is the position which is
/// related to the "incoming" text section. E.g. for the string "abcDEF" (rendered `abcFED`),
/// with the cursor at "abc|DEF" with downstream affinity, the cursor would be rendered at the
/// position `abcDEF|`
Downstream,
}
impl Affinity {
/// Convert into the `parley` form of "leading"
pub fn is_leading(&self) -> bool {
match self {
Affinity::Upstream => false,
Affinity::Downstream => true,
}
}
}
/// Indicates a horizontal direction for writing text.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum WritingDirection {
LeftToRight,
RightToLeft,
// /// Indicates writing direction should be automatically detected based on
// /// the text contents.
// See also `is_upstream_for_direction` if adding back in
// Natural,
}
/// Indicates a vertical movement in a text document.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum VerticalMovement {
LineUp,
LineDown,
PageUp,
PageDown,
DocumentStart,
DocumentEnd,
}
/// Given a position in some text, return the containing word boundaries.
///
/// The returned range may not necessary be a 'word'; for instance it could be
/// the sequence of whitespace between two words.
///
/// If the position is on a word boundary, that will be considered the start
/// of the range.
///
/// This uses Unicode word boundaries, as defined in [UAX#29].
///
/// [UAX#29]: http://www.unicode.org/reports/tr29/
pub(crate) fn word_range_for_pos(text: &str, pos: usize) -> Range<usize> {
text.split_word_bound_indices()
.map(|(ix, word)| ix..(ix + word.len()))
.find(|range| range.contains(&pos))
.unwrap_or(pos..pos)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn word_range_simple() {
assert_eq!(word_range_for_pos("hello world", 3), 0..5);
assert_eq!(word_range_for_pos("hello world", 8), 6..11);
}
#[test]
fn word_range_whitespace() {
assert_eq!(word_range_for_pos("hello world", 5), 5..6);
}
#[test]
fn word_range_rtl() {
let rtl = "مرحبا بالعالم";
assert_eq!(word_range_for_pos(rtl, 5), 0..10);
assert_eq!(word_range_for_pos(rtl, 16), 11..25);
assert_eq!(word_range_for_pos(rtl, 10), 10..11);
}
#[test]
fn word_range_mixed() {
let mixed = "hello مرحبا بالعالم world";
assert_eq!(word_range_for_pos(mixed, 3), 0..5);
assert_eq!(word_range_for_pos(mixed, 8), 6..16);
assert_eq!(word_range_for_pos(mixed, 19), 17..31);
assert_eq!(word_range_for_pos(mixed, 36), 32..37);
}
}

View File

@ -7,7 +7,7 @@ use kurbo::{Line, Rect, Stroke};
use parley::Layout;
use vello::{kurbo::Affine, peniko::Fill, Scene};
use crate::{text2::TextBrush, WidgetId};
use crate::{text::TextBrush, WidgetId};
/// A reference counted string slice.
///

View File

@ -11,7 +11,7 @@ use vello::Scene;
use crate::action::Action;
use crate::event::PointerButton;
use crate::paint_scene_helpers::{fill_lin_gradient, stroke, UnitPoint};
use crate::text2::TextStorage;
use crate::text::TextStorage;
use crate::widget::{Label, WidgetMut, WidgetPod};
use crate::{
theme, AccessCtx, AccessEvent, ArcStr, BoxConstraints, EventCtx, Insets, LayoutCtx, LifeCycle,

View File

@ -12,7 +12,7 @@ use vello::Scene;
use crate::action::Action;
use crate::kurbo::{BezPath, Cap, Join, Size};
use crate::paint_scene_helpers::{fill_lin_gradient, stroke, UnitPoint};
use crate::text2::TextStorage;
use crate::text::TextStorage;
use crate::widget::{Label, WidgetMut};
use crate::{
theme, AccessCtx, AccessEvent, ArcStr, BoxConstraints, EventCtx, LayoutCtx, LifeCycle,

View File

@ -12,7 +12,7 @@ use tracing::{trace, trace_span, Span};
use vello::peniko::BlendMode;
use vello::Scene;
use crate::text2::{TextBrush, TextLayout, TextStorage};
use crate::text::{TextBrush, TextLayout, TextStorage};
use crate::widget::WidgetMut;
use crate::{
AccessCtx, AccessEvent, ArcStr, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx,

View File

@ -13,7 +13,7 @@ use vello::{peniko::BlendMode, Scene};
use crate::widget::{LineBreaking, WidgetMut};
use crate::{
text2::{TextBrush, TextStorage, TextWithSelection},
text::{TextBrush, TextStorage, TextWithSelection},
widget::label::LABEL_X_PADDING,
AccessCtx, AccessEvent, ArcStr, BoxConstraints, CursorIcon, EventCtx, LayoutCtx, LifeCycle,
LifeCycleCtx, PaintCtx, PointerEvent, StatusChange, TextEvent, Widget, WidgetId,

View File

@ -17,7 +17,7 @@ use vello::{
use crate::widget::{LineBreaking, WidgetMut};
use crate::{
dpi::{LogicalPosition, LogicalSize},
text2::{TextBrush, TextEditor, TextStorage, TextWithSelection},
text::{TextBrush, TextEditor, TextStorage, TextWithSelection},
AccessCtx, AccessEvent, BoxConstraints, CursorIcon, EventCtx, LayoutCtx, LifeCycle,
LifeCycleCtx, PaintCtx, PointerEvent, StatusChange, TextEvent, Widget, WidgetId,
};

View File

@ -1,7 +1,7 @@
// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0
use masonry::{text2::TextBrush, widget, ArcStr};
use masonry::{text::TextBrush, widget, ArcStr};
use xilem_core::Mut;
use crate::{Color, MessageResult, Pod, TextAlignment, View, ViewCtx, ViewId};

View File

@ -1,7 +1,7 @@
// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0
use masonry::{text2::TextBrush, widget, ArcStr};
use masonry::{text::TextBrush, widget, ArcStr};
use xilem_core::Mut;
use crate::{Color, MessageResult, Pod, TextAlignment, View, ViewCtx, ViewId};

View File

@ -1,7 +1,7 @@
// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0
use masonry::{text2::TextBrush, widget};
use masonry::{text::TextBrush, widget};
use xilem_core::{Mut, View};
use crate::{Color, MessageResult, Pod, TextAlignment, ViewCtx, ViewId};