diff --git a/src/display/glyph_pos.rs b/src/forme/glyph_pos.rs similarity index 97% rename from src/display/glyph_pos.rs rename to src/forme/glyph_pos.rs index 738e34a..b8df262 100644 --- a/src/display/glyph_pos.rs +++ b/src/forme/glyph_pos.rs @@ -5,7 +5,7 @@ //! Methods using positioned glyphs -use super::TextDisplay; +use super::Forme; use crate::conv::to_usize; use crate::fonts::{self, FaceId, ScaledFaceRef}; use crate::{Glyph, Range, Vec2, shaper}; @@ -47,6 +47,9 @@ impl MarkerPos { } } +/// An iterator over [`MarkerPos`] items. +/// +/// Represents a maximum of two elements. pub struct MarkerPosIter { v: [MarkerPos; 2], a: usize, @@ -100,7 +103,7 @@ impl ExactSizeIterator for MarkerPosIter {} /// A sequence of positioned glyphs with effects /// -/// Yielded by [`TextDisplay::runs`]. +/// Yielded by [`Forme::runs`]. pub struct GlyphRun<'a, E> { run: &'a shaper::GlyphRun, range: Range, @@ -273,11 +276,10 @@ impl<'a, E: Copy + Default> GlyphRun<'a, E> { } } -impl TextDisplay { +impl Forme { /// Find the starting position (top-left) of the glyph at the given index /// - /// [Requires status][Self#status-of-preparation]: - /// text is fully prepared for display. + /// Expects state: [`Status::Ready`](crate::Status::Ready). /// /// The index should be no greater than the text length. It is not required /// to be on a code-point boundary. Returns an iterator over matching @@ -370,6 +372,8 @@ impl TextDisplay { /// Iterate over runs of positioned glyphs /// + /// Expects state: [`Status::Ready`](crate::Status::Ready). + /// /// All glyphs are translated by the given `offset` (this is practically /// free). /// @@ -383,9 +387,6 @@ impl TextDisplay { /// default value of `E` if no such `i` exists. /// /// Runs are yielded in undefined order. - /// - /// [Requires status][Self#status-of-preparation]: - /// text is fully prepared for display. pub fn runs<'a, E: Copy + Debug + Default>( &'a self, offset: Vec2, @@ -399,7 +400,7 @@ impl TextDisplay { && effect.0 <= i { panic!( - "TextDisplay::runs: effect start indices are not strictly increasing in {effects:?}" + "Forme::runs: effect start indices are not strictly increasing in {effects:?}" ); } start = Some(effect.0); diff --git a/src/display/mod.rs b/src/forme/mod.rs similarity index 72% rename from src/display/mod.rs rename to src/forme/mod.rs index ee928f0..d07a433 100644 --- a/src/display/mod.rs +++ b/src/forme/mod.rs @@ -5,6 +5,8 @@ //! Text prepared for display +#[allow(unused)] +use crate::Status; use crate::conv::to_usize; use crate::{Vec2, shaper}; use smallvec::SmallVec; @@ -26,36 +28,36 @@ use wrap_lines::RunPart; #[error("not ready")] pub struct NotReady; -/// Text type-setting object (low-level, without text and configuration) +/// A representation of typeset text glyphs, with reflow support /// -/// This struct caches type-setting data at multiple levels of preparation. -/// Its end result is a sequence of type-set glyphs. +/// In typesetting using [movable type](https://en.wikipedia.org/wiki/Movable_type), +/// a *forme* (fr/en) refers to a page of type metal locked into a heavy steel +/// frame known as a *chase*. While not a direct equivalent, this type is used +/// to construct and represent a set of positioned glyphs, ready for "printing" +/// to a digital screen. /// -/// It is usually recommended to use [`Text`] instead, which includes -/// the source text, type-setting configuration and status tracking. +/// It may be preferable to use [`Text`] instead which bundles a `Forme` with +/// the source text, formatting data and status tracking. /// -/// ### Status of preparation +/// ## States of preparation /// -/// This struct does not track the state-of-preparation internally. It is -/// recommended to use [`Text`] or a custom wrapper to do this. The [`Status`] -/// enum may be helpful here. +/// A `Forme` may have multiple states. The [`Status`] struct has corresponding +/// variants. `Forme` does not directly track its state; [`Text`] does. /// -/// Methods note the expected status-of-preparation. Violating this expectation -/// is memory-safe but may cause a panic or an unexpected result. +/// - [`Status::Empty`]: The result of default-constructing a `Forme` or +/// calling [`Self::clear`] (from any prior state). The `Forme` has zero +/// size and no content. +/// - [`Status::Shaped`]: The result of calling [`Self::set_text`] (from any +/// prior state). While [`Self::measure_width`] and [`Self::measure_height`] +/// are able to operate on content in this state, all queries on lines or +/// glyphs will consider the `Forme` empty. +/// - [`Status::Wrapped`]: The result of calling [`Self::prepare_lines`] (from +/// any prior state, though if called on [`Status::Empty`] the result will +/// of course be empty). +/// - [`Status::Ready`]: The result of calling [`Self::prepare_lines`] and +/// adjusting alignment (if required). /// -/// Steps of preparation are as follows: -/// -/// 1. Run-breaking: call [`Self::prepare_runs`] to break text into runs, -/// resolve fonts and apply shaping. (This is the most expensive step, -/// especially when shaping is enabled.) -/// 2. (Optional) Measure size requirements using [`Self::measure_width`] and -/// [`Self::measure_height`]. -/// 3. Line-wrapping: call [`Self::prepare_lines`] to perform line-wrapping at -/// the given wrap-width. This also re-orders segments (where lines are -/// bi-directional) and performs horizontal alignment. -/// 4. (Optional) Tweak alignment (e.g. to vertically center text). -/// -/// ### Text navigation +/// ## Text navigation /// /// Despite lacking a copy of the underlying text, text-indices may be mapped to /// glyphs and lines, and vice-versa. @@ -68,7 +70,7 @@ pub struct NotReady; /// direction in bi-directional text). /// /// Navigating to the start or end of a line can be done with -/// [`TextDisplay::find_line`], [`TextDisplay::get_line`] and [`Line::text_range`]. +/// [`Forme::find_line`], [`Forme::get_line`] and [`Line::text_range`]. /// /// Navigating forwards or backwards should be done via a library such as /// [`unicode-segmentation`](https://github.com/unicode-rs/unicode-segmentation) @@ -78,24 +80,17 @@ pub struct NotReady; /// The direction of navigation may be reversed for [right-to-left text](Self::text_is_rtl) /// (i.e. reversed logical-order navigation). /// -/// To navigate "up" and "down" lines, use [`TextDisplay::text_glyph_pos`] to -/// get the position of the cursor, [`TextDisplay::find_line`] to get the line -/// number, then [`TextDisplay::line_index_nearest`] to find the new index. +/// To navigate "up" and "down" lines, use [`Forme::text_glyph_pos`] to +/// get the position of the cursor, [`Forme::find_line`] to get the line +/// number, then [`Forme::line_index_nearest`] to find the new index. /// /// [`Text`]: crate::Text -/// [`Status`]: crate::Status #[derive(Clone, Debug)] -pub struct TextDisplay { - // NOTE: typical numbers of elements: - // Simple labels: runs=1, wrapped_runs=1, lines=1 - // Longer texts wrapped over n lines: runs=1, wrapped_runs=n, lines=n - // Justified wrapped text: similar, but wrapped_runs is the word count - // Simple texts with explicit breaks over n lines: all=n - // Single-line bidi text: runs=n, wrapped_runs=n, lines=1 - // Complex bidi or formatted texts: all=many - // Conclusion: SmallVec<[T; 1]> saves allocations in many cases. - // - /// Level runs within the text, in logical order +pub struct Forme { + /// Level runs (i.e. one BiDi level) of shaped glyphs in logical text order + /// + /// Frequently (especially for small labels) a single run represents the + /// whole text. Complex texts may involve many runs. runs: SmallVec<[shaper::GlyphRun; 1]>, /// Contiguous runs, in logical order /// @@ -112,15 +107,15 @@ pub struct TextDisplay { fn size_of_elts() { use std::mem::size_of; assert_eq!(size_of::>(), 24); - assert_eq!(size_of::(), 120); + assert_eq!(size_of::(), 112); assert_eq!(size_of::(), 24); assert_eq!(size_of::(), 24); - assert_eq!(size_of::(), 208); + assert_eq!(size_of::(), 200); } -impl Default for TextDisplay { +impl Default for Forme { fn default() -> Self { - TextDisplay { + Forme { runs: Default::default(), wrapped_runs: Default::default(), lines: Default::default(), @@ -130,10 +125,21 @@ impl Default for TextDisplay { } } -impl TextDisplay { +impl Forme { + /// Reset the `Forme` to empty + /// + /// May be called from any [`Status`]; results in [`Status::Empty`]. + pub fn clear(&mut self) { + self.runs.clear(); + self.wrapped_runs.clear(); + self.lines.clear(); + self.l_bound = 0.0; + self.r_bound = 0.0; + } + /// Get the number of lines (after wrapping) /// - /// [Requires status][Self#status-of-preparation]: lines have been wrapped. + /// Expects state: [`Status::Wrapped`] or higher. #[inline] pub fn num_lines(&self) -> usize { self.lines.len() @@ -141,7 +147,8 @@ impl TextDisplay { /// Get line properties /// - /// [Requires status][Self#status-of-preparation]: lines have been wrapped. + /// Expects state [`Status::Wrapped`] or higher. + /// Methods [`Line::top`] and [`Line::bottom`] expect state [`Status::Ready`]. #[inline] pub fn get_line(&self, index: usize) -> Option<&Line> { self.lines.get(index) @@ -149,7 +156,8 @@ impl TextDisplay { /// Iterate over line properties /// - /// [Requires status][Self#status-of-preparation]: lines have been wrapped. + /// Expects state [`Status::Wrapped`] or higher. + /// Methods [`Line::top`] and [`Line::bottom`] expect state [`Status::Ready`]. #[inline] pub fn lines(&self) -> impl Iterator { self.lines.iter() @@ -157,11 +165,7 @@ impl TextDisplay { /// Get the size of the required bounding box /// - /// [Requires status][Self#status-of-preparation]: lines have been wrapped. - /// - /// Returns the position of the upper-left and lower-right corners of a - /// bounding box on content. - /// Alignment and input bounds do affect the result. + /// Expects state [`Status::Ready`]. pub fn bounding_box(&self) -> (Vec2, Vec2) { if self.lines.is_empty() { return (Vec2::ZERO, Vec2::ZERO); @@ -174,13 +178,13 @@ impl TextDisplay { /// Find the line containing text `index` /// - /// [Requires status][Self#status-of-preparation]: lines have been wrapped. - /// /// Returns the line number and the text-range of the line. /// /// Returns `None` in case `index` does not line on or at the end of a line /// (which means either that `index` is beyond the end of the text or that /// `index` is within a mult-byte line break). + /// + /// Expects state [`Status::Wrapped`] or higher. pub fn find_line(&self, index: usize) -> Option<(usize, std::ops::Range)> { let mut first = None; for (n, line) in self.lines.iter().enumerate() { @@ -199,7 +203,7 @@ impl TextDisplay { /// Get the base directionality of the first paragraph /// - /// [Requires status][Self#status-of-preparation]: run-breaking is complete. + /// Expects state [`Status::Shaped`] or higher. /// /// This returns the direction inferred from the `text` and [`Direction`] /// used during run-breaking. See also [`Direction::text_is_rtl`]. @@ -216,7 +220,7 @@ impl TextDisplay { /// Get the directionality of the current line /// - /// [Requires status][Self#status-of-preparation]: lines have been wrapped. + /// Expects state [`Status::Wrapped`] or higher. /// /// Returns: /// @@ -239,8 +243,7 @@ impl TextDisplay { /// Find the text index for the glyph nearest the given `pos` /// - /// [Requires status][Self#status-of-preparation]: - /// text is fully prepared for display. + /// Expects state [`Status::Ready`]. /// /// This includes the index immediately after the last glyph, thus /// `result ≤ text.len()`. @@ -261,9 +264,9 @@ impl TextDisplay { /// Find the text index nearest horizontal-coordinate `x` on `line` /// - /// [Requires status][Self#status-of-preparation]: lines have been wrapped. + /// Expects state [`Status::Ready`]. /// - /// This is similar to [`TextDisplay::text_index_nearest`], but allows the + /// This is similar to [`Forme::text_index_nearest`], but allows the /// line to be specified explicitly. Returns `None` only on invalid `line`. pub fn line_index_nearest(&self, line: usize, x: f32) -> Option { if line >= self.lines.len() { diff --git a/src/display/text_runs.rs b/src/forme/text_runs.rs similarity index 92% rename from src/display/text_runs.rs rename to src/forme/text_runs.rs index 3743668..69a4a83 100644 --- a/src/display/text_runs.rs +++ b/src/forme/text_runs.rs @@ -5,7 +5,9 @@ //! Text preparation: line breaking and BIDI -use super::TextDisplay; +use super::Forme; +#[allow(unused)] +use crate::Status; use crate::conv::{to_u32, to_usize}; use crate::fonts::{self, FaceId, FontSelector, NoFontMatch}; use crate::util::{AnalyzedText, ends_with_hard_break, to_fontique_script}; @@ -17,6 +19,7 @@ use icu_properties::props::{ }; use icu_segmenter::LineSegmenter; use icu_segmenter::options::{LineBreakStrictness, LineBreakWordOption}; +use std::ops::Bound; use std::sync::OnceLock; #[derive(Clone, Copy, Debug, PartialEq)] @@ -30,54 +33,7 @@ pub(crate) enum RunSpecial { HTab, } -impl TextDisplay { - /// Reset the `TextDisplay` - /// - /// This removes all text runs, resetting the display. - pub fn clear(&mut self) { - self.runs.clear(); - self.wrapped_runs.clear(); - self.lines.clear(); - self.l_bound = 0.0; - self.r_bound = 0.0; - } - - /// Update font size for existing text runs - /// - /// [Requires status][Self#status-of-preparation]: run-breaking is complete. - /// - /// This is a fast way to resize text. Parameters (aside from - /// [`FontToken::dpem`] values) must match those passed to - /// [`Self::prepare_runs`]. - pub fn resize_runs(&mut self, text: &str, mut font_tokens: FT) - where - FT: Iterator, - { - let (mut dpem, _) = read_initial_token(&mut font_tokens); - let mut next_token = font_tokens.next(); - - for run in &mut self.runs { - while let Some(token) = next_token.as_ref() { - if token.start > run.range.start { - break; - } - dpem = token.dpem; - next_token = font_tokens.next(); - } - - let input = shaper::Input { - text, - dpem, - base_level: run.base_level, - level: run.level, - script: run.script, - }; - let mut breaks = Default::default(); - std::mem::swap(&mut breaks, &mut run.breaks); - *run = shaper::shape(input, run.range, run.face_id, breaks, run.special); - } - } - +impl Forme { /// Break `text` into runs, replacing existing content /// /// The `text` is split into a sequence of runs according to the text @@ -95,11 +51,10 @@ impl TextDisplay { /// /// # Preparation status /// - /// [Requires status][Self#status-of-preparation]: none. + /// [Requires status][Self#states-of-preparation]: none. /// /// Must be called again if any of `text`, `direction` or `font_tokens` /// change. - /// If only `dpem` changes, [`Self::resize_runs`] may be called instead. #[deprecated(since = "0.10.0", note = "use Self::set_text instead")] #[inline] pub fn prepare_runs( @@ -113,26 +68,37 @@ impl TextDisplay { Ok(()) } - /// Replace prior content and typeset + /// Replace text content and construct shaped glyph-runs + /// + /// May be called from any [`Status`]; results in [`Status::Shaped`]. /// - /// This method performs most demanding typesetting steps: run-breaking, - /// font matching and [shaping](https://en.wikipedia.org/wiki/Text_shaping). + /// Call [`Appender::with_tokens`] or [`Appender::with_font`] on the result. /// - /// This method may be called from any - /// [state of preparation]([Self#status-of-preparation]) but - /// [`Self::prepare_lines`] should be called afterwards. + /// This method performs most demanding typesetting steps: /// - /// By itself this method does nothing; see the methods on [`Appender`]. + /// 1. Run-breaking splits the input text into the longest *runs* possible + /// such that runs do not contain mandatory line-breaks and have a single + /// text direction (more accurately: a single BiDi embedding level), + /// [script](https://en.wikipedia.org/wiki/Script_(Unicode)) and set of + /// font properties. + /// 2. Font matching assigns a font to each run based on the script and font + /// properties; in some cases, further run-breaking is required to use + /// fallback fonts. + /// 3. [Shaping](https://en.wikipedia.org/wiki/Text_shaping); this either + /// uses [rustybuzz](https://crates.io/crates/rustybuzz) (requires the + /// `shaping` crate feature) or just does kerning. (Differences between + /// the two methods are most apparent when using emojis or complex + /// scripts such as Arabic.) // // Note: the only real difficulty in adding `fn push_text(..)` (to support // using multiple disjoint pieces of text) is that text indices get used in // various places; such a method would need to offset all text indices used // in GlyphRun to make them distinct and correctly ordered. - #[must_use = "set_text(..) has no effect without also calling a method on the return value"] + #[must_use = "set_text(..) has no effect without calling with_tokens(..) or with_font(..) on the result"] pub fn set_text<'a>(&'a mut self, text: &'a str, direction: Direction) -> Appender<'a> { self.clear(); Appender { - display: self, + forme: self, text: AnalyzedText::new(text, direction), } } @@ -140,10 +106,10 @@ impl TextDisplay { /// A shim for appending text runs /// -/// See [`TextDisplay::set_text`]. +/// See [`Forme::set_text`]. #[must_use] pub struct Appender<'a> { - display: &'a mut TextDisplay, + forme: &'a mut Forme, text: AnalyzedText<'a>, } @@ -187,25 +153,36 @@ impl<'a> Appender<'a> { font_tokens: impl Iterator, imply_empty_final_line: bool, ) -> Result<(), NoFontMatch> { - self.display + self.forme .push_text(&self.text, font_tokens, imply_empty_final_line) } /// Append `&text[range]` using a single font + /// + /// This method may be called multiple times with non-overlapping ranges. #[inline] pub fn with_font( &mut self, - range: std::ops::Range, + range: impl std::ops::RangeBounds, font: FontSelector, dpem: f32, ) -> Result<&mut Self, NoFontMatch> { - self.display - .push_text_range(&self.text, range, font, dpem)?; + let l = match range.start_bound() { + Bound::Included(x) => *x, + Bound::Excluded(x) => x + 1, + Bound::Unbounded => 0, + }; + let h = match range.end_bound() { + Bound::Included(x) => x + 1, + Bound::Excluded(x) => *x, + Bound::Unbounded => self.text.len(), + }; + self.forme.push_text_range(&self.text, l..h, font, dpem)?; Ok(self) } } -impl TextDisplay { +impl Forme { /// Resolve font face and shape run /// /// This may sub-divide text as required to find matching fonts. @@ -715,15 +692,10 @@ mod test { font: Default::default(), }); - let mut display = TextDisplay::default(); - assert!( - display - .set_text(text, dir) - .with_tokens(fonts, false) - .is_ok() - ); + let mut forme = Forme::default(); + assert!(forme.set_text(text, dir).with_tokens(fonts, false).is_ok()); - for (i, (run, expected)) in display.runs.iter().zip(expected.iter()).enumerate() { + for (i, (run, expected)) in forme.runs.iter().zip(expected.iter()).enumerate() { assert_eq!( run.range.to_std(), expected.0, @@ -735,14 +707,13 @@ mod test { ); assert_eq!(run.base_level, expected.2, "for text \"{text}\", run {i}"); assert_eq!(run.level, expected.3, "for text \"{text}\", run {i}"); - assert_eq!(run.script, expected.4, "for text \"{text}\", run {i}"); assert_eq!( run.breaks.iter().map(|b| b.index).collect::>(), expected.5, "wrap-points for text \"{text}\", run {i}" ); } - assert_eq!(display.runs.len(), expected.len(), "number of runs"); + assert_eq!(forme.runs.len(), expected.len(), "number of runs"); } #[test] diff --git a/src/display/wrap_lines.rs b/src/forme/wrap_lines.rs similarity index 95% rename from src/display/wrap_lines.rs rename to src/forme/wrap_lines.rs index 1eaa789..71012af 100644 --- a/src/display/wrap_lines.rs +++ b/src/forme/wrap_lines.rs @@ -5,7 +5,9 @@ //! Text preparation: wrapping -use super::{RunSpecial, TextDisplay}; +use super::{Forme, RunSpecial}; +#[allow(unused)] +use crate::Status; use crate::conv::{to_u32, to_usize}; use crate::fonts::{self, FontLibrary}; use crate::shaper::{GlyphRun, PartMetrics}; @@ -53,10 +55,10 @@ impl Line { } } -impl TextDisplay { +impl Forme { /// Measure required width, up to some `max_width` /// - /// [Requires status][Self#status-of-preparation]: run-breaking is complete. + /// Expects state [`Status::Shaped`] or higher. /// /// This method allows calculation of the width requirement of a text object /// without full wrapping and glyph placement. Whenever the requirement @@ -94,7 +96,7 @@ impl TextDisplay { /// /// Stops after `max_lines`, if provided. /// - /// [Requires status][Self#status-of-preparation]: run-breaking is complete. + /// Expects state [`Status::Shaped`] or higher. pub fn measure_height(&self, wrap_width: f32, max_lines: Option) -> f32 { #[derive(Default)] struct MeasureAdder { @@ -177,15 +179,20 @@ impl TextDisplay { adder.vcaret } - /// Prepare lines ("wrap") + /// Flow text content into lines /// - /// [Requires status][Self#status-of-preparation]: run-breaking is complete. + /// May be called from any [`Status`], though [`Status::Shaped`] or higher + /// is expected for non-empty content; results in [`Status::Wrapped`]. /// - /// This does text layout, including wrapping and horizontal alignment but - /// excluding vertical alignment. + /// This method constructs displayable lines (see + /// [Line-wrapping](Self#line-wrapping)) by wrapping long lines at the given + /// `wrap_width`. Calling this method is necessary even if line-wrapping is + /// not desired; in this case use `wrap_width = f32::INFINITY`. /// - /// Lines longer than `wrap_width` will be wrapped at this width. Use - /// `f32::INFINITY` to disable wrapping. + /// This method is fairly fast, allowing immediate re-wrapping of content + /// whenever the `wrap_width` changes. + /// + /// ## Horizontal alignment /// /// Text will be aligned to the horizontal interval `0..align_width`, thus /// `align_width` must be finite. It usually makes sense to use @@ -207,6 +214,11 @@ impl TextDisplay { /// stretching spaces within the text. Other lines are aligned in the /// same way as [`Align::Default`]. /// + /// ## Vertical alignment + /// + /// Vertically, content is top-aligned. Call [`Self::vertically_align`] + /// after this method to use a different alignment. + /// /// Returns the required height. pub fn prepare_lines(&mut self, wrap_width: f32, align_width: f32, h_align: Align) -> f32 { debug_assert!(align_width.is_finite()); @@ -358,7 +370,8 @@ impl TextDisplay { /// Vertically align lines /// - /// [Requires status][Self#status-of-preparation]: lines have been wrapped. + /// [`Self::prepare_lines`] produces top-aligned content. This method + /// vertically aligns content within the input height `bound`. pub fn vertically_align(&mut self, bound: f32, v_align: Align) { debug_assert!(bound.is_finite()); diff --git a/src/lib.rs b/src/lib.rs index 44b4aba..a0de556 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,15 +3,22 @@ // You may obtain a copy of the License in the LICENSE-APACHE file or at: // https://www.apache.org/licenses/LICENSE-2.0 -//! KAS-text: text layout library +//! # Kas-text: font library and text engine //! -//! KAS-text supports plain text input, custom formatted text objects (see the -//! [`format`] module) and a subset of Markdown ([`format::Markdown`], -//! feature-gated). +//! ## Font library //! -//! The library also supports glyph rastering (depending on feature flags). +//! The [`fonts`] module represents a "library" tracking discovered fonts along +//! with helpers for font selection and reading font properties. //! -//! [`format`]: mod@format +//! ## Text engine +//! +//! The [`Forme`] struct is able to transform an `&str` into a set of type-set +//! glyphs, and rapidly re-flow these glyphs to meet any page width. +//! +//! ## Formatted text +//! +//! The [`Text`] struct provides a slightly higher-level API, using the +//! [`format`](mod@format) module to control formatting of content. #![cfg_attr(docsrs, feature(doc_cfg))] #![allow(text_direction_codepoint_in_literal)] @@ -26,8 +33,8 @@ mod data; use data::Range; pub use data::Vec2; -mod display; -pub use display::*; +mod forme; +pub use forme::*; pub mod fonts; #[cfg(feature = "text")] diff --git a/src/shaper.rs b/src/shaper.rs index cf860af..6f39272 100644 --- a/src/shaper.rs +++ b/src/shaper.rs @@ -18,8 +18,8 @@ //! This module *does not* perform line-breaking, wrapping or text reversal. use crate::conv::{DPU, to_u32, to_usize}; -use crate::display::RunSpecial; use crate::fonts::{self, FaceId}; +use crate::forme::RunSpecial; use crate::{Range, Vec2}; use icu_properties::props::Script; use tinyvec::TinyVec; @@ -103,10 +103,6 @@ pub(crate) struct GlyphRun { pub base_level: Level, /// BiDi level of this run pub level: Level, - /// Script - /// - /// We store this only to support `resize_runs`. - pub script: Script, /// Sequence of all glyphs, in left-to-right order pub glyphs: Vec, @@ -339,7 +335,6 @@ pub(crate) fn shape( special, base_level: input.base_level, level: input.level, - script: input.script, glyphs, breaks, @@ -522,10 +517,10 @@ fn shape_simple( /// Warning: test results may depend on system fonts /// -/// Tests are extensions of those in `display/text_runs.rs`. +/// Tests are extensions of those in `forme/text_runs.rs`. #[cfg(test)] mod test { - use crate::{Direction, FontToken, TextDisplay}; + use crate::{Direction, FontToken, Forme}; use std::iter; use std::ops::Range; @@ -538,15 +533,10 @@ mod test { font: Default::default(), }); - let mut display = TextDisplay::default(); - assert!( - display - .set_text(text, dir) - .with_tokens(fonts, false) - .is_ok() - ); + let mut forme = Forme::default(); + assert!(forme.set_text(text, dir).with_tokens(fonts, false).is_ok()); - for (i, (run, expected)) in display.raw_runs().iter().zip(expected.iter()).enumerate() { + for (i, (run, expected)) in forme.raw_runs().iter().zip(expected.iter()).enumerate() { assert_eq!( run.range.to_std(), expected.0, @@ -563,7 +553,7 @@ mod test { "glyph break indices for text \"{text}\", run {i}" ); } - assert_eq!(display.raw_runs().len(), expected.len(), "number of runs"); + assert_eq!(forme.raw_runs().len(), expected.len(), "number of runs"); } #[test] diff --git a/src/text.rs b/src/text.rs index caa8614..2c667ea 100644 --- a/src/text.rs +++ b/src/text.rs @@ -5,9 +5,9 @@ //! Text object -use crate::display::{MarkerPosIter, NotReady, TextDisplay}; use crate::fonts::{FontSelector, NoFontMatch}; use crate::format::FormattableText; +use crate::forme::{Forme, MarkerPosIter, NotReady}; use crate::{Align, Direction, GlyphRun, Line, Status, Vec2}; use std::fmt::Debug; use std::num::NonZeroUsize; @@ -16,15 +16,15 @@ use std::num::NonZeroUsize; /// /// This struct contains: /// - A [`FormattableText`] -/// - A [`TextDisplay`] +/// - A [`Forme`] /// - A [`FontSelector`] /// - Font size; this defaults to 16px (the web default). /// - Text direction and alignment; by default this is inferred from the text. /// - Line-wrap width; see [`Text::set_wrap_width`]. /// - The bounds used for alignment; these [must be set][Text::set_bounds]. /// -/// This struct tracks the [`TextDisplay`]'s -/// [state of preparation][TextDisplay#status-of-preparation] and will perform +/// This struct tracks the [`Forme`]'s +/// [state of preparation][Forme#status-of-preparation] and will perform /// steps as required. To use this struct: /// ``` /// use kas_text::{Text, Vec2}; @@ -57,7 +57,7 @@ pub struct Text { direction: Direction, status: Status, - display: TextDisplay, + forme: Forme, text: T, } @@ -82,26 +82,26 @@ impl Text { wrap_width: f32::INFINITY, align: Default::default(), direction: Direction::default(), - status: Status::New, + status: Status::Empty, text, - display: Default::default(), + forme: Default::default(), } } - /// Replace the [`TextDisplay`] + /// Replace the [`Forme`] /// /// This may be used with [`Self::new`] to reconstruct an object which was /// disolved [`into_parts`][Self::into_parts]. #[inline] - pub fn with_display(mut self, display: TextDisplay) -> Self { - self.display = display; + pub fn with_forme(mut self, forme: Forme) -> Self { + self.forme = forme; self } /// Decompose into parts #[inline] - pub fn into_parts(self) -> (TextDisplay, T) { - (self.display, self.text) + pub fn into_parts(self) -> (Forme, T) { + (self.forme, self.text) } /// Clone the formatted text @@ -134,7 +134,7 @@ impl Text { } self.text = text; - self.set_max_status(Status::New); + self.set_max_status(Status::Empty); } } @@ -182,7 +182,7 @@ impl Text { pub fn set_font(&mut self, font: FontSelector) { if font != self.font { self.font = font; - self.set_max_status(Status::New); + self.set_max_status(Status::Empty); } } @@ -207,7 +207,7 @@ impl Text { pub fn set_font_size(&mut self, dpem: f32) { if dpem != self.dpem { self.dpem = dpem; - self.set_max_status(Status::ResizeLevelRuns); + self.set_max_status(Status::Empty); } } @@ -234,7 +234,7 @@ impl Text { pub fn set_direction(&mut self, direction: Direction) { if direction != self.direction { self.direction = direction; - self.set_max_status(Status::New); + self.set_max_status(Status::Empty); } } @@ -258,7 +258,7 @@ impl Text { debug_assert!(wrap_width >= 0.0); if wrap_width != self.wrap_width { self.wrap_width = wrap_width; - self.set_max_status(Status::LevelRuns); + self.set_max_status(Status::Shaped); } } @@ -277,7 +277,7 @@ impl Text { if align.0 == self.align.0 { self.set_max_status(Status::Wrapped); } else { - self.set_max_status(Status::LevelRuns); + self.set_max_status(Status::Shaped); } self.align = align; } @@ -300,7 +300,7 @@ impl Text { debug_assert!(bounds.is_finite()); if bounds != self.bounds { if bounds.0 != self.bounds.0 { - self.set_max_status(Status::LevelRuns); + self.set_max_status(Status::Shaped); } else { self.set_max_status(Status::Wrapped); } @@ -313,8 +313,8 @@ impl Text { /// This does not require that the text is prepared. #[inline] pub fn text_is_rtl(&self) -> bool { - if self.status >= Status::ResizeLevelRuns { - return self.display.text_is_rtl(); + if self.status >= Status::Shaped { + return self.forme.text_is_rtl(); } self.direction.text_is_rtl(self.text.as_str()) @@ -344,7 +344,7 @@ impl Text { /// Check whether the text is fully prepared and ready for usage #[inline] - pub fn is_prepared(&self) -> bool { + pub fn is_ready(&self) -> bool { self.status == Status::Ready } @@ -358,47 +358,43 @@ impl Text { self.status = self.status.min(status); } - /// Read the [`TextDisplay`], without checking status + /// Read the [`Forme`], without checking status #[inline] - pub fn unchecked_display(&self) -> &TextDisplay { - &self.display + pub fn unchecked_forme(&self) -> &Forme { + &self.forme } - /// Read the [`TextDisplay`], if fully prepared + /// Read the [`Forme`], if ready for usage #[inline] - pub fn display(&self) -> Result<&TextDisplay, NotReady> { + pub fn forme(&self) -> Result<&Forme, NotReady> { self.check_status(Status::Ready)?; - Ok(self.unchecked_display()) + Ok(self.unchecked_forme()) } - /// Read the [`TextDisplay`], if at least wrapped + /// Read the [`Forme`], if at least wrapped #[inline] - pub fn wrapped_display(&self) -> Result<&TextDisplay, NotReady> { + pub fn wrapped_forme(&self) -> Result<&Forme, NotReady> { self.check_status(Status::Wrapped)?; - Ok(self.unchecked_display()) + Ok(self.unchecked_forme()) } #[inline] fn prepare_runs(&mut self) -> Result<(), NoFontMatch> { match self.status { - Status::New => self - .display + Status::Empty => self + .forme .set_text(self.text.as_str(), self.direction) .with_tokens(self.text.font_tokens(self.dpem, self.font), true)?, - Status::ResizeLevelRuns => self.display.resize_runs( - self.text.as_str(), - self.text.font_tokens(self.dpem, self.font), - ), _ => (), } - self.status = Status::LevelRuns; + self.status = Status::Shaped; Ok(()) } /// Measure required width, up to some `max_width` /// - /// This method partially prepares the [`TextDisplay`] as required. + /// This method partially prepares the [`Forme`] as required. /// /// This method allows calculation of the width requirement of a text object /// without full wrapping and glyph placement. Whenever the requirement @@ -408,7 +404,7 @@ impl Text { pub fn measure_width(&mut self, max_width: f32) -> Result { self.prepare_runs()?; - Ok(self.display.measure_width(max_width)) + Ok(self.forme.measure_width(max_width)) } /// Measure required vertical height, wrapping as configured @@ -416,12 +412,12 @@ impl Text { /// Stops after `max_lines`, if provided. pub fn measure_height(&mut self, max_lines: Option) -> Result { if self.status >= Status::Wrapped { - let (tl, br) = self.display.bounding_box(); + let (tl, br) = self.forme.bounding_box(); return Ok(br.1 - tl.1); } self.prepare_runs()?; - Ok(self.display.measure_height(self.wrap_width, max_lines)) + Ok(self.forme.measure_height(self.wrap_width, max_lines)) } /// Prepare text for display, as necessary @@ -434,22 +430,22 @@ impl Text { /// Returns `Ok(true)` on success when some action is performed, `Ok(false)` /// when the text is already prepared. pub fn prepare(&mut self) -> Result { - if self.is_prepared() { + if self.is_ready() { return Ok(false); } else if !self.bounds.is_finite() { return Err(NotReady); } self.prepare_runs().unwrap(); - debug_assert!(self.status >= Status::LevelRuns); + debug_assert!(self.status >= Status::Shaped); - if self.status == Status::LevelRuns { - self.display + if self.status == Status::Shaped { + self.forme .prepare_lines(self.wrap_width, self.bounds.0, self.align.0); } if self.status <= Status::Wrapped { - self.display.vertically_align(self.bounds.1, self.align.1); + self.forme.vertically_align(self.bounds.1, self.align.1); } self.status = Status::Ready; @@ -463,71 +459,72 @@ impl Text { /// Alignment and input bounds do affect the result. #[inline] pub fn bounding_box(&self) -> Result<(Vec2, Vec2), NotReady> { - Ok(self.wrapped_display()?.bounding_box()) + Ok(self.wrapped_forme()?.bounding_box()) } /// Get the number of lines (after wrapping) /// - /// See [`TextDisplay::num_lines`]. + /// See [`Forme::num_lines`]. #[inline] pub fn num_lines(&self) -> Result { - Ok(self.wrapped_display()?.num_lines()) + Ok(self.wrapped_forme()?.num_lines()) } /// Get line properties #[inline] pub fn get_line(&self, index: usize) -> Result, NotReady> { - Ok(self.wrapped_display()?.get_line(index)) + Ok(self.wrapped_forme()?.get_line(index)) } /// Iterate over line properties /// - /// [Requires status][Self#status-of-preparation]: lines have been wrapped. + /// Expects state [`Status::Wrapped`] or higher. + /// Methods [`Line::top`] and [`Line::bottom`] expect state [`Status::Ready`]. #[inline] pub fn lines(&self) -> Result, NotReady> { - Ok(self.wrapped_display()?.lines()) + Ok(self.wrapped_forme()?.lines()) } /// Find the line containing text `index` /// - /// See [`TextDisplay::find_line`]. + /// See [`Forme::find_line`]. #[inline] pub fn find_line( &self, index: usize, ) -> Result)>, NotReady> { - Ok(self.wrapped_display()?.find_line(index)) + Ok(self.wrapped_forme()?.find_line(index)) } /// Get the directionality of the current line /// - /// See [`TextDisplay::line_is_rtl`]. + /// See [`Forme::line_is_rtl`]. #[inline] pub fn line_is_rtl(&self, line: usize) -> Result, NotReady> { - Ok(self.wrapped_display()?.line_is_rtl(line)) + Ok(self.wrapped_forme()?.line_is_rtl(line)) } /// Find the text index for the glyph nearest the given `pos` /// - /// See [`TextDisplay::text_index_nearest`]. + /// See [`Forme::text_index_nearest`]. #[inline] pub fn text_index_nearest(&self, pos: Vec2) -> Result { - Ok(self.display()?.text_index_nearest(pos)) + Ok(self.forme()?.text_index_nearest(pos)) } /// Find the text index nearest horizontal-coordinate `x` on `line` /// - /// See [`TextDisplay::line_index_nearest`]. + /// See [`Forme::line_index_nearest`]. #[inline] pub fn line_index_nearest(&self, line: usize, x: f32) -> Result, NotReady> { - Ok(self.wrapped_display()?.line_index_nearest(line, x)) + Ok(self.wrapped_forme()?.line_index_nearest(line, x)) } /// Find the starting position (top-left) of the glyph at the given index /// - /// See [`TextDisplay::text_glyph_pos`]. + /// See [`Forme::text_glyph_pos`]. pub fn text_glyph_pos(&self, index: usize) -> Result { - Ok(self.display()?.text_glyph_pos(index)) + Ok(self.forme()?.text_glyph_pos(index)) } /// Iterate over runs of positioned glyphs @@ -542,7 +539,7 @@ impl Text { &'a self, offset: Vec2, ) -> Result> + 'a, NotReady> { - Ok(self.display()?.runs(offset, self.text.effect_tokens())) + Ok(self.forme()?.runs(offset, self.text.effect_tokens())) } /// Iterate over runs of positioned glyphs using a custom effects list @@ -564,6 +561,6 @@ impl Text { offset: Vec2, effects: &'a [(u32, E)], ) -> Result> + 'a, NotReady> { - Ok(self.display()?.runs(offset, effects)) + Ok(self.forme()?.runs(offset, effects)) } } diff --git a/src/util.rs b/src/util.rs index 5a42c39..8d093c8 100644 --- a/src/util.rs +++ b/src/util.rs @@ -5,6 +5,9 @@ //! Utility types and traits +#[allow(unused)] +use crate::Forme; +use crate::{Direction, fonts::FontSelector}; use icu_properties::{CodePointMapData, props::LineBreak}; use icu_segmenter::{ LineSegmenter, iterators::LineBreakIterator, options::LineBreakOptions, scaffold::Utf8, @@ -12,21 +15,17 @@ use icu_segmenter::{ use std::ops::Range; use unicode_bidi::{BidiInfo, LTR_LEVEL, Level, ParagraphInfo, RTL_LEVEL}; -use crate::{Direction, fonts::FontSelector}; - -/// Describes the state-of-preparation of a [`TextDisplay`][crate::TextDisplay] +/// Describes the [state-of-preparation](Forme#states-of-preparation) of a [`Forme`] #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Ord, PartialOrd, Hash)] pub enum Status { - /// Nothing done yet + /// An empty [`Forme`]. #[default] - New, - /// As [`Self::LevelRuns`], except these need resizing - ResizeLevelRuns, - /// Source text has been broken into level runs - LevelRuns, - /// Line wrapping and horizontal alignment is done + Empty, + /// A [`Forme`] with [set text](Forme::set_text). + Shaped, + /// A [`Forme`] with [prepared lines](Forme::prepare_lines). Wrapped, - /// The text is ready for display + /// A [`Forme`] that is ready for display. Ready, }