diff --git a/examples/swash_render/src/main.rs b/examples/swash_render/src/main.rs index b7cc2c12b..810889dd6 100644 --- a/examples/swash_render/src/main.rs +++ b/examples/swash_render/src/main.rs @@ -10,16 +10,16 @@ reason = "Deferred" )] +use std::fs::File; + use image::codecs::png::PngEncoder; use image::{self, Pixel, Rgba, RgbaImage}; -use parley::layout::{Alignment, Glyph, GlyphRun, Layout, PositionedLayoutItem}; -use parley::style::{FontStack, FontWeight, StyleProperty, TextStyle}; -use parley::{FontContext, InlineBox, LayoutContext}; -use std::fs::File; +use parley::inputs::{FontContext, FontStack, FontWeight, LayoutContext, StyleProperty, TextStyle}; +use parley::outputs::{Alignment, Glyph, GlyphRun, Layout, PositionedLayoutItem}; +use parley::InlineBox; use swash::scale::image::Content; use swash::scale::{Render, ScaleContext, Scaler, Source, StrikeWith}; -use swash::zeno; -use swash::FontRef; +use swash::{zeno, FontRef}; use zeno::{Format, Vector}; #[derive(Clone, Copy, Debug, PartialEq)] diff --git a/examples/tiny_skia_render/src/main.rs b/examples/tiny_skia_render/src/main.rs index fcfd4e51e..2a9638b76 100644 --- a/examples/tiny_skia_render/src/main.rs +++ b/examples/tiny_skia_render/src/main.rs @@ -14,8 +14,9 @@ )] use parley::{ - Alignment, FontContext, FontWeight, GenericFamily, GlyphRun, InlineBox, Layout, LayoutContext, - PositionedLayoutItem, StyleProperty, + inputs::{FontContext, FontWeight, GenericFamily, LayoutContext, StyleProperty}, + outputs::{Alignment, GlyphRun, Layout, PositionedLayoutItem}, + InlineBox, }; use skrifa::{ instance::{LocationRef, NormalizedCoord, Size}, diff --git a/examples/vello_editor/src/access_ids.rs b/examples/vello_editor/src/access_ids.rs index 60d694bd1..2b8be6004 100644 --- a/examples/vello_editor/src/access_ids.rs +++ b/examples/vello_editor/src/access_ids.rs @@ -1,9 +1,10 @@ // Copyright 2024 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -use accesskit::NodeId; use core::sync::atomic::{AtomicU64, Ordering}; +use accesskit::NodeId; + pub const WINDOW_ID: NodeId = NodeId(0); pub const TEXT_INPUT_ID: NodeId = NodeId(1); diff --git a/examples/vello_editor/src/main.rs b/examples/vello_editor/src/main.rs index f1ec9a8cc..4c02d41ba 100644 --- a/examples/vello_editor/src/main.rs +++ b/examples/vello_editor/src/main.rs @@ -12,15 +12,15 @@ reason = "Deferred" )] -use accesskit::{Node, Role, Tree, TreeUpdate}; -use anyhow::Result; use std::num::NonZeroUsize; use std::sync::Arc; -use vello::kurbo; + +use accesskit::{Node, Role, Tree, TreeUpdate}; +use anyhow::Result; +use parley::editing::Generation; use vello::peniko::Color; use vello::util::{RenderContext, RenderSurface}; -use vello::wgpu; -use vello::{AaConfig, Renderer, RendererOptions, Scene}; +use vello::{kurbo, wgpu, AaConfig, Renderer, RendererOptions, Scene}; use winit::application::ApplicationHandler; use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize}; use winit::event::{StartCause, WindowEvent}; @@ -97,7 +97,7 @@ struct SimpleVelloApp<'s> { editor: text::Editor, /// The last generation of the editor layout that we drew. - last_drawn_generation: text::Generation, + last_drawn_generation: Generation, /// The IME cursor area we last sent to the platform. last_sent_ime_cursor_area: kurbo::Rect, @@ -185,7 +185,7 @@ impl ApplicationHandler for SimpleVelloApp<'_> { self.editor.cursor_blink(); if let Some(next_time) = self.editor.next_blink_time() { - self.last_drawn_generation = text::Generation::default(); + self.last_drawn_generation = Generation::default(); if let RenderState::Active(state) = &self.state { state.window.request_redraw(); } @@ -260,7 +260,7 @@ impl ApplicationHandler for SimpleVelloApp<'_> { WindowEvent::Focused(false) => { self.editor.disable_blink(); self.editor.cursor_blink(); - self.last_drawn_generation = text::Generation::default(); + self.last_drawn_generation = Generation::default(); if let RenderState::Active(state) = &self.state { state.window.request_redraw(); } diff --git a/examples/vello_editor/src/text.rs b/examples/vello_editor/src/text.rs index 1bfe91c75..2b791dc2c 100644 --- a/examples/vello_editor/src/text.rs +++ b/examples/vello_editor/src/text.rs @@ -3,7 +3,6 @@ use accesskit::{Node, TreeUpdate}; use core::default::Default; -use parley::{editor::SplitString, layout::PositionedLayoutItem, GenericFamily, StyleProperty}; use std::time::{Duration, Instant}; use vello::{ kurbo::{Affine, Line, Stroke}, @@ -16,8 +15,9 @@ use winit::{ keyboard::{Key, NamedKey}, }; -pub use parley::layout::editor::Generation; -use parley::{FontContext, LayoutContext, PlainEditor, PlainEditorDriver}; +use parley::editing::{Generation, PlainEditor, PlainEditorDriver, SplitString}; +use parley::inputs::{FontContext, GenericFamily, LayoutContext, StyleProperty}; +use parley::outputs::PositionedLayoutItem; use crate::access_ids::next_node_id; diff --git a/parley/src/bidi.rs b/parley/src/algos/bidi.rs similarity index 100% rename from parley/src/bidi.rs rename to parley/src/algos/bidi.rs diff --git a/parley/src/algos/mod.rs b/parley/src/algos/mod.rs new file mode 100644 index 000000000..40811f748 --- /dev/null +++ b/parley/src/algos/mod.rs @@ -0,0 +1,9 @@ +// Copyright 2024 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +// TODO - Remove pub(crate) + +pub(crate) mod bidi; +pub(crate) mod resolve; +pub(crate) mod shape; +pub(crate) mod swash_convert; diff --git a/parley/src/resolve/mod.rs b/parley/src/algos/resolve/mod.rs similarity index 97% rename from parley/src/resolve/mod.rs rename to parley/src/algos/resolve/mod.rs index a05eaa8cb..e4cdec3a9 100644 --- a/parley/src/resolve/mod.rs +++ b/parley/src/algos/resolve/mod.rs @@ -8,21 +8,21 @@ pub(crate) mod tree; pub(crate) use range::RangedStyleBuilder; -use alloc::{vec, vec::Vec}; - -use super::style::{ - Brush, FontFamily, FontFeature, FontSettings, FontStack, FontStyle, FontVariation, FontWeight, - FontWidth, StyleProperty, -}; -use crate::font::FontContext; -use crate::layout; -use crate::style::TextStyle; -use crate::util::nearly_eq; +use alloc::vec; +use alloc::vec::Vec; use core::borrow::Borrow; use core::ops::Range; + use fontique::FamilyId; use swash::text::Language; +use crate::inputs::{ + Brush, FontContext, FontFamily, FontFeature, FontSettings, FontStack, FontStyle, FontVariation, + FontWeight, FontWidth, StyleProperty, TextStyle, +}; +use crate::outputs; +use crate::util::nearly_eq; + /// Style with an associated range. #[derive(Debug, Clone)] pub(crate) struct RangedStyle { @@ -476,8 +476,8 @@ impl ResolvedStyle { } } - pub(crate) fn as_layout_style(&self) -> layout::Style { - layout::Style { + pub(crate) fn as_layout_style(&self) -> outputs::Style { + outputs::Style { brush: self.brush.clone(), underline: self.underline.as_layout_decoration(&self.brush), strikethrough: self.strikethrough.as_layout_decoration(&self.brush), @@ -501,9 +501,9 @@ pub(crate) struct ResolvedDecoration { impl ResolvedDecoration { /// Convert into a layout Decoration (filtering out disabled decorations) - pub(crate) fn as_layout_decoration(&self, default_brush: &B) -> Option> { + pub(crate) fn as_layout_decoration(&self, default_brush: &B) -> Option> { if self.enabled { - Some(layout::Decoration { + Some(outputs::Decoration { brush: self.brush.clone().unwrap_or_else(|| default_brush.clone()), offset: self.offset, size: self.size, diff --git a/parley/src/resolve/range.rs b/parley/src/algos/resolve/range.rs similarity index 100% rename from parley/src/resolve/range.rs rename to parley/src/algos/resolve/range.rs index 658273d5f..4e0f79f1a 100644 --- a/parley/src/resolve/range.rs +++ b/parley/src/algos/resolve/range.rs @@ -4,9 +4,9 @@ //! Range based style application. use alloc::vec; +use core::ops::{Bound, Range, RangeBounds}; use super::{Brush, RangedProperty, RangedStyle, ResolvedProperty, ResolvedStyle, Vec}; -use core::ops::{Bound, Range, RangeBounds}; /// Builder for constructing an ordered sequence of non-overlapping ranged /// styles from a collection of ranged style properties. diff --git a/parley/src/resolve/tree.rs b/parley/src/algos/resolve/tree.rs similarity index 98% rename from parley/src/resolve/tree.rs rename to parley/src/algos/resolve/tree.rs index 0ac5b039a..7ba88b800 100644 --- a/parley/src/resolve/tree.rs +++ b/parley/src/algos/resolve/tree.rs @@ -3,11 +3,11 @@ //! Hierarchical tree based style application. use alloc::borrow::Cow; -use alloc::{string::String, vec::Vec}; - -use crate::style::WhiteSpaceCollapse; +use alloc::string::String; +use alloc::vec::Vec; use super::{Brush, RangedStyle, ResolvedProperty, ResolvedStyle}; +use crate::inputs::WhiteSpaceCollapse; #[derive(Debug, Clone)] struct StyleTreeNode { diff --git a/parley/src/shape.rs b/parley/src/algos/shape.rs similarity index 96% rename from parley/src/shape.rs rename to parley/src/algos/shape.rs index 4543ef9fe..c84bb8c62 100644 --- a/parley/src/shape.rs +++ b/parley/src/algos/shape.rs @@ -1,21 +1,21 @@ // Copyright 2021 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -use super::layout::Layout; -use super::resolve::{RangedStyle, ResolveContext, Resolved}; -use super::style::{Brush, FontFeature, FontVariation}; -use crate::util::nearly_eq; -use crate::Font; -use fontique::QueryFamily; -use fontique::{self, Query, QueryFont}; +use alloc::vec::Vec; + +use fontique::{self, Query, QueryFamily, QueryFont}; use swash::shape::{partition, Direction, ShapeContext}; use swash::text::cluster::{CharCluster, CharInfo, Token}; use swash::text::{Language, Script}; use swash::{FontRef, Synthesis}; -use alloc::vec::Vec; - +use crate::algos::resolve::{RangedStyle, ResolveContext, Resolved}; +use crate::algos::swash_convert::{locale_to_fontique, script_to_fontique, synthesis_to_swash}; use crate::inline_box::InlineBox; +use crate::inputs::{Brush, FontFeature, FontVariation}; +use crate::outputs::Layout; +use crate::util::nearly_eq; +use crate::Font; struct Item { style_index: u16, @@ -248,8 +248,8 @@ impl<'a, 'b, B: Brush> FontSelector<'a, 'b, B> { let variations = rcx.variations(style.font_variations).unwrap_or(&[]); let features = rcx.features(style.font_features).unwrap_or(&[]); query.set_families(fonts.iter().copied()); - let fb_script = crate::swash_convert::script_to_fontique(script); - let fb_language = locale.and_then(crate::swash_convert::locale_to_fontique); + let fb_script = script_to_fontique(script); + let fb_language = locale.and_then(locale_to_fontique); query.set_fallbacks(fontique::FallbackKey::new(fb_script, fb_language.as_ref())); query.set_attributes(attrs); Self { @@ -306,7 +306,6 @@ impl partition::Selector for FontSelector<'_, '_, B> { let mut selected_font = None; self.query.matches_with(|font| { if let Ok(font_ref) = skrifa::FontRef::from_index(font.blob.as_ref(), font.index) { - use crate::swash_convert::synthesis_to_swash; use skrifa::MetadataProvider; use swash::text::cluster::Status as MapStatus; let charmap = font_ref.charmap(); diff --git a/parley/src/swash_convert.rs b/parley/src/algos/swash_convert.rs similarity index 100% rename from parley/src/swash_convert.rs rename to parley/src/algos/swash_convert.rs diff --git a/parley/src/editing/cursor.rs b/parley/src/editing/cursor.rs new file mode 100644 index 000000000..37de10a89 --- /dev/null +++ b/parley/src/editing/cursor.rs @@ -0,0 +1,462 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Text selection support. + +use peniko::kurbo::Rect; + +#[cfg(feature = "accesskit")] +use accesskit::TextPosition; +#[cfg(feature = "accesskit")] +use swash::text::cluster::Whitespace; + +use crate::inputs::Brush; +use crate::outputs::{Affinity, BreakReason, Cluster, ClusterSide, Layout, Line}; + +#[cfg(feature = "accesskit")] +use crate::outputs::LayoutAccessibility; + +/// Defines a position with a text layout. +#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)] +pub struct Cursor { + pub(crate) index: usize, + pub(crate) affinity: Affinity, +} + +impl Cursor { + /// Creates a new cursor from the given byte index and affinity. + pub fn from_byte_index(layout: &Layout, index: usize, affinity: Affinity) -> Self { + if let Some(cluster) = Cluster::from_byte_index(layout, index) { + let index = cluster.text_range().start; + let affinity = if cluster.is_line_break() == Some(BreakReason::Explicit) { + Affinity::Downstream + } else { + affinity + }; + Self { index, affinity } + } else { + Self { + index: layout.data.text_len, + affinity: Affinity::Upstream, + } + } + } + + /// Creates a new cursor from the given coordinates. + pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { + let (index, affinity) = if let Some((cluster, side)) = Cluster::from_point(layout, x, y) { + let is_leading = side == ClusterSide::Left; + if cluster.is_rtl() { + if is_leading { + (cluster.text_range().end, Affinity::Upstream) + } else { + (cluster.text_range().start, Affinity::Downstream) + } + } else { + // We never want to position the cursor _after_ a hard + // line since that cursor appears visually at the start + // of the next line + if is_leading || cluster.is_line_break() == Some(BreakReason::Explicit) { + (cluster.text_range().start, Affinity::Downstream) + } else { + (cluster.text_range().end, Affinity::Upstream) + } + } + } else { + (layout.data.text_len, Affinity::Downstream) + }; + Self { index, affinity } + } + + #[cfg(feature = "accesskit")] + pub fn from_access_position( + pos: &TextPosition, + layout: &Layout, + layout_access: &LayoutAccessibility, + ) -> Option { + let (line_index, run_index) = *layout_access.run_paths_by_access_id.get(&pos.node)?; + let line = layout.get(line_index)?; + let run = line.run(run_index)?; + let index = run + .get(pos.character_index) + .map(|cluster| cluster.text_range().start) + .unwrap_or(layout.data.text_len); + Some(Self::from_byte_index(layout, index, Affinity::Downstream)) + } + + pub(crate) fn from_cluster( + layout: &Layout, + cluster: Cluster<'_, B>, + moving_right: bool, + ) -> Self { + Self::from_byte_index( + layout, + cluster.text_range().start, + affinity_for_dir(cluster.is_rtl(), moving_right), + ) + } + + /// Returns the logical text index of the cursor. + pub fn index(&self) -> usize { + self.index + } + + /// Returns the affinity of the cursor. + /// + /// This defines the direction from which the cursor entered its current + /// position and affects the visual location of the rendered cursor. + pub fn affinity(&self) -> Affinity { + self.affinity + } + + /// Returns a new cursor that is guaranteed to be within the bounds of the + /// given layout. + #[must_use] + pub fn refresh(&self, layout: &Layout) -> Self { + Self::from_byte_index(layout, self.index, self.affinity) + } + + /// Returns a new cursor that is positioned at the previous cluster boundary + /// in visual order. + #[must_use] + pub fn previous_visual(&self, layout: &Layout) -> Self { + let [left, right] = self.visual_clusters(layout); + if let (Some(left), Some(right)) = (&left, &right) { + if left.is_soft_line_break() { + if left.is_rtl() && self.affinity == Affinity::Upstream { + let index = if right.is_rtl() { + left.text_range().start + } else { + left.text_range().end + }; + return Self::from_byte_index(layout, index, Affinity::Downstream); + } else if !left.is_rtl() && self.affinity == Affinity::Downstream { + let index = if right.is_rtl() { + right.text_range().end + } else { + right.text_range().start + }; + return Self::from_byte_index(layout, index, Affinity::Upstream); + } + } + } + if let Some(left) = left { + let index = if left.is_rtl() { + left.text_range().end + } else { + left.text_range().start + }; + return Self::from_byte_index(layout, index, affinity_for_dir(left.is_rtl(), false)); + } + *self + } + + /// Returns a new cursor that is positioned at the next cluster boundary + /// in visual order. + #[must_use] + pub fn next_visual(&self, layout: &Layout) -> Self { + let [left, right] = self.visual_clusters(layout); + if let (Some(left), Some(right)) = (&left, &right) { + if left.is_soft_line_break() { + if left.is_rtl() && self.affinity == Affinity::Downstream { + let index = if right.is_rtl() { + right.text_range().end + } else { + right.text_range().start + }; + return Self::from_byte_index(layout, index, Affinity::Upstream); + } else if !left.is_rtl() && self.affinity == Affinity::Upstream { + let index = if right.is_rtl() { + right.text_range().end + } else { + right.text_range().start + }; + return Self::from_byte_index(layout, index, Affinity::Downstream); + } + } + let index = if right.is_rtl() { + right.text_range().start + } else { + right.text_range().end + }; + return Self::from_byte_index(layout, index, affinity_for_dir(right.is_rtl(), true)); + } + if let Some(right) = right { + let index = if right.is_rtl() { + right.text_range().start + } else { + right.text_range().end + }; + return Self::from_byte_index(layout, index, affinity_for_dir(right.is_rtl(), true)); + } + *self + } + + /// Returns a new cursor that is positioned at the next word boundary + /// in visual order. + #[must_use] + pub fn next_visual_word(&self, layout: &Layout) -> Self { + let mut cur = *self; + loop { + let next = cur.next_visual(layout); + if next == cur { + break; + } + cur = next; + let [Some(left), Some(right)] = cur.visual_clusters(layout) else { + break; + }; + if left.is_rtl() { + if left.is_word_boundary() && !left.is_space_or_nbsp() { + break; + } + } else if right.is_word_boundary() && !left.is_space_or_nbsp() { + break; + } + } + cur + } + + /// Returns a new cursor that is positioned at the previous word boundary + /// in visual order. + #[must_use] + pub fn previous_visual_word(&self, layout: &Layout) -> Self { + let mut cur = *self; + loop { + let next = cur.previous_visual(layout); + if next == cur { + break; + } + cur = next; + let [Some(left), Some(right)] = cur.visual_clusters(layout) else { + break; + }; + if left.is_rtl() { + if left.is_word_boundary() + && (left.is_space_or_nbsp() + || (right.is_word_boundary() && !right.is_space_or_nbsp())) + { + break; + } + } else if right.is_word_boundary() && !right.is_space_or_nbsp() { + break; + } + } + cur + } + + /// Returns a new cursor that is positioned at the next word boundary + /// in logical order. + #[must_use] + pub fn next_logical_word(&self, layout: &Layout) -> Self { + let [left, right] = self.logical_clusters(layout); + if let Some(cluster) = right.or(left) { + let start = cluster.clone(); + let cluster = cluster.next_logical_word().unwrap_or(cluster); + if cluster.path == start.path { + return Self::from_byte_index(layout, usize::MAX, Affinity::Downstream); + } + return Self::from_cluster(layout, cluster, true); + } + *self + } + + /// Returns a new cursor that is positioned at the previous word boundary + /// in logical order. + #[must_use] + pub fn previous_logical_word(&self, layout: &Layout) -> Self { + let [left, right] = self.logical_clusters(layout); + if let Some(cluster) = left.or(right) { + let cluster = cluster.previous_logical_word().unwrap_or(cluster); + return Self::from_cluster(layout, cluster, true); + } + *self + } + + /// Returns a rectangle that represents the visual geometry of the cursor + /// in layout space. + /// + /// The `width` parameter defines the width of the resulting rectangle. + pub fn geometry(&self, layout: &Layout, width: f32) -> Rect { + match self.visual_clusters(layout) { + [Some(left), Some(right)] => { + if left.is_end_of_line() { + if left.is_soft_line_break() { + let (cluster, at_end) = if left.is_rtl() + && self.affinity == Affinity::Downstream + || !left.is_rtl() && self.affinity == Affinity::Upstream + { + (left, true) + } else { + (right, false) + }; + cursor_rect(&cluster, at_end, width) + } else { + cursor_rect(&right, false, width) + } + } else { + cursor_rect(&left, true, width) + } + } + [Some(left), None] if left.is_hard_line_break() => last_line_cursor_rect(layout, width), + [Some(left), _] => cursor_rect(&left, true, width), + [_, Some(right)] => cursor_rect(&right, false, width), + _ => last_line_cursor_rect(layout, width), + } + } + + /// Returns the pair of clusters that logically bound the cursor + /// position. + /// + /// The order in the array is upstream followed by downstream. + pub fn logical_clusters<'a, B: Brush>( + &self, + layout: &'a Layout, + ) -> [Option>; 2] { + let upstream = self + .index + .checked_sub(1) + .and_then(|index| Cluster::from_byte_index(layout, index)); + let downstream = Cluster::from_byte_index(layout, self.index); + [upstream, downstream] + } + + /// Returns the pair of clusters that visually bound the cursor + /// position. + /// + /// The order in the array is left followed by right. + pub fn visual_clusters<'a, B: Brush>( + &self, + layout: &'a Layout, + ) -> [Option>; 2] { + if self.affinity == Affinity::Upstream { + if let Some(cluster) = self.upstream_cluster(layout) { + if cluster.is_rtl() { + [cluster.previous_visual(), Some(cluster)] + } else { + [Some(cluster.clone()), cluster.next_visual()] + } + } else if let Some(cluster) = self.downstream_cluster(layout) { + if cluster.is_rtl() { + [None, Some(cluster)] + } else { + [Some(cluster), None] + } + } else { + [None, None] + } + } else if let Some(cluster) = self.downstream_cluster(layout) { + if cluster.is_rtl() { + [Some(cluster.clone()), cluster.next_visual()] + } else { + [cluster.previous_visual(), Some(cluster)] + } + } else if let Some(cluster) = self.upstream_cluster(layout) { + if cluster.is_rtl() { + [None, Some(cluster)] + } else { + [Some(cluster), None] + } + } else { + [None, None] + } + } + + pub(crate) fn line(self, layout: &Layout) -> Option<(usize, Line<'_, B>)> { + let geometry = self.geometry(layout, 0.0); + layout.line_for_offset(geometry.y0 as f32) + } + + fn upstream_cluster(self, layout: &Layout) -> Option> { + self.index + .checked_sub(1) + .and_then(|index| Cluster::from_byte_index(layout, index)) + } + + fn downstream_cluster(self, layout: &Layout) -> Option> { + Cluster::from_byte_index(layout, self.index) + } + + #[cfg(feature = "accesskit")] + pub fn to_access_position( + &self, + layout: &Layout, + layout_access: &LayoutAccessibility, + ) -> Option { + if layout.data.text_len == 0 { + // If the text is empty, just return the first node with a + // character index of 0. + return Some(TextPosition { + node: *layout_access.access_ids_by_run_path.get(&(0, 0))?, + character_index: 0, + }); + } + // Prefer the downstream cluster except at the end of the text + // where we'll choose the upstream cluster and add 1 to the + // character index. + let (offset, path) = self + .downstream_cluster(layout) + .map(|cluster| (0, cluster.path)) + .or_else(|| { + self.upstream_cluster(layout) + .map(|cluster| (1, cluster.path)) + })?; + // If we're at the end of the layout and the layout ends with a newline + // then make sure we use the "phantom" run at the end so that + // AccessKit has correct visual geometry for the cursor. + let (run_path, character_index) = if self.index == layout.data.text_len + && layout + .data + .clusters + .last() + .map(|cluster| cluster.info.whitespace() == Whitespace::Newline) + .unwrap_or_default() + { + ((path.line_index() + 1, 0), 0) + } else { + ( + (path.line_index(), path.run_index()), + path.logical_index() + offset, + ) + }; + let id = layout_access.access_ids_by_run_path.get(&run_path)?; + Some(TextPosition { + node: *id, + character_index, + }) + } +} + +fn cursor_rect(cluster: &Cluster<'_, B>, at_end: bool, size: f32) -> Rect { + let line_x = (cluster.visual_offset().unwrap_or_default() + + at_end.then(|| cluster.advance()).unwrap_or_default()) as f64; + let line = cluster.line(); + let metrics = line.metrics(); + Rect::new( + line_x, + metrics.min_coord as f64, + line_x + size as f64, + metrics.max_coord as f64, + ) +} + +fn last_line_cursor_rect(layout: &Layout, size: f32) -> Rect { + if let Some(line) = layout.get(layout.len().saturating_sub(1)) { + let metrics = line.metrics(); + Rect::new( + 0.0, + metrics.min_coord as f64, + size as f64, + metrics.max_coord as f64, + ) + } else { + Rect::default() + } +} + +fn affinity_for_dir(is_rtl: bool, moving_right: bool) -> Affinity { + match (is_rtl, moving_right) { + (true, true) | (false, false) => Affinity::Downstream, + _ => Affinity::Upstream, + } +} diff --git a/parley/src/layout/editor.rs b/parley/src/editing/editor.rs similarity index 98% rename from parley/src/layout/editor.rs rename to parley/src/editing/editor.rs index 403dc8365..7db91e577 100644 --- a/parley/src/layout/editor.rs +++ b/parley/src/editing/editor.rs @@ -3,28 +3,26 @@ //! A simple plain text editor and related types. -use crate::{ - layout::{ - cursor::{Cursor, Selection}, - Affinity, Alignment, Layout, - }, - resolve::ResolvedStyle, - style::Brush, - FontContext, LayoutContext, Rect, StyleProperty, StyleSet, -}; -use alloc::{borrow::ToOwned, string::String, vec::Vec}; -use core::{ - cmp::PartialEq, - default::Default, - fmt::{Debug, Display}, - ops::Range, -}; +use alloc::borrow::ToOwned; +use alloc::string::String; +use alloc::vec::Vec; +use core::cmp::PartialEq; +use core::default::Default; +use core::fmt::{Debug, Display}; +use core::ops::Range; -#[cfg(feature = "accesskit")] -use crate::layout::LayoutAccessibility; #[cfg(feature = "accesskit")] use accesskit::{Node, NodeId, TreeUpdate}; +use crate::algos::resolve::ResolvedStyle; +use crate::editing::{Cursor, Selection}; +use crate::inputs::{Brush, FontContext, LayoutContext, StyleProperty, StyleSet}; +use crate::outputs::{Affinity, Alignment, Layout}; +use crate::Rect; + +#[cfg(feature = "accesskit")] +use crate::outputs::LayoutAccessibility; + /// Opaque representation of a generation. /// /// Obtained from [`PlainEditor::generation`]. diff --git a/parley/src/editing/mod.rs b/parley/src/editing/mod.rs new file mode 100644 index 000000000..5b4739177 --- /dev/null +++ b/parley/src/editing/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2024 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +mod cursor; +mod editor; +mod selection; + +pub use self::cursor::*; +pub use self::editor::*; +pub use self::selection::*; diff --git a/parley/src/layout/cursor.rs b/parley/src/editing/selection.rs similarity index 52% rename from parley/src/layout/cursor.rs rename to parley/src/editing/selection.rs index a60784c27..c201e488e 100644 --- a/parley/src/layout/cursor.rs +++ b/parley/src/editing/selection.rs @@ -1,428 +1,24 @@ -// Copyright 2021 the Parley Authors +// Copyright 2024 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -//! Text selection support. - -#[cfg(feature = "accesskit")] -use super::LayoutAccessibility; -use super::{Affinity, BreakReason, Brush, Cluster, ClusterSide, Layout, Line}; -#[cfg(feature = "accesskit")] -use accesskit::TextPosition; use alloc::vec::Vec; use core::ops::Range; -use peniko::kurbo::Rect; -#[cfg(feature = "accesskit")] -use swash::text::cluster::Whitespace; - -/// Defines a position with a text layout. -#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)] -pub struct Cursor { - index: usize, - affinity: Affinity, -} - -impl Cursor { - /// Creates a new cursor from the given byte index and affinity. - pub fn from_byte_index(layout: &Layout, index: usize, affinity: Affinity) -> Self { - if let Some(cluster) = Cluster::from_byte_index(layout, index) { - let index = cluster.text_range().start; - let affinity = if cluster.is_line_break() == Some(BreakReason::Explicit) { - Affinity::Downstream - } else { - affinity - }; - Self { index, affinity } - } else { - Self { - index: layout.data.text_len, - affinity: Affinity::Upstream, - } - } - } - - /// Creates a new cursor from the given coordinates. - pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { - let (index, affinity) = if let Some((cluster, side)) = Cluster::from_point(layout, x, y) { - let is_leading = side == ClusterSide::Left; - if cluster.is_rtl() { - if is_leading { - (cluster.text_range().end, Affinity::Upstream) - } else { - (cluster.text_range().start, Affinity::Downstream) - } - } else { - // We never want to position the cursor _after_ a hard - // line since that cursor appears visually at the start - // of the next line - if is_leading || cluster.is_line_break() == Some(BreakReason::Explicit) { - (cluster.text_range().start, Affinity::Downstream) - } else { - (cluster.text_range().end, Affinity::Upstream) - } - } - } else { - (layout.data.text_len, Affinity::Downstream) - }; - Self { index, affinity } - } - - #[cfg(feature = "accesskit")] - pub fn from_access_position( - pos: &TextPosition, - layout: &Layout, - layout_access: &LayoutAccessibility, - ) -> Option { - let (line_index, run_index) = *layout_access.run_paths_by_access_id.get(&pos.node)?; - let line = layout.get(line_index)?; - let run = line.run(run_index)?; - let index = run - .get(pos.character_index) - .map(|cluster| cluster.text_range().start) - .unwrap_or(layout.data.text_len); - Some(Self::from_byte_index(layout, index, Affinity::Downstream)) - } - - fn from_cluster( - layout: &Layout, - cluster: Cluster<'_, B>, - moving_right: bool, - ) -> Self { - Self::from_byte_index( - layout, - cluster.text_range().start, - affinity_for_dir(cluster.is_rtl(), moving_right), - ) - } - - /// Returns the logical text index of the cursor. - pub fn index(&self) -> usize { - self.index - } - - /// Returns the affinity of the cursor. - /// - /// This defines the direction from which the cursor entered its current - /// position and affects the visual location of the rendered cursor. - pub fn affinity(&self) -> Affinity { - self.affinity - } - - /// Returns a new cursor that is guaranteed to be within the bounds of the - /// given layout. - #[must_use] - pub fn refresh(&self, layout: &Layout) -> Self { - Self::from_byte_index(layout, self.index, self.affinity) - } - - /// Returns a new cursor that is positioned at the previous cluster boundary - /// in visual order. - #[must_use] - pub fn previous_visual(&self, layout: &Layout) -> Self { - let [left, right] = self.visual_clusters(layout); - if let (Some(left), Some(right)) = (&left, &right) { - if left.is_soft_line_break() { - if left.is_rtl() && self.affinity == Affinity::Upstream { - let index = if right.is_rtl() { - left.text_range().start - } else { - left.text_range().end - }; - return Self::from_byte_index(layout, index, Affinity::Downstream); - } else if !left.is_rtl() && self.affinity == Affinity::Downstream { - let index = if right.is_rtl() { - right.text_range().end - } else { - right.text_range().start - }; - return Self::from_byte_index(layout, index, Affinity::Upstream); - } - } - } - if let Some(left) = left { - let index = if left.is_rtl() { - left.text_range().end - } else { - left.text_range().start - }; - return Self::from_byte_index(layout, index, affinity_for_dir(left.is_rtl(), false)); - } - *self - } - - /// Returns a new cursor that is positioned at the next cluster boundary - /// in visual order. - #[must_use] - pub fn next_visual(&self, layout: &Layout) -> Self { - let [left, right] = self.visual_clusters(layout); - if let (Some(left), Some(right)) = (&left, &right) { - if left.is_soft_line_break() { - if left.is_rtl() && self.affinity == Affinity::Downstream { - let index = if right.is_rtl() { - right.text_range().end - } else { - right.text_range().start - }; - return Self::from_byte_index(layout, index, Affinity::Upstream); - } else if !left.is_rtl() && self.affinity == Affinity::Upstream { - let index = if right.is_rtl() { - right.text_range().end - } else { - right.text_range().start - }; - return Self::from_byte_index(layout, index, Affinity::Downstream); - } - } - let index = if right.is_rtl() { - right.text_range().start - } else { - right.text_range().end - }; - return Self::from_byte_index(layout, index, affinity_for_dir(right.is_rtl(), true)); - } - if let Some(right) = right { - let index = if right.is_rtl() { - right.text_range().start - } else { - right.text_range().end - }; - return Self::from_byte_index(layout, index, affinity_for_dir(right.is_rtl(), true)); - } - *self - } - - /// Returns a new cursor that is positioned at the next word boundary - /// in visual order. - #[must_use] - pub fn next_visual_word(&self, layout: &Layout) -> Self { - let mut cur = *self; - loop { - let next = cur.next_visual(layout); - if next == cur { - break; - } - cur = next; - let [Some(left), Some(right)] = cur.visual_clusters(layout) else { - break; - }; - if left.is_rtl() { - if left.is_word_boundary() && !left.is_space_or_nbsp() { - break; - } - } else if right.is_word_boundary() && !left.is_space_or_nbsp() { - break; - } - } - cur - } - - /// Returns a new cursor that is positioned at the previous word boundary - /// in visual order. - #[must_use] - pub fn previous_visual_word(&self, layout: &Layout) -> Self { - let mut cur = *self; - loop { - let next = cur.previous_visual(layout); - if next == cur { - break; - } - cur = next; - let [Some(left), Some(right)] = cur.visual_clusters(layout) else { - break; - }; - if left.is_rtl() { - if left.is_word_boundary() - && (left.is_space_or_nbsp() - || (right.is_word_boundary() && !right.is_space_or_nbsp())) - { - break; - } - } else if right.is_word_boundary() && !right.is_space_or_nbsp() { - break; - } - } - cur - } - - /// Returns a new cursor that is positioned at the next word boundary - /// in logical order. - #[must_use] - pub fn next_logical_word(&self, layout: &Layout) -> Self { - let [left, right] = self.logical_clusters(layout); - if let Some(cluster) = right.or(left) { - let start = cluster.clone(); - let cluster = cluster.next_logical_word().unwrap_or(cluster); - if cluster.path == start.path { - return Self::from_byte_index(layout, usize::MAX, Affinity::Downstream); - } - return Self::from_cluster(layout, cluster, true); - } - *self - } - - /// Returns a new cursor that is positioned at the previous word boundary - /// in logical order. - #[must_use] - pub fn previous_logical_word(&self, layout: &Layout) -> Self { - let [left, right] = self.logical_clusters(layout); - if let Some(cluster) = left.or(right) { - let cluster = cluster.previous_logical_word().unwrap_or(cluster); - return Self::from_cluster(layout, cluster, true); - } - *self - } - - /// Returns a rectangle that represents the visual geometry of the cursor - /// in layout space. - /// - /// The `width` parameter defines the width of the resulting rectangle. - pub fn geometry(&self, layout: &Layout, width: f32) -> Rect { - match self.visual_clusters(layout) { - [Some(left), Some(right)] => { - if left.is_end_of_line() { - if left.is_soft_line_break() { - let (cluster, at_end) = if left.is_rtl() - && self.affinity == Affinity::Downstream - || !left.is_rtl() && self.affinity == Affinity::Upstream - { - (left, true) - } else { - (right, false) - }; - cursor_rect(&cluster, at_end, width) - } else { - cursor_rect(&right, false, width) - } - } else { - cursor_rect(&left, true, width) - } - } - [Some(left), None] if left.is_hard_line_break() => last_line_cursor_rect(layout, width), - [Some(left), _] => cursor_rect(&left, true, width), - [_, Some(right)] => cursor_rect(&right, false, width), - _ => last_line_cursor_rect(layout, width), - } - } - - /// Returns the pair of clusters that logically bound the cursor - /// position. - /// - /// The order in the array is upstream followed by downstream. - pub fn logical_clusters<'a, B: Brush>( - &self, - layout: &'a Layout, - ) -> [Option>; 2] { - let upstream = self - .index - .checked_sub(1) - .and_then(|index| Cluster::from_byte_index(layout, index)); - let downstream = Cluster::from_byte_index(layout, self.index); - [upstream, downstream] - } - /// Returns the pair of clusters that visually bound the cursor - /// position. - /// - /// The order in the array is left followed by right. - pub fn visual_clusters<'a, B: Brush>( - &self, - layout: &'a Layout, - ) -> [Option>; 2] { - if self.affinity == Affinity::Upstream { - if let Some(cluster) = self.upstream_cluster(layout) { - if cluster.is_rtl() { - [cluster.previous_visual(), Some(cluster)] - } else { - [Some(cluster.clone()), cluster.next_visual()] - } - } else if let Some(cluster) = self.downstream_cluster(layout) { - if cluster.is_rtl() { - [None, Some(cluster)] - } else { - [Some(cluster), None] - } - } else { - [None, None] - } - } else if let Some(cluster) = self.downstream_cluster(layout) { - if cluster.is_rtl() { - [Some(cluster.clone()), cluster.next_visual()] - } else { - [cluster.previous_visual(), Some(cluster)] - } - } else if let Some(cluster) = self.upstream_cluster(layout) { - if cluster.is_rtl() { - [None, Some(cluster)] - } else { - [Some(cluster), None] - } - } else { - [None, None] - } - } +use peniko::kurbo::Rect; - fn line(self, layout: &Layout) -> Option<(usize, Line<'_, B>)> { - let geometry = self.geometry(layout, 0.0); - layout.line_for_offset(geometry.y0 as f32) - } +use crate::editing::Cursor; +use crate::inputs::Brush; +use crate::outputs::{Affinity, BreakReason, Cluster, Layout}; - fn upstream_cluster(self, layout: &Layout) -> Option> { - self.index - .checked_sub(1) - .and_then(|index| Cluster::from_byte_index(layout, index)) - } - - fn downstream_cluster(self, layout: &Layout) -> Option> { - Cluster::from_byte_index(layout, self.index) - } +#[cfg(feature = "accesskit")] +use crate::outputs::LayoutAccessibility; - #[cfg(feature = "accesskit")] - pub fn to_access_position( - &self, - layout: &Layout, - layout_access: &LayoutAccessibility, - ) -> Option { - if layout.data.text_len == 0 { - // If the text is empty, just return the first node with a - // character index of 0. - return Some(TextPosition { - node: *layout_access.access_ids_by_run_path.get(&(0, 0))?, - character_index: 0, - }); - } - // Prefer the downstream cluster except at the end of the text - // where we'll choose the upstream cluster and add 1 to the - // character index. - let (offset, path) = self - .downstream_cluster(layout) - .map(|cluster| (0, cluster.path)) - .or_else(|| { - self.upstream_cluster(layout) - .map(|cluster| (1, cluster.path)) - })?; - // If we're at the end of the layout and the layout ends with a newline - // then make sure we use the "phantom" run at the end so that - // AccessKit has correct visual geometry for the cursor. - let (run_path, character_index) = if self.index == layout.data.text_len - && layout - .data - .clusters - .last() - .map(|cluster| cluster.info.whitespace() == Whitespace::Newline) - .unwrap_or_default() - { - ((path.line_index() + 1, 0), 0) - } else { - ( - (path.line_index(), path.run_index()), - path.logical_index() + offset, - ) - }; - let id = layout_access.access_ids_by_run_path.get(&run_path)?; - Some(TextPosition { - node: *id, - character_index, - }) - } +#[derive(Copy, Clone, Default, Debug)] +enum AnchorBase { + #[default] + Cluster, + Word(Cursor, Cursor), + Line(Cursor, Cursor), } /// Defines a range within a text layout. @@ -892,48 +488,6 @@ impl From for Selection { } } -#[derive(Copy, Clone, Default, Debug)] -enum AnchorBase { - #[default] - Cluster, - Word(Cursor, Cursor), - Line(Cursor, Cursor), -} - -fn cursor_rect(cluster: &Cluster<'_, B>, at_end: bool, size: f32) -> Rect { - let line_x = (cluster.visual_offset().unwrap_or_default() - + at_end.then(|| cluster.advance()).unwrap_or_default()) as f64; - let line = cluster.line(); - let metrics = line.metrics(); - Rect::new( - line_x, - metrics.min_coord as f64, - line_x + size as f64, - metrics.max_coord as f64, - ) -} - -fn last_line_cursor_rect(layout: &Layout, size: f32) -> Rect { - if let Some(line) = layout.get(layout.len().saturating_sub(1)) { - let metrics = line.metrics(); - Rect::new( - 0.0, - metrics.min_coord as f64, - size as f64, - metrics.max_coord as f64, - ) - } else { - Rect::default() - } -} - -fn affinity_for_dir(is_rtl: bool, moving_right: bool) -> Affinity { - match (is_rtl, moving_right) { - (true, true) | (false, false) => Affinity::Downstream, - _ => Affinity::Upstream, - } -} - /// Given four cursors, return the left-most and right-most cursors from /// the set. /// diff --git a/parley/src/layout/line/greedy.rs b/parley/src/inputs/break_lines.rs similarity index 99% rename from parley/src/layout/line/greedy.rs rename to parley/src/inputs/break_lines.rs index 6cb1dc267..3073484c1 100644 --- a/parley/src/layout/line/greedy.rs +++ b/parley/src/inputs/break_lines.rs @@ -4,20 +4,19 @@ //! Greedy line breaking. use alloc::vec::Vec; -use swash::text::cluster::Whitespace; +use core::ops::Range; #[cfg(feature = "libm")] #[allow(unused_imports)] use core_maths::CoreFloat; +use swash::text::cluster::{Boundary, Whitespace}; -use crate::layout::alignment::unjustify; -use crate::layout::{ - Alignment, Boundary, BreakReason, Layout, LayoutData, LayoutItem, LayoutItemKind, LineData, - LineItemData, LineMetrics, Run, +use crate::inputs::{Brush, LineMetrics}; +use crate::outputs::alignment::unjustify; +use crate::outputs::{ + Alignment, BreakReason, Layout, LayoutData, LayoutItem, LayoutItemKind, LineData, LineItemData, + Run, }; -use crate::style::Brush; - -use core::ops::Range; #[derive(Default)] struct LineLayout { diff --git a/parley/src/builder.rs b/parley/src/inputs/builder.rs similarity index 96% rename from parley/src/builder.rs rename to parley/src/inputs/builder.rs index abac14686..75a6f01ba 100644 --- a/parley/src/builder.rs +++ b/parley/src/inputs/builder.rs @@ -3,16 +3,15 @@ //! Context for layout. -use super::context::LayoutContext; -use super::style::{Brush, StyleProperty, TextStyle, WhiteSpaceCollapse}; -use super::FontContext; - -use super::layout::Layout; - use alloc::string::String; use core::ops::RangeBounds; +use crate::algos::shape::shape_text; use crate::inline_box::InlineBox; +use crate::inputs::{ + Brush, FontContext, LayoutContext, StyleProperty, TextStyle, WhiteSpaceCollapse, +}; +use crate::outputs::Layout; /// Builder for constructing a text layout with ranged attributes. pub struct RangedBuilder<'a, B: Brush> { @@ -165,7 +164,7 @@ fn build_into_layout( { let query = fcx.collection.query(&mut fcx.source_cache); - super::shape::shape_text( + shape_text( &lcx.rcx, query, &lcx.styles, diff --git a/parley/src/context.rs b/parley/src/inputs/context.rs similarity index 91% rename from parley/src/context.rs rename to parley/src/inputs/context.rs index 11d9753ca..01c63e03c 100644 --- a/parley/src/context.rs +++ b/parley/src/inputs/context.rs @@ -3,21 +3,17 @@ //! Context for layout. -use alloc::{vec, vec::Vec}; - -use self::tree::TreeStyleBuilder; - -use super::bidi; -use super::builder::RangedBuilder; -use super::resolve::{tree, RangedStyle, RangedStyleBuilder, ResolveContext, ResolvedStyle}; -use super::style::{Brush, TextStyle}; -use super::FontContext; +use alloc::vec; +use alloc::vec::Vec; use swash::shape::ShapeContext; use swash::text::cluster::CharInfo; -use crate::builder::TreeBuilder; +use crate::algos::bidi; +use crate::algos::resolve::tree::TreeStyleBuilder; +use crate::algos::resolve::{RangedStyle, RangedStyleBuilder, ResolveContext, ResolvedStyle}; use crate::inline_box::InlineBox; +use crate::inputs::{Brush, FontContext, RangedBuilder, TextStyle, TreeBuilder}; /// Shared scratch space used when constructing text layouts. /// diff --git a/parley/src/font.rs b/parley/src/inputs/font.rs similarity index 91% rename from parley/src/font.rs rename to parley/src/inputs/font.rs index 02aba3f2b..812a6c05d 100644 --- a/parley/src/font.rs +++ b/parley/src/inputs/font.rs @@ -1,9 +1,7 @@ // Copyright 2021 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -use fontique::Collection; - -use fontique::SourceCache; +use fontique::{Collection, SourceCache}; /// A font database/cache (wrapper around a Fontique [`Collection`] and [`SourceCache`]). /// diff --git a/parley/src/inputs/line_metrics.rs b/parley/src/inputs/line_metrics.rs new file mode 100644 index 000000000..c32dd3bc2 --- /dev/null +++ b/parley/src/inputs/line_metrics.rs @@ -0,0 +1,41 @@ +// Copyright 2024 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +/// Metrics information for a line. +#[derive(Copy, Clone, Default, Debug)] +pub struct LineMetrics { + /// Typographic ascent. + pub ascent: f32, + /// Typographic descent. + pub descent: f32, + /// Typographic leading. + pub leading: f32, + /// The absolute line height (in layout units). + /// It matches the CSS definition of line height where it is derived as a multiple of the font size. + pub line_height: f32, + /// Offset to the baseline. + pub baseline: f32, + /// Offset for alignment. + pub offset: f32, + /// Full advance of the line. + pub advance: f32, + /// Advance of trailing whitespace. + pub trailing_whitespace: f32, + /// Minimum coordinate in the direction orthogonal to line + /// direction. + /// + /// For horizontal text, this would be the top of the line. + pub min_coord: f32, + /// Maximum coordinate in the direction orthogonal to line + /// direction. + /// + /// For horizontal text, this would be the bottom of the line. + pub max_coord: f32, +} + +impl LineMetrics { + /// Returns the size of the line + pub fn size(&self) -> f32 { + self.line_height + } +} diff --git a/parley/src/inputs/mod.rs b/parley/src/inputs/mod.rs new file mode 100644 index 000000000..5bdfd6c2c --- /dev/null +++ b/parley/src/inputs/mod.rs @@ -0,0 +1,18 @@ +// Copyright 2024 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +// TODO - Remove pub(crate) + +mod break_lines; +mod builder; +mod context; +mod font; +mod line_metrics; +mod style; + +pub use break_lines::*; +pub use builder::*; +pub use context::*; +pub use font::*; +pub use line_metrics::*; +pub use style::*; diff --git a/parley/src/style/brush.rs b/parley/src/inputs/style/brush.rs similarity index 100% rename from parley/src/style/brush.rs rename to parley/src/inputs/style/brush.rs diff --git a/parley/src/style/font.rs b/parley/src/inputs/style/font.rs similarity index 96% rename from parley/src/style/font.rs rename to parley/src/inputs/style/font.rs index 90ac1180c..df4cc192f 100644 --- a/parley/src/style/font.rs +++ b/parley/src/inputs/style/font.rs @@ -1,8 +1,7 @@ // Copyright 2021 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -use alloc::borrow::Cow; -use alloc::borrow::ToOwned; +use alloc::borrow::{Cow, ToOwned}; use core::fmt; pub use fontique::{FontStyle, FontWeight, FontWidth, GenericFamily}; @@ -44,8 +43,8 @@ impl<'a> FontFamily<'a> { /// ``` /// # extern crate alloc; /// use alloc::borrow::Cow; - /// use parley::style::FontFamily::{self, *}; - /// use parley::style::GenericFamily::*; + /// use parley::inputs::FontFamily::{self, *}; + /// use parley::inputs::GenericFamily::*; /// /// assert_eq!(FontFamily::parse("Palatino Linotype"), Some(Named(Cow::Borrowed("Palatino Linotype")))); /// assert_eq!(FontFamily::parse("monospace"), Some(Generic(Monospace))); @@ -64,8 +63,8 @@ impl<'a> FontFamily<'a> { /// ``` /// # extern crate alloc; /// use alloc::borrow::Cow; - /// use parley::style::FontFamily::{self, *}; - /// use parley::style::GenericFamily::*; + /// use parley::inputs::FontFamily::{self, *}; + /// use parley::inputs::GenericFamily::*; /// /// let source = "Arial, 'Times New Roman', serif"; /// diff --git a/parley/src/style/mod.rs b/parley/src/inputs/style/mod.rs similarity index 100% rename from parley/src/style/mod.rs rename to parley/src/inputs/style/mod.rs diff --git a/parley/src/style/styleset.rs b/parley/src/inputs/style/styleset.rs similarity index 65% rename from parley/src/style/styleset.rs rename to parley/src/inputs/style/styleset.rs index f3dfd7ee5..598d09d28 100644 --- a/parley/src/style/styleset.rs +++ b/parley/src/inputs/style/styleset.rs @@ -2,28 +2,29 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT use core::mem::Discriminant; + use hashbrown::HashMap; -type StyleProperty = crate::StyleProperty<'static, Brush>; +use crate::inputs::Brush; + +type StyleProperty = crate::inputs::StyleProperty<'static, Brush>; /// A long-lived collection of [`StyleProperties`](super::StyleProperty), containing at /// most one of each property. /// -/// This is used by [`PlainEditor`](crate::editor::PlainEditor) to provide a reasonably ergonomic +/// This is used by [`PlainEditor`](crate::editing::PlainEditor) to provide a reasonably ergonomic /// mutable API for styles applied to all text managed by it. -/// This can be accessed using [`PlainEditor::edit_styles`](crate::editor::PlainEditor::edit_styles). +/// This can be accessed using [`PlainEditor::edit_styles`](crate::editing::PlainEditor::edit_styles). /// /// These styles do not have a corresponding range, and are generally unsuited for rich text. #[derive(Clone, Debug)] -pub struct StyleSet( - HashMap>, StyleProperty>, -); +pub struct StyleSet(HashMap>, StyleProperty>); -impl StyleSet { +impl StyleSet { /// Create a new collection of styles. /// /// The font size will be `font_size`, and can be overwritten at runtime by - /// [inserting](Self::insert) a new [`FontSize`](crate::StyleProperty::FontSize). + /// [inserting](Self::insert) a new [`FontSize`](crate::inputs::StyleProperty::FontSize). pub fn new(font_size: f32) -> Self { let mut this = Self(Default::default()); this.insert(StyleProperty::FontSize(font_size)); @@ -32,9 +33,9 @@ impl StyleSet { /// Add `style` to this collection, returning any overwritten value. /// - /// Note: Adding a [font stack](crate::StyleProperty::FontStack) to this collection is not + /// Note: Adding a [font stack](crate::inputs::StyleProperty::FontStack) to this collection is not /// additive, and instead overwrites any previously added font stack. - pub fn insert(&mut self, style: StyleProperty) -> Option> { + pub fn insert(&mut self, style: StyleProperty) -> Option> { let discriminant = core::mem::discriminant(&style); self.0.insert(discriminant, style) } @@ -43,9 +44,9 @@ impl StyleSet { /// /// Styles which are removed return to their default values. /// - /// Removing the [font size](crate::StyleProperty::FontSize) is not recommended, as an unspecified + /// Removing the [font size](crate::inputs::StyleProperty::FontSize) is not recommended, as an unspecified /// fallback font size will be used. - pub fn retain(&mut self, mut f: impl FnMut(&StyleProperty) -> bool) { + pub fn retain(&mut self, mut f: impl FnMut(&StyleProperty) -> bool) { self.0.retain(|_, v| f(v)); } @@ -57,12 +58,9 @@ impl StyleSet { /// the desired property and passing it to [`core::mem::discriminant`]. /// Getting this discriminant is usually possible in a `const` context. /// - /// Removing the [font size](crate::StyleProperty::FontSize) is not recommended, as an unspecified + /// Removing the [font size](crate::inputs::StyleProperty::FontSize) is not recommended, as an unspecified /// fallback font size will be used. - pub fn remove( - &mut self, - property: Discriminant>, - ) -> Option> { + pub fn remove(&mut self, property: Discriminant>) -> Option> { self.0.remove(&property) } @@ -70,7 +68,7 @@ impl StyleSet { /// /// Write access is not provided due to the invariant that keys /// are the discriminant of their corresponding value. - pub fn inner(&self) -> &HashMap>, StyleProperty> { + pub fn inner(&self) -> &HashMap>, StyleProperty> { &self.0 } } diff --git a/parley/src/layout/mod.rs b/parley/src/layout/mod.rs deleted file mode 100644 index a4fb9a174..000000000 --- a/parley/src/layout/mod.rs +++ /dev/null @@ -1,421 +0,0 @@ -// Copyright 2021 the Parley Authors -// SPDX-License-Identifier: Apache-2.0 OR MIT - -//! Layout types. - -mod alignment; -mod cluster; -mod line; -mod run; - -pub(crate) mod data; - -pub mod cursor; -pub mod editor; - -use self::alignment::align; - -use super::style::Brush; -use crate::{Font, InlineBox}; -#[cfg(feature = "accesskit")] -use accesskit::{Node, NodeId, Role, TextDirection, TreeUpdate}; -#[cfg(feature = "accesskit")] -use alloc::vec::Vec; -use core::{cmp::Ordering, ops::Range}; -use data::{ - BreakReason, ClusterData, LayoutData, LayoutItem, LayoutItemKind, LineData, LineItemData, - RunData, -}; -#[cfg(feature = "accesskit")] -use hashbrown::{HashMap, HashSet}; -use swash::text::cluster::{Boundary, ClusterInfo}; -use swash::{GlyphId, NormalizedCoord, Synthesis}; - -pub use cluster::{Affinity, ClusterPath, ClusterSide}; -pub use cursor::{Cursor, Selection}; -pub use line::greedy::BreakLines; -pub use line::{GlyphRun, LineMetrics, PositionedInlineBox, PositionedLayoutItem}; -pub use run::RunMetrics; - -/// Alignment of a layout. -#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)] -#[repr(u8)] -pub enum Alignment { - #[default] - Start, - Middle, - End, - Justified, -} - -/// Text layout. -#[derive(Clone)] -pub struct Layout { - pub(crate) data: LayoutData, -} - -impl Layout { - /// Creates an empty layout. - pub fn new() -> Self { - Self::default() - } - - /// Returns the scale factor provided when creating the layout. - pub fn scale(&self) -> f32 { - self.data.scale - } - - /// Returns the style collection for the layout. - pub fn styles(&self) -> &[Style] { - &self.data.styles - } - - /// Returns the width of the layout. - pub fn width(&self) -> f32 { - self.data.width - } - - /// Returns the width of the layout, including the width of any trailing - /// whitespace. - pub fn full_width(&self) -> f32 { - self.data.full_width - } - - /// Returns the height of the layout. - pub fn height(&self) -> f32 { - self.data.height - } - - /// Returns the number of lines in the layout. - pub fn len(&self) -> usize { - self.data.lines.len() - } - - /// Returns `true` if the layout is empty. - pub fn is_empty(&self) -> bool { - self.data.lines.is_empty() - } - - /// Returns the line at the specified index. - pub fn get(&self, index: usize) -> Option> { - Some(Line { - index: index as u32, - layout: self, - data: self.data.lines.get(index)?, - }) - } - - /// Returns true if the dominant direction of the layout is right-to-left. - pub fn is_rtl(&self) -> bool { - self.data.base_level & 1 != 0 - } - - pub fn inline_boxes(&self) -> &[InlineBox] { - &self.data.inline_boxes - } - - pub fn inline_boxes_mut(&mut self) -> &mut [InlineBox] { - &mut self.data.inline_boxes - } - - /// Returns an iterator over the lines in the layout. - pub fn lines(&self) -> impl Iterator> + '_ + Clone { - self.data - .lines - .iter() - .enumerate() - .map(move |(index, data)| Line { - index: index as u32, - layout: self, - data, - }) - } - - /// Returns line breaker to compute lines for the layout. - pub fn break_lines(&mut self) -> BreakLines<'_, B> { - BreakLines::new(self) - } - - /// Breaks all lines with the specified maximum advance. - pub fn break_all_lines(&mut self, max_advance: Option) { - self.break_lines() - .break_remaining(max_advance.unwrap_or(f32::MAX)); - } - - // Apply to alignment to layout relative to the specified container width. If container_width is not - // specified then the max line length is used. - pub fn align(&mut self, container_width: Option, alignment: Alignment) { - align(&mut self.data, container_width, alignment); - } - - /// Returns the index and `Line` object for the line containing the - /// given byte `index` in the source text. - pub(crate) fn line_for_byte_index(&self, index: usize) -> Option<(usize, Line<'_, B>)> { - let line_index = self - .data - .lines - .binary_search_by(|line| { - if index < line.text_range.start { - Ordering::Greater - } else if index >= line.text_range.end { - Ordering::Less - } else { - Ordering::Equal - } - }) - .ok()?; - Some((line_index, self.get(line_index)?)) - } - - /// Returns the index and `Line` object for the line containing the - /// given `offset`. - /// - /// The offset is specified in the direction orthogonal to line direction. - /// For horizontal text, this is a vertical or y offset. If the offset is - /// on a line boundary, it is considered to be contained by the later line. - pub(crate) fn line_for_offset(&self, offset: f32) -> Option<(usize, Line<'_, B>)> { - if offset < 0.0 { - return Some((0, self.get(0)?)); - } - let maybe_line_index = self.data.lines.binary_search_by(|line| { - if offset < line.metrics.min_coord { - Ordering::Greater - } else if offset >= line.metrics.max_coord { - Ordering::Less - } else { - Ordering::Equal - } - }); - let line_index = match maybe_line_index { - Ok(index) => index, - Err(index) => index.saturating_sub(1), - }; - Some((line_index, self.get(line_index)?)) - } -} - -impl Default for Layout { - fn default() -> Self { - Self { - data: Default::default(), - } - } -} - -/// Sequence of clusters with a single font and style. -#[derive(Copy, Clone)] -pub struct Run<'a, B: Brush> { - layout: &'a Layout, - line_index: u32, - index: u32, - data: &'a RunData, - line_data: Option<&'a LineItemData>, -} - -/// Atomic unit of text. -#[derive(Copy, Clone)] -pub struct Cluster<'a, B: Brush> { - path: ClusterPath, - run: Run<'a, B>, - data: &'a ClusterData, -} - -/// Glyph with an offset and advance. -#[derive(Copy, Clone, Default, Debug)] -pub struct Glyph { - pub id: GlyphId, - pub style_index: u16, - pub x: f32, - pub y: f32, - pub advance: f32, -} - -impl Glyph { - /// Returns the index into the layout style collection. - pub fn style_index(&self) -> usize { - self.style_index as usize - } -} - -/// Line in a text layout. -#[derive(Copy, Clone)] -pub struct Line<'a, B: Brush> { - layout: &'a Layout, - index: u32, - data: &'a LineData, -} - -#[allow(clippy::partial_pub_fields)] -/// Style properties. -#[derive(Clone, Debug)] -pub struct Style { - /// Brush for drawing glyphs. - pub brush: B, - /// Underline decoration. - pub underline: Option>, - /// Strikethrough decoration. - pub strikethrough: Option>, - /// Absolute line height in layout units (style line height * font size) - pub(crate) line_height: f32, -} - -/// Underline or strikethrough decoration. -#[derive(Clone, Debug)] -pub struct Decoration { - /// Brush used to draw the decoration. - pub brush: B, - /// Offset of the decoration from the baseline. If `None`, use the metrics - /// of the containing run. - pub offset: Option, - /// Thickness of the decoration. If `None`, use the metrics of the - /// containing run. - pub size: Option, -} - -#[cfg(feature = "accesskit")] -#[derive(Clone, Default)] -pub struct LayoutAccessibility { - // The following two fields maintain a two-way mapping between runs - // and AccessKit node IDs, where each run is identified by its line index - // and run index within that line, or a run path for short. These maps - // are maintained by `LayoutAccess::build_nodes`, which ensures that removed - // runs are removed from the maps on the next accessibility pass. - pub(crate) access_ids_by_run_path: HashMap<(usize, usize), NodeId>, - pub(crate) run_paths_by_access_id: HashMap, -} - -#[cfg(feature = "accesskit")] -impl LayoutAccessibility { - #[allow(clippy::too_many_arguments)] - pub fn build_nodes( - &mut self, - text: &str, - layout: &Layout, - update: &mut TreeUpdate, - parent_node: &mut Node, - mut next_node_id: impl FnMut() -> NodeId, - x_offset: f64, - y_offset: f64, - ) { - // Build a set of node IDs for the runs encountered in this pass. - let mut ids = HashSet::::new(); - // Reuse scratch space for storing a sorted list of runs. - let mut runs = Vec::new(); - - for (line_index, line) in layout.lines().enumerate() { - let metrics = line.metrics(); - // Defer adding each run node until we reach either the next run - // or the end of the line. That way, we can set relations between - // runs in a line and do anything special that might be required - // for the last run in a line. - let mut last_node: Option<(NodeId, Node)> = None; - - // Iterate over the runs from left to right, computing their offsets, - // then sort them into text order. - runs.clear(); - runs.reserve(line.len()); - { - let mut run_offset = metrics.offset; - for run in line.runs() { - let advance = run.advance(); - runs.push((run, run_offset)); - run_offset += advance; - } - } - runs.sort_by_key(|(r, _)| r.text_range().start); - - for (run, run_offset) in runs.drain(..) { - let run_path = (line_index, run.index()); - // If we encountered this same run path in the previous - // accessibility pass, reuse the same AccessKit ID. Otherwise, - // allocate a new one. This enables stable node IDs when merely - // updating the content of existing runs. - let id = self - .access_ids_by_run_path - .get(&run_path) - .copied() - .unwrap_or_else(|| { - let id = next_node_id(); - self.access_ids_by_run_path.insert(run_path, id); - self.run_paths_by_access_id.insert(id, run_path); - id - }); - ids.insert(id); - let mut node = Node::new(Role::TextRun); - - if let Some((last_id, mut last_node)) = last_node.take() { - last_node.set_next_on_line(id); - node.set_previous_on_line(last_id); - update.nodes.push((last_id, last_node)); - parent_node.push_child(last_id); - } - - node.set_bounds(accesskit::Rect { - x0: x_offset + run_offset as f64, - y0: y_offset + metrics.min_coord as f64, - x1: x_offset + (run_offset + run.advance()) as f64, - y1: y_offset + metrics.max_coord as f64, - }); - node.set_text_direction(if run.is_rtl() { - TextDirection::RightToLeft - } else { - TextDirection::LeftToRight - }); - - let run_text = &text[run.text_range()]; - node.set_value(run_text); - - let mut character_lengths = Vec::new(); - let mut cluster_offset = 0.0; - let mut character_positions = Vec::new(); - let mut character_widths = Vec::new(); - let mut word_lengths = Vec::new(); - let mut last_word_start = 0; - - for cluster in run.clusters() { - let cluster_text = &text[cluster.text_range()]; - if cluster.is_word_boundary() - && !cluster.is_space_or_nbsp() - && !character_lengths.is_empty() - { - word_lengths.push((character_lengths.len() - last_word_start) as _); - last_word_start = character_lengths.len(); - } - character_lengths.push(cluster_text.len() as _); - character_positions.push(cluster_offset); - character_widths.push(cluster.advance()); - cluster_offset += cluster.advance(); - } - - word_lengths.push((character_lengths.len() - last_word_start) as _); - node.set_character_lengths(character_lengths); - node.set_character_positions(character_positions); - node.set_character_widths(character_widths); - node.set_word_lengths(word_lengths); - - last_node = Some((id, node)); - } - - if let Some((id, node)) = last_node { - update.nodes.push((id, node)); - parent_node.push_child(id); - } - } - - // Remove mappings for runs that no longer exist. - let mut ids_to_remove = Vec::::new(); - let mut run_paths_to_remove = Vec::<(usize, usize)>::new(); - for (access_id, run_path) in self.run_paths_by_access_id.iter() { - if !ids.contains(access_id) { - ids_to_remove.push(*access_id); - run_paths_to_remove.push(*run_path); - } - } - for id in ids_to_remove { - self.run_paths_by_access_id.remove(&id); - } - for run_path in run_paths_to_remove { - self.access_ids_by_run_path.remove(&run_path); - } - } -} diff --git a/parley/src/lib.rs b/parley/src/lib.rs index 0ba0d9721..f38f4e9c9 100644 --- a/parley/src/lib.rs +++ b/parley/src/lib.rs @@ -4,15 +4,15 @@ //! Parley is a library for rich text layout. //! //! Some key types are: -//! - [`FontContext`] and [`LayoutContext`] are resources which should be shared globally (or at coarse-grained boundaries). -//! - [`FontContext`] is database of fonts. -//! - [`LayoutContext`] is scratch space that allows for reuse of allocations between layouts. -//! - [`RangedBuilder`] and [`TreeBuilder`] which are builders for creating a [`Layout`]. -//! - [`RangedBuilder`] allows styles to be specified as a flat `Vec` of spans -//! - [`TreeBuilder`] allows styles to be specified as a tree of spans +//! - [`FontContext`](crate::inputs::FontContext) and [`LayoutContext`](crate::inputs::LayoutContext) are resources which should be shared globally (or at coarse-grained boundaries). +//! - [`FontContext`](crate::inputs::FontContext) is database of fonts. +//! - [`LayoutContext`](crate::inputs::LayoutContext) is scratch space that allows for reuse of allocations between layouts. +//! - [`RangedBuilder`](crate::inputs::RangedBuilder) and [`TreeBuilder`](crate::inputs::TreeBuilder) which are builders for creating a [`Layout`](crate::outputs::Layout). +//! - [`RangedBuilder`](crate::inputs::RangedBuilder) allows styles to be specified as a flat `Vec` of spans +//! - [`TreeBuilder`](crate::inputs::TreeBuilder) allows styles to be specified as a tree of spans //! -//! They are constructed using the [`ranged_builder`](LayoutContext::ranged_builder) and [`tree_builder`](LayoutContext::ranged_builder) methods on [`LayoutContext`]. -//! - [`Layout`] which represents styled paragraph(s) of text and can perform shaping, line-breaking, bidi-reordering, and alignment of that text. +//! They are constructed using the [`ranged_builder`](crate::inputs::LayoutContext::ranged_builder) and [`tree_builder`](crate::inputs::LayoutContext::tree_builder) methods on [`LayoutContext`](crate::inputs::LayoutContext). +//! - [`Layout`](crate::outputs::Layout) which represents styled paragraph(s) of text and can perform shaping, line-breaking, bidi-reordering, and alignment of that text. //! //! `Layout` supports re-linebreaking and re-aligning many times (in case the width at which wrapping should occur changes). But if the text content or //! the styles applied to that content change then a new `Layout` must be created using a new `RangedBuilder` or `TreeBuilder`. @@ -22,10 +22,9 @@ //! See the [examples](https://github.com/linebender/parley/tree/main/examples) directory for more complete usage examples that include rendering. //! //! ```rust -//! use parley::{ -//! Alignment, FontContext, FontWeight, InlineBox, Layout, LayoutContext, PositionedLayoutItem, -//! StyleProperty, -//! }; +//! use parley::InlineBox; +//! use parley::inputs::{FontContext, FontWeight, LayoutContext, StyleProperty}; +//! use parley::outputs::{Alignment, Layout, PositionedLayoutItem}; //! //! // Create a FontContext (font database) and LayoutContext (scratch space). //! // These are both intended to be constructed rarely (perhaps even once per app): @@ -85,7 +84,6 @@ #![expect( missing_debug_implementations, single_use_lifetimes, - unnameable_types, clippy::allow_attributes, clippy::allow_attributes_without_reason, clippy::cast_possible_truncation, @@ -104,33 +102,19 @@ extern crate alloc; pub use fontique; pub use swash; -mod bidi; -mod builder; -mod context; -mod font; +pub(crate) mod algos; + +pub mod editing; +pub mod inputs; +pub mod outputs; + mod inline_box; -mod resolve; -mod shape; -mod swash_convert; mod util; -pub mod layout; -pub mod style; - #[cfg(test)] mod tests; pub use peniko::kurbo::Rect; pub use peniko::Font; -pub use builder::{RangedBuilder, TreeBuilder}; -pub use context::LayoutContext; -pub use font::FontContext; pub use inline_box::InlineBox; -#[doc(inline)] -pub use layout::Layout; - -pub use layout::editor::{PlainEditor, PlainEditorDriver}; - -pub use layout::*; -pub use style::*; diff --git a/parley/src/outputs/accessibility.rs b/parley/src/outputs/accessibility.rs new file mode 100644 index 000000000..d87c1a09d --- /dev/null +++ b/parley/src/outputs/accessibility.rs @@ -0,0 +1,156 @@ +// Copyright 2024 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use alloc::vec::Vec; + +use accesskit::{Node, NodeId, Role, TextDirection, TreeUpdate}; +use hashbrown::{HashMap, HashSet}; + +use crate::inputs::Brush; +use crate::outputs::Layout; + +#[derive(Clone, Default)] +pub struct LayoutAccessibility { + // The following two fields maintain a two-way mapping between runs + // and AccessKit node IDs, where each run is identified by its line index + // and run index within that line, or a run path for short. These maps + // are maintained by `LayoutAccess::build_nodes`, which ensures that removed + // runs are removed from the maps on the next accessibility pass. + pub(crate) access_ids_by_run_path: HashMap<(usize, usize), NodeId>, + pub(crate) run_paths_by_access_id: HashMap, +} + +impl LayoutAccessibility { + #[allow(clippy::too_many_arguments)] + pub fn build_nodes( + &mut self, + text: &str, + layout: &Layout, + update: &mut TreeUpdate, + parent_node: &mut Node, + mut next_node_id: impl FnMut() -> NodeId, + x_offset: f64, + y_offset: f64, + ) { + // Build a set of node IDs for the runs encountered in this pass. + let mut ids = HashSet::::new(); + // Reuse scratch space for storing a sorted list of runs. + let mut runs = Vec::new(); + + for (line_index, line) in layout.lines().enumerate() { + let metrics = line.metrics(); + // Defer adding each run node until we reach either the next run + // or the end of the line. That way, we can set relations between + // runs in a line and do anything special that might be required + // for the last run in a line. + let mut last_node: Option<(NodeId, Node)> = None; + + // Iterate over the runs from left to right, computing their offsets, + // then sort them into text order. + runs.clear(); + runs.reserve(line.len()); + { + let mut run_offset = metrics.offset; + for run in line.runs() { + let advance = run.advance(); + runs.push((run, run_offset)); + run_offset += advance; + } + } + runs.sort_by_key(|(r, _)| r.text_range().start); + + for (run, run_offset) in runs.drain(..) { + let run_path = (line_index, run.index()); + // If we encountered this same run path in the previous + // accessibility pass, reuse the same AccessKit ID. Otherwise, + // allocate a new one. This enables stable node IDs when merely + // updating the content of existing runs. + let id = self + .access_ids_by_run_path + .get(&run_path) + .copied() + .unwrap_or_else(|| { + let id = next_node_id(); + self.access_ids_by_run_path.insert(run_path, id); + self.run_paths_by_access_id.insert(id, run_path); + id + }); + ids.insert(id); + let mut node = Node::new(Role::TextRun); + + if let Some((last_id, mut last_node)) = last_node.take() { + last_node.set_next_on_line(id); + node.set_previous_on_line(last_id); + update.nodes.push((last_id, last_node)); + parent_node.push_child(last_id); + } + + node.set_bounds(accesskit::Rect { + x0: x_offset + run_offset as f64, + y0: y_offset + metrics.min_coord as f64, + x1: x_offset + (run_offset + run.advance()) as f64, + y1: y_offset + metrics.max_coord as f64, + }); + node.set_text_direction(if run.is_rtl() { + TextDirection::RightToLeft + } else { + TextDirection::LeftToRight + }); + + let run_text = &text[run.text_range()]; + node.set_value(run_text); + + let mut character_lengths = Vec::new(); + let mut cluster_offset = 0.0; + let mut character_positions = Vec::new(); + let mut character_widths = Vec::new(); + let mut word_lengths = Vec::new(); + let mut last_word_start = 0; + + for cluster in run.clusters() { + let cluster_text = &text[cluster.text_range()]; + if cluster.is_word_boundary() + && !cluster.is_space_or_nbsp() + && !character_lengths.is_empty() + { + word_lengths.push((character_lengths.len() - last_word_start) as _); + last_word_start = character_lengths.len(); + } + character_lengths.push(cluster_text.len() as _); + character_positions.push(cluster_offset); + character_widths.push(cluster.advance()); + cluster_offset += cluster.advance(); + } + + word_lengths.push((character_lengths.len() - last_word_start) as _); + node.set_character_lengths(character_lengths); + node.set_character_positions(character_positions); + node.set_character_widths(character_widths); + node.set_word_lengths(word_lengths); + + last_node = Some((id, node)); + } + + if let Some((id, node)) = last_node { + update.nodes.push((id, node)); + parent_node.push_child(id); + } + } + + // Remove mappings for runs that no longer exist. + let mut ids_to_remove = Vec::::new(); + let mut run_paths_to_remove = Vec::<(usize, usize)>::new(); + for (access_id, run_path) in self.run_paths_by_access_id.iter() { + if !ids.contains(access_id) { + ids_to_remove.push(*access_id); + run_paths_to_remove.push(*run_path); + } + } + for id in ids_to_remove { + self.run_paths_by_access_id.remove(&id); + } + for run_path in run_paths_to_remove { + self.access_ids_by_run_path.remove(&run_path); + } + } +} diff --git a/parley/src/layout/alignment.rs b/parley/src/outputs/alignment.rs similarity index 95% rename from parley/src/layout/alignment.rs rename to parley/src/outputs/alignment.rs index cc45cc6fc..97ff2bed3 100644 --- a/parley/src/layout/alignment.rs +++ b/parley/src/outputs/alignment.rs @@ -1,8 +1,19 @@ // Copyright 2024 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -use super::{Alignment, BreakReason, LayoutData}; -use crate::style::Brush; +use crate::inputs::Brush; +use crate::outputs::{BreakReason, LayoutData}; + +/// Alignment of a layout. +#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)] +#[repr(u8)] +pub enum Alignment { + #[default] + Start, + Middle, + End, + Justified, +} pub(crate) fn align( layout: &mut LayoutData, diff --git a/parley/src/layout/cluster.rs b/parley/src/outputs/cluster.rs similarity index 91% rename from parley/src/layout/cluster.rs rename to parley/src/outputs/cluster.rs index b500164cd..b204dc228 100644 --- a/parley/src/layout/cluster.rs +++ b/parley/src/outputs/cluster.rs @@ -1,8 +1,57 @@ // Copyright 2021 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -use super::{BreakReason, Brush, Cluster, ClusterInfo, Glyph, Layout, Line, Range, Run}; -use swash::text::cluster::Whitespace; +use core::ops::Range; + +use swash::text::cluster::{ClusterInfo, Whitespace}; + +use crate::outputs::{BreakReason, Brush, Glyph, Layout, Line, Run, RunData}; + +/// Atomic unit of text. +#[derive(Copy, Clone)] +pub struct Cluster<'a, B: Brush> { + pub(crate) path: ClusterPath, + pub(crate) run: Run<'a, B>, + pub(crate) data: &'a ClusterData, +} + +#[derive(Copy, Clone)] +pub(crate) struct ClusterData { + pub(crate) info: ClusterInfo, + pub(crate) flags: u16, + pub(crate) style_index: u16, + pub(crate) glyph_len: u8, + pub(crate) text_len: u8, + /// If `glyph_len == 0xFF`, then `glyph_offset` is a glyph identifier, + /// otherwise, it's an offset into the glyph array with the base + /// taken from the owning run. + pub(crate) glyph_offset: u16, + pub(crate) text_offset: u16, + pub(crate) advance: f32, +} + +impl ClusterData { + pub(crate) const LIGATURE_START: u16 = 1; + pub(crate) const LIGATURE_COMPONENT: u16 = 2; + pub(crate) const DIVERGENT_STYLES: u16 = 4; + + pub(crate) fn is_ligature_start(self) -> bool { + self.flags & Self::LIGATURE_START != 0 + } + + pub(crate) fn is_ligature_component(self) -> bool { + self.flags & Self::LIGATURE_COMPONENT != 0 + } + + pub(crate) fn has_divergent_styles(self) -> bool { + self.flags & Self::DIVERGENT_STYLES != 0 + } + + pub(crate) fn text_range(self, run: &RunData) -> Range { + let start = run.text_range.start + self.text_offset as usize; + start..start + self.text_len as usize + } +} /// Defines the visual side of the cluster for hit testing. /// @@ -466,9 +515,8 @@ impl Iterator for GlyphIter<'_> { #[cfg(test)] mod tests { - use crate::{ - Alignment, Cluster, FontContext, Layout, LayoutContext, PositionedLayoutItem, StyleProperty, - }; + use crate::inputs::{FontContext, LayoutContext, StyleProperty}; + use crate::outputs::{Alignment, Cluster, Layout, PositionedLayoutItem}; type Brush = (); diff --git a/parley/src/layout/data.rs b/parley/src/outputs/layout.rs similarity index 64% rename from parley/src/layout/data.rs rename to parley/src/outputs/layout.rs index 42e80dcfe..9dad32c67 100644 --- a/parley/src/layout/data.rs +++ b/parley/src/outputs/layout.rs @@ -1,190 +1,26 @@ -// Copyright 2021 the Parley Authors +// Copyright 2024 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -use crate::inline_box::InlineBox; -use crate::layout::{Alignment, Glyph, LineMetrics, RunMetrics, Style}; -use crate::style::Brush; -use crate::util::nearly_zero; -use crate::Font; -use core::ops::Range; -use swash::shape::Shaper; -use swash::text::cluster::{Boundary, ClusterInfo}; -use swash::Synthesis; - use alloc::vec::Vec; +use core::cmp::Ordering; -#[derive(Copy, Clone)] -pub(crate) struct ClusterData { - pub(crate) info: ClusterInfo, - pub(crate) flags: u16, - pub(crate) style_index: u16, - pub(crate) glyph_len: u8, - pub(crate) text_len: u8, - /// If `glyph_len == 0xFF`, then `glyph_offset` is a glyph identifier, - /// otherwise, it's an offset into the glyph array with the base - /// taken from the owning run. - pub(crate) glyph_offset: u16, - pub(crate) text_offset: u16, - pub(crate) advance: f32, -} - -impl ClusterData { - pub(crate) const LIGATURE_START: u16 = 1; - pub(crate) const LIGATURE_COMPONENT: u16 = 2; - pub(crate) const DIVERGENT_STYLES: u16 = 4; - - pub(crate) fn is_ligature_start(self) -> bool { - self.flags & Self::LIGATURE_START != 0 - } - - pub(crate) fn is_ligature_component(self) -> bool { - self.flags & Self::LIGATURE_COMPONENT != 0 - } - - pub(crate) fn has_divergent_styles(self) -> bool { - self.flags & Self::DIVERGENT_STYLES != 0 - } +use swash::shape::Shaper; +use swash::text::cluster::Boundary; +use swash::Synthesis; - pub(crate) fn text_range(self, run: &RunData) -> Range { - let start = run.text_range.start + self.text_offset as usize; - start..start + self.text_len as usize - } -} +use crate::inputs::{BreakLines, Brush}; +use crate::outputs::run::RunMetrics; +use crate::outputs::{ + align, Alignment, ClusterData, Glyph, LayoutItem, LayoutItemKind, Line, LineData, LineItemData, + RunData, Style, +}; +use crate::util::nearly_zero; +use crate::{Font, InlineBox}; +/// Text layout. #[derive(Clone)] -pub(crate) struct RunData { - /// Index of the font for the run. - pub(crate) font_index: usize, - /// Font size. - pub(crate) font_size: f32, - /// Synthesis information for the font. - pub(crate) synthesis: Synthesis, - /// Range of normalized coordinates in the layout data. - pub(crate) coords_range: Range, - /// Range of the source text. - pub(crate) text_range: Range, - /// Bidi level for the run. - pub(crate) bidi_level: u8, - /// True if the run ends with a newline. - pub(crate) ends_with_newline: bool, - /// Range of clusters. - pub(crate) cluster_range: Range, - /// Base for glyph indices. - pub(crate) glyph_start: usize, - /// Metrics for the run. - pub(crate) metrics: RunMetrics, - /// Additional word spacing. - pub(crate) word_spacing: f32, - /// Additional letter spacing. - pub(crate) letter_spacing: f32, - /// Total advance of the run. - pub(crate) advance: f32, -} - -#[derive(Copy, Clone, Default, PartialEq, Debug)] -pub enum BreakReason { - #[default] - None, - Regular, - Explicit, - Emergency, -} - -#[derive(Clone, Default)] -pub(crate) struct LineData { - /// Range of the source text. - pub(crate) text_range: Range, - /// Range of line items. - pub(crate) item_range: Range, - /// Metrics for the line. - pub(crate) metrics: LineMetrics, - /// The cause of the line break. - pub(crate) break_reason: BreakReason, - /// Alignment. - pub(crate) alignment: Alignment, - /// Maximum advance for the line. - pub(crate) max_advance: f32, - /// Number of justified clusters on the line. - pub(crate) num_spaces: usize, -} - -impl LineData { - pub(crate) fn size(&self) -> f32 { - self.metrics.ascent + self.metrics.descent + self.metrics.leading - } -} - -#[derive(Debug, Clone)] -pub(crate) struct LineItemData { - /// Whether the item is a run or an inline box - pub(crate) kind: LayoutItemKind, - /// The index of the run or inline box in the runs or `inline_boxes` vec - pub(crate) index: usize, - /// Bidi level for the item (used for reordering) - pub(crate) bidi_level: u8, - /// Advance (size in direction of text flow) for the run. - pub(crate) advance: f32, - - // Fields that only apply to text runs (Ignored for boxes) - // TODO: factor this out? - /// True if the run is composed entirely of whitespace. - pub(crate) is_whitespace: bool, - /// True if the run ends in whitespace. - pub(crate) has_trailing_whitespace: bool, - /// Range of the source text. - pub(crate) text_range: Range, - /// Range of clusters. - pub(crate) cluster_range: Range, -} - -impl LineItemData { - pub(crate) fn is_text_run(&self) -> bool { - self.kind == LayoutItemKind::TextRun - } - - pub(crate) fn compute_line_height(&self, layout: &LayoutData) -> f32 { - match self.kind { - LayoutItemKind::TextRun => { - let mut line_height = 0_f32; - let run = &layout.runs[self.index]; - let glyph_start = run.glyph_start; - for cluster in &layout.clusters[run.cluster_range.clone()] { - if cluster.glyph_len != 0xFF && cluster.has_divergent_styles() { - let start = glyph_start + cluster.glyph_offset as usize; - let end = start + cluster.glyph_len as usize; - for glyph in &layout.glyphs[start..end] { - line_height = - line_height.max(layout.styles[glyph.style_index()].line_height); - } - } else { - line_height = line_height - .max(layout.styles[cluster.style_index as usize].line_height); - } - } - line_height - } - LayoutItemKind::InlineBox => { - // TODO: account for vertical alignment (e.g. baseline alignment) - layout.inline_boxes[self.index].height - } - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum LayoutItemKind { - TextRun, - InlineBox, -} - -#[derive(Debug, Clone)] -pub(crate) struct LayoutItem { - /// Whether the item is a run or an inline box - pub(crate) kind: LayoutItemKind, - /// The index of the run or inline box in the runs or `inline_boxes` vec - pub(crate) index: usize, - /// Bidi level for the item (used for reordering) - pub(crate) bidi_level: u8, +pub struct Layout { + pub(crate) data: LayoutData, } #[derive(Clone)] @@ -471,3 +307,151 @@ impl LayoutData { } } } + +impl Layout { + /// Creates an empty layout. + pub fn new() -> Self { + Self::default() + } + + /// Returns the scale factor provided when creating the layout. + pub fn scale(&self) -> f32 { + self.data.scale + } + + /// Returns the style collection for the layout. + pub fn styles(&self) -> &[Style] { + &self.data.styles + } + + /// Returns the width of the layout. + pub fn width(&self) -> f32 { + self.data.width + } + + /// Returns the width of the layout, including the width of any trailing + /// whitespace. + pub fn full_width(&self) -> f32 { + self.data.full_width + } + + /// Returns the height of the layout. + pub fn height(&self) -> f32 { + self.data.height + } + + /// Returns the number of lines in the layout. + pub fn len(&self) -> usize { + self.data.lines.len() + } + + /// Returns `true` if the layout is empty. + pub fn is_empty(&self) -> bool { + self.data.lines.is_empty() + } + + /// Returns the line at the specified index. + pub fn get(&self, index: usize) -> Option> { + Some(Line { + index: index as u32, + layout: self, + data: self.data.lines.get(index)?, + }) + } + + /// Returns true if the dominant direction of the layout is right-to-left. + pub fn is_rtl(&self) -> bool { + self.data.base_level & 1 != 0 + } + + pub fn inline_boxes(&self) -> &[InlineBox] { + &self.data.inline_boxes + } + + pub fn inline_boxes_mut(&mut self) -> &mut [InlineBox] { + &mut self.data.inline_boxes + } + + /// Returns an iterator over the lines in the layout. + pub fn lines(&self) -> impl Iterator> + '_ + Clone { + self.data + .lines + .iter() + .enumerate() + .map(move |(index, data)| Line { + index: index as u32, + layout: self, + data, + }) + } + + /// Returns line breaker to compute lines for the layout. + pub fn break_lines(&mut self) -> BreakLines<'_, B> { + BreakLines::new(self) + } + + /// Breaks all lines with the specified maximum advance. + pub fn break_all_lines(&mut self, max_advance: Option) { + self.break_lines() + .break_remaining(max_advance.unwrap_or(f32::MAX)); + } + + // Apply to alignment to layout relative to the specified container width. If container_width is not + // specified then the max line length is used. + pub fn align(&mut self, container_width: Option, alignment: Alignment) { + align(&mut self.data, container_width, alignment); + } + + /// Returns the index and `Line` object for the line containing the + /// given byte `index` in the source text. + pub(crate) fn line_for_byte_index(&self, index: usize) -> Option<(usize, Line<'_, B>)> { + let line_index = self + .data + .lines + .binary_search_by(|line| { + if index < line.text_range.start { + Ordering::Greater + } else if index >= line.text_range.end { + Ordering::Less + } else { + Ordering::Equal + } + }) + .ok()?; + Some((line_index, self.get(line_index)?)) + } + + /// Returns the index and `Line` object for the line containing the + /// given `offset`. + /// + /// The offset is specified in the direction orthogonal to line direction. + /// For horizontal text, this is a vertical or y offset. If the offset is + /// on a line boundary, it is considered to be contained by the later line. + pub(crate) fn line_for_offset(&self, offset: f32) -> Option<(usize, Line<'_, B>)> { + if offset < 0.0 { + return Some((0, self.get(0)?)); + } + let maybe_line_index = self.data.lines.binary_search_by(|line| { + if offset < line.metrics.min_coord { + Ordering::Greater + } else if offset >= line.metrics.max_coord { + Ordering::Less + } else { + Ordering::Equal + } + }); + let line_index = match maybe_line_index { + Ok(index) => index, + Err(index) => index.saturating_sub(1), + }; + Some((line_index, self.get(line_index)?)) + } +} + +impl Default for Layout { + fn default() -> Self { + Self { + data: Default::default(), + } + } +} diff --git a/parley/src/layout/line/mod.rs b/parley/src/outputs/line.rs similarity index 66% rename from parley/src/layout/line/mod.rs rename to parley/src/outputs/line.rs index 05ba6a8f5..3f0b95149 100644 --- a/parley/src/layout/line/mod.rs +++ b/parley/src/outputs/line.rs @@ -1,9 +1,18 @@ // Copyright 2021 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -use super::{BreakReason, Brush, Glyph, LayoutItemKind, Line, LineItemData, Range, Run, Style}; +use core::ops::Range; -pub(crate) mod greedy; +use crate::inputs::LineMetrics; +use crate::outputs::{Alignment, BreakReason, Brush, Glyph, Layout, LayoutData, Run, Style}; + +/// Line in a text layout. +#[derive(Copy, Clone)] +pub struct Line<'a, B: Brush> { + pub(crate) layout: &'a Layout, + pub(crate) index: u32, + pub(crate) data: &'a LineData, +} impl<'a, B: Brush> Line<'a, B> { /// Returns the metrics for the line. @@ -91,45 +100,103 @@ impl<'a, B: Brush> Line<'a, B> { } } -/// Metrics information for a line. -#[derive(Copy, Clone, Default, Debug)] -pub struct LineMetrics { - /// Typographic ascent. - pub ascent: f32, - /// Typographic descent. - pub descent: f32, - /// Typographic leading. - pub leading: f32, - /// The absolute line height (in layout units). - /// It matches the CSS definition of line height where it is derived as a multiple of the font size. - pub line_height: f32, - /// Offset to the baseline. - pub baseline: f32, - /// Offset for alignment. - pub offset: f32, - /// Full advance of the line. - pub advance: f32, - /// Advance of trailing whitespace. - pub trailing_whitespace: f32, - /// Minimum coordinate in the direction orthogonal to line - /// direction. - /// - /// For horizontal text, this would be the top of the line. - pub min_coord: f32, - /// Maximum coordinate in the direction orthogonal to line - /// direction. - /// - /// For horizontal text, this would be the bottom of the line. - pub max_coord: f32, +#[derive(Clone, Default)] +pub(crate) struct LineData { + /// Range of the source text. + pub(crate) text_range: Range, + /// Range of line items. + pub(crate) item_range: Range, + /// Metrics for the line. + pub(crate) metrics: LineMetrics, + /// The cause of the line break. + pub(crate) break_reason: BreakReason, + /// Alignment. + pub(crate) alignment: Alignment, + /// Maximum advance for the line. + pub(crate) max_advance: f32, + /// Number of justified clusters on the line. + pub(crate) num_spaces: usize, +} + +impl LineData { + pub(crate) fn size(&self) -> f32 { + self.metrics.ascent + self.metrics.descent + self.metrics.leading + } } -impl LineMetrics { - /// Returns the size of the line - pub fn size(&self) -> f32 { - self.line_height +#[derive(Debug, Clone)] +pub(crate) struct LineItemData { + /// Whether the item is a run or an inline box + pub(crate) kind: LayoutItemKind, + /// The index of the run or inline box in the runs or `inline_boxes` vec + pub(crate) index: usize, + /// Bidi level for the item (used for reordering) + pub(crate) bidi_level: u8, + /// Advance (size in direction of text flow) for the run. + pub(crate) advance: f32, + + // Fields that only apply to text runs (Ignored for boxes) + // TODO: factor this out? + /// True if the run is composed entirely of whitespace. + pub(crate) is_whitespace: bool, + /// True if the run ends in whitespace. + pub(crate) has_trailing_whitespace: bool, + /// Range of the source text. + pub(crate) text_range: Range, + /// Range of clusters. + pub(crate) cluster_range: Range, +} + +impl LineItemData { + pub(crate) fn is_text_run(&self) -> bool { + self.kind == LayoutItemKind::TextRun + } + + pub(crate) fn compute_line_height(&self, layout: &LayoutData) -> f32 { + match self.kind { + LayoutItemKind::TextRun => { + let mut line_height = 0_f32; + let run = &layout.runs[self.index]; + let glyph_start = run.glyph_start; + for cluster in &layout.clusters[run.cluster_range.clone()] { + if cluster.glyph_len != 0xFF && cluster.has_divergent_styles() { + let start = glyph_start + cluster.glyph_offset as usize; + let end = start + cluster.glyph_len as usize; + for glyph in &layout.glyphs[start..end] { + line_height = + line_height.max(layout.styles[glyph.style_index()].line_height); + } + } else { + line_height = line_height + .max(layout.styles[cluster.style_index as usize].line_height); + } + } + line_height + } + LayoutItemKind::InlineBox => { + // TODO: account for vertical alignment (e.g. baseline alignment) + layout.inline_boxes[self.index].height + } + } } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum LayoutItemKind { + TextRun, + InlineBox, +} + +#[derive(Debug, Clone)] +pub(crate) struct LayoutItem { + /// Whether the item is a run or an inline box + pub(crate) kind: LayoutItemKind, + /// The index of the run or inline box in the runs or `inline_boxes` vec + pub(crate) index: usize, + /// Bidi level for the item (used for reordering) + pub(crate) bidi_level: u8, +} + /// The computed result of an item (glyph run or inline box) within a layout #[derive(Clone)] pub enum PositionedLayoutItem<'a, B: Brush> { diff --git a/parley/src/outputs/mod.rs b/parley/src/outputs/mod.rs new file mode 100644 index 000000000..78b9655c8 --- /dev/null +++ b/parley/src/outputs/mod.rs @@ -0,0 +1,84 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Layout types. + +pub(crate) mod alignment; +pub(crate) mod cluster; +pub(crate) mod layout; +pub(crate) mod line; +pub(crate) mod run; + +#[cfg(feature = "accesskit")] +mod accessibility; + +#[cfg(feature = "accesskit")] +pub use accessibility::LayoutAccessibility; + +pub use self::alignment::Alignment; +pub use self::cluster::{Affinity, Cluster, ClusterPath, ClusterSide}; +pub use self::layout::Layout; +pub use self::line::{GlyphRun, Line, PositionedInlineBox, PositionedLayoutItem}; +pub use self::run::{Run, RunMetrics}; + +pub(crate) use self::alignment::align; +pub(crate) use self::cluster::ClusterData; +pub(crate) use self::layout::LayoutData; +pub(crate) use self::line::{LayoutItem, LayoutItemKind, LineData, LineItemData}; +pub(crate) use self::run::RunData; + +use crate::inputs::Brush; +use swash::GlyphId; + +#[derive(Copy, Clone, Default, PartialEq, Debug)] +pub enum BreakReason { + #[default] + None, + Regular, + Explicit, + Emergency, +} + +/// Glyph with an offset and advance. +#[derive(Copy, Clone, Default, Debug)] +pub struct Glyph { + pub id: GlyphId, + pub style_index: u16, + pub x: f32, + pub y: f32, + pub advance: f32, +} + +impl Glyph { + /// Returns the index into the layout style collection. + pub fn style_index(&self) -> usize { + self.style_index as usize + } +} + +#[allow(clippy::partial_pub_fields)] +/// Style properties. +#[derive(Clone, Debug)] +pub struct Style { + /// Brush for drawing glyphs. + pub brush: B, + /// Underline decoration. + pub underline: Option>, + /// Strikethrough decoration. + pub strikethrough: Option>, + /// Absolute line height in layout units (style line height * font size) + pub(crate) line_height: f32, +} + +/// Underline or strikethrough decoration. +#[derive(Clone, Debug)] +pub struct Decoration { + /// Brush used to draw the decoration. + pub brush: B, + /// Offset of the decoration from the baseline. If `None`, use the metrics + /// of the containing run. + pub offset: Option, + /// Thickness of the decoration. If `None`, use the metrics of the + /// containing run. + pub size: Option, +} diff --git a/parley/src/layout/run.rs b/parley/src/outputs/run.rs similarity index 80% rename from parley/src/layout/run.rs rename to parley/src/outputs/run.rs index 601f66340..149647e17 100644 --- a/parley/src/layout/run.rs +++ b/parley/src/outputs/run.rs @@ -1,10 +1,52 @@ // Copyright 2021 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -use super::{ - Brush, Cluster, ClusterPath, Font, Layout, LineItemData, NormalizedCoord, Range, Run, RunData, - Synthesis, -}; +use core::ops::Range; + +use peniko::Font; +use swash::{NormalizedCoord, Synthesis}; + +use crate::outputs::{Brush, Cluster, ClusterPath, Layout, LineItemData}; + +/// Sequence of clusters with a single font and style. +#[derive(Copy, Clone)] +pub struct Run<'a, B: Brush> { + pub(crate) layout: &'a Layout, + pub(crate) line_index: u32, + pub(crate) index: u32, + pub(crate) data: &'a RunData, + pub(crate) line_data: Option<&'a LineItemData>, +} + +#[derive(Clone)] +pub(crate) struct RunData { + /// Index of the font for the run. + pub(crate) font_index: usize, + /// Font size. + pub(crate) font_size: f32, + /// Synthesis information for the font. + pub(crate) synthesis: Synthesis, + /// Range of normalized coordinates in the layout data. + pub(crate) coords_range: Range, + /// Range of the source text. + pub(crate) text_range: Range, + /// Bidi level for the run. + pub(crate) bidi_level: u8, + /// True if the run ends with a newline. + pub(crate) ends_with_newline: bool, + /// Range of clusters. + pub(crate) cluster_range: Range, + /// Base for glyph indices. + pub(crate) glyph_start: usize, + /// Metrics for the run. + pub(crate) metrics: RunMetrics, + /// Additional word spacing. + pub(crate) word_spacing: f32, + /// Additional letter spacing. + pub(crate) letter_spacing: f32, + /// Total advance of the run. + pub(crate) advance: f32, +} impl<'a, B: Brush> Run<'a, B> { pub(crate) fn new( diff --git a/parley/src/tests/test_basic.rs b/parley/src/tests/test_basic.rs index 8e779b620..15be65e6d 100644 --- a/parley/src/tests/test_basic.rs +++ b/parley/src/tests/test_basic.rs @@ -1,7 +1,8 @@ // Copyright 2024 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -use crate::{testenv, Alignment, InlineBox}; +use crate::outputs::Alignment; +use crate::{testenv, InlineBox}; #[test] fn plain_multiline_text() { diff --git a/parley/src/tests/test_cursor.rs b/parley/src/tests/test_cursor.rs index 8cccac437..79f1c92be 100644 --- a/parley/src/tests/test_cursor.rs +++ b/parley/src/tests/test_cursor.rs @@ -1,8 +1,9 @@ // Copyright 2024 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT +use crate::editing::Cursor; +use crate::inputs::{FontContext, LayoutContext}; use crate::tests::utils::CursorTest; -use crate::{Cursor, FontContext, LayoutContext}; #[test] fn cursor_previous_visual() { diff --git a/parley/src/tests/utils/cursor_test.rs b/parley/src/tests/utils/cursor_test.rs index a7b1ced38..35862e33f 100644 --- a/parley/src/tests/utils/cursor_test.rs +++ b/parley/src/tests/utils/cursor_test.rs @@ -1,7 +1,9 @@ // Copyright 2024 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -use crate::{Affinity, Cursor, FontContext, Layout, LayoutContext}; +use crate::editing::Cursor; +use crate::inputs::{FontContext, LayoutContext}; +use crate::outputs::{Affinity, Layout}; // Note: This module is only compiled when running tests, which requires std, // so we don't have to worry about being no_std-compatible. diff --git a/parley/src/tests/utils/env.rs b/parley/src/tests/utils/env.rs index 180130a87..967f684f6 100644 --- a/parley/src/tests/utils/env.rs +++ b/parley/src/tests/utils/env.rs @@ -1,15 +1,19 @@ // Copyright 2024 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -use crate::tests::utils::renderer::{render_layout, ColorBrush, RenderingConfig}; -use crate::{ - FontContext, FontFamily, FontStack, Layout, LayoutContext, PlainEditor, PlainEditorDriver, - RangedBuilder, Rect, StyleProperty, -}; -use fontique::{Collection, CollectionOptions}; use std::path::{Path, PathBuf}; + +use fontique::{Collection, CollectionOptions}; use tiny_skia::{Color, Pixmap}; +use crate::editing::{PlainEditor, PlainEditorDriver}; +use crate::inputs::{ + FontContext, FontFamily, FontStack, LayoutContext, RangedBuilder, StyleProperty, +}; +use crate::outputs::Layout; +use crate::tests::utils::renderer::{render_layout, ColorBrush, RenderingConfig}; +use crate::Rect; + // Creates a new instance of TestEnv and put current function name in constructor #[macro_export] macro_rules! testenv { diff --git a/parley/src/tests/utils/renderer.rs b/parley/src/tests/utils/renderer.rs index 2169eb7b7..7e9c5df3e 100644 --- a/parley/src/tests/utils/renderer.rs +++ b/parley/src/tests/utils/renderer.rs @@ -7,15 +7,14 @@ //! Note: Emoji rendering is not currently implemented in this example. See the swash example //! if you need emoji rendering. -use crate::{GlyphRun, Layout, PositionedLayoutItem}; -use skrifa::{ - instance::{LocationRef, NormalizedCoord, Size}, - outline::{DrawSettings, OutlinePen}, - raw::FontRef as ReadFontsRef, - GlyphId, MetadataProvider, OutlineGlyph, -}; +use skrifa::instance::{LocationRef, NormalizedCoord, Size}; +use skrifa::outline::{DrawSettings, OutlinePen}; +use skrifa::raw::FontRef as ReadFontsRef; +use skrifa::{GlyphId, MetadataProvider, OutlineGlyph}; use tiny_skia::{Color, FillRule, Paint, PathBuilder, Pixmap, PixmapMut, Rect, Transform}; +use crate::outputs::{GlyphRun, Layout, PositionedLayoutItem}; + #[derive(Clone, Copy, Debug, PartialEq)] pub(crate) struct ColorBrush { pub(crate) color: Color,