├── .gitignore ├── COPYRIGHT ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .config ├── extra.dict └── spellcheck.toml ├── src ├── lib.rs ├── format │ ├── plain.rs │ └── markdown.rs ├── util.rs ├── swash_convert.rs ├── conv.rs ├── fonts │ ├── mod.rs │ ├── face.rs │ ├── resolver.rs │ ├── library.rs │ └── attributes.rs ├── env.rs ├── data.rs ├── format.rs ├── display │ ├── text_runs.rs │ ├── mod.rs │ └── glyph_pos.rs ├── shaper.rs └── text.rs ├── Cargo.toml ├── design ├── complex-layout.md └── requirements.md ├── README.md ├── LICENSE └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | This work, is copyrighted by the following contributors: 2 | 3 | Diggory Hardy 4 | 5 | This list may be incomplete. 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | -------------------------------------------------------------------------------- /.config/extra.dict: -------------------------------------------------------------------------------- 1 | 49 2 | 2D 3 | Arial 4 | ascender 5 | BIDI 6 | boolean 7 | bulleted 8 | codepoint 9 | codepoints 10 | CPUs 11 | CSS 12 | directionality 13 | DPEM 14 | DPP 15 | fallbacks 16 | formattable 17 | formatter 18 | glyphs 19 | grapheme 20 | grayscale 21 | HarfBuzz 22 | hashable 23 | iterable 24 | iPhones 25 | kas 26 | KAS 27 | kerning 28 | lookups 29 | LTR 30 | MacOS 31 | obliqued 32 | parametrization 33 | Parsers 34 | rastered 35 | rastering 36 | rect 37 | rects 38 | renderer 39 | resize 40 | resizing 41 | RTL 42 | Rustybuzz 43 | shaper 44 | struct 45 | TODO 46 | TR9 47 | tuple 48 | unformatted 49 | unscaled 50 | whitespace 51 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License in the LICENSE-APACHE file or at: 4 | // https://www.apache.org/licenses/LICENSE-2.0 5 | 6 | //! KAS-text: text layout library 7 | //! 8 | //! KAS-text supports plain text input, custom formatted text objects (see the 9 | //! [`format`] module) and a subset of Markdown ([`format::Markdown`], 10 | //! feature-gated). 11 | //! 12 | //! The library also supports glyph rastering (depending on feature flags). 13 | //! 14 | //! [`format`]: mod@format 15 | 16 | mod env; 17 | pub use env::*; 18 | 19 | mod conv; 20 | pub use conv::DPU; 21 | 22 | mod data; 23 | use data::Range; 24 | pub use data::Vec2; 25 | 26 | mod display; 27 | pub use display::*; 28 | 29 | pub mod fonts; 30 | pub mod format; 31 | 32 | mod swash_convert; 33 | pub(crate) use swash_convert::script_to_fontique; 34 | 35 | mod text; 36 | pub use text::*; 37 | 38 | mod util; 39 | pub use util::{OwningVecIter, Status}; 40 | 41 | pub(crate) mod shaper; 42 | pub use shaper::{Glyph, GlyphId}; 43 | -------------------------------------------------------------------------------- /src/format/plain.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License in the LICENSE-APACHE file or at: 4 | // https://www.apache.org/licenses/LICENSE-2.0 5 | 6 | //! Implementations for plain text 7 | 8 | use super::{FontToken, FormattableText}; 9 | use crate::Effect; 10 | 11 | impl FormattableText for str { 12 | type FontTokenIter<'a> 13 | = std::iter::Empty 14 | where 15 | Self: 'a; 16 | 17 | fn as_str(&self) -> &str { 18 | self 19 | } 20 | 21 | fn font_tokens<'a>(&'a self, _: f32) -> Self::FontTokenIter<'a> { 22 | std::iter::empty() 23 | } 24 | 25 | fn effect_tokens(&self) -> &[Effect] { 26 | &[] 27 | } 28 | } 29 | 30 | impl FormattableText for String { 31 | type FontTokenIter<'a> = std::iter::Empty; 32 | 33 | fn as_str(&self) -> &str { 34 | self 35 | } 36 | 37 | fn font_tokens<'a>(&'a self, _: f32) -> Self::FontTokenIter<'a> { 38 | std::iter::empty() 39 | } 40 | 41 | fn effect_tokens(&self) -> &[Effect] { 42 | &[] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | nightly: 14 | name: Nightly 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v5 18 | - name: Install toolchain 19 | uses: dtolnay/rust-toolchain@nightly 20 | with: 21 | components: rustfmt, clippy 22 | - name: Rustfmt check 23 | run: | 24 | cargo fmt --all -- --check 25 | - name: doc 26 | run: RUSTDOCFLAGS="--cfg doc_cfg" cargo doc --all-features --no-deps 27 | - name: Clippy 28 | run: cargo +nightly clippy --all-features 29 | - name: Test (all features including GAT) 30 | run: cargo test --all-features 31 | 32 | test: 33 | name: Test 34 | runs-on: ${{ matrix.os }} 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | os: [macos-latest, windows-latest] 39 | toolchain: [stable] 40 | include: 41 | - os: ubuntu-latest 42 | toolchain: "1.88.0" 43 | - os: ubuntu-latest 44 | toolchain: beta 45 | 46 | steps: 47 | - uses: actions/checkout@v5 48 | - name: Install toolchain 49 | uses: actions-rs/toolchain@v1 50 | with: 51 | profile: minimal 52 | toolchain: ${{ matrix.toolchain }} 53 | override: true 54 | - name: Test (reduced features) 55 | run: cargo test --all-targets --features markdown 56 | - name: Test (all features except GAT) 57 | run: cargo test --features markdown,shaping,serde,num_glyphs 58 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kas-text" 3 | version = "0.8.1" 4 | authors = ["Diggory Hardy "] 5 | edition = "2024" 6 | license = "Apache-2.0" 7 | description = "Text layout and font management" 8 | readme = "README.md" 9 | documentation = "https://docs.rs/kas-text/" 10 | keywords = ["text", "bidi", "shaping"] 11 | categories = ["text-processing"] 12 | repository = "https://github.com/kas-gui/kas-text" 13 | exclude = ["design"] 14 | rust-version = "1.88.0" 15 | 16 | [package.metadata.docs.rs] 17 | # To build locally: 18 | # cargo +nightly doc --all-features --no-deps --open 19 | all-features = true 20 | 21 | [features] 22 | # Support num_glyphs method 23 | num_glyphs = [] 24 | 25 | # Enable shaping with the default dependency. 26 | shaping = ["rustybuzz"] 27 | # Enable shaping via rustybuzz. 28 | rustybuzz = ["dep:rustybuzz"] 29 | 30 | # Enable Markdown parsing 31 | markdown = ["pulldown-cmark"] 32 | 33 | # Serialization is optionally supported for some types: 34 | serde = ["dep:serde", "bitflags/serde"] 35 | 36 | # Optional: expose ab_glyph font face 37 | ab_glyph = ["dep:ab_glyph"] 38 | 39 | [dependencies] 40 | cfg-if = "1.0.0" 41 | easy-cast = "0.5.0" 42 | bitflags = "2.4.2" 43 | ttf-parser = "0.25.1" 44 | smallvec = "1.6.1" 45 | tinyvec = { version = "1.9.0", features = ["alloc"] } 46 | unicode-bidi = "0.3.4" 47 | unicode-bidi-mirroring = "0.4.0" 48 | thiserror = "2.0.12" 49 | pulldown-cmark = { version = "0.13.0", optional = true } 50 | log = "0.4" 51 | serde = { version = "1.0.123", features = ["derive"], optional = true } 52 | ab_glyph = { version = "0.2.10", optional = true } 53 | swash = "0.2.4" 54 | fontique = "0.6.0" 55 | 56 | [dependencies.rustybuzz] 57 | version = "0.20.1" 58 | optional = true 59 | 60 | [lints.clippy] 61 | len_zero = "allow" 62 | type_complexity = "allow" 63 | unit_arg = "allow" 64 | needless_lifetimes = "allow" 65 | neg_cmp_op_on_partial_ord = "allow" 66 | manual_range_contains = "allow" 67 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License in the LICENSE-APACHE file or at: 4 | // https://www.apache.org/licenses/LICENSE-2.0 5 | 6 | //! Utility types and traits 7 | 8 | /// Describes the state-of-preparation of a [`TextDisplay`][crate::TextDisplay] 9 | #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Ord, PartialOrd, Hash)] 10 | pub enum Status { 11 | /// Nothing done yet 12 | #[default] 13 | New, 14 | /// As [`Self::LevelRuns`], except these need resizing 15 | ResizeLevelRuns, 16 | /// Source text has been broken into level runs 17 | LevelRuns, 18 | /// Line wrapping and horizontal alignment is done 19 | Wrapped, 20 | /// The text is ready for display 21 | Ready, 22 | } 23 | 24 | impl Status { 25 | /// True if status is `Status::Ready` 26 | #[inline] 27 | pub fn is_ready(&self) -> bool { 28 | *self == Status::Ready 29 | } 30 | } 31 | 32 | /// An iterator over a `Vec` which clones elements 33 | pub struct OwningVecIter { 34 | v: Vec, 35 | i: usize, 36 | } 37 | 38 | impl OwningVecIter { 39 | /// Construct from a `Vec` 40 | pub fn new(v: Vec) -> Self { 41 | let i = 0; 42 | OwningVecIter { v, i } 43 | } 44 | } 45 | 46 | impl Iterator for OwningVecIter { 47 | type Item = T; 48 | fn next(&mut self) -> Option { 49 | if self.i < self.v.len() { 50 | let item = self.v[self.i].clone(); 51 | self.i += 1; 52 | Some(item) 53 | } else { 54 | None 55 | } 56 | } 57 | 58 | fn size_hint(&self) -> (usize, Option) { 59 | let len = self.v.len() - self.i; 60 | (len, Some(len)) 61 | } 62 | } 63 | 64 | impl ExactSizeIterator for OwningVecIter {} 65 | impl std::iter::FusedIterator for OwningVecIter {} 66 | -------------------------------------------------------------------------------- /src/swash_convert.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Parley Authors 2 | // This file is copied from the Parley project. 3 | // SPDX-License-Identifier: Apache-2.0 OR MIT 4 | 5 | pub(crate) fn script_to_fontique(script: swash::text::Script) -> fontique::Script { 6 | fontique::Script(*SCRIPT_TAGS.get(script as usize).unwrap_or(b"Zzzz")) 7 | } 8 | 9 | #[rustfmt::skip] 10 | const SCRIPT_TAGS: [[u8; 4]; 157] = [ 11 | *b"Adlm", *b"Aghb", *b"Ahom", *b"Arab", *b"Armi", *b"Armn", *b"Avst", *b"Bali", *b"Bamu", 12 | *b"Bass", *b"Batk", *b"Beng", *b"Bhks", *b"Bopo", *b"Brah", *b"Brai", *b"Bugi", *b"Buhd", 13 | *b"Cakm", *b"Cans", *b"Cari", *b"Cham", *b"Cher", *b"Chrs", *b"Copt", *b"Cprt", *b"Cyrl", 14 | *b"Deva", *b"Diak", *b"Dogr", *b"Dsrt", *b"Dupl", *b"Egyp", *b"Elba", *b"Elym", *b"Ethi", 15 | *b"Geor", *b"Glag", *b"Gong", *b"Gonm", *b"Goth", *b"Gran", *b"Grek", *b"Gujr", *b"Guru", 16 | *b"Hang", *b"Hani", *b"Hano", *b"Hatr", *b"Hebr", *b"Hira", *b"Hluw", *b"Hmng", *b"Hmnp", 17 | *b"Hung", *b"Ital", *b"Java", *b"Kali", *b"Kana", *b"Khar", *b"Khmr", *b"Khoj", *b"Kits", 18 | *b"Knda", *b"Kthi", *b"Lana", *b"Laoo", *b"Latn", *b"Lepc", *b"Limb", *b"Lina", *b"Linb", 19 | *b"Lisu", *b"Lyci", *b"Lydi", *b"Mahj", *b"Maka", *b"Mand", *b"Mani", *b"Marc", *b"Medf", 20 | *b"Mend", *b"Merc", *b"Mero", *b"Mlym", *b"Modi", *b"Mong", *b"Mroo", *b"Mtei", *b"Mult", 21 | *b"Mymr", *b"Nand", *b"Narb", *b"Nbat", *b"Newa", *b"Nkoo", *b"Nshu", *b"Ogam", *b"Olck", 22 | *b"Orkh", *b"Orya", *b"Osge", *b"Osma", *b"Palm", *b"Pauc", *b"Perm", *b"Phag", *b"Phli", 23 | *b"Phlp", *b"Phnx", *b"Plrd", *b"Prti", *b"Rjng", *b"Rohg", *b"Runr", *b"Samr", *b"Sarb", 24 | *b"Saur", *b"Sgnw", *b"Shaw", *b"Shrd", *b"Sidd", *b"Sind", *b"Sinh", *b"Sogd", *b"Sogo", 25 | *b"Sora", *b"Soyo", *b"Sund", *b"Sylo", *b"Syrc", *b"Tagb", *b"Takr", *b"Tale", *b"Talu", 26 | *b"Taml", *b"Tang", *b"Tavt", *b"Telu", *b"Tfng", *b"Tglg", *b"Thaa", *b"Thai", *b"Tibt", 27 | *b"Tirh", *b"Ugar", *b"Vaii", *b"Wara", *b"Wcho", *b"Xpeo", *b"Xsux", *b"Yezi", *b"Yiii", 28 | *b"Zanb", *b"Zinh", *b"Zyyy", *b"Zzzz", 29 | ]; 30 | -------------------------------------------------------------------------------- /src/conv.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License in the LICENSE-APACHE file or at: 4 | // https://www.apache.org/licenses/LICENSE-2.0 5 | 6 | //! Type conversion utilities 7 | //! 8 | //! Many indices are represented as `u32` instead of `usize` by this library in 9 | //! order to save space (note that we do not expect `usize` smaller than `u32` 10 | //! and our text representations are not intended to scale anywhere close to 11 | //! `u32::MAX` bytes of text, so `u32` is always an appropriate index type). 12 | 13 | use easy_cast::Cast; 14 | 15 | /// Convert `usize` → `u32` 16 | /// 17 | /// This is a "safer" wrapper around `as` ensuring (on debug builds) that the 18 | /// input value may be represented correctly by `u32`. 19 | #[inline] 20 | pub fn to_u32(x: usize) -> u32 { 21 | x.cast() 22 | } 23 | 24 | /// Convert `u32` → `usize` 25 | /// 26 | /// This is a "safer" wrapper around `as` ensuring that the operation is 27 | /// zero-extension. 28 | #[inline] 29 | pub fn to_usize(x: u32) -> usize { 30 | x.cast() 31 | } 32 | 33 | /// Scale factor: pixels per font unit 34 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 35 | pub struct DPU(pub f32); 36 | 37 | impl DPU { 38 | #[cfg(feature = "rustybuzz")] 39 | pub(crate) fn i32_to_px(self, x: i32) -> f32 { 40 | x as f32 * self.0 41 | } 42 | pub(crate) fn i16_to_px(self, x: i16) -> f32 { 43 | f32::from(x) * self.0 44 | } 45 | pub(crate) fn u16_to_px(self, x: u16) -> f32 { 46 | f32::from(x) * self.0 47 | } 48 | pub(crate) fn to_line_metrics(self, metrics: ttf_parser::LineMetrics) -> LineMetrics { 49 | LineMetrics { 50 | position: self.i16_to_px(metrics.position), 51 | thickness: self.i16_to_px(metrics.thickness), 52 | } 53 | } 54 | } 55 | 56 | /// Metrics for line marks 57 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 58 | pub struct LineMetrics { 59 | pub position: f32, 60 | pub thickness: f32, 61 | } 62 | -------------------------------------------------------------------------------- /src/fonts/mod.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License in the LICENSE-APACHE file or at: 4 | // https://www.apache.org/licenses/LICENSE-2.0 5 | 6 | //! Font selection and loading 7 | //! 8 | //! Fonts are managed by the [`FontLibrary`], of which a static singleton 9 | //! exists and can be accessed via [`library()`]. 10 | //! 11 | //! ### Font sizes 12 | //! 13 | //! Typically, font sizes are specified in "Points". Several other units and 14 | //! measures come into play here. Lets start with those dating back to the 15 | //! early printing press: 16 | //! 17 | //! - 1 *Point* = 1/72 inch (~0.35mm), by the usual DTP standard 18 | //! - 1 *Em* is the width of a capital `M` (inclusive of margin) in a font 19 | //! - The *point size* of a font refers to the number of *points* per *em* 20 | //! in this font 21 | //! 22 | //! Thus, with a "12 point font", one 'M' occupies 12/72 of an inch on paper. 23 | //! 24 | //! In digital typography, one must translate to/from pixel sizes. Here we have: 25 | //! 26 | //! - DPI (Dots Per Inch) is the number of pixels per inch 27 | //! - A *scale factor* is a measure of the number of pixels relative to a 28 | //! standard DPI, usually 96 29 | //! 30 | //! We introduce two measures used by this library: 31 | //! 32 | //! - DPP (Dots Per Point): `dpp = dpi / 72 = scale_factor × (96 / 72)` 33 | //! - DPEM (Dots Per Em): `dpem = point_size × dpp` 34 | //! 35 | //! Warning: on MacOS and Apple systems, a *point* sometimes refers to a 36 | //! (virtual) pixel, yielding `dpp = 1` (or `dpp = 2` on Retina screens, or 37 | //! something else entirely on iPhones). On any system, DPI/DPP/scale factor 38 | //! values may be set according to the user's taste rather than physical 39 | //! measurements. 40 | //! 41 | //! Finally, note that digital font files have an internally defined unit 42 | //! known as the *font unit*. We introduce one final unit: 43 | //! 44 | //! - [`crate::DPU`]: pixels per font unit 45 | 46 | use crate::GlyphId; 47 | 48 | mod attributes; 49 | mod face; 50 | mod library; 51 | mod resolver; 52 | 53 | pub use attributes::{FontStyle, FontWeight, FontWidth}; 54 | pub use face::{FaceRef, ScaledFaceRef}; 55 | pub use fontique::GenericFamily; 56 | pub use library::{FaceId, FaceStore, FontId, FontLibrary, InvalidFontId, NoFontMatch, library}; 57 | pub use resolver::*; 58 | 59 | impl From for ttf_parser::GlyphId { 60 | fn from(id: GlyphId) -> Self { 61 | ttf_parser::GlyphId(id.0) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.config/spellcheck.toml: -------------------------------------------------------------------------------- 1 | # Project settings where a Cargo.toml exists and is passed 2 | # ${CARGO_MANIFEST_DIR}/.config/spellcheck.toml 3 | 4 | # Also take into account developer comments 5 | dev_comments = false 6 | 7 | # Skip the README.md file as defined in the cargo manifest 8 | skip_readme = false 9 | 10 | [Hunspell] 11 | # lang and name of `.dic` file 12 | lang = "en_US" 13 | # OS specific additives 14 | # Linux: [ /usr/share/myspell ] 15 | # Windows: [] 16 | # macOS [ /home/alice/Libraries/hunspell, /Libraries/hunspell ] 17 | 18 | # Additional search paths, which take presedence over the default 19 | # os specific search dirs, searched in order, defaults last 20 | search_dirs = ["."] 21 | 22 | # Adds additional dictionaries, can be specified as 23 | # absolute paths or relative in the search dirs (in this order). 24 | # Relative paths are resolved relative to the configuration file 25 | # which is used. 26 | # Refer to `man 5 hunspell` 27 | # or https://www.systutorials.com/docs/linux/man/4-hunspell/#lbAE 28 | # on how to define a custom dictionary file. 29 | extra_dictionaries = ["extra.dict"] 30 | 31 | # If set to `true`, the OS specific default search paths 32 | # are skipped and only explicitly specified ones are used. 33 | skip_os_lookups = true 34 | 35 | # Use the builtin dictionaries if none were found in 36 | # in the configured lookup paths. 37 | # Usually combined with `skip_os_lookups=true` 38 | # to enforce the `builtin` usage for consistent 39 | # results across distributions and CI runs. 40 | # Setting this will still use the dictionaries 41 | # specified in `extra_dictionaries = [..]` 42 | # for topic specific lingo. 43 | use_builtin = true 44 | 45 | 46 | [Hunspell.quirks] 47 | # Transforms words that are provided by the tokenizer 48 | # into word fragments based on the capture groups which are to 49 | # be checked. 50 | # If no capture groups are present, the matched word is whitelisted. 51 | transform_regex = ["^'([^\\s])'$", "^[0-9]+x$"] 52 | # Accepts `alphabeta` variants if the checker provides a replacement suggestion 53 | # of `alpha-beta`. 54 | allow_concatenation = true 55 | # And the counterpart, which accepts words with dashes, when the suggestion has 56 | # recommendations without the dashes. This is less common. 57 | allow_dashed = false 58 | 59 | [NlpRules] 60 | # Allows the user to override the default included 61 | # exports of LanguageTool, with other custom 62 | # languages 63 | 64 | # override_rules = "/path/to/rules_binencoded.bin" 65 | # override_tokenizer = "/path/to/tokenizer_binencoded.bin" 66 | 67 | [Reflow] 68 | # Reflows doc comments to adhere to adhere to a given maximum line width limit. 69 | max_line_length = 100 70 | -------------------------------------------------------------------------------- /src/env.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License in the LICENSE-APACHE file or at: 4 | // https://www.apache.org/licenses/LICENSE-2.0 5 | 6 | //! KAS Rich-Text library — text-display environment 7 | 8 | /// Alignment of contents 9 | /// 10 | /// Note that alignment information is often passed as a `(horiz, vert)` pair. 11 | #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Ord, PartialOrd, Hash)] 12 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 13 | pub enum Align { 14 | /// Default alignment 15 | /// 16 | /// This is context dependent. For example, for Left-To-Right text it means 17 | /// `TL`; for things which want to stretch it may mean `Stretch`. 18 | #[default] 19 | Default, 20 | /// Align to top or left 21 | TL, 22 | /// Align to center 23 | Center, 24 | /// Align to bottom or right 25 | BR, 26 | /// Stretch to fill space 27 | /// 28 | /// For text, this is known as "justified alignment". 29 | Stretch, 30 | } 31 | 32 | /// Directionality of text 33 | /// 34 | /// Texts may be bi-directional as specified by Unicode Technical Report #9. 35 | #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Ord, PartialOrd, Hash)] 36 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 37 | #[repr(u8)] 38 | pub enum Direction { 39 | /// Auto-detect direction 40 | /// 41 | /// The text direction is inferred from the first strongly-directional 42 | /// character. In case no such character is found, the text will be 43 | /// left-to-right. 44 | #[default] 45 | Auto = 2, 46 | /// Auto-detect, default right-to-left 47 | /// 48 | /// The text direction is inferred from the first strongly-directional 49 | /// character. In case no such character is found, the text will be 50 | /// right-to-left. 51 | AutoRtl = 3, 52 | /// The base text direction is left-to-right 53 | /// 54 | /// If the text contains right-to-left content, this will be considered an 55 | /// embedded right-to-left run. Non-directional leading and trailing 56 | /// characters (e.g. a full stop) will normally not be included within this 57 | /// right-to-left section. 58 | /// 59 | /// This uses Unicode TR9 HL1 to set an explicit paragraph embedding level of 0. 60 | Ltr = 0, 61 | /// The base text direction is right-to-left 62 | /// 63 | /// If the text contains left-to-right content, this will be considered an 64 | /// embedded left-to-right run. Non-directional leading and trailing 65 | /// characters (e.g. a full stop) will normally not be included within this 66 | /// left-to-right section. 67 | /// 68 | /// This uses Unicode TR9 HL1 to set an explicit paragraph embedding level of 1. 69 | Rtl = 1, 70 | } 71 | -------------------------------------------------------------------------------- /design/complex-layout.md: -------------------------------------------------------------------------------- 1 | Complex text layout 2 | =========== 3 | 4 | KAS-text now has partial support for Markdown formatting: bold and italic emphasis, headings (font size), and strikethrough (in progress). Indentation is supported via `\t`, but this is only suitable for indenting the first line of a wrapped paragraph. 5 | 6 | This leaves several things poorly or not supported: 7 | 8 | - vertical spacing between paragraphs/headings/blocks must currently be a whole number of lines 9 | - horizontal rules are not supported 10 | - list items with wrapped text do not indent subsequent lines 11 | - block quotes do not use a different colour (although arguably this would be possible without extra kas-text functionality) 12 | - code samples do not support coloured background boxes 13 | - tables are not supported 14 | - embedded images are not supported 15 | 16 | Potentially kas-text could be extended with enough text-layout features to support all the above (except tables) without too much trouble, but is this the right way to go? Full support for Markdown is not a goal for text; rather Markdown is merely a convenient way to input (some) formatted text. Beyond that we have HTML and perhaps other forms of rich text, and potentially yet more layout features (HTML is after all quite flexible). 17 | 18 | Also to consider is how to make kas-text scalable to larger text documents: support for updating individual paragraphs/other parts of a text (potentially plus support for only partially laying out large texts), or alternately facilitating use of a separate `TextDisplay` object for each paragraph. Or... perhaps this is simply all beyond the sensible scope of the project? 19 | 20 | 21 | Text sizing 22 | ------------ 23 | 24 | Our sizing approach: 25 | 26 | 1. We check horizontal size requirements (may be a small or may be very large with a full paragraph). 27 | Small improvement: start with a width limit; as soon as this is exceeded stop doing further 28 | text layout. Caveat: long texts of many short lines will still be processed (unless we also 29 | have a small height limit, e.g. 1 pixel, during this step). 30 | 2. We check vertical size requirements given a horizontal size. 31 | 3. We prepare for drawing, hopefully reusing the layout from the previous step. 32 | 33 | ### Partial sizing 34 | 35 | Potentially we can take a few shortcuts somewhere: 36 | 37 | - given a column of text widgets, we have a lower-bound on width (initially 38 | zero, updated for each row processed) and an upper-bound (the point at 39 | which wrapping is forced, or perhaps one derived from the window size); 40 | if these meet then we know there is no need to check further lines 41 | - we do not need to layout lines not currently visible, except to compute 42 | height for scrollbar sizing, but we may assume a minimum height of one 43 | line for each row and beyond a certain total height the scrollbar size is 44 | unaffected anyway (although relative position is still affected) 45 | 46 | Alternatively we might simply use arbitrary limits on the number of lines we 47 | check when sizing a column (e.g. 20 rows) and make assumptions about the rest 48 | based on these; this potentially has caveats however (e.g. if somehow the entire 49 | contents are visible, size will be incorrect). Perhaps when sizing the widget 50 | should be given an upper bound on the size? 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Kas Text 2 | ======== 3 | 4 | [![kas](https://img.shields.io/badge/GitHub-kas-blueviolet)](https://github.com/kas-gui/kas/) 5 | [![Docs](https://docs.rs/kas-text/badge.svg)](https://docs.rs/kas-text/) 6 | 7 | A pure-Rust rich-text processing library suitable for KAS and other GUI tools. 8 | 9 | Kas-text is intended to address the needs of common GUI text tasks: fast, able to handle plain text well in common scripts including common effects like bold text and underline, along with support for editing plain texts. 10 | 11 | More on what Kas-text does do: 12 | 13 | - [x] Font discovery via [Fontique](https://github.com/linebender/parley?tab=readme-ov-file#fontique) 14 | - [x] Font loading and management 15 | - [x] Script-aware font selection and glyph-level fallback 16 | - [x] Text layout via a choice of [rustybuzz](https://github.com/harfbuzz/rustybuzz) or a simple built-in shaper 17 | - [ ] Vertical text support 18 | - [x] Supports bi-directional texts 19 | - [x] A low-level API for text editing including logical-order and mouse navigation 20 | - [ ] Visual-order navigation 21 | - [ ] Sub-ligature navigation 22 | - [x] Font styles (weight, width, italic) 23 | - [x] Text decorations: highlight range, underline 24 | - [x] Decently optimized: good enough for snappy GUIs 25 | 26 | Rich text support is limited to changing font properties (e.g. weight, italic), size, family and underline/strikethrough decorations. A (very limited) Markdown processor is included to facilitate construction of these texts using the lower-level `FormattableText` trait. 27 | 28 | Glyph rastering and painting is not implemented here, though `kas-text` can provide font references for [Swash] and (optionally) [ab_glyph] libraries. Check the [`kas-wgpu`] code for an example of rastering and painting. 29 | 30 | Text editing is only supported via a low-level API. [`kas_widgets::edit::EditField`](https://docs.rs/kas-widgets/latest/kas_widgets/edit/struct.EditField.html) is a simple editor built over this API. 31 | 32 | 33 | Examples 34 | -------- 35 | 36 | Since `kas-text` only concerns text-layout, all examples here are courtesy of KAS GUI. See [the examples directory](https://github.com/kas-gui/kas/tree/master/examples). 37 | 38 | ![BIDI layout and editing](https://github.com/kas-gui/data-dump/blob/master/screenshots/layout.png) 39 | ![Markdown](https://github.com/kas-gui/data-dump/blob/master/screenshots/markdown.png) 40 | 41 | 42 | Alternatives 43 | ------------ 44 | 45 | Pure-Rust alternatives for typesetting and rendering text: 46 | 47 | - [Parley] provides an API for implementing rich text layout. It is backed by [Swash]. 48 | - [COSMIC Text] provides advanced text shaping, layout, and rendering wrapped up into a simple abstraction. 49 | - [glyph_brush](https://github.com/alexheretic/glyph-brush) is a fast caching text render library using [ab_glyph]. 50 | 51 | 52 | Contributing 53 | -------- 54 | 55 | Contributions are welcome. For the less straightforward contributions it is 56 | advisable to discuss in an issue before creating a pull-request. 57 | 58 | Testing is done in an ad-hoc manner using examples. The [Layout Demo](https://github.com/kas-gui/kas/tree/master/examples#layout) often proves useful for quick tests. It helps to use a patch like that below in `kas/Cargo.toml`: 59 | ```toml 60 | [patch.crates-io.kas-text] 61 | path = "../kas-text" 62 | ``` 63 | 64 | 65 | Copyright and License 66 | ------- 67 | 68 | The [COPYRIGHT](COPYRIGHT) file includes a list of contributors who claim 69 | copyright on this project. This list may be incomplete; new contributors may 70 | optionally add themselves to this list. 71 | 72 | The KAS library is published under the terms of the Apache License, Version 2.0. 73 | You may obtain a copy of this license from the [LICENSE](LICENSE) file or on 74 | the following web page: 75 | 76 | 77 | [ab_glyph]: https://github.com/alexheretic/ab-glyph 78 | [Swash]: https://github.com/dfrg/swash 79 | [Parley]: https://github.com/linebender/parley 80 | [COSMIC Text]: https://github.com/linebender/parley 81 | [`kas-wgpu`]: https://crates.io/crates/kas-wgpu 82 | -------------------------------------------------------------------------------- /src/fonts/face.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License in the LICENSE-APACHE file or at: 4 | // https://www.apache.org/licenses/LICENSE-2.0 5 | 6 | //! Font face types 7 | 8 | use crate::GlyphId; 9 | use crate::conv::{DPU, LineMetrics}; 10 | use ttf_parser::Face; 11 | 12 | /// Handle to a loaded font face 13 | #[derive(Copy, Clone, Debug)] 14 | pub struct FaceRef<'a>(pub(crate) &'a Face<'a>); 15 | 16 | impl<'a> FaceRef<'a> { 17 | /// Get glyph identifier for a char 18 | /// 19 | /// If the char is not found, `GlyphId(0)` is returned (the 'missing glyph' 20 | /// representation). 21 | #[inline] 22 | pub fn glyph_index(&self, c: char) -> GlyphId { 23 | // GlyphId 0 is required to be a special glyph representing a missing 24 | // character (see cmap table / TrueType specification). 25 | GlyphId(self.0.glyph_index(c).map(|id| id.0).unwrap_or(0)) 26 | } 27 | 28 | /// Convert `dpem` to `dpu` 29 | /// 30 | /// Output: a font-specific scale. 31 | /// 32 | /// Input: `dpem` is pixels/em 33 | /// 34 | /// ```none 35 | /// dpem 36 | /// = pt_size × dpp 37 | /// = pt_size × dpi / 72 38 | /// = pt_size × scale_factor × (96 / 72) 39 | /// ``` 40 | #[inline] 41 | pub fn dpu(self, dpem: f32) -> DPU { 42 | DPU(dpem / f32::from(self.0.units_per_em())) 43 | } 44 | 45 | /// Get a scaled reference 46 | /// 47 | /// Units: `dpem` is dots (pixels) per Em (module documentation). 48 | #[inline] 49 | pub fn scale_by_dpem(self, dpem: f32) -> ScaledFaceRef<'a> { 50 | ScaledFaceRef(self.0, self.dpu(dpem)) 51 | } 52 | 53 | /// Get a scaled reference 54 | /// 55 | /// Units: `dpu` is dots (pixels) per font-unit (see module documentation). 56 | #[inline] 57 | pub fn scale_by_dpu(self, dpu: DPU) -> ScaledFaceRef<'a> { 58 | ScaledFaceRef(self.0, dpu) 59 | } 60 | } 61 | 62 | /// Handle to a loaded font face 63 | /// 64 | /// TODO: verify whether these values need adjustment for variations. 65 | #[derive(Copy, Clone, Debug)] 66 | pub struct ScaledFaceRef<'a>(&'a Face<'a>, DPU); 67 | impl<'a> ScaledFaceRef<'a> { 68 | /// Unscaled face 69 | #[inline] 70 | pub fn face(&self) -> FaceRef<'_> { 71 | FaceRef(self.0) 72 | } 73 | 74 | /// Scale 75 | #[inline] 76 | pub fn dpu(&self) -> DPU { 77 | self.1 78 | } 79 | 80 | /// Horizontal advancement after this glyph, without shaping or kerning 81 | #[inline] 82 | pub fn h_advance(&self, id: GlyphId) -> f32 { 83 | let x = self.0.glyph_hor_advance(id.into()).unwrap(); 84 | self.1.u16_to_px(x) 85 | } 86 | 87 | /// Horizontal side bearing 88 | /// 89 | /// If unspecified by the font this resolves to 0. 90 | #[inline] 91 | pub fn h_side_bearing(&self, id: GlyphId) -> f32 { 92 | let x = self.0.glyph_hor_side_bearing(id.into()).unwrap_or(0); 93 | self.1.i16_to_px(x) 94 | } 95 | 96 | /// Ascender 97 | #[inline] 98 | pub fn ascent(&self) -> f32 { 99 | self.1.i16_to_px(self.0.ascender()) 100 | } 101 | 102 | /// Descender 103 | #[inline] 104 | pub fn descent(&self) -> f32 { 105 | self.1.i16_to_px(self.0.descender()) 106 | } 107 | 108 | /// Line gap 109 | #[inline] 110 | pub fn line_gap(&self) -> f32 { 111 | self.1.i16_to_px(self.0.line_gap()) 112 | } 113 | 114 | /// Line height 115 | #[inline] 116 | pub fn height(&self) -> f32 { 117 | self.1.i16_to_px(self.0.height()) 118 | } 119 | 120 | /// Metrics for underline 121 | #[inline] 122 | pub fn underline_metrics(&self) -> Option { 123 | self.0 124 | .underline_metrics() 125 | .map(|m| self.1.to_line_metrics(m)) 126 | } 127 | 128 | /// Metrics for strike-through 129 | #[inline] 130 | pub fn strikethrough_metrics(&self) -> Option { 131 | self.0 132 | .strikeout_metrics() 133 | .map(|m| self.1.to_line_metrics(m)) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/data.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License in the LICENSE-APACHE file or at: 4 | // https://www.apache.org/licenses/LICENSE-2.0 5 | 6 | //! KAS Rich-Text library — simple data types 7 | 8 | use crate::conv::{to_u32, to_usize}; 9 | 10 | /// 2D vector (position/size/offset) over `f32` 11 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 12 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 13 | pub struct Vec2(pub f32, pub f32); 14 | 15 | impl Vec2 { 16 | /// Zero 17 | pub const ZERO: Vec2 = Vec2(0.0, 0.0); 18 | 19 | /// Positive infinity 20 | pub const INFINITY: Vec2 = Vec2(f32::INFINITY, f32::INFINITY); 21 | 22 | /// Take the absolute value of each component 23 | #[inline] 24 | pub fn abs(self) -> Self { 25 | Vec2(self.0.abs(), self.1.abs()) 26 | } 27 | 28 | /// Return the minimum, component-wise 29 | #[inline] 30 | pub fn min(self, other: Self) -> Self { 31 | Vec2(self.0.min(other.0), self.1.min(other.1)) 32 | } 33 | 34 | /// Return the maximum, component-wise 35 | #[inline] 36 | pub fn max(self, other: Self) -> Self { 37 | Vec2(self.0.max(other.0), self.1.max(other.1)) 38 | } 39 | 40 | /// Whether both components are finite 41 | #[inline] 42 | pub fn is_finite(self) -> bool { 43 | self.0.is_finite() && self.1.is_finite() 44 | } 45 | } 46 | 47 | impl std::ops::Add for Vec2 { 48 | type Output = Self; 49 | 50 | #[inline] 51 | fn add(self, other: Self) -> Self { 52 | Vec2(self.0 + other.0, self.1 + other.1) 53 | } 54 | } 55 | impl std::ops::AddAssign for Vec2 { 56 | #[inline] 57 | fn add_assign(&mut self, rhs: Self) { 58 | self.0 += rhs.0; 59 | self.1 += rhs.1; 60 | } 61 | } 62 | 63 | impl std::ops::Sub for Vec2 { 64 | type Output = Self; 65 | 66 | #[inline] 67 | fn sub(self, other: Self) -> Self { 68 | Vec2(self.0 - other.0, self.1 - other.1) 69 | } 70 | } 71 | impl std::ops::SubAssign for Vec2 { 72 | #[inline] 73 | fn sub_assign(&mut self, rhs: Self) { 74 | self.0 -= rhs.0; 75 | self.1 -= rhs.1; 76 | } 77 | } 78 | 79 | impl From for (f32, f32) { 80 | fn from(size: Vec2) -> Self { 81 | (size.0, size.1) 82 | } 83 | } 84 | 85 | /// Range type 86 | /// 87 | /// Essentially this is just a `std::ops::Range`, but with convenient 88 | /// implementations. 89 | /// 90 | /// Note that we consider `u32` large enough for any text we wish to display 91 | /// and the library is too complex to be useful on 16-bit CPUs, so using `u32` 92 | /// makes more sense than `usize`. 93 | #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] 94 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 95 | pub struct Range { 96 | pub start: u32, 97 | pub end: u32, 98 | } 99 | 100 | impl Range { 101 | /// The start, as `usize` 102 | pub fn start(self) -> usize { 103 | to_usize(self.start) 104 | } 105 | 106 | /// The end, as `usize` 107 | pub fn end(self) -> usize { 108 | to_usize(self.end) 109 | } 110 | 111 | /// True if the range is empty 112 | pub fn is_empty(self) -> bool { 113 | self.start >= self.end 114 | } 115 | 116 | /// The number of iterable items, as `usize` 117 | pub fn len(self) -> usize { 118 | to_usize(self.end) - to_usize(self.start) 119 | } 120 | 121 | /// Convert to a standard range 122 | pub fn to_std(self) -> std::ops::Range { 123 | to_usize(self.start)..to_usize(self.end) 124 | } 125 | 126 | /// Convert to `usize` and iterate 127 | pub fn iter(self) -> impl Iterator { 128 | self.to_std() 129 | } 130 | } 131 | 132 | impl std::ops::Index for String { 133 | type Output = str; 134 | 135 | fn index(&self, range: Range) -> &str { 136 | let range = std::ops::Range::::from(range); 137 | &self[range] 138 | } 139 | } 140 | 141 | impl std::ops::Index for str { 142 | type Output = str; 143 | 144 | fn index(&self, range: Range) -> &str { 145 | let range = std::ops::Range::::from(range); 146 | &self[range] 147 | } 148 | } 149 | 150 | impl std::ops::Index for [T] { 151 | type Output = [T]; 152 | 153 | fn index(&self, range: Range) -> &[T] { 154 | let range = std::ops::Range::::from(range); 155 | &self[range] 156 | } 157 | } 158 | 159 | impl std::ops::IndexMut for String { 160 | fn index_mut(&mut self, range: Range) -> &mut str { 161 | let range = std::ops::Range::::from(range); 162 | &mut self[range] 163 | } 164 | } 165 | 166 | impl std::ops::IndexMut for str { 167 | fn index_mut(&mut self, range: Range) -> &mut str { 168 | let range = std::ops::Range::::from(range); 169 | &mut self[range] 170 | } 171 | } 172 | 173 | impl std::ops::IndexMut for [T] { 174 | fn index_mut(&mut self, range: Range) -> &mut [T] { 175 | let range = std::ops::Range::::from(range); 176 | &mut self[range] 177 | } 178 | } 179 | 180 | impl From for std::ops::Range { 181 | fn from(range: Range) -> std::ops::Range { 182 | to_usize(range.start)..to_usize(range.end) 183 | } 184 | } 185 | 186 | impl From> for Range { 187 | fn from(range: std::ops::Range) -> Range { 188 | Range { 189 | start: range.start, 190 | end: range.end, 191 | } 192 | } 193 | } 194 | 195 | impl From> for Range { 196 | fn from(range: std::ops::Range) -> Range { 197 | Range { 198 | start: to_u32(range.start), 199 | end: to_u32(range.end), 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/format.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License in the LICENSE-APACHE file or at: 4 | // https://www.apache.org/licenses/LICENSE-2.0 5 | 6 | //! Parsers for formatted text 7 | 8 | use crate::fonts::FontSelector; 9 | use crate::{Effect, OwningVecIter}; 10 | #[allow(unused)] 11 | use crate::{Text, TextDisplay}; // for doc-links 12 | 13 | mod plain; 14 | 15 | #[cfg(feature = "markdown")] 16 | mod markdown; 17 | #[cfg(feature = "markdown")] 18 | pub use markdown::{Error as MarkdownError, Markdown}; 19 | 20 | /// Text, optionally with formatting data 21 | /// 22 | /// Any `F: FormattableText` automatically support [`FormattableTextDyn`]. 23 | /// Implement either this or [`FormattableTextDyn`], not both. 24 | pub trait FormattableText: std::cmp::PartialEq + std::fmt::Debug { 25 | type FontTokenIter<'a>: Iterator 26 | where 27 | Self: 'a; 28 | 29 | /// Length of text 30 | /// 31 | /// Default implementation uses [`FormattableText::as_str`]. 32 | #[inline] 33 | fn str_len(&self) -> usize { 34 | self.as_str().len() 35 | } 36 | 37 | /// Access whole text as contiguous `str` 38 | fn as_str(&self) -> &str; 39 | 40 | /// Construct an iterator over formatting items 41 | /// 42 | /// It is expected that [`FontToken::start`] of yielded items is strictly 43 | /// increasing; if not, formatting may not be applied correctly. 44 | /// 45 | /// The default [font size][crate::Text::set_font_size] (`dpem`) is passed 46 | /// as a reference. 47 | /// 48 | /// For plain text this iterator will be empty. 49 | fn font_tokens<'a>(&'a self, dpem: f32) -> Self::FontTokenIter<'a>; 50 | 51 | /// Get the sequence of effect tokens 52 | /// 53 | /// This method has some limitations: (1) it may only return a reference to 54 | /// an existing sequence, (2) effect tokens cannot be generated dependent 55 | /// on input state, and (3) it does not incorporate color information. For 56 | /// most uses it should still be sufficient, but for other cases it may be 57 | /// preferable not to use this method (use a dummy implementation returning 58 | /// `&[]` and use inherent methods on the text object via [`Text::text`]). 59 | fn effect_tokens(&self) -> &[Effect]; 60 | } 61 | 62 | impl FormattableText for &F { 63 | type FontTokenIter<'a> 64 | = F::FontTokenIter<'a> 65 | where 66 | Self: 'a; 67 | 68 | fn as_str(&self) -> &str { 69 | F::as_str(self) 70 | } 71 | 72 | fn font_tokens<'a>(&'a self, dpem: f32) -> Self::FontTokenIter<'a> { 73 | F::font_tokens(self, dpem) 74 | } 75 | 76 | fn effect_tokens(&self) -> &[Effect] { 77 | F::effect_tokens(self) 78 | } 79 | } 80 | 81 | /// Text, optionally with formatting data 82 | /// 83 | /// This is an object-safe version of the [`FormattableText`] trait (i.e. 84 | /// `dyn FormattableTextDyn` is a valid type). 85 | /// 86 | /// This trait is auto-implemented for every implementation of [`FormattableText`]. 87 | /// The type `&dyn FormattableTextDyn` implements [`FormattableText`]. 88 | /// Implement either this or (preferably) [`FormattableText`], not both. 89 | pub trait FormattableTextDyn: std::fmt::Debug { 90 | /// Produce a boxed clone of self 91 | fn clone_boxed(&self) -> Box; 92 | 93 | /// Length of text 94 | fn str_len(&self) -> usize; 95 | 96 | /// Access whole text as contiguous `str` 97 | fn as_str(&self) -> &str; 98 | 99 | /// Construct an iterator over formatting items 100 | /// 101 | /// It is expected that [`FontToken::start`] of yielded items is strictly 102 | /// increasing; if not, formatting may not be applied correctly. 103 | /// 104 | /// The default [font size][crate::Text::set_font_size] (`dpem`) is passed 105 | /// as a reference. 106 | /// 107 | /// For plain text this iterator will be empty. 108 | fn font_tokens(&self, dpem: f32) -> OwningVecIter; 109 | 110 | /// Get the sequence of effect tokens 111 | /// 112 | /// This method has some limitations: (1) it may only return a reference to 113 | /// an existing sequence, (2) effect tokens cannot be generated dependent 114 | /// on input state, and (3) it does not incorporate color information. For 115 | /// most uses it should still be sufficient, but for other cases it may be 116 | /// preferable not to use this method (use a dummy implementation returning 117 | /// `&[]` and use inherent methods on the text object via [`Text::text`]). 118 | fn effect_tokens(&self) -> &[Effect]; 119 | } 120 | 121 | impl FormattableTextDyn for F { 122 | fn clone_boxed(&self) -> Box { 123 | Box::new(self.clone()) 124 | } 125 | 126 | fn str_len(&self) -> usize { 127 | FormattableText::str_len(self) 128 | } 129 | fn as_str(&self) -> &str { 130 | FormattableText::as_str(self) 131 | } 132 | 133 | fn font_tokens(&self, dpem: f32) -> OwningVecIter { 134 | let iter = FormattableText::font_tokens(self, dpem); 135 | OwningVecIter::new(iter.collect()) 136 | } 137 | 138 | fn effect_tokens(&self) -> &[Effect] { 139 | FormattableText::effect_tokens(self) 140 | } 141 | } 142 | 143 | /// References to [`FormattableTextDyn`] always compare unequal 144 | impl<'t> PartialEq for &'t dyn FormattableTextDyn { 145 | fn eq(&self, _: &Self) -> bool { 146 | false 147 | } 148 | } 149 | 150 | impl<'t> FormattableText for &'t dyn FormattableTextDyn { 151 | type FontTokenIter<'a> 152 | = OwningVecIter 153 | where 154 | Self: 'a; 155 | 156 | #[inline] 157 | fn str_len(&self) -> usize { 158 | FormattableTextDyn::str_len(*self) 159 | } 160 | 161 | #[inline] 162 | fn as_str(&self) -> &str { 163 | FormattableTextDyn::as_str(*self) 164 | } 165 | 166 | #[inline] 167 | fn font_tokens(&self, dpem: f32) -> OwningVecIter { 168 | FormattableTextDyn::font_tokens(*self, dpem) 169 | } 170 | 171 | fn effect_tokens(&self) -> &[Effect] { 172 | FormattableTextDyn::effect_tokens(*self) 173 | } 174 | } 175 | 176 | impl Clone for Box { 177 | fn clone(&self) -> Self { 178 | (**self).clone_boxed() 179 | } 180 | } 181 | 182 | /// Font formatting token 183 | #[derive(Clone, Debug, PartialEq)] 184 | pub struct FontToken { 185 | /// Index in text at which formatting becomes active 186 | /// 187 | /// (Note that we use `u32` not `usize` since it can be assumed text length 188 | /// will never exceed `u32::MAX`.) 189 | pub start: u32, 190 | /// Font size, in dots-per-em (pixel width of an 'M') 191 | /// 192 | /// This may be calculated from point size as `pt_size * dpp`, where `dpp` 193 | /// is the number of pixels per point (see [`crate::fonts`] documentation). 194 | pub dpem: f32, 195 | /// Font selector 196 | pub font: FontSelector, 197 | } 198 | -------------------------------------------------------------------------------- /src/fonts/resolver.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License in the LICENSE-APACHE file or at: 4 | // https://www.apache.org/licenses/LICENSE-2.0 5 | 6 | //! KAS Rich-Text library — font resolver 7 | //! 8 | //! Many items are copied from font-kit to avoid any public dependency. 9 | 10 | use super::{FontStyle, FontWeight, FontWidth}; 11 | use fontique::{ 12 | Attributes, Collection, FamilyId, GenericFamily, QueryFamily, QueryFont, QueryStatus, Script, 13 | SourceCache, 14 | }; 15 | use log::debug; 16 | #[cfg(feature = "serde")] 17 | use serde::{Deserialize, Serialize}; 18 | use std::collections::HashMap; 19 | use std::collections::hash_map::Entry; 20 | use std::hash::{BuildHasher, Hash}; 21 | 22 | /// A tool to resolve a single font face given a family and style 23 | pub struct Resolver { 24 | collection: Collection, 25 | cache: SourceCache, 26 | /// Cached family selectors: 27 | families: HashMap, 28 | } 29 | 30 | impl Resolver { 31 | pub(crate) fn new() -> Self { 32 | Resolver { 33 | collection: Collection::new(Default::default()), 34 | cache: SourceCache::new(Default::default()), 35 | families: HashMap::new(), 36 | } 37 | } 38 | 39 | /// Get a font family name from an id 40 | pub fn font_family(&mut self, id: FamilyId) -> Option<&str> { 41 | self.collection.family_name(id) 42 | } 43 | 44 | /// Get a font family name for some generic font family 45 | pub fn font_family_from_generic(&mut self, generic: GenericFamily) -> Option<&str> { 46 | let id = self.collection.generic_families(generic).next()?; 47 | self.collection.family_name(id) 48 | } 49 | 50 | /// Construct a [`FamilySelector`] for the given `families` 51 | pub fn select_families(&mut self, families: I) -> FamilySelector 52 | where 53 | I: IntoIterator, 54 | F: Into, 55 | { 56 | let set = FamilySet(families.into_iter().map(|f| f.into()).collect()); 57 | let hash = self.families.hasher().hash_one(&set); 58 | let sel = FamilySelector(hash | (1 << 63)); 59 | 60 | match self.families.entry(sel) { 61 | Entry::Vacant(entry) => { 62 | entry.insert(set); 63 | } 64 | Entry::Occupied(entry) => { 65 | // Unlikely but possible case: 66 | log::warn!( 67 | "Resolver::select_families: hash collision for family selector {set:?} and {:?}", 68 | entry.get() 69 | ); 70 | // TODO: inject a random value into the FamilySet and rehash? 71 | } 72 | } 73 | 74 | sel 75 | } 76 | 77 | /// Resolve families from a [`FamilySelector`] 78 | /// 79 | /// Returns an empty [`Vec`] on error. 80 | pub fn resolve_families(&self, selector: &FamilySelector) -> Vec { 81 | if let Some(gf) = selector.as_generic() { 82 | vec![FamilyName::Generic(gf)] 83 | } else if let Some(set) = self.families.get(selector) { 84 | set.0.clone() 85 | } else { 86 | vec![] 87 | } 88 | } 89 | } 90 | 91 | /// A family name 92 | #[derive(Clone, Debug, Eq, PartialEq, Hash)] 93 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 94 | pub enum FamilyName { 95 | /// A family named with a `String` 96 | Named(String), 97 | /// A generic family 98 | #[cfg_attr(feature = "serde", serde(with = "remote::GenericFamily"))] 99 | Generic(GenericFamily), 100 | } 101 | 102 | impl From for FamilyName { 103 | fn from(gf: GenericFamily) -> Self { 104 | FamilyName::Generic(gf) 105 | } 106 | } 107 | 108 | impl<'a> From<&'a FamilyName> for QueryFamily<'a> { 109 | fn from(family: &'a FamilyName) -> Self { 110 | match family { 111 | FamilyName::Named(name) => QueryFamily::Named(name), 112 | FamilyName::Generic(gf) => QueryFamily::Generic(*gf), 113 | } 114 | } 115 | } 116 | 117 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 118 | struct FamilySet(Vec); 119 | 120 | /// A (cached) family selector 121 | /// 122 | /// This may be constructed directly for some generic families; for other 123 | /// families use [`Resolver::select_families`]. 124 | /// 125 | /// This is a small, `Copy` type (a newtype over `u64`). 126 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 127 | pub struct FamilySelector(u64); 128 | 129 | impl FamilySelector { 130 | /// Use a serif font 131 | pub const SERIF: FamilySelector = FamilySelector(0); 132 | 133 | /// Use a sans-serif font 134 | pub const SANS_SERIF: FamilySelector = FamilySelector(1); 135 | 136 | /// Use a monospace font 137 | pub const MONOSPACE: FamilySelector = FamilySelector(2); 138 | 139 | /// Use a cursive font 140 | pub const CURSIVE: FamilySelector = FamilySelector(3); 141 | 142 | /// Use the system UI font 143 | pub const SYSTEM_UI: FamilySelector = FamilySelector(5); 144 | 145 | /// Use an emoji font 146 | pub const FANG_SONG: FamilySelector = FamilySelector(12); 147 | 148 | fn as_generic(self) -> Option { 149 | match self.0 { 150 | 0 => Some(GenericFamily::Serif), 151 | 1 => Some(GenericFamily::SansSerif), 152 | 2 => Some(GenericFamily::Monospace), 153 | 3 => Some(GenericFamily::Cursive), 154 | 5 => Some(GenericFamily::SystemUi), 155 | 12 => Some(GenericFamily::FangSong), 156 | _ => None, 157 | } 158 | } 159 | } 160 | 161 | /// Default-constructs to [`FamilySelector::SYSTEM_UI`]. 162 | impl Default for FamilySelector { 163 | fn default() -> Self { 164 | FamilySelector::SYSTEM_UI 165 | } 166 | } 167 | 168 | /// A font face selection tool 169 | /// 170 | /// This tool selects a font according to the given criteria from available 171 | /// system fonts. Selection criteria are based on CSS. 172 | /// 173 | /// This can be converted [from](From) a [`FamilySelector`], selecting the 174 | /// default styles. 175 | #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)] 176 | pub struct FontSelector { 177 | /// Family selector 178 | pub family: FamilySelector, 179 | /// Weight 180 | pub weight: FontWeight, 181 | /// Width 182 | pub width: FontWidth, 183 | /// Italic / oblique style 184 | pub style: FontStyle, 185 | } 186 | 187 | impl FontSelector { 188 | /// Synonym for default 189 | /// 190 | /// Without further parametrization, this will select a generic sans-serif 191 | /// font which should be suitable for most uses. 192 | #[inline] 193 | pub fn new() -> Self { 194 | FontSelector::default() 195 | } 196 | 197 | /// Resolve font faces for each matching font 198 | /// 199 | /// All font faces matching steps 1-4 will be returned through the `add_face` closure. 200 | pub(crate) fn select(&self, resolver: &mut Resolver, script: Script, add_face: F) 201 | where 202 | F: FnMut(&QueryFont) -> QueryStatus, 203 | { 204 | let mut query = resolver.collection.query(&mut resolver.cache); 205 | if let Some(gf) = self.family.as_generic() { 206 | debug!( 207 | "select: Script::{:?}, GenericFamily::{:?}, {:?}, {:?}, {:?}", 208 | &script, gf, &self.weight, &self.width, &self.style 209 | ); 210 | 211 | query.set_families([gf]); 212 | } else if let Some(set) = resolver.families.get(&self.family) { 213 | debug!( 214 | "select: Script::{:?}, {:?}, {:?}, {:?}, {:?}", 215 | &script, set, &self.weight, &self.width, &self.style 216 | ); 217 | 218 | query.set_families(set.0.iter()); 219 | } 220 | 221 | query.set_attributes(Attributes { 222 | width: self.width.into(), 223 | style: self.style.into(), 224 | weight: self.weight.into(), 225 | }); 226 | 227 | query.set_fallbacks(script); 228 | 229 | query.matches_with(add_face); 230 | } 231 | } 232 | 233 | impl From for FontSelector { 234 | #[inline] 235 | fn from(family: FamilySelector) -> Self { 236 | FontSelector { 237 | family, 238 | ..Default::default() 239 | } 240 | } 241 | } 242 | 243 | // See: https://serde.rs/remote-derive.html 244 | #[cfg(feature = "serde")] 245 | mod remote { 246 | use serde::{Deserialize, Serialize}; 247 | 248 | #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)] 249 | #[repr(u8)] 250 | #[serde(remote = "fontique::GenericFamily")] 251 | pub enum GenericFamily { 252 | Serif = 0, 253 | SansSerif = 1, 254 | Monospace = 2, 255 | Cursive = 3, 256 | Fantasy = 4, 257 | SystemUi = 5, 258 | UiSerif = 6, 259 | UiSansSerif = 7, 260 | UiMonospace = 8, 261 | UiRounded = 9, 262 | Emoji = 10, 263 | Math = 11, 264 | FangSong = 12, 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /design/requirements.md: -------------------------------------------------------------------------------- 1 | Rich Text processing 2 | ======== 3 | 4 | Objective: allow text to be input in various forms (plain text, markdown, html) 5 | then displayed in a given enviroment (an AABB plus default font selection, 6 | including properties such as size and colour). 7 | 8 | Essentially, this is meant to abstract over the entire font processing line 9 | except actual rendering, spitting out either a list of pre-positioned glyphs or 10 | the input values required by compatible rendering libraries. 11 | [Modern text rendering with Linux](https://mrandri19.github.io/2019/07/24/modern-text-rendering-linux-overview.html) 12 | 13 | Scope: 14 | 15 | - Rich-text representation: able to build a model of a rich text document 16 | or paragraph. It may be desirable to let this model cache transformations 17 | needed to fit its contents into a given display environment. 18 | - Font management and selection: maintain a collection of loaded fonts and 19 | assign each piece of text a `FontId` based on font family/property 20 | restrictions. Initially this will lean heavily on [`font-kit`]. 21 | - Rich-text parsing: able to convert input formats (e.g. via Markdown and 22 | HTML) to the internal model. (Later these translators may be moved to 23 | external libraries.) 24 | - Bidirectional text support: this will be omitted in initial versions but the 25 | design should support incorporating this in an update. 26 | - Text layout and shaping: the design should be compatible with external 27 | text shapers such as HarfBuzz, although it may not initially support them. 28 | This implies that the output sent to the rasteriser should be in the form of 29 | positioned glyphs. The library may embed a simple shaper, comparable with 30 | [`glyph_brush_layout`]. 31 | - Line-wrapping: support for this must be integrated with the bidirectional 32 | algorithm; additionally, it is required for multi-line text editing. 33 | - Text metrics: able to calculate text bounds and translate string indices to 34 | glyph coordinates and vice-versa. 35 | - Embedded objects: ideally this should support user-defined objects such as 36 | images and widgets being embedded within text. 37 | 38 | Dependencies: 39 | 40 | - [`font-kit`] (possibly only for font selection only) 41 | - [`ab_glyph`] (possibly) 42 | - [`unicode-linebreak`] for determining line-break positions 43 | - [`unicode-bidi`] — but likely not since its API appears incompatible with 44 | rich text and embedded objects 45 | - [`harfbuzz_rs`] (possibly only later and behind a feature gate) 46 | - [`allsorts`] (possibly only later and behind a feature gate) 47 | - [`palette`] for colours? 48 | 49 | Dependent libraries: 50 | 51 | - [`kas`] is my reason for building this, but hopefully it will be useful for 52 | [`iced`] and other libraries (GUIs, games) 53 | - some library binding this with [`wgpu_glyph`] (probably this will be simple 54 | enough to embed in `kas_wgpu` and `iced_wgpu`) 55 | 56 | 57 | Environments 58 | --------------- 59 | 60 | Text needs to be displayed *somewhere*; this *environment* must specify at least 61 | the following: 62 | 63 | - an axis-aligned box within which text is displayed 64 | - whether to line-wrap text 65 | - default alignment of text 66 | - the default font size 67 | - the default font colour 68 | - any extra data to forward to the rasteriser, such as depth value 69 | (note: rich text may influence colour but not depth value) 70 | 71 | Note: we need to specify a type to use for dimensions. We could just use `f32`; 72 | HarfBuzz uses `i32` but IIRC shifted to allow 6-bits of sub-integer precision. 73 | 74 | 75 | Font management 76 | ------------------- 77 | 78 | Potentially, text may use quite a few different fonts; we should therefore load 79 | fonts on demand unless we heavily restrict the number available. 80 | 81 | Input should be able to select: 82 | 83 | - a default font 84 | - a bold variant, italic variant, bold-italic variant 85 | - possibly also light and condensed variants 86 | - a monospace font, with applicable variants 87 | - possibly other families 88 | 89 | For now, we can let font-kit can do the work for us and not make this 90 | configurable (though later configuration support is a must). 91 | Relevent font-kit docs: [Properties](https://docs.rs/font-kit/0.8.0/font_kit/properties/struct.Properties.html), [FamilyName](https://docs.rs/font-kit/0.8.0/font_kit/family_name/enum.FamilyName.html). 92 | 93 | ### Font & property selection 94 | 95 | Input should be able to select: 96 | 97 | - font family (including "default") 98 | - italic property 99 | - weight (possibly only binary: bold or normal) 100 | - underline (not font selection but external effect) 101 | - strikethrough? 102 | - foreground colour (default, a name like "blue", or arbitrary value) 103 | - background colour 104 | 105 | 106 | Rich text 107 | ---------- 108 | 109 | Several properties of text will be derived from the display environment (see 110 | above). Some properties may be specified for rich text: 111 | 112 | - font selection, but likely only by family and properties including italic, 113 | bold and monospace (possibly using the [`font-kit`] API) 114 | - font size, but possibly only relative to the default size (this side-steps 115 | all the scaling problems associated with usage of units like pt, mm and 116 | pixels); we may also allow users to force size limits 117 | - foreground colour: possibly with the option to select from a list of named 118 | colours (specified by the theme) and the option to specify an absolute 119 | colour (according to some fixed colour space) 120 | - alignment may be overridden at the paragraph level or with tabulation 121 | 122 | Some types of flow-control should also be supported: 123 | 124 | - explicit line breaks 125 | - indentation (e.g. paragraph start, code blocks) 126 | - tables? 127 | - bullet points and enumeration? 128 | 129 | ### Translation from HTML 130 | 131 | HTML and CSS allow font sizes to be specified in various units. For now we can 132 | ignore this problem, but it may be necessary to make the input parser aware of 133 | the display's DPI / physical size and/or scaling factor. 134 | 135 | 136 | Challenges 137 | -------------- 138 | 139 | ### Line-splitting 140 | 141 | Text may include explicit line breaks, but usually line-breaks are introduced 142 | via line-wrapping. The Unicode BIDI algorithm is explicit about how this works: 143 | https://www.unicode.org/reports/tr9/#Reordering_Resolved_Levels 144 | To summarise: 145 | 146 | 1. text is split into paragraphs 147 | 2. paragraphs are split into runs with given embedding level by the BIDI algorithm 148 | 3. a shaper is applied to the result and used to calculate word positions 149 | 4. line-breaking occurs 150 | 5. the BIDI algorithm re-orders characters 151 | 152 | Until we implement BIDI support, we may use a simpler model: 153 | 154 | 1. text is split into paragraphs 155 | 2. a shaper is applied to the result and used to calculate word positions 156 | 3. line-breaking occurs 157 | 158 | To do this line-breaking, we need to know: 159 | 160 | - where within a text line breaks may occur (another Unicode specification) 161 | - which characters are whitespace (trailing whitespace never wraps) 162 | - the position at which each word ends 163 | 164 | Line splitting may be a view transformation, excepting in multi-line text 165 | editors where it must be explicit. 166 | 167 | ### Rich text 168 | 169 | It must be possible to adjust certain properties within a sub-span: 170 | 171 | - font (e.g. for bold and italic) 172 | - font size and weight — maybe? 173 | - subtext / supertext — maybe? 174 | - colours (foreground and background) 175 | - external effects (underline, strikethrough) 176 | 177 | In general, text shapers are not equipped to deal with these, thus all font 178 | changes must start a new "run". 179 | 180 | #### Syntax highlighting 181 | 182 | This is definitely not a short-term goal, but it's worth considering how this 183 | might work. Essentially, a highlighter is just another input parser. It needs to 184 | be able to: 185 | 186 | - select a font (usually monospaced) 187 | - select variants: bold, italic, underlined, strikethrough(?) 188 | - select colours 189 | - select background colours 190 | 191 | Simple highlighters might choose colours from a list of names like "green" and 192 | "red", thus allowing theme-customisation but not a great degree of flexibility. 193 | Complex highlighters may wish to choose from a wider palette of colours and 194 | allow user-adjustment; these must recognise that "theme" and "highlighting 195 | scheme" cannot truely be separated from one another. To be fully customisable, 196 | users should be able to adjust bold/italic/underline properties of each named 197 | style used by the highlighter as well as foreground and background colours, 198 | and possibly also explicitly choose colours for selected text. 199 | 200 | #### Text selection 201 | 202 | This operates on top of other processors: it must be possible to transform 203 | processed output to make a span appear selected. 204 | 205 | ### External effects 206 | 207 | Underline, strikethrough and edit carets are not directly part of rasterised 208 | text, but should be supported somehow (though for edit carets probably only by 209 | reporting the coordinates and line-height where it should occur). 210 | 211 | ### Embedded objects 212 | 213 | We would like to support embedding objects (e.g. check-boxes and icons) within 214 | paragraph text. For now, lets assume that such objects have a fixed size 215 | (possibly derived from the base line height but not from the specific line in 216 | which they appear). The library must provide a trait to bound such objects. 217 | 218 | ### Bidirectional text support 219 | 220 | We may have to re-implement the Unicode BIDI algorithm over our paragraph 221 | representation in order to handle rich text and embedded objects. 222 | 223 | 224 | Design 225 | ------ 226 | 227 | We need essentially three phases of transforms: 228 | 229 | 1. Input markup to internal stable representation 230 | 2. Space fitting: at least this must perform line wrapping; it may also 231 | perform shaping (into a list of positioned glyphs) 232 | 3. Rastorisation (which may or may not do layout/shaping on the fly) 233 | 234 | We also require an "internal stable representation". This must be a concrete 235 | type that users of the API can store; it might be acceptable if this is only 236 | exposed as an associated type on the input parser. It must support testing 237 | required dimensions (with line-wrapping) and position look-ups. 238 | 239 | Additionally, we require a font library. 240 | This will initially be a small abstraction over [`font-kit`] to select fonts based 241 | on properties and load into memory. 242 | 243 | 244 | [`font-kit`]: https://crates.io/crates/font-kit 245 | [`ab_glyph`]: https://crates.io/crates/ab_glyph 246 | [`unicode-linebreak`]: https://crates.io/crates/unicode-linebreak 247 | [`unicode-bidi`]: https://crates.io/crates/unicode-linebreak 248 | [`harfbuzz_rs`]: https://crates.io/crates/harfbuzz_rs 249 | [`allsorts`]: https://crates.io/crates/allsorts 250 | [`palette`]: https://crates.io/crates/allsorts 251 | [`kas`]: https://crates.io/crates/kas 252 | [`iced`]: https://crates.io/crates/iced 253 | [`wgpu_glyph`]: https://crates.io/crates/wgpu_glyph 254 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/display/text_runs.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License in the LICENSE-APACHE file or at: 4 | // https://www.apache.org/licenses/LICENSE-2.0 5 | 6 | //! Text preparation: line breaking and BIDI 7 | 8 | #![allow(clippy::unnecessary_unwrap)] 9 | 10 | use super::TextDisplay; 11 | use crate::conv::{to_u32, to_usize}; 12 | use crate::fonts::{self, FontSelector, NoFontMatch}; 13 | use crate::format::FormattableText; 14 | use crate::{Direction, script_to_fontique, shaper}; 15 | use swash::text::LineBreak as LB; 16 | use swash::text::cluster::Boundary; 17 | use unicode_bidi::{BidiInfo, LTR_LEVEL, RTL_LEVEL}; 18 | 19 | #[derive(Clone, Copy, Debug, PartialEq)] 20 | pub(crate) enum RunSpecial { 21 | None, 22 | /// Run ends with a hard break 23 | HardBreak, 24 | /// Run does not end with a break 25 | NoBreak, 26 | /// Run is a horizontal tab (run is a single char only) 27 | HTab, 28 | } 29 | 30 | impl TextDisplay { 31 | /// Update font size 32 | /// 33 | /// [Requires status][Self#status-of-preparation]: level runs have been 34 | /// prepared and are valid in all ways except size (`dpem`). 35 | /// 36 | /// This updates the result of [`TextDisplay::prepare_runs`] due to change 37 | /// in font size. 38 | pub fn resize_runs(&mut self, text: &F, mut dpem: f32) { 39 | let mut font_tokens = text.font_tokens(dpem); 40 | let mut next_fmt = font_tokens.next(); 41 | 42 | let text = text.as_str(); 43 | 44 | for run in &mut self.runs { 45 | while let Some(fmt) = next_fmt.as_ref() { 46 | if fmt.start > run.range.start { 47 | break; 48 | } 49 | dpem = fmt.dpem; 50 | next_fmt = font_tokens.next(); 51 | } 52 | 53 | let input = shaper::Input { 54 | text, 55 | dpem, 56 | level: run.level, 57 | script: run.script, 58 | }; 59 | let mut breaks = Default::default(); 60 | std::mem::swap(&mut breaks, &mut run.breaks); 61 | if run.level.is_rtl() { 62 | breaks.reverse(); 63 | } 64 | *run = shaper::shape(input, run.range, run.face_id, breaks, run.special); 65 | } 66 | } 67 | 68 | /// Break text into level runs 69 | /// 70 | /// [Requires status][Self#status-of-preparation]: none. 71 | /// 72 | /// Must be called again if any of `text`, `direction` or `font` change. 73 | /// If only `dpem` changes, [`Self::resize_runs`] may be called instead. 74 | /// 75 | /// The text is broken into a set of contiguous "level runs". These runs are 76 | /// maximal slices of the `text` which do not contain explicit line breaks 77 | /// and have a single text direction according to the 78 | /// [Unicode Bidirectional Algorithm](http://www.unicode.org/reports/tr9/). 79 | pub fn prepare_runs( 80 | &mut self, 81 | text: &F, 82 | direction: Direction, 83 | mut font: FontSelector, 84 | mut dpem: f32, 85 | ) -> Result<(), NoFontMatch> { 86 | // This method constructs a list of "hard lines" (the initial line and any 87 | // caused by a hard break), each composed of a list of "level runs" (the 88 | // result of splitting and reversing according to Unicode TR9 aka 89 | // Bidirectional algorithm), plus a list of "soft break" positions 90 | // (where wrapping may introduce new lines depending on available space). 91 | 92 | self.runs.clear(); 93 | 94 | let mut font_tokens = text.font_tokens(dpem); 95 | let mut next_fmt = font_tokens.next(); 96 | if let Some(fmt) = next_fmt.as_ref() { 97 | if fmt.start == 0 { 98 | font = fmt.font; 99 | dpem = fmt.dpem; 100 | next_fmt = font_tokens.next(); 101 | } 102 | } 103 | 104 | let fonts = fonts::library(); 105 | let text = text.as_str(); 106 | 107 | let default_para_level = match direction { 108 | Direction::Auto => None, 109 | Direction::AutoRtl => { 110 | use unicode_bidi::Direction::*; 111 | match unicode_bidi::get_base_direction(text) { 112 | Ltr | Rtl => None, 113 | Mixed => Some(RTL_LEVEL), 114 | } 115 | } 116 | Direction::Ltr => Some(LTR_LEVEL), 117 | Direction::Rtl => Some(RTL_LEVEL), 118 | }; 119 | let info = BidiInfo::new(text, default_para_level); 120 | let levels = info.levels; 121 | assert_eq!(text.len(), levels.len()); 122 | 123 | let mut input = shaper::Input { 124 | text, 125 | dpem, 126 | level: levels.first().cloned().unwrap_or(LTR_LEVEL), 127 | script: UNKNOWN_SCRIPT, 128 | }; 129 | 130 | let mut start = 0; 131 | let mut breaks = Default::default(); 132 | 133 | let mut analyzer = swash::text::analyze(text.chars()); 134 | let mut last_props = None; 135 | 136 | let mut face_id = None; 137 | let mut last_real_face = None; 138 | 139 | let mut last_is_control = false; 140 | let mut last_is_htab = false; 141 | let mut non_control_end = 0; 142 | 143 | for (index, c) in text.char_indices() { 144 | // Handling for control chars 145 | if !last_is_control { 146 | non_control_end = index; 147 | } 148 | let is_control = c.is_control(); 149 | let is_htab = c == '\t'; 150 | let control_break = is_htab || (last_is_control && !is_control); 151 | 152 | let (props, boundary) = analyzer.next().unwrap(); 153 | last_props = Some(props); 154 | 155 | // Forcibly end the line? 156 | let hard_break = boundary == Boundary::Mandatory; 157 | // Is wrapping allowed at this position? 158 | let is_break = hard_break || boundary == Boundary::Line; 159 | 160 | // Force end of current run? 161 | let bidi_break = levels[index] != input.level; 162 | 163 | if let Some(fmt) = next_fmt.as_ref() { 164 | if to_usize(fmt.start) == index { 165 | font = fmt.font; 166 | dpem = fmt.dpem; 167 | next_fmt = font_tokens.next(); 168 | } 169 | } 170 | 171 | if input.script == UNKNOWN_SCRIPT && props.script().is_real() { 172 | input.script = script_to_fontique(props.script()); 173 | } 174 | 175 | let opt_last_face = if !props.script().is_real() { 176 | last_real_face 177 | } else { 178 | None 179 | }; 180 | let font_id = fonts.select_font(&font, input.script)?; 181 | let new_face_id = fonts 182 | .face_for_char(font_id, opt_last_face, c) 183 | .expect("invalid FontId"); 184 | let font_break = face_id.is_some() && new_face_id != face_id; 185 | 186 | if let Some(face) = face_id 187 | && (hard_break || control_break || bidi_break || font_break) 188 | { 189 | // TODO: sometimes this results in empty runs immediately 190 | // following another run. Ideally we would either merge these 191 | // into the previous run or not simply break in this case. 192 | // Note: the prior run may end with NoBreak while the latter 193 | // (and the merge result) do not. 194 | let range = (start..non_control_end).into(); 195 | let special = match () { 196 | _ if hard_break => RunSpecial::HardBreak, 197 | _ if last_is_htab => RunSpecial::HTab, 198 | _ if last_is_control || is_break => RunSpecial::None, 199 | _ => RunSpecial::NoBreak, 200 | }; 201 | 202 | self.runs 203 | .push(shaper::shape(input, range, face, breaks, special)); 204 | 205 | start = index; 206 | non_control_end = index; 207 | input.level = levels[index]; 208 | breaks = Default::default(); 209 | input.script = UNKNOWN_SCRIPT; 210 | } else if is_break && !is_control { 211 | // We do break runs when hitting control chars, but only when 212 | // encountering the next non-control character. 213 | breaks.push(shaper::GlyphBreak::new(to_u32(index))); 214 | } 215 | 216 | last_is_control = is_control; 217 | last_is_htab = is_htab; 218 | face_id = new_face_id; 219 | if props.script().is_real() { 220 | last_real_face = face_id; 221 | } else if font_break || face_id.is_none() { 222 | last_real_face = None; 223 | } 224 | input.dpem = dpem; 225 | } 226 | 227 | debug_assert!(analyzer.next().is_none()); 228 | let hard_break = last_props 229 | .map(|props| matches!(props.line_break(), LB::BK | LB::CR | LB::LF | LB::NL)) 230 | .unwrap_or(false); 231 | 232 | // Conclude: add last run. This may be empty, but we want it anyway. 233 | if !last_is_control { 234 | non_control_end = text.len(); 235 | } 236 | let range = (start..non_control_end).into(); 237 | let special = match () { 238 | _ if hard_break => RunSpecial::HardBreak, 239 | _ if last_is_htab => RunSpecial::HTab, 240 | _ => RunSpecial::None, 241 | }; 242 | 243 | let font_id = fonts.select_font(&font, input.script)?; 244 | if let Some(id) = face_id { 245 | if !fonts.contains_face(font_id, id).expect("invalid FontId") { 246 | face_id = None; 247 | } 248 | } 249 | let face_id = 250 | face_id.unwrap_or_else(|| fonts.first_face_for(font_id).expect("invalid FontId")); 251 | self.runs 252 | .push(shaper::shape(input, range, face_id, breaks, special)); 253 | 254 | // Following a hard break we have an implied empty line. 255 | if hard_break { 256 | let range = (text.len()..text.len()).into(); 257 | input.level = default_para_level.unwrap_or(LTR_LEVEL); 258 | breaks = Default::default(); 259 | self.runs.push(shaper::shape( 260 | input, 261 | range, 262 | face_id, 263 | breaks, 264 | RunSpecial::None, 265 | )); 266 | } 267 | 268 | /* 269 | println!("text: {}", text); 270 | for run in &self.runs { 271 | let slice = &text[run.range]; 272 | print!( 273 | "\t{:?}, text[{}..{}]: '{}', ", 274 | run.level, run.range.start, run.range.end, slice 275 | ); 276 | match run.special { 277 | RunSpecial::None => (), 278 | RunSpecial::HardBreak => print!("HardBreak, "), 279 | RunSpecial::NoBreak => print!("NoBreak, "), 280 | RunSpecial::HTab => print!("HTab, "), 281 | } 282 | print!("breaks=["); 283 | let mut iter = run.breaks.iter(); 284 | if let Some(b) = iter.next() { 285 | print!("{}", b.index); 286 | } 287 | for b in iter { 288 | print!(", {}", b.index); 289 | } 290 | println!("]"); 291 | } 292 | */ 293 | Ok(()) 294 | } 295 | } 296 | 297 | trait ScriptExt { 298 | #[allow(clippy::wrong_self_convention)] 299 | fn is_real(self) -> bool; 300 | } 301 | impl ScriptExt for swash::text::Script { 302 | fn is_real(self) -> bool { 303 | use swash::text::Script::*; 304 | !matches!(self, Common | Unknown | Inherited) 305 | } 306 | } 307 | 308 | pub(crate) const UNKNOWN_SCRIPT: fontique::Script = fontique::Script(*b"Zzzz"); 309 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 3 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 4 | 5 | ## [0.8.1] — 2025-09-17 6 | 7 | - Fix sizing of empty lines by prefering to match some font than nothing (#102) 8 | 9 | ## [0.8.0] — 2025-09-12 10 | 11 | - Remove `raster` module (#91) 12 | - Removed `FontLibrary::init` and `Text::configure` (#94) 13 | - Remove support for `harfbuzz_rs` and `fontdue` (#95) 14 | - Remove trait `EditableText` (#98) 15 | - Replace `fontdb` with `fontique`, replacing usage of hard-coded font families and removing configuration of font aliases (#93, #94) 16 | - Revise `FontSelector` type: make into a small `Copy` type and embed in `Text` objects. Replaces usage of pre-resolved `FontId`, allowing font selection to be delayed until run-breaking and to use inferred script. (#94) 17 | - Replace field `Effect::aux` with `Effect::e` (#99) 18 | - Support Swash in font library (#91) and use for text analysis (#94) 19 | - Support font synthesis (#95) 20 | - Add `GlyphRun` type, replacing fns `Text{,Display}::glyphs{,_with_effects}` with `runs` (#96, #99) 21 | - Add fn `TextDisplay::apply_offset` (#98, #100) 22 | - Fix behaviour of `Align::Stretch` in some cases (#100) 23 | 24 | ## [0.7.0] — 2024-12-02 25 | 26 | - Add `Direction::AutoRtl`, `text_is_rtl` (#80) 27 | - Set the default font size to 16px (#80) 28 | - `Text` API: do not expose `prepare_runs`, `required_action` (#81); more tweaks (#84) 29 | - Move status tracking and `Env` fields into `Text` (#82) 30 | - Remove `TextApi`, `TextApiExt` and `EditableTextApi` traits (#85) 31 | - Revise `fonts` module: rename `selector::Database` to `Resolver`, separate out `fontdb::Database`, revise `init` (#87, #88) 32 | - Bump MSRV to 1.80.0 (#87) 33 | 34 | ## [0.6.0] — 2022-12-13 35 | 36 | Stabilise support for Generic Associated Types (GATs). This requires Rust 1.65.0, 37 | removes the `gat` feature flag and affects the `FormattableText` trait. #75 38 | 39 | Bump dependency versions: `ttf-parser` v0.17.1, `rustybuzz` v0.6.0 (#76), 40 | `fontdb` v0.10.0 (#77). 41 | 42 | ## [0.5.0] — 2022-08-20 43 | 44 | Error handling (#65): 45 | 46 | - Add `NotReady` error type 47 | - Most methods now return `Result` instead of panicking 48 | 49 | Text environment (#68): 50 | 51 | - Remove `UpdateEnv` type 52 | - Rename `Text::new` to `new_env`, `Text::new_multi` to `Text::new` and 53 | remove `Text::new_single`. Note: in usage, the `Environment::wrap` flag 54 | is usually set anyway. 55 | - `Environment` is now `Copy` 56 | - `Text::env` returns `Environment` by copy not reference 57 | - `Text::env_mut` replaced with `Text::set_env`, which sets required actions 58 | - `Environment::dir` renamed to `direction` 59 | - Enum `Direction` adjusted to include bidi and non-bidi modes. 60 | - `Environment::flags` and its type `EnvFlags` removed. 61 | `Environment::wrap: bool` added and `Direction` adjusted (see above). 62 | `PX_PALIGN` option removed (behaviour is now always enabled). 63 | - Parameters `dpp` and `pt_size` of `Environment`, `TextDisplay::prepare_runs` 64 | and `FormattableText::font_tokens` are replaced with the single `dpem`. 65 | 66 | Text preparation: 67 | 68 | - Add `Action::VAlign` requiring only `TextDisplay::vertically_align` action 69 | - Remove `TextDisplay::prepare` (but `TextApi::prepare` remains) 70 | - `TextDisplay::resize_runs` is no longer a public method 71 | - `TextDisplay::prepare_runs` may call `resize_runs` automatically depending 72 | on preparation status 73 | - Remove `TextApi::resize_runs` and `TextApi::prepare_lines` 74 | - All `TextApi` and `TextApiExt` methods doing any preparation now do all 75 | required preparation, and avoid unnecessary steps. 76 | 77 | Text measurements (#68): 78 | 79 | - Add `TextDisplay::bounding_box` and `TextApiExt::bounding_box` (#68, #69) 80 | - Add `TextDisplay::measure_width` and `TextDisplay::vertically_align` 81 | - Add `TextApi::measure_width` and `TextApi::measure_height` 82 | - Remove `TextDisplay::line_is_ltr` and `TextApiExt::line_is_ltr` 83 | - Add `TextApiExt::text_is_rtl` 84 | - `TextDisplay::line_is_rtl` and `TextApiExt::line_is_rtl` now return type 85 | `Result, NotReady>`, returning `Ok(None)` if text is empty 86 | - `TextDisplay::prepare_lines` returns the bottom-right corner of the bounding 87 | box around content instead of the size of content. 88 | 89 | Font fallback: 90 | 91 | - `FontLibrary::face_for_char` and `face_for_char_or_first` take an extra 92 | parameter: `last_face_id: Option`. This allows the font fallback 93 | mechanism to avoid switching the font unnecessarily. In usage, letters and 94 | numbers are selected as before while other characters are selected from the 95 | last font face used if possible, resulting in longer runs being passed to 96 | the shaper when using fallback fonts. 97 | 98 | Misc: 99 | 100 | - CI: test stable and check Clippy lints (#69). 101 | - Add `Range::is_empty` 102 | - Add `num_glyphs` feature flag (#69) 103 | - Memory optimisations for `TextDisplay`: remove `line_runs` (#71) 104 | - Replace `highlight_lines` with `highlight_range` (#72) 105 | - Add `fonts::any_loaded` (#73) 106 | 107 | Fixes: 108 | 109 | - Do not add "line gap" before first line. (In practice this is often 0 anyway.) 110 | - Do not vertically align text too tall for the input bounds. 111 | - Markdown formatter: use heading level sizes as defined by CSS 112 | - Fix position of text highlights on vertically aligned text (#67). 113 | - Fix `r_bound` for trailing space (#71) 114 | 115 | ## [0.4.2] — 2022-02-10 116 | 117 | - Spellcheck documentation (#62) 118 | - Fix selection of best font from a family (#63) 119 | 120 | ## [0.4.1] — 2021-09-07 121 | 122 | - Document loading order requirements (#59) 123 | - Additional logging of loaded fonts (#59) 124 | 125 | ## [0.4.0] — 2021-09-03 126 | 127 | This is a minor release (mostly non-breaking). The primary motivation is to 128 | enable `resvg` to access the loaded font database. See PR #58: 129 | 130 | - **Breaking:** update dependencies: `bitflags = 1.3.1`, `fontdb = 0.6.0`, 131 | `harfbuzz_rs = 2.0`, `rustybuzz = 0.4.0` 132 | - Make `fontdb::Database` externally readable and set font families 133 | - Additional control over loading fonts 134 | - Performance improvements for alias lookups (trim and capitalise earlier) 135 | 136 | ## [0.3.4] — 2021-07-28 137 | 138 | - Fix sub-pixel positioning (#57) 139 | 140 | ## [0.3.3] — 2021-07-19 141 | 142 | - Document `raster` module and `Markdown` formatter (#56) 143 | - Export `DPU` (#56) 144 | - `Default`, `Debug` and `PartialEq` impls for some `raster` types (#56) 145 | 146 | ## [0.3.2] — 2021-06-30 147 | 148 | - Minor optimisations to `SpriteDescriptor::new` (#53) 149 | 150 | ## [0.3.1] — 2021-06-17 151 | 152 | - Make all families fall back to "sans-serif" fonts. 153 | - Cache `FontLibrary::face_for_char` glyph lookups. 154 | 155 | ## [0.3.0] — 2021-06-15 156 | 157 | This release replaces all non-Rust dependencies allowing easier build/deployment 158 | (though HarfBuzz is kept as an optional dependency). There is also direct 159 | support for glyph rastering and some tweaks to improve raster quality making 160 | even very small font sizes quite legible. 161 | 162 | - Add `Effect::default` method and `default_aux` param to `glyphs_with_effects` (#45) 163 | - Replace `font-kit` dependency with the pure-Rust `fontdb` using custom font-family lists (#46), 164 | with support for run-time configuration (#48) 165 | - Support font fallbacks (#47) 166 | - Support [rustybuzz](https://github.com/RazrFalcon/rustybuzz) for pure-Rust shaping (#47) 167 | - Vertical pixel alignment (#49) 168 | - Extend public API relating to fonts (#49) 169 | - Add (glyph) `raster` module with `Config` struct and `SpriteDescriptor` cache key (#50) 170 | - Use pixels-per-Em (dpem) for most glyph sizing, not pixels-per-font-unit (DPU) or height (#50) 171 | 172 | ## [0.2.1] — 2021-03-31 173 | 174 | - Add `Option` return value to `TextDisplay::prepare` and `TextApi::prepare` (#41) 175 | - Fix missing run for empty text lines 176 | - Fix justified text layout (#44) 177 | - Explicitly avoid justifying last line of justified text (#44) 178 | - Update `smallvec` to 0.6.1 179 | - Update `ttf-parser` to 0.12.0 180 | 181 | ## [0.2.0] — 2020-11-23 182 | 183 | This release changes a *very large* part of the API. Both `prepared` and `rich` 184 | modules are removed/hidden; three new modules `conv`, `fonts` and `format` 185 | are added/exposed. Formatted text traits are added. The API around the main 186 | `Text` type changes massively. 187 | 188 | ### Text type 189 | 190 | - Export `prepared::Text` directly and hide the `prepared` module (#30) 191 | - Split what was `prepared::Text` into `TextDisplay` (which excludes the text, 192 | environment and formatting data but includes positioned glyph information), 193 | and `Text` struct which wraps `TextDisplay` along with text and environment (#32) 194 | - Add `TextApi` trait for run-time polymorphism over `FormattableText` texts (#32, #33) 195 | - Add `TextApiExt` auto-implemented extension-trait (#33) 196 | - Add `EditableTextApi` trait (#33) 197 | - Replace `prepared::Prepare` with `Action` (#30, #33) 198 | - Rename `Text::positioned_glyphs` → `glyphs` (#31) 199 | - Add `Text::glyphs_with_effects` for glyph-drawing with underline/strikethrough (#31, #32) 200 | - Make `TextDisplay::prepare_runs` and `prepare_lines` functions public (#33) 201 | - Support indentation with tab character `\t` (#34) 202 | 203 | ### Parsing and representation 204 | 205 | - Add `Markdown` parser (#29 - #37) 206 | - Add `FontToken` struct, `FormattableText` and `EditableText` traits with 207 | impls for `&str`, `String` and `Markdown`; these allow custom 208 | representations of formatted text (#32) 209 | - Add `FormattableTextDyn` for run-time polymorphism (#33) 210 | - Add `FormattableText::effect_tokens` for custom underlike/strikethrough effects (#34) 211 | 212 | ### Fonts API 213 | 214 | - All font API is moved into the public `fonts` module (#28) 215 | - Add `FontSelector` and `FontLibrary::load_font`, `load_pathbuf` functions (#28) 216 | - Adjust how loaded fonts are exposed; instead of an `ab_glyph` font we 217 | expose the font file data and font index (#30, #31) 218 | - Adjust use of font-size units and add documentation (#31, #33) 219 | - Switch dependency from `ab_glyph` to `ttf-parser`, but keep compatibility 220 | with `ab_glyph` (#31) 221 | - Remove `FontScale` data type (unused; #30) 222 | 223 | ### Miscellaneous 224 | 225 | - Add type-conversion helpers `conv::to_32` and `to_usize` (#31) 226 | - Add `Effect` and `EffectFlags` types used for underline and strikethrough (#31) 227 | - Add `gat` (Generic Associated Types) experimental feature (#33) 228 | - Move `Environment::bidi` and `wrap` fields to new `flags` field; combine 229 | `halign` and `valign` to into `align` field (#33) 230 | - Update `xi-unicode` dependency, allowing cleaner code (#34) 231 | 232 | ## [0.1.5] — 2020-09-21 233 | 234 | - Rewrite line-wrapping code, supporting must-not-wrap-at-end runs and 235 | resulting in much cleaner code (#24) 236 | - Do not allow selection of the position after a space causing a line-wrap (#25) 237 | - Fix alignment for wrapped RTL text with multiple spaces at wrap point (#26) 238 | 239 | ## [0.1.4] — 2020-09-09 240 | 241 | - Fixes for empty RTL lines (#21, #23) 242 | - When wrapping text against paragraph's direction, do not force a line break (#22) 243 | 244 | ## [0.1.3] — 2020-08-14 245 | 246 | - Fix re-ordering of runs on a line (#18) 247 | 248 | ## [0.1.2] — 2020-08-14 249 | 250 | - Add embedding level to result of `Text::text_glyph_pos` (#17) 251 | - Fix start offset for wrapped RTL text (#17) 252 | - Fix `Text::line_index_nearest` for right-most position in line (#17) 253 | 254 | ## [0.1.1] — 2020-08-13 255 | 256 | - `prepared::Text::positioned_glyphs` now takes an `FnMut` closure and 257 | emits glyphs in logical order (#13) 258 | 259 | ## [0.1.0] — 2020-08-11 260 | 261 | Initial release version, comprising: 262 | 263 | - basic font loading and font metrics 264 | - `Environment` specifying font and layout properties 265 | - `rich::Text` struct (mostly placeholder around a raw `String`) 266 | - `prepared::Text` struct with state tracking required preparation steps 267 | 268 | Text preparation includes: 269 | 270 | - run-breaking with BIDI support 271 | - glyph shaping via internal algorithm or via HarfBuzz 272 | - line wrapping and alignment 273 | - generating a vec of positioned glyphs 274 | - cursor position lookup (index→coord and coord→index) 275 | - generating highlighting rects for a range 276 | -------------------------------------------------------------------------------- /src/format/markdown.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License in the LICENSE-APACHE file or at: 4 | // https://www.apache.org/licenses/LICENSE-2.0 5 | 6 | //! Markdown parsing 7 | 8 | use super::{FontToken, FormattableText}; 9 | use crate::conv::to_u32; 10 | use crate::fonts::{FamilySelector, FontSelector, FontStyle, FontWeight}; 11 | use crate::{Effect, EffectFlags}; 12 | use pulldown_cmark::{Event, HeadingLevel, Tag, TagEnd}; 13 | use std::fmt::Write; 14 | use std::iter::FusedIterator; 15 | use thiserror::Error; 16 | 17 | /// Markdown parsing errors 18 | #[derive(Error, Debug)] 19 | pub enum Error { 20 | #[error("Not supported by Markdown parser: {0}")] 21 | NotSupported(&'static str), 22 | } 23 | 24 | /// Basic Markdown formatter 25 | /// 26 | /// Currently this misses several important Markdown features, but may still 27 | /// prove a convenient way of constructing formatted texts. 28 | /// 29 | /// Supported: 30 | /// 31 | /// - Text paragraphs 32 | /// - Code (embedded and blocks); caveat: extra line after code blocks 33 | /// - Explicit line breaks 34 | /// - Headings 35 | /// - Lists (numerated and bulleted); caveat: indentation after first line 36 | /// - Bold, italic (emphasis), strike-through 37 | /// 38 | /// Not supported: 39 | /// 40 | /// - Block quotes 41 | /// - Footnotes 42 | /// - HTML 43 | /// - Horizontal rules 44 | /// - Images 45 | /// - Links 46 | /// - Tables 47 | /// - Task lists 48 | #[derive(Clone, Debug, Default, PartialEq)] 49 | pub struct Markdown { 50 | text: String, 51 | fmt: Vec, 52 | effects: Vec, 53 | } 54 | 55 | impl Markdown { 56 | /// Parse the input as Markdown 57 | /// 58 | /// Parsing happens immediately. Fonts must be initialized before calling 59 | /// this method. 60 | #[inline] 61 | pub fn new(input: &str) -> Result { 62 | parse(input) 63 | } 64 | } 65 | 66 | pub struct FontTokenIter<'a> { 67 | index: usize, 68 | fmt: &'a [Fmt], 69 | base_dpem: f32, 70 | } 71 | 72 | impl<'a> FontTokenIter<'a> { 73 | fn new(fmt: &'a [Fmt], base_dpem: f32) -> Self { 74 | FontTokenIter { 75 | index: 0, 76 | fmt, 77 | base_dpem, 78 | } 79 | } 80 | } 81 | 82 | impl<'a> Iterator for FontTokenIter<'a> { 83 | type Item = FontToken; 84 | 85 | fn next(&mut self) -> Option { 86 | if self.index < self.fmt.len() { 87 | let fmt = &self.fmt[self.index]; 88 | self.index += 1; 89 | Some(FontToken { 90 | start: fmt.start, 91 | font: fmt.font, 92 | dpem: self.base_dpem * fmt.rel_size, 93 | }) 94 | } else { 95 | None 96 | } 97 | } 98 | 99 | fn size_hint(&self) -> (usize, Option) { 100 | let len = self.fmt.len(); 101 | (len, Some(len)) 102 | } 103 | } 104 | 105 | impl<'a> ExactSizeIterator for FontTokenIter<'a> {} 106 | impl<'a> FusedIterator for FontTokenIter<'a> {} 107 | 108 | impl FormattableText for Markdown { 109 | type FontTokenIter<'a> = FontTokenIter<'a>; 110 | 111 | #[inline] 112 | fn as_str(&self) -> &str { 113 | &self.text 114 | } 115 | 116 | #[inline] 117 | fn font_tokens<'a>(&'a self, dpem: f32) -> Self::FontTokenIter<'a> { 118 | FontTokenIter::new(&self.fmt, dpem) 119 | } 120 | 121 | fn effect_tokens(&self) -> &[Effect] { 122 | &self.effects 123 | } 124 | } 125 | 126 | fn parse(input: &str) -> Result { 127 | let mut text = String::with_capacity(input.len()); 128 | let mut fmt: Vec = Vec::new(); 129 | let mut set_last = |item: &StackItem| { 130 | let f = Fmt::new(item); 131 | if let Some(last) = fmt.last_mut() { 132 | if last.start >= item.start { 133 | *last = f; 134 | return; 135 | } 136 | } 137 | fmt.push(f); 138 | }; 139 | 140 | let mut state = State::None; 141 | let mut stack = Vec::with_capacity(16); 142 | let mut item = StackItem::default(); 143 | 144 | let options = pulldown_cmark::Options::ENABLE_STRIKETHROUGH; 145 | for ev in pulldown_cmark::Parser::new_ext(input, options) { 146 | match ev { 147 | Event::Start(tag) => { 148 | item.start = to_u32(text.len()); 149 | if let Some(clone) = item.start_tag(&mut text, &mut state, tag)? { 150 | stack.push(item); 151 | item = clone; 152 | set_last(&item); 153 | } 154 | } 155 | Event::End(tag) => { 156 | if item.end_tag(&mut state, tag) { 157 | item = stack.pop().unwrap(); 158 | item.start = to_u32(text.len()); 159 | set_last(&item); 160 | } 161 | } 162 | Event::Text(part) => { 163 | state.part(&mut text); 164 | text.push_str(&part); 165 | } 166 | Event::Code(part) => { 167 | state.part(&mut text); 168 | item.start = to_u32(text.len()); 169 | 170 | let mut item2 = item.clone(); 171 | item2.sel.family = FamilySelector::MONOSPACE; 172 | set_last(&item2); 173 | 174 | text.push_str(&part); 175 | 176 | item.start = to_u32(text.len()); 177 | set_last(&item); 178 | } 179 | Event::InlineMath(_) | Event::DisplayMath(_) => { 180 | return Err(Error::NotSupported("math expressions")); 181 | } 182 | Event::Html(_) | Event::InlineHtml(_) => { 183 | return Err(Error::NotSupported("embedded HTML")); 184 | } 185 | Event::FootnoteReference(_) => return Err(Error::NotSupported("footnote")), 186 | Event::SoftBreak => state.soft_break(&mut text), 187 | Event::HardBreak => state.hard_break(&mut text), 188 | Event::Rule => return Err(Error::NotSupported("horizontal rule")), 189 | Event::TaskListMarker(_) => return Err(Error::NotSupported("task list")), 190 | } 191 | } 192 | 193 | // TODO(opt): don't need to store flags in fmt? 194 | let mut effects = Vec::new(); 195 | let mut flags = EffectFlags::default(); 196 | for token in &fmt { 197 | if token.flags != flags { 198 | effects.push(Effect { 199 | start: token.start, 200 | e: 0, 201 | flags: token.flags, 202 | }); 203 | flags = token.flags; 204 | } 205 | } 206 | 207 | Ok(Markdown { text, fmt, effects }) 208 | } 209 | 210 | #[derive(Copy, Clone, Debug, PartialEq)] 211 | enum State { 212 | None, 213 | BlockStart, 214 | BlockEnd, 215 | ListItem, 216 | Part, 217 | } 218 | 219 | impl State { 220 | fn start_block(&mut self, text: &mut String) { 221 | match *self { 222 | State::None | State::BlockStart => (), 223 | State::BlockEnd | State::ListItem | State::Part => text.push_str("\n\n"), 224 | } 225 | *self = State::BlockStart; 226 | } 227 | fn end_block(&mut self) { 228 | *self = State::BlockEnd; 229 | } 230 | fn part(&mut self, text: &mut String) { 231 | match *self { 232 | State::None | State::BlockStart | State::Part | State::ListItem => (), 233 | State::BlockEnd => text.push_str("\n\n"), 234 | } 235 | *self = State::Part; 236 | } 237 | fn list_item(&mut self, text: &mut String) { 238 | match *self { 239 | State::None | State::BlockStart | State::BlockEnd => { 240 | debug_assert_eq!(*self, State::BlockStart); 241 | } 242 | State::ListItem | State::Part => text.push('\n'), 243 | } 244 | *self = State::ListItem; 245 | } 246 | fn soft_break(&mut self, text: &mut String) { 247 | text.push(' '); 248 | } 249 | fn hard_break(&mut self, text: &mut String) { 250 | text.push('\n'); 251 | } 252 | } 253 | 254 | #[derive(Clone, Debug, PartialEq)] 255 | pub struct Fmt { 256 | start: u32, 257 | font: FontSelector, 258 | rel_size: f32, 259 | flags: EffectFlags, 260 | } 261 | 262 | impl Fmt { 263 | fn new(item: &StackItem) -> Self { 264 | Fmt { 265 | start: item.start, 266 | font: item.sel, 267 | rel_size: item.rel_size, 268 | flags: item.flags, 269 | } 270 | } 271 | } 272 | 273 | #[derive(Clone, Debug)] 274 | struct StackItem { 275 | list: Option, 276 | start: u32, 277 | sel: FontSelector, 278 | rel_size: f32, 279 | flags: EffectFlags, 280 | } 281 | 282 | impl Default for StackItem { 283 | fn default() -> Self { 284 | StackItem { 285 | list: None, 286 | start: 0, 287 | sel: Default::default(), 288 | rel_size: 1.0, 289 | flags: EffectFlags::empty(), 290 | } 291 | } 292 | } 293 | 294 | impl StackItem { 295 | // process a tag; may modify current item and may return new item 296 | fn start_tag( 297 | &mut self, 298 | text: &mut String, 299 | state: &mut State, 300 | tag: Tag, 301 | ) -> Result, Error> { 302 | fn with_clone(s: &mut StackItem, c: F) -> Option { 303 | let mut item = s.clone(); 304 | c(&mut item); 305 | Some(item) 306 | } 307 | 308 | Ok(match tag { 309 | Tag::Paragraph => { 310 | state.start_block(text); 311 | None 312 | } 313 | Tag::Heading { level, .. } => { 314 | state.start_block(text); 315 | self.start = to_u32(text.len()); 316 | with_clone(self, |item| { 317 | // CSS sizes: https://www.w3.org/TR/2018/REC-css-fonts-3-20180920/#font-size-prop 318 | item.rel_size = match level { 319 | HeadingLevel::H1 => 2.0 / 1.0, 320 | HeadingLevel::H2 => 3.0 / 2.0, 321 | HeadingLevel::H3 => 6.0 / 5.0, 322 | HeadingLevel::H4 => 1.0, 323 | HeadingLevel::H5 => 8.0 / 9.0, 324 | HeadingLevel::H6 => 3.0 / 5.0, 325 | } 326 | }) 327 | } 328 | Tag::CodeBlock(_) => { 329 | state.start_block(text); 330 | self.start = to_u32(text.len()); 331 | with_clone(self, |item| { 332 | item.sel.family = FamilySelector::MONOSPACE; 333 | }) 334 | // TODO: within a code block, the last \n should be suppressed? 335 | } 336 | Tag::HtmlBlock => return Err(Error::NotSupported("embedded HTML")), 337 | Tag::List(start) => { 338 | state.start_block(text); 339 | self.list = start; 340 | None 341 | } 342 | Tag::Item => { 343 | state.list_item(text); 344 | // NOTE: we use \t for indent, which indents only the first 345 | // line. Without better flow control we cannot fix this. 346 | match &mut self.list { 347 | Some(x) => { 348 | write!(text, "{x}\t").unwrap(); 349 | *x += 1; 350 | } 351 | None => text.push_str("•\t"), 352 | } 353 | None 354 | } 355 | Tag::Emphasis => with_clone(self, |item| item.sel.style = FontStyle::Italic), 356 | Tag::Strong => with_clone(self, |item| item.sel.weight = FontWeight::BOLD), 357 | Tag::Strikethrough => with_clone(self, |item| { 358 | item.flags.set(EffectFlags::STRIKETHROUGH, true) 359 | }), 360 | Tag::BlockQuote(_) => return Err(Error::NotSupported("block quote")), 361 | Tag::FootnoteDefinition(_) => return Err(Error::NotSupported("footnote")), 362 | Tag::DefinitionList | Tag::DefinitionListTitle | Tag::DefinitionListDefinition => { 363 | return Err(Error::NotSupported("definition")); 364 | } 365 | Tag::Table(_) | Tag::TableHead | Tag::TableRow | Tag::TableCell => { 366 | return Err(Error::NotSupported("table")); 367 | } 368 | Tag::Superscript | Tag::Subscript => { 369 | // kas-text doesn't support adjusting the baseline 370 | return Err(Error::NotSupported("super/subscript")); 371 | } 372 | Tag::Link { .. } => return Err(Error::NotSupported("link")), 373 | Tag::Image { .. } => return Err(Error::NotSupported("image")), 374 | Tag::MetadataBlock(_) => return Err(Error::NotSupported("metadata block")), 375 | }) 376 | } 377 | // returns true if stack must be popped 378 | fn end_tag(&self, state: &mut State, tag: TagEnd) -> bool { 379 | match tag { 380 | TagEnd::Paragraph | TagEnd::List(_) => { 381 | state.end_block(); 382 | false 383 | } 384 | TagEnd::Heading(_) | TagEnd::CodeBlock => { 385 | state.end_block(); 386 | true 387 | } 388 | TagEnd::Item => false, 389 | TagEnd::Emphasis | TagEnd::Strong | TagEnd::Strikethrough => true, 390 | tag => unimplemented!("{:?}", tag), 391 | } 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /src/display/mod.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License in the LICENSE-APACHE file or at: 4 | // https://www.apache.org/licenses/LICENSE-2.0 5 | 6 | //! Text prepared for display 7 | 8 | #[allow(unused)] 9 | use crate::Text; 10 | use crate::conv::to_usize; 11 | use crate::{Direction, Vec2, shaper}; 12 | use smallvec::SmallVec; 13 | use tinyvec::TinyVec; 14 | 15 | mod glyph_pos; 16 | mod text_runs; 17 | mod wrap_lines; 18 | pub use glyph_pos::{Effect, EffectFlags, GlyphRun, MarkerPos, MarkerPosIter}; 19 | pub(crate) use text_runs::RunSpecial; 20 | pub use wrap_lines::Line; 21 | use wrap_lines::RunPart; 22 | 23 | /// Error returned on operations if not ready 24 | /// 25 | /// This error is returned if `prepare` must be called. 26 | #[derive(Clone, Copy, Default, Debug, PartialEq, Eq, thiserror::Error)] 27 | #[error("not ready")] 28 | pub struct NotReady; 29 | 30 | /// Text type-setting object (low-level, without text and configuration) 31 | /// 32 | /// This struct caches type-setting data at multiple levels of preparation. 33 | /// Its end result is a sequence of type-set glyphs. 34 | /// 35 | /// It is usually recommended to use [`Text`] instead, which includes 36 | /// the source text, type-setting configuration and status tracking. 37 | /// 38 | /// ### Status of preparation 39 | /// 40 | /// Stages of preparation are as follows: 41 | /// 42 | /// 1. Ensure all required [fonts](crate::fonts) are loaded. 43 | /// 2. Call [`Self::prepare_runs`] to break text into level runs, then shape 44 | /// these runs into glyph runs (unwrapped but with weak break points). 45 | /// 46 | /// This method must be called again if the `text`, text `direction` or 47 | /// `font_id` change. If only the text size (`dpem`) changes, it is 48 | /// sufficient to instead call [`Self::resize_runs`]. 49 | /// 3. Optionally, [`Self::measure_width`] and [`Self::measure_height`] may be 50 | /// used at this point to determine size requirements. 51 | /// 4. Call [`Self::prepare_lines`] to wrap text and perform re-ordering (where 52 | /// lines are bi-directional) and horizontal alignment. 53 | /// 54 | /// This must be called again if any of `wrap_width`, `width_bound` or 55 | /// `h_align` change. 56 | /// 5. Call [`Self::vertically_align`] to set or adjust vertical alignment. 57 | /// (Not technically required if alignment is always top.) 58 | /// 59 | /// All methods are idempotent (that is, they may be called multiple times 60 | /// without affecting the result). Later stages of preparation do not affect 61 | /// earlier stages, but if an earlier stage is repeated to account for adjusted 62 | /// configuration then later stages must also be repeated. 63 | /// 64 | /// This struct does not track the state of preparation. It is recommended to 65 | /// use [`Text`] or a custom wrapper for that purpose. Failure to observe the 66 | /// correct sequence is memory-safe but may cause panic or an unexpected result. 67 | /// 68 | /// ### Text navigation 69 | /// 70 | /// Despite lacking a copy of the underlying text, text-indices may be mapped to 71 | /// glyphs and lines, and vice-versa. 72 | /// 73 | /// The text range is `0..self.text_len()`. Any index within this range 74 | /// (inclusive of end point) is valid for usage in all methods taking a text index. 75 | /// Multiple indices may map to the same glyph (e.g. within multi-byte chars, 76 | /// with combining-diacritics, and with ligatures). In some cases a single index 77 | /// corresponds to multiple glyph positions (due to line-wrapping or change of 78 | /// direction in bi-directional text). 79 | /// 80 | /// Navigating to the start or end of a line can be done with 81 | /// [`TextDisplay::find_line`], [`TextDisplay::get_line`] and [`Line::text_range`]. 82 | /// 83 | /// Navigating forwards or backwards should be done via a library such as 84 | /// [`unicode-segmentation`](https://github.com/unicode-rs/unicode-segmentation) 85 | /// which provides a 86 | /// [`GraphemeCursor`](https://unicode-rs.github.io/unicode-segmentation/unicode_segmentation/struct.GraphemeCursor.html) 87 | /// to step back or forward one "grapheme", in logical text order. 88 | /// Optionally, the direction may 89 | /// be reversed for right-to-left lines [`TextDisplay::line_is_rtl`], but note 90 | /// that the result may be confusing since not all text on the line follows the 91 | /// line's base direction and adjacent lines may have different directions. 92 | /// 93 | /// Navigating glyphs left or right in display-order is not currently supported. 94 | /// 95 | /// To navigate "up" and "down" lines, use [`TextDisplay::text_glyph_pos`] to 96 | /// get the position of the cursor, [`TextDisplay::find_line`] to get the line 97 | /// number, then [`TextDisplay::line_index_nearest`] to find the new index. 98 | #[derive(Clone, Debug)] 99 | pub struct TextDisplay { 100 | // NOTE: typical numbers of elements: 101 | // Simple labels: runs=1, wrapped_runs=1, lines=1 102 | // Longer texts wrapped over n lines: runs=1, wrapped_runs=n, lines=n 103 | // Justified wrapped text: similar, but wrapped_runs is the word count 104 | // Simple texts with explicit breaks over n lines: all=n 105 | // Single-line bidi text: runs=n, wrapped_runs=n, lines=1 106 | // Complex bidi or formatted texts: all=many 107 | // Conclusion: SmallVec<[T; 1]> saves allocations in many cases. 108 | // 109 | /// Level runs within the text, in logical order 110 | runs: SmallVec<[shaper::GlyphRun; 1]>, 111 | /// Contiguous runs, in logical order 112 | /// 113 | /// Within a line, runs may not be in visual order due to BIDI reversals. 114 | wrapped_runs: TinyVec<[RunPart; 1]>, 115 | /// Visual (wrapped) lines, in visual and logical order 116 | lines: TinyVec<[Line; 1]>, 117 | #[cfg(feature = "num_glyphs")] 118 | num_glyphs: u32, 119 | l_bound: f32, 120 | r_bound: f32, 121 | } 122 | 123 | #[cfg(test)] 124 | #[test] 125 | fn size_of_elts() { 126 | use std::mem::size_of; 127 | assert_eq!(size_of::>(), 24); 128 | assert_eq!(size_of::(), 120); 129 | assert_eq!(size_of::(), 24); 130 | assert_eq!(size_of::(), 24); 131 | #[cfg(not(feature = "num_glyphs"))] 132 | assert_eq!(size_of::(), 208); 133 | #[cfg(feature = "num_glyphs")] 134 | assert_eq!(size_of::(), 216); 135 | } 136 | 137 | impl Default for TextDisplay { 138 | fn default() -> Self { 139 | TextDisplay { 140 | runs: Default::default(), 141 | wrapped_runs: Default::default(), 142 | lines: Default::default(), 143 | #[cfg(feature = "num_glyphs")] 144 | num_glyphs: 0, 145 | l_bound: 0.0, 146 | r_bound: 0.0, 147 | } 148 | } 149 | } 150 | 151 | impl TextDisplay { 152 | /// Get the number of lines (after wrapping) 153 | /// 154 | /// [Requires status][Self#status-of-preparation]: lines have been wrapped. 155 | #[inline] 156 | pub fn num_lines(&self) -> usize { 157 | self.lines.len() 158 | } 159 | 160 | /// Get line properties 161 | /// 162 | /// [Requires status][Self#status-of-preparation]: lines have been wrapped. 163 | #[inline] 164 | pub fn get_line(&self, index: usize) -> Option<&Line> { 165 | self.lines.get(index) 166 | } 167 | 168 | /// Iterate over line properties 169 | /// 170 | /// [Requires status][Self#status-of-preparation]: lines have been wrapped. 171 | #[inline] 172 | pub fn lines(&self) -> impl Iterator { 173 | self.lines.iter() 174 | } 175 | 176 | /// Get the size of the required bounding box 177 | /// 178 | /// [Requires status][Self#status-of-preparation]: lines have been wrapped. 179 | /// 180 | /// Returns the position of the upper-left and lower-right corners of a 181 | /// bounding box on content. 182 | /// Alignment and input bounds do affect the result. 183 | pub fn bounding_box(&self) -> (Vec2, Vec2) { 184 | if self.lines.is_empty() { 185 | return (Vec2::ZERO, Vec2::ZERO); 186 | } 187 | 188 | let top = self.lines.first().unwrap().top; 189 | let bottom = self.lines.last().unwrap().bottom; 190 | (Vec2(self.l_bound, top), Vec2(self.r_bound, bottom)) 191 | } 192 | 193 | /// Find the line containing text `index` 194 | /// 195 | /// [Requires status][Self#status-of-preparation]: lines have been wrapped. 196 | /// 197 | /// Returns the line number and the text-range of the line. 198 | /// 199 | /// Returns `None` in case `index` does not line on or at the end of a line 200 | /// (which means either that `index` is beyond the end of the text or that 201 | /// `index` is within a mult-byte line break). 202 | pub fn find_line(&self, index: usize) -> Option<(usize, std::ops::Range)> { 203 | let mut first = None; 204 | for (n, line) in self.lines.iter().enumerate() { 205 | let text_range = line.text_range(); 206 | if text_range.end == index { 207 | // When line wrapping, this also matches the start of the next 208 | // line which is the preferred location. At the end of other 209 | // lines it does not match any other location. 210 | first = Some((n, text_range)); 211 | } else if text_range.contains(&index) { 212 | return Some((n, text_range)); 213 | } 214 | } 215 | first 216 | } 217 | 218 | /// Get the base directionality of the text 219 | /// 220 | /// [Requires status][Self#status-of-preparation]: none. 221 | pub fn text_is_rtl(&self, text: &str, direction: Direction) -> bool { 222 | let (is_auto, mut is_rtl) = match direction { 223 | Direction::Ltr => (false, false), 224 | Direction::Rtl => (false, true), 225 | Direction::Auto => (true, false), 226 | Direction::AutoRtl => (true, true), 227 | }; 228 | 229 | if is_auto { 230 | match unicode_bidi::get_base_direction(text) { 231 | unicode_bidi::Direction::Ltr => is_rtl = false, 232 | unicode_bidi::Direction::Rtl => is_rtl = true, 233 | unicode_bidi::Direction::Mixed => (), 234 | } 235 | } 236 | 237 | is_rtl 238 | } 239 | 240 | /// Get the directionality of the current line 241 | /// 242 | /// [Requires status][Self#status-of-preparation]: lines have been wrapped. 243 | /// 244 | /// Returns: 245 | /// 246 | /// - `None` if text is empty 247 | /// - `Some(line_is_right_to_left)` otherwise 248 | /// 249 | /// Note: indeterminate lines (e.g. empty lines) have their direction 250 | /// determined from the passed environment, by default left-to-right. 251 | pub fn line_is_rtl(&self, line: usize) -> Option { 252 | if let Some(line) = self.lines.get(line) { 253 | let first_run = line.run_range.start(); 254 | let glyph_run = to_usize(self.wrapped_runs[first_run].glyph_run); 255 | Some(self.runs[glyph_run].level.is_rtl()) 256 | } else { 257 | None 258 | } 259 | } 260 | 261 | /// Find the text index for the glyph nearest the given `pos` 262 | /// 263 | /// [Requires status][Self#status-of-preparation]: 264 | /// text is fully prepared for display. 265 | /// 266 | /// This includes the index immediately after the last glyph, thus 267 | /// `result ≤ text.len()`. 268 | /// 269 | /// Note: if the font's `rect` does not start at the origin, then its top-left 270 | /// coordinate should first be subtracted from `pos`. 271 | pub fn text_index_nearest(&self, pos: Vec2) -> usize { 272 | let mut n = 0; 273 | for (i, line) in self.lines.iter().enumerate() { 274 | if line.top > pos.1 { 275 | break; 276 | } 277 | n = i; 278 | } 279 | // Expected to return Some(..) value but None has been observed: 280 | self.line_index_nearest(n, pos.0).unwrap_or(0) 281 | } 282 | 283 | /// Find the text index nearest horizontal-coordinate `x` on `line` 284 | /// 285 | /// [Requires status][Self#status-of-preparation]: lines have been wrapped. 286 | /// 287 | /// This is similar to [`TextDisplay::text_index_nearest`], but allows the 288 | /// line to be specified explicitly. Returns `None` only on invalid `line`. 289 | pub fn line_index_nearest(&self, line: usize, x: f32) -> Option { 290 | if line >= self.lines.len() { 291 | return None; 292 | } 293 | let line = &self.lines[line]; 294 | let run_range = line.run_range.to_std(); 295 | 296 | let mut best = line.text_range().start; 297 | let mut best_dist = f32::INFINITY; 298 | let mut try_best = |dist, index: u32| { 299 | if dist < best_dist { 300 | best = to_usize(index); 301 | best_dist = dist; 302 | } 303 | }; 304 | 305 | for run_part in &self.wrapped_runs[run_range] { 306 | let glyph_run = &self.runs[to_usize(run_part.glyph_run)]; 307 | let rel_pos = x - run_part.offset.0; 308 | 309 | let end_index; 310 | if glyph_run.level.is_ltr() { 311 | for glyph in &glyph_run.glyphs[run_part.glyph_range.to_std()] { 312 | let dist = (glyph.position.0 - rel_pos).abs(); 313 | try_best(dist, glyph.index); 314 | } 315 | end_index = run_part.text_end; 316 | } else { 317 | let mut index = run_part.text_end; 318 | for glyph in &glyph_run.glyphs[run_part.glyph_range.to_std()] { 319 | let dist = (glyph.position.0 - rel_pos).abs(); 320 | try_best(dist, index); 321 | index = glyph.index 322 | } 323 | end_index = index; 324 | } 325 | 326 | let end_pos = if run_part.glyph_range.end() < glyph_run.glyphs.len() { 327 | glyph_run.glyphs[run_part.glyph_range.end()].position.0 328 | } else { 329 | glyph_run.caret 330 | }; 331 | try_best((end_pos - rel_pos).abs(), end_index); 332 | } 333 | 334 | Some(best) 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /src/fonts/library.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License in the LICENSE-APACHE file or at: 4 | // https://www.apache.org/licenses/LICENSE-2.0 5 | 6 | //! Font library 7 | 8 | #![allow(clippy::len_without_is_empty)] 9 | 10 | use super::{FaceRef, FontSelector, Resolver}; 11 | use crate::conv::{to_u32, to_usize}; 12 | use fontique::{Blob, QueryStatus, Script, Synthesis}; 13 | use std::collections::hash_map::{Entry, HashMap}; 14 | use std::sync::{LazyLock, Mutex, MutexGuard, RwLock}; 15 | use thiserror::Error; 16 | pub(crate) use ttf_parser::Face; 17 | 18 | /// Font loading errors 19 | #[derive(Error, Debug)] 20 | enum FontError { 21 | #[error("font load error")] 22 | TtfParser(#[from] ttf_parser::FaceParsingError), 23 | #[cfg(feature = "ab_glyph")] 24 | #[error("font load error")] 25 | AbGlyph(#[from] ab_glyph::InvalidFont), 26 | #[error("font load error")] 27 | Swash, 28 | } 29 | 30 | /// Bad [`FontId`] or no font loaded 31 | /// 32 | /// This error should be impossible to observe, but exists to avoid panic in 33 | /// lower level methods. 34 | #[derive(Error, Debug)] 35 | #[error("invalid FontId")] 36 | pub struct InvalidFontId; 37 | 38 | /// No matching font found 39 | /// 40 | /// Text layout failed. 41 | #[derive(Error, Debug)] 42 | #[error("no font match")] 43 | pub struct NoFontMatch; 44 | 45 | /// Font face identifier 46 | /// 47 | /// Identifies a loaded font face within the [`FontLibrary`] by index. 48 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] 49 | pub struct FaceId(pub(crate) u32); 50 | impl FaceId { 51 | /// Get as `usize` 52 | pub fn get(self) -> usize { 53 | to_usize(self.0) 54 | } 55 | } 56 | 57 | impl From for FaceId { 58 | fn from(id: u32) -> Self { 59 | FaceId(id) 60 | } 61 | } 62 | 63 | /// Font face identifier 64 | /// 65 | /// Identifies a font list within the [`FontLibrary`] by index. 66 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] 67 | pub struct FontId(u32); 68 | impl FontId { 69 | /// Get as `usize` 70 | pub fn get(self) -> usize { 71 | to_usize(self.0) 72 | } 73 | } 74 | 75 | /// A store of data for a font face, supporting various backends 76 | pub struct FaceStore { 77 | blob: Blob, 78 | index: u32, 79 | face: Face<'static>, 80 | #[cfg(feature = "rustybuzz")] 81 | rustybuzz: rustybuzz::Face<'static>, 82 | #[cfg(feature = "ab_glyph")] 83 | ab_glyph: ab_glyph::FontRef<'static>, 84 | swash: (u32, swash::CacheKey), // (offset, key) 85 | synthesis: Synthesis, 86 | } 87 | 88 | impl FaceStore { 89 | /// Construct, given a file path, a reference to the loaded data and the face index 90 | /// 91 | /// The `path` is to be stored; its contents are already loaded in `data`. 92 | fn new(blob: Blob, index: u32, synthesis: Synthesis) -> Result { 93 | // Safety: this is a private fn used to construct a FaceStore instance 94 | // to be stored in FontLibrary which is never deallocated. This 95 | // FaceStore holds onto `blob`, so `data` is valid until program exit. 96 | let data = unsafe { extend_lifetime(blob.data()) }; 97 | 98 | let face = Face::parse(data, index)?; 99 | 100 | Ok(FaceStore { 101 | blob, 102 | index, 103 | #[cfg(feature = "rustybuzz")] 104 | rustybuzz: { 105 | use {rustybuzz::Variation, ttf_parser::Tag}; 106 | 107 | let len = synthesis.variation_settings().len(); 108 | debug_assert!(len <= 3); 109 | let mut vars = [Variation { 110 | tag: Tag(0), 111 | value: 0.0, 112 | }; 3]; 113 | for (r, (tag, value)) in vars.iter_mut().zip(synthesis.variation_settings()) { 114 | r.tag = Tag::from_bytes(&tag.to_be_bytes()); 115 | r.value = *value; 116 | } 117 | 118 | let mut rustybuzz = rustybuzz::Face::from_face(face.clone()); 119 | rustybuzz.set_variations(&vars[0..len]); 120 | rustybuzz 121 | }, 122 | face, 123 | #[cfg(feature = "ab_glyph")] 124 | ab_glyph: { 125 | let mut font = ab_glyph::FontRef::try_from_slice_and_index(data, index)?; 126 | for (tag, value) in synthesis.variation_settings() { 127 | ab_glyph::VariableFont::set_variation(&mut font, &tag.to_be_bytes(), *value); 128 | } 129 | font 130 | }, 131 | swash: { 132 | use easy_cast::Cast; 133 | let f = swash::FontRef::from_index(data, index.cast()).ok_or(FontError::Swash)?; 134 | (f.offset, f.key) 135 | }, 136 | synthesis, 137 | }) 138 | } 139 | 140 | /// Access the [`Face`] object 141 | pub fn face(&self) -> &Face<'static> { 142 | &self.face 143 | } 144 | 145 | /// Access a [`FaceRef`] object 146 | pub fn face_ref(&self) -> FaceRef<'_> { 147 | FaceRef(&self.face) 148 | } 149 | 150 | /// Access the [`rustybuzz`] object 151 | #[cfg(feature = "rustybuzz")] 152 | pub fn rustybuzz(&self) -> &rustybuzz::Face<'static> { 153 | &self.rustybuzz 154 | } 155 | 156 | /// Access the [`ab_glyph`] object 157 | #[cfg(feature = "ab_glyph")] 158 | pub fn ab_glyph(&self) -> &ab_glyph::FontRef<'static> { 159 | &self.ab_glyph 160 | } 161 | 162 | /// Get a swash `FontRef` 163 | pub fn swash(&self) -> swash::FontRef<'_> { 164 | swash::FontRef { 165 | data: self.face.raw_face().data, 166 | offset: self.swash.0, 167 | key: self.swash.1, 168 | } 169 | } 170 | 171 | /// Get font variation settings 172 | pub fn synthesis(&self) -> &Synthesis { 173 | &self.synthesis 174 | } 175 | } 176 | 177 | #[derive(Default)] 178 | struct FaceList { 179 | // Safety: unsafe code depends on entries never moving (hence the otherwise 180 | // redundant use of Box). See e.g. FontLibrary::get_face(). 181 | #[allow(clippy::vec_box)] 182 | faces: Vec>, 183 | // These are vec-maps. Why? Because length should be short. 184 | source_hash: Vec<(u64, FaceId)>, 185 | } 186 | 187 | impl FaceList { 188 | fn push(&mut self, face: Box, source_hash: u64) -> FaceId { 189 | let id = FaceId(to_u32(self.faces.len())); 190 | self.faces.push(face); 191 | self.source_hash.push((source_hash, id)); 192 | id 193 | } 194 | } 195 | 196 | #[derive(Default)] 197 | struct FontList { 198 | // A "font" is a list of faces (primary + fallbacks); we cache glyph-lookups per char 199 | fonts: Vec<(FontId, Vec, HashMap>)>, 200 | sel_hash: Vec<(u64, FontId)>, 201 | } 202 | 203 | impl FontList { 204 | fn push(&mut self, list: Vec, sel_hash: u64) -> FontId { 205 | let id = FontId(to_u32(self.fonts.len())); 206 | self.fonts.push((id, list, HashMap::new())); 207 | self.sel_hash.push((sel_hash, id)); 208 | id 209 | } 210 | } 211 | 212 | /// Library of loaded fonts 213 | /// 214 | /// This is the type of the global singleton accessible via the [`library()`] 215 | /// function. Thread-safety is handled via internal locks. 216 | pub struct FontLibrary { 217 | resolver: Mutex, 218 | faces: RwLock, 219 | fonts: RwLock, 220 | } 221 | 222 | /// Font management 223 | impl FontLibrary { 224 | /// Get a reference to the font resolver 225 | pub fn resolver(&self) -> MutexGuard<'_, Resolver> { 226 | self.resolver.lock().unwrap() 227 | } 228 | 229 | /// Get the first face for a font 230 | /// 231 | /// Each font identifier has at least one font face. This resolves the first 232 | /// (default) one. 233 | pub fn first_face_for(&self, font_id: FontId) -> Result { 234 | let fonts = self.fonts.read().unwrap(); 235 | for (id, list, _) in &fonts.fonts { 236 | if *id == font_id { 237 | return Ok(*list.first().unwrap()); 238 | } 239 | } 240 | Err(InvalidFontId) 241 | } 242 | 243 | /// Get the first face for a font 244 | /// 245 | /// This is a wrapper around [`FontLibrary::first_face_for`] and [`FontLibrary::get_face`]. 246 | #[inline] 247 | pub fn get_first_face(&self, font_id: FontId) -> Result, InvalidFontId> { 248 | let face_id = self.first_face_for(font_id)?; 249 | Ok(self.get_face(face_id)) 250 | } 251 | 252 | /// Check whether a [`FaceId`] is part of a [`FontId`] 253 | pub fn contains_face(&self, font_id: FontId, face_id: FaceId) -> Result { 254 | let fonts = self.fonts.read().unwrap(); 255 | for (id, list, _) in &fonts.fonts { 256 | if *id == font_id { 257 | return Ok(list.contains(&face_id)); 258 | } 259 | } 260 | Err(InvalidFontId) 261 | } 262 | 263 | /// Resolve the font face for a character 264 | /// 265 | /// If `last_face_id` is a face used by `font_id` and this face covers `c`, 266 | /// then return `last_face_id`. (This is to avoid changing the font face 267 | /// unnecessarily, such as when encountering a space amid Arabic text.) 268 | /// 269 | /// Otherwise, return the first face of `font_id` which covers `c`. 270 | /// 271 | /// Otherwise (if no face covers `c`) return the first face (if any). 272 | pub fn face_for_char( 273 | &self, 274 | font_id: FontId, 275 | last_face_id: Option, 276 | c: char, 277 | ) -> Result, InvalidFontId> { 278 | // TODO: `face.glyph_index` is a bit slow to use like this where several 279 | // faces may return no result before we find a match. Caching results 280 | // in a HashMap helps. Perhaps better would be to (somehow) determine 281 | // the script/language in use and check whether the font face supports 282 | // that, perhaps also checking it has shaping support. 283 | let mut fonts = self.fonts.write().unwrap(); 284 | let font = fonts 285 | .fonts 286 | .iter_mut() 287 | .find(|item| item.0 == font_id) 288 | .ok_or(InvalidFontId)?; 289 | 290 | let faces = self.faces.read().unwrap(); 291 | 292 | if let Some(face_id) = last_face_id { 293 | if font.1.contains(&face_id) { 294 | let face = &faces.faces[face_id.get()]; 295 | // TODO(opt): should we cache this lookup? 296 | if face.face.glyph_index(c).is_some() { 297 | return Ok(Some(face_id)); 298 | } 299 | } 300 | } 301 | 302 | Ok(match font.2.entry(c) { 303 | Entry::Occupied(entry) => *entry.get(), 304 | Entry::Vacant(entry) => { 305 | let mut id: Option = None; 306 | for face_id in font.1.iter() { 307 | let face = &faces.faces[face_id.get()]; 308 | if face.face.glyph_index(c).is_some() { 309 | id = Some(*face_id); 310 | break; 311 | } 312 | } 313 | 314 | // Prefer to match some font face, even without a match 315 | // TODO: we need some mechanism to widen the search when this 316 | // fails (certain chars might only be found in a special font). 317 | if id.is_none() { 318 | id = font.1.first().map(|id| *id); 319 | } 320 | 321 | entry.insert(id); 322 | id 323 | } 324 | }) 325 | } 326 | 327 | /// Select a font 328 | /// 329 | /// This method uses internal caching to enable fast look-ups of existing 330 | /// (loaded) fonts. Resolving new fonts may be slower. 331 | pub fn select_font( 332 | &self, 333 | selector: &FontSelector, 334 | script: Script, 335 | ) -> Result { 336 | let sel_hash = { 337 | use std::collections::hash_map::DefaultHasher; 338 | use std::hash::{Hash, Hasher}; 339 | 340 | let mut s = DefaultHasher::new(); 341 | selector.hash(&mut s); 342 | script.hash(&mut s); 343 | s.finish() 344 | }; 345 | 346 | let fonts = self.fonts.read().unwrap(); 347 | for (h, id) in &fonts.sel_hash { 348 | if *h == sel_hash { 349 | return Ok(*id); 350 | } 351 | } 352 | drop(fonts); 353 | 354 | let mut faces = Vec::new(); 355 | let mut families = Vec::new(); 356 | let mut resolver = self.resolver.lock().unwrap(); 357 | let mut face_list = self.faces.write().unwrap(); 358 | 359 | selector.select(&mut resolver, script, |qf| { 360 | if log::log_enabled!(log::Level::Debug) { 361 | families.push(qf.family); 362 | } 363 | 364 | let source_hash = { 365 | use std::hash::{DefaultHasher, Hash, Hasher}; 366 | 367 | let mut hasher = DefaultHasher::new(); 368 | qf.blob.id().hash(&mut hasher); 369 | hasher.write_u32(qf.index); 370 | hasher.finish() 371 | }; 372 | 373 | for (h, id) in face_list.source_hash.iter().cloned() { 374 | if h == source_hash { 375 | let face = &face_list.faces[id.get()]; 376 | if face.blob.id() == qf.blob.id() && face.index == qf.index { 377 | faces.push(id); 378 | return QueryStatus::Continue; 379 | } 380 | } 381 | } 382 | 383 | match FaceStore::new(qf.blob.clone(), qf.index, qf.synthesis) { 384 | Ok(store) => { 385 | let id = face_list.push(Box::new(store), source_hash); 386 | faces.push(id); 387 | } 388 | Err(err) => { 389 | log::error!("Failed to load font: {err}"); 390 | } 391 | } 392 | 393 | QueryStatus::Continue 394 | }); 395 | 396 | for family in families { 397 | if let Some(name) = resolver.font_family(family.0) { 398 | log::debug!("match: {name}"); 399 | } 400 | } 401 | 402 | if faces.is_empty() { 403 | return Err(NoFontMatch); 404 | } 405 | let font = self.fonts.write().unwrap().push(faces, sel_hash); 406 | Ok(font) 407 | } 408 | } 409 | 410 | /// Face management 411 | impl FontLibrary { 412 | /// Get a font face from its identifier 413 | /// 414 | /// Panics if `id` is not valid (required: `id.get() < self.num_faces()`). 415 | pub fn get_face(&self, id: FaceId) -> FaceRef<'static> { 416 | self.get_face_store(id).face_ref() 417 | } 418 | 419 | /// Get access to the [`FaceStore`] 420 | /// 421 | /// Panics if `id` is not valid (required: `id.get() < self.num_faces()`). 422 | pub fn get_face_store(&self, id: FaceId) -> &'static FaceStore { 423 | let faces = self.faces.read().unwrap(); 424 | assert!(id.get() < faces.faces.len(), "FontLibrary: invalid {id:?}!",); 425 | let faces: &FaceStore = &faces.faces[id.get()]; 426 | // Safety: elements of self.faces are never dropped or modified 427 | unsafe { extend_lifetime(faces) } 428 | } 429 | } 430 | 431 | pub(crate) unsafe fn extend_lifetime<'b, T: ?Sized>(r: &'b T) -> &'static T { 432 | unsafe { std::mem::transmute::<&'b T, &'static T>(r) } 433 | } 434 | 435 | static LIBRARY: LazyLock = LazyLock::new(|| FontLibrary { 436 | resolver: Mutex::new(Resolver::new()), 437 | faces: Default::default(), 438 | fonts: Default::default(), 439 | }); 440 | 441 | /// Access the [`FontLibrary`] singleton 442 | pub fn library() -> &'static FontLibrary { 443 | &LIBRARY 444 | } 445 | -------------------------------------------------------------------------------- /src/fonts/attributes.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License in the LICENSE-APACHE file or at: 4 | // https://www.apache.org/licenses/LICENSE-2.0 5 | // 6 | // This file is copied from https://github.com/linebender/parley PR #359. 7 | // Copyright 2024 the Parley Authors 8 | 9 | //! Properties for specifying font weight, width and style. 10 | 11 | use core::fmt; 12 | use easy_cast::Cast; 13 | #[cfg(feature = "serde")] 14 | use serde::{Deserialize, Serialize}; 15 | 16 | /// Visual width of a font-- a relative change from the normal aspect 17 | /// ratio, typically in the 50% - 200% range. 18 | /// 19 | /// The default value is [`FontWidth::NORMAL`]. 20 | /// 21 | /// In variable fonts, this can be controlled with the `wdth` axis. 22 | /// 23 | /// In Open Type, the `u16` [`usWidthClass`] field has 9 values, from 1-9, 24 | /// which doesn't allow for the wide range of values possible with variable 25 | /// fonts. 26 | /// 27 | /// See 28 | /// 29 | /// In CSS, this corresponds to the [`font-width`] property. 30 | /// 31 | /// This has also been known as "stretch" and has a legacy CSS name alias, 32 | /// [`font-stretch`]. 33 | /// 34 | /// [`usWidthClass`]: https://learn.microsoft.com/en-us/typography/opentype/spec/os2#uswidthclass 35 | /// [`font-width`]: https://www.w3.org/TR/css-fonts-4/#font-width-prop 36 | /// [`font-stretch`]: https://www.w3.org/TR/css-fonts-4/#font-stretch-prop 37 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] 38 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 39 | pub struct FontWidth(u16); 40 | 41 | impl FontWidth { 42 | /// Width that is 50% of normal. 43 | pub const ULTRA_CONDENSED: Self = Self(128); 44 | 45 | /// Width that is 62.5% of normal. 46 | pub const EXTRA_CONDENSED: Self = Self(160); 47 | 48 | /// Width that is 75% of normal. 49 | pub const CONDENSED: Self = Self(192); 50 | 51 | /// Width that is 87.5% of normal. 52 | pub const SEMI_CONDENSED: Self = Self(224); 53 | 54 | /// Width that is 100% of normal. This is the default value. 55 | pub const NORMAL: Self = Self(256); 56 | 57 | /// Width that is 112.5% of normal. 58 | pub const SEMI_EXPANDED: Self = Self(288); 59 | 60 | /// Width that is 125% of normal. 61 | pub const EXPANDED: Self = Self(320); 62 | 63 | /// Width that is 150% of normal. 64 | pub const EXTRA_EXPANDED: Self = Self(384); 65 | 66 | /// Width that is 200% of normal. 67 | pub const ULTRA_EXPANDED: Self = Self(512); 68 | } 69 | 70 | impl FontWidth { 71 | /// Creates a new width attribute with the given ratio. 72 | /// 73 | /// Panics if the ratio is not between `0` and `255.996`. 74 | /// 75 | /// This can also be created [from a percentage](Self::from_percentage). 76 | /// 77 | /// # Example 78 | /// 79 | /// ``` 80 | /// # use kas_text::fonts::FontWidth; 81 | /// assert_eq!(FontWidth::from_ratio(1.5), FontWidth::EXTRA_EXPANDED); 82 | /// ``` 83 | pub fn from_ratio(ratio: f32) -> Self { 84 | let value = (ratio * 256.0).round(); 85 | assert!(0.0 <= value && value <= (u16::MAX as f32)); 86 | Self(value as u16) 87 | } 88 | 89 | /// Creates a width attribute from a percentage. 90 | /// 91 | /// Panics if the percentage is not between `0%` and `25599.6%`. 92 | /// 93 | /// This can also be created [from a ratio](Self::from_ratio). 94 | /// 95 | /// # Example 96 | /// 97 | /// ``` 98 | /// # use kas_text::fonts::FontWidth; 99 | /// assert_eq!(FontWidth::from_percentage(87.5), FontWidth::SEMI_CONDENSED); 100 | /// ``` 101 | pub fn from_percentage(percentage: f32) -> Self { 102 | Self::from_ratio(percentage / 100.0) 103 | } 104 | 105 | /// Returns the width attribute as a ratio. 106 | /// 107 | /// This is a linear scaling factor with `1.0` being "normal" width. 108 | /// 109 | /// # Example 110 | /// 111 | /// ``` 112 | /// # use kas_text::fonts::FontWidth; 113 | /// assert_eq!(FontWidth::NORMAL.ratio(), 1.0); 114 | /// ``` 115 | pub fn ratio(self) -> f32 { 116 | (self.0 as f32) / 256.0 117 | } 118 | 119 | /// Returns the width attribute as a percentage value. 120 | /// 121 | /// This is generally the value associated with the `wdth` axis. 122 | pub fn percentage(self) -> f32 { 123 | self.ratio() * 100.0 124 | } 125 | 126 | /// Returns `true` if the width is [normal]. 127 | /// 128 | /// [normal]: FontWidth::NORMAL 129 | pub fn is_normal(self) -> bool { 130 | self == Self::NORMAL 131 | } 132 | 133 | /// Returns `true` if the width is condensed (less than [normal]). 134 | /// 135 | /// [normal]: FontWidth::NORMAL 136 | pub fn is_condensed(self) -> bool { 137 | self < Self::NORMAL 138 | } 139 | 140 | /// Returns `true` if the width is expanded (greater than [normal]). 141 | /// 142 | /// [normal]: FontWidth::NORMAL 143 | pub fn is_expanded(self) -> bool { 144 | self > Self::NORMAL 145 | } 146 | 147 | /// Parses the width from a CSS style keyword or a percentage value. 148 | /// 149 | /// # Examples 150 | /// 151 | /// ``` 152 | /// # use kas_text::fonts::FontWidth; 153 | /// assert_eq!(FontWidth::parse("semi-condensed"), Some(FontWidth::SEMI_CONDENSED)); 154 | /// assert_eq!(FontWidth::parse("80%"), Some(FontWidth::from_percentage(80.0))); 155 | /// assert_eq!(FontWidth::parse("wideload"), None); 156 | /// ``` 157 | pub fn parse(s: &str) -> Option { 158 | let s = s.trim(); 159 | Some(match s { 160 | "ultra-condensed" => Self::ULTRA_CONDENSED, 161 | "extra-condensed" => Self::EXTRA_CONDENSED, 162 | "condensed" => Self::CONDENSED, 163 | "semi-condensed" => Self::SEMI_CONDENSED, 164 | "normal" => Self::NORMAL, 165 | "semi-expanded" => Self::SEMI_EXPANDED, 166 | "extra-expanded" => Self::EXTRA_EXPANDED, 167 | "ultra-expanded" => Self::ULTRA_EXPANDED, 168 | _ => { 169 | if s.ends_with('%') { 170 | let p = s.get(..s.len() - 1)?.parse::().ok()?; 171 | return Some(Self::from_percentage(p)); 172 | } 173 | return None; 174 | } 175 | }) 176 | } 177 | } 178 | 179 | impl fmt::Display for FontWidth { 180 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 181 | let keyword = match *self { 182 | v if v == Self::ULTRA_CONDENSED => "ultra-condensed", 183 | v if v == Self::EXTRA_CONDENSED => "extra-condensed", 184 | v if v == Self::CONDENSED => "condensed", 185 | v if v == Self::SEMI_CONDENSED => "semi-condensed", 186 | v if v == Self::NORMAL => "normal", 187 | v if v == Self::SEMI_EXPANDED => "semi-expanded", 188 | v if v == Self::EXPANDED => "expanded", 189 | v if v == Self::EXTRA_EXPANDED => "extra-expanded", 190 | v if v == Self::ULTRA_EXPANDED => "ultra-expanded", 191 | _ => { 192 | return write!(f, "{}%", self.percentage()); 193 | } 194 | }; 195 | write!(f, "{keyword}") 196 | } 197 | } 198 | 199 | impl Default for FontWidth { 200 | fn default() -> Self { 201 | Self::NORMAL 202 | } 203 | } 204 | 205 | impl From for fontique::FontWidth { 206 | #[inline] 207 | fn from(width: FontWidth) -> Self { 208 | fontique::FontWidth::from_ratio(width.ratio()) 209 | } 210 | } 211 | 212 | /// Visual weight class of a font, typically on a scale from 1 to 1000. 213 | /// 214 | /// The default value is [`FontWeight::NORMAL`] or `400`. 215 | /// 216 | /// In variable fonts, this can be controlled with the `wght` axis. This 217 | /// is a `u16` so that it can represent the same range of values as the 218 | /// `wght` axis. 219 | /// 220 | /// See 221 | /// 222 | /// In CSS, this corresponds to the [`font-weight`] property. 223 | /// 224 | /// [`font-weight`]: https://www.w3.org/TR/css-fonts-4/#font-weight-prop 225 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] 226 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 227 | pub struct FontWeight(u16); 228 | 229 | impl FontWeight { 230 | /// Weight value of 100. 231 | pub const THIN: Self = Self(100); 232 | 233 | /// Weight value of 200. 234 | pub const EXTRA_LIGHT: Self = Self(200); 235 | 236 | /// Weight value of 300. 237 | pub const LIGHT: Self = Self(300); 238 | 239 | /// Weight value of 350. 240 | pub const SEMI_LIGHT: Self = Self(350); 241 | 242 | /// Weight value of 400. This is the default value. 243 | pub const NORMAL: Self = Self(400); 244 | 245 | /// Weight value of 500. 246 | pub const MEDIUM: Self = Self(500); 247 | 248 | /// Weight value of 600. 249 | pub const SEMI_BOLD: Self = Self(600); 250 | 251 | /// Weight value of 700. 252 | pub const BOLD: Self = Self(700); 253 | 254 | /// Weight value of 800. 255 | pub const EXTRA_BOLD: Self = Self(800); 256 | 257 | /// Weight value of 900. 258 | pub const BLACK: Self = Self(900); 259 | 260 | /// Weight value of 950. 261 | pub const EXTRA_BLACK: Self = Self(950); 262 | } 263 | 264 | impl FontWeight { 265 | /// Creates a new weight attribute with the given value. 266 | pub fn new(weight: u16) -> Self { 267 | Self(weight) 268 | } 269 | 270 | /// Returns the underlying weight value. 271 | pub fn value(self) -> u16 { 272 | self.0 273 | } 274 | 275 | /// Parses a CSS style font weight attribute. 276 | /// 277 | /// # Examples 278 | /// 279 | /// ``` 280 | /// # use kas_text::fonts::FontWeight; 281 | /// assert_eq!(FontWeight::parse("normal"), Some(FontWeight::NORMAL)); 282 | /// assert_eq!(FontWeight::parse("bold"), Some(FontWeight::BOLD)); 283 | /// assert_eq!(FontWeight::parse("850"), Some(FontWeight::new(850))); 284 | /// assert_eq!(FontWeight::parse("invalid"), None); 285 | /// ``` 286 | pub fn parse(s: &str) -> Option { 287 | let s = s.trim(); 288 | Some(match s { 289 | "normal" => Self::NORMAL, 290 | "bold" => Self::BOLD, 291 | _ => Self(s.parse::().ok()?), 292 | }) 293 | } 294 | } 295 | 296 | impl Default for FontWeight { 297 | fn default() -> Self { 298 | Self::NORMAL 299 | } 300 | } 301 | 302 | impl fmt::Display for FontWeight { 303 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 304 | let keyword = match self.0 { 305 | 100 => "thin", 306 | 200 => "extra-light", 307 | 300 => "light", 308 | 400 => "normal", 309 | 500 => "medium", 310 | 600 => "semi-bold", 311 | 700 => "bold", 312 | 800 => "extra-bold", 313 | 900 => "black", 314 | _ => return write!(f, "{}", self.0), 315 | }; 316 | write!(f, "{keyword}") 317 | } 318 | } 319 | 320 | impl From for fontique::FontWeight { 321 | #[inline] 322 | fn from(weight: FontWeight) -> Self { 323 | fontique::FontWeight::new(weight.value().cast()) 324 | } 325 | } 326 | 327 | /// Visual style or 'slope' of a font. 328 | /// 329 | /// The default value is [`FontStyle::Normal`]. 330 | /// 331 | /// In variable fonts, this can be controlled with the `ital` 332 | /// and `slnt` axes for italic and oblique styles, respectively. 333 | /// This uses an `f32` for the `Oblique` variant so so that it 334 | /// can represent the same range of values as the `slnt` axis. 335 | /// 336 | /// See 337 | /// 338 | /// In CSS, this corresponds to the [`font-style`] property. 339 | /// 340 | /// [`font-style`]: https://www.w3.org/TR/css-fonts-4/#font-style-prop 341 | #[derive(Copy, Clone, PartialEq, Eq, Default, Debug, Hash)] 342 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 343 | pub enum FontStyle { 344 | /// An upright or "roman" style. 345 | #[default] 346 | Normal, 347 | /// Generally a slanted style, originally based on semi-cursive forms. 348 | /// This often has a different structure from the normal style. 349 | Italic, 350 | /// Oblique (or slanted) style with an optional angle in degrees times 256, 351 | /// counter-clockwise from the vertical. 352 | /// 353 | /// To convert `Some(angle)` to degrees, use 354 | /// `degrees = (angle as f32) / 256.0` or [`FontStyle::oblique_degrees`]. 355 | Oblique(Option), 356 | } 357 | 358 | impl FontStyle { 359 | /// Parses a font style from a CSS value. 360 | pub fn parse(mut s: &str) -> Option { 361 | s = s.trim(); 362 | Some(match s { 363 | "normal" => Self::Normal, 364 | "italic" => Self::Italic, 365 | "oblique" => Self::Oblique(Some(14 * 256)), 366 | _ => { 367 | if s.starts_with("oblique ") { 368 | s = s.get(8..)?; 369 | if s.ends_with("deg") { 370 | s = s.get(..s.len() - 3)?; 371 | if let Ok(degrees) = s.trim().parse::() { 372 | return Some(Self::from_degrees(degrees)); 373 | } 374 | } else if s.ends_with("grad") { 375 | s = s.get(..s.len() - 4)?; 376 | if let Ok(gradians) = s.trim().parse::() { 377 | return Some(Self::from_degrees(gradians / 400.0 * 360.0)); 378 | } 379 | } else if s.ends_with("rad") { 380 | s = s.get(..s.len() - 3)?; 381 | if let Ok(radians) = s.trim().parse::() { 382 | return Some(Self::from_degrees(radians.to_degrees())); 383 | } 384 | } else if s.ends_with("turn") { 385 | s = s.get(..s.len() - 4)?; 386 | if let Ok(turns) = s.trim().parse::() { 387 | return Some(Self::from_degrees(turns * 360.0)); 388 | } 389 | } 390 | return Some(Self::Oblique(None)); 391 | } 392 | return None; 393 | } 394 | }) 395 | } 396 | } 397 | 398 | impl FontStyle { 399 | /// Convert an `Oblique` payload to an angle in degrees. 400 | pub const fn oblique_degrees(angle: Option) -> f32 { 401 | if let Some(a) = angle { 402 | (a as f32) / 256.0 403 | } else { 404 | 14.0 405 | } 406 | } 407 | 408 | /// Creates a new oblique style with angle specified in degrees. 409 | /// 410 | /// Panics if `degrees` is not between `-90` and `90`. 411 | pub fn from_degrees(degrees: f32) -> Self { 412 | let a = (degrees * 256.0).round(); 413 | assert!(-90.0 <= a && a <= 90.0); 414 | Self::Oblique(Some(a as i16)) 415 | } 416 | } 417 | 418 | impl fmt::Display for FontStyle { 419 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 420 | let value = match *self { 421 | Self::Normal => "normal", 422 | Self::Italic => "italic", 423 | Self::Oblique(None) => "oblique", 424 | Self::Oblique(Some(angle)) if angle == 14 * 256 => "oblique", 425 | Self::Oblique(Some(angle)) => { 426 | let degrees = (angle as f32) / 256.0; 427 | return write!(f, "oblique({degrees}deg)"); 428 | } 429 | }; 430 | write!(f, "{value}") 431 | } 432 | } 433 | 434 | impl From for fontique::FontStyle { 435 | #[inline] 436 | fn from(style: FontStyle) -> Self { 437 | match style { 438 | FontStyle::Normal => fontique::FontStyle::Normal, 439 | FontStyle::Italic => fontique::FontStyle::Italic, 440 | FontStyle::Oblique(None) => fontique::FontStyle::Oblique(None), 441 | FontStyle::Oblique(slant) => { 442 | fontique::FontStyle::Oblique(Some(FontStyle::oblique_degrees(slant))) 443 | } 444 | } 445 | } 446 | } 447 | -------------------------------------------------------------------------------- /src/shaper.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License in the LICENSE-APACHE file or at: 4 | // https://www.apache.org/licenses/LICENSE-2.0 5 | 6 | //! Text shaping 7 | //! 8 | //! To quote the HarfBuzz manual: 9 | //! 10 | //! > Text shaping is the process of translating a string of character codes 11 | //! > (such as Unicode codepoints) into a properly arranged sequence of glyphs 12 | //! > that can be rendered onto a screen or into final output form for 13 | //! > inclusion in a document. 14 | //! 15 | //! This module provides the [`shape`] function, which produces a sequence of 16 | //! [`Glyph`]s based on the given text. 17 | //! 18 | //! This module *does not* perform line-breaking, wrapping or text reversal. 19 | 20 | use crate::conv::{DPU, to_u32, to_usize}; 21 | use crate::display::RunSpecial; 22 | use crate::fonts::{self, FaceId}; 23 | use crate::{Range, Vec2}; 24 | use fontique::Script; 25 | use tinyvec::TinyVec; 26 | use unicode_bidi::Level; 27 | 28 | /// A type-safe wrapper for glyph ID. 29 | #[repr(transparent)] 30 | #[derive(Clone, Copy, Ord, PartialOrd, Eq, PartialEq, Default, Debug)] 31 | pub struct GlyphId(pub u16); 32 | 33 | /// A positioned glyph 34 | #[derive(Clone, Copy, Debug)] 35 | pub struct Glyph { 36 | /// Index of char in source text 37 | pub index: u32, 38 | /// Glyph identifier in font 39 | pub id: GlyphId, 40 | /// Position of glyph 41 | pub position: Vec2, 42 | } 43 | 44 | #[derive(Clone, Copy, Debug, Default)] 45 | pub(crate) struct GlyphBreak { 46 | /// Index of char in source text 47 | pub index: u32, 48 | /// Position in sequence of glyphs 49 | pub gi: u32, 50 | /// End position of previous "word" excluding space 51 | pub no_space_end: f32, 52 | } 53 | impl GlyphBreak { 54 | /// Constructs with first field only 55 | /// 56 | /// Other fields are set later by shaper. 57 | pub(crate) fn new(index: u32) -> Self { 58 | GlyphBreak { 59 | index, 60 | gi: u32::MAX, 61 | no_space_end: f32::NAN, 62 | } 63 | } 64 | } 65 | 66 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 67 | pub(crate) struct PartMetrics { 68 | /// The distance from the origin to the start of the left-most part 69 | pub offset: f32, 70 | /// Length (excluding whitespace) 71 | pub len_no_space: f32, 72 | /// Length (including trailing whitespace) 73 | pub len: f32, 74 | } 75 | 76 | /// A glyph run 77 | /// 78 | /// A glyph run is a sequence of glyphs, starting from the origin: 0.0. 79 | /// Whether the run is left-to-right text or right-to-left, glyphs are 80 | /// positioned between 0.0 and `run.caret` (usually with some internal 81 | /// margin due to side bearings — though this could even be negative). 82 | /// The first glyph in the run should not be invisible (space) except where the 83 | /// run occurs at the start of a line with explicit initial spacing, however 84 | /// the run may end with white-space. `no_space_end` gives the "caret" position 85 | /// of the *logical* end of the run, excluding white-space (for right-to-left 86 | /// text, this is the end nearer the origin than `caret`). 87 | #[derive(Clone, Debug)] 88 | pub(crate) struct GlyphRun { 89 | /// Range in source text 90 | pub range: Range, 91 | /// Font size (pixels/em) 92 | pub dpem: f32, 93 | pub dpu: DPU, 94 | 95 | /// Font face identifier 96 | pub face_id: FaceId, 97 | /// Tab or no-break property 98 | pub special: RunSpecial, 99 | /// BIDI level 100 | pub level: Level, 101 | /// Script 102 | pub script: Script, 103 | 104 | /// Sequence of all glyphs, in left-to-right order 105 | pub glyphs: Vec, 106 | /// All soft-breaks within this run, in left-to-right order 107 | /// 108 | /// Note: it would be equivalent to use a separate `Run` for each sub-range 109 | /// in the text instead of tracking breaks via this field. 110 | pub breaks: TinyVec<[GlyphBreak; 4]>, 111 | 112 | /// End position, excluding whitespace 113 | /// 114 | /// Use [`GlyphRun::start_no_space`] or [`GlyphRun::end_no_space`]. 115 | pub no_space_end: f32, 116 | /// Position of next glyph, if this run is followed by another 117 | pub caret: f32, 118 | } 119 | 120 | impl GlyphRun { 121 | /// Number of parts 122 | /// 123 | /// Parts are in logical order 124 | pub fn num_parts(&self) -> usize { 125 | self.breaks.len() + 1 126 | } 127 | 128 | /// Calculate lengths for a part range 129 | /// 130 | /// Parts are identified in logical order with end index up to 131 | /// `self.num_parts()`. 132 | pub fn part_lengths(&self, range: std::ops::Range) -> PartMetrics { 133 | // TODO: maybe we should adjust self.breaks to clean this up? 134 | assert!(range.start <= range.end); 135 | 136 | let mut part = PartMetrics::default(); 137 | if self.level.is_ltr() { 138 | if range.end > 0 { 139 | part.len_no_space = self.no_space_end; 140 | part.len = self.caret; 141 | if range.end <= self.breaks.len() { 142 | let b = self.breaks[range.end - 1]; 143 | part.len_no_space = b.no_space_end; 144 | if to_usize(b.gi) < self.glyphs.len() { 145 | part.len = self.glyphs[to_usize(b.gi)].position.0 146 | } 147 | } 148 | } 149 | 150 | if range.start > 0 { 151 | let glyph = to_usize(self.breaks[range.start - 1].gi); 152 | part.offset = self.glyphs[glyph].position.0; 153 | part.len_no_space -= part.offset; 154 | part.len -= part.offset; 155 | } 156 | } else { 157 | if range.start <= self.breaks.len() { 158 | part.len = self.caret; 159 | if range.start > 0 { 160 | let b = self.breaks.len() - range.start; 161 | let gi = to_usize(self.breaks[b].gi); 162 | if gi < self.glyphs.len() { 163 | part.len = self.glyphs[gi].position.0; 164 | } 165 | } 166 | part.len_no_space = part.len; 167 | } 168 | if range.end <= self.breaks.len() { 169 | part.offset = self.caret; 170 | if range.end == 0 { 171 | part.len_no_space = 0.0; 172 | } else { 173 | let b = self.breaks.len() - range.end; 174 | let b = self.breaks[b]; 175 | part.len_no_space -= b.no_space_end; 176 | if to_usize(b.gi) < self.glyphs.len() { 177 | part.offset = self.glyphs[to_usize(b.gi)].position.0; 178 | } 179 | } 180 | part.len -= part.offset; 181 | } 182 | } 183 | 184 | part 185 | } 186 | 187 | /// Get glyph index from part index 188 | pub fn to_glyph_range(&self, range: std::ops::Range) -> Range { 189 | let mut start = range.start; 190 | let mut end = range.end; 191 | 192 | let rtl = self.level.is_rtl(); 193 | if rtl { 194 | let num_parts = self.num_parts(); 195 | start = num_parts - start; 196 | end = num_parts - end; 197 | } 198 | 199 | let map = |part: usize| { 200 | if part == 0 { 201 | 0 202 | } else if part <= self.breaks.len() { 203 | to_usize(self.breaks[part - 1].gi) 204 | } else { 205 | debug_assert_eq!(part, self.breaks.len() + 1); 206 | self.glyphs.len() 207 | } 208 | }; 209 | 210 | let mut start = map(start); 211 | let mut end = map(end); 212 | 213 | if rtl { 214 | std::mem::swap(&mut start, &mut end); 215 | } 216 | 217 | Range::from(start..end) 218 | } 219 | } 220 | 221 | #[derive(Clone, Copy, Debug)] 222 | pub(crate) struct Input<'a> { 223 | /// Contiguous text 224 | pub text: &'a str, 225 | pub dpem: f32, 226 | pub level: Level, 227 | pub script: Script, 228 | } 229 | 230 | /// Shape a `run` of text 231 | /// 232 | /// A "run" is expected to be the maximal sequence of code points of the same 233 | /// embedding level (as defined by Unicode TR9 aka BIDI algorithm) *and* 234 | /// excluding all hard line breaks (e.g. `\n`). 235 | pub(crate) fn shape( 236 | input: Input, 237 | range: Range, // range in text 238 | face_id: FaceId, 239 | // All soft-break locations within this run, excluding the end 240 | mut breaks: TinyVec<[GlyphBreak; 4]>, 241 | special: RunSpecial, 242 | ) -> GlyphRun { 243 | /* 244 | print!("shape[{:?}]:\t", special); 245 | let mut start = range.start(); 246 | for b in &breaks { 247 | print!("\"{}\" ", &text[start..(b.index as usize)]); 248 | start = b.index as usize; 249 | } 250 | println!("\"{}\"", &text[start..range.end()]); 251 | */ 252 | 253 | if input.level.is_rtl() { 254 | breaks.reverse(); 255 | } 256 | 257 | let mut glyphs = vec![]; 258 | let mut no_space_end = 0.0; 259 | let mut caret = 0.0; 260 | 261 | let face = fonts::library().get_face(face_id); 262 | let dpu = face.dpu(input.dpem); 263 | let sf = face.scale_by_dpu(dpu); 264 | 265 | if input.dpem >= 0.0 { 266 | #[cfg(feature = "rustybuzz")] 267 | let r = shape_rustybuzz(input, range, face_id, &mut breaks); 268 | 269 | #[cfg(not(feature = "rustybuzz"))] 270 | let r = shape_simple(sf, input, range, &mut breaks); 271 | 272 | glyphs = r.0; 273 | no_space_end = r.1; 274 | caret = r.2; 275 | } 276 | 277 | if input.level.is_rtl() { 278 | // With RTL text, no_space_end means start_no_space; recalculate 279 | let mut break_i = breaks.len().wrapping_sub(1); 280 | let mut start_no_space = caret; 281 | let mut last_id = None; 282 | let side_bearing = |id: Option| id.map(|id| sf.h_side_bearing(id)).unwrap_or(0.0); 283 | for (gi, glyph) in glyphs.iter().enumerate().rev() { 284 | if break_i < breaks.len() && to_usize(breaks[break_i].gi) == gi { 285 | assert!(gi < glyphs.len()); 286 | breaks[break_i].gi = to_u32(gi) + 1; 287 | breaks[break_i].no_space_end = start_no_space - side_bearing(last_id); 288 | break_i = break_i.wrapping_sub(1); 289 | } 290 | if !input.text[to_usize(glyph.index)..] 291 | .chars() 292 | .next() 293 | .map(|c| c.is_whitespace()) 294 | .unwrap_or(true) 295 | { 296 | last_id = Some(glyph.id); 297 | start_no_space = glyph.position.0; 298 | } 299 | } 300 | no_space_end = start_no_space - side_bearing(last_id); 301 | } 302 | 303 | GlyphRun { 304 | range, 305 | dpem: input.dpem, 306 | dpu, 307 | face_id, 308 | special, 309 | level: input.level, 310 | script: input.script, 311 | 312 | glyphs, 313 | breaks, 314 | no_space_end, 315 | caret, 316 | } 317 | } 318 | 319 | // Use Rustybuzz lib 320 | #[cfg(feature = "rustybuzz")] 321 | fn shape_rustybuzz( 322 | input: Input<'_>, 323 | range: Range, 324 | face_id: FaceId, 325 | breaks: &mut [GlyphBreak], 326 | ) -> (Vec, f32, f32) { 327 | let Input { 328 | text, 329 | dpem, 330 | level, 331 | script, 332 | } = input; 333 | 334 | let fonts = fonts::library(); 335 | let store = fonts.get_face_store(face_id); 336 | let dpu = store.face_ref().dpu(dpem); 337 | let face = store.rustybuzz(); 338 | 339 | // ppem affects hinting but does not scale layout, so this has little effect: 340 | // face.set_pixels_per_em(Some((dpem as u16, dpem as u16))); 341 | 342 | let slice = &text[range]; 343 | let idx_offset = range.start; 344 | let rtl = level.is_rtl(); 345 | 346 | // TODO: cache the buffer for reuse later? 347 | let mut buffer = rustybuzz::UnicodeBuffer::new(); 348 | buffer.set_direction(match rtl { 349 | false => rustybuzz::Direction::LeftToRight, 350 | true => rustybuzz::Direction::RightToLeft, 351 | }); 352 | buffer.push_str(slice); 353 | let tag = ttf_parser::Tag(u32::from_be_bytes(script.0)); 354 | if let Some(script) = rustybuzz::Script::from_iso15924_tag(tag) { 355 | buffer.set_script(script); 356 | } 357 | let features = []; 358 | 359 | let output = rustybuzz::shape(face, &features, buffer); 360 | 361 | let mut caret = 0.0; 362 | let mut no_space_end = caret; 363 | let mut break_i = 0; 364 | 365 | let mut glyphs = Vec::with_capacity(output.len()); 366 | 367 | for (info, pos) in output 368 | .glyph_infos() 369 | .iter() 370 | .zip(output.glyph_positions().iter()) 371 | { 372 | let index = idx_offset + info.cluster; 373 | assert!(info.glyph_id <= u16::MAX as u32, "failed to map glyph id"); 374 | let id = GlyphId(info.glyph_id as u16); 375 | 376 | if breaks 377 | .get(break_i) 378 | .map(|b| b.index == index) 379 | .unwrap_or(false) 380 | { 381 | breaks[break_i].gi = to_u32(glyphs.len()); 382 | breaks[break_i].no_space_end = no_space_end; 383 | break_i += 1; 384 | } 385 | 386 | let position = Vec2( 387 | caret + dpu.i32_to_px(pos.x_offset), 388 | dpu.i32_to_px(pos.y_offset), 389 | ); 390 | glyphs.push(Glyph { 391 | index, 392 | id, 393 | position, 394 | }); 395 | 396 | // IIRC this is only applicable to vertical text, which we don't 397 | // currently support: 398 | debug_assert_eq!(pos.y_advance, 0); 399 | caret += dpu.i32_to_px(pos.x_advance); 400 | if text[to_usize(index)..] 401 | .chars() 402 | .next() 403 | .map(|c| !c.is_whitespace()) 404 | .unwrap() 405 | { 406 | no_space_end = caret; 407 | } 408 | } 409 | 410 | (glyphs, no_space_end, caret) 411 | } 412 | 413 | // Simple implementation (kerning but no shaping) 414 | #[cfg(not(feature = "rustybuzz"))] 415 | fn shape_simple( 416 | sf: crate::fonts::ScaledFaceRef, 417 | input: Input<'_>, 418 | range: Range, 419 | breaks: &mut [GlyphBreak], 420 | ) -> (Vec, f32, f32) { 421 | let Input { text, level, .. } = input; 422 | 423 | use unicode_bidi_mirroring::get_mirrored; 424 | 425 | let slice = &text[range]; 426 | let idx_offset = range.start; 427 | let rtl = level.is_rtl(); 428 | 429 | let mut caret = 0.0; 430 | let mut no_space_end = caret; 431 | let mut prev_glyph_id: Option = None; 432 | let mut break_i = 0; 433 | 434 | // Allocate with an over-estimate and shrink later: 435 | let mut glyphs = Vec::with_capacity(slice.len()); 436 | let mut iter = slice.char_indices(); 437 | let mut next_char_index = || match rtl { 438 | false => iter.next(), 439 | true => iter.next_back(), 440 | }; 441 | while let Some((index, mut c)) = next_char_index() { 442 | let index = idx_offset + to_u32(index); 443 | if rtl { 444 | if let Some(m) = get_mirrored(c) { 445 | c = m; 446 | } 447 | } 448 | let id = sf.face().glyph_index(c); 449 | 450 | if breaks 451 | .get(break_i) 452 | .map(|b| b.index == index) 453 | .unwrap_or(false) 454 | { 455 | breaks[break_i].gi = to_u32(glyphs.len()); 456 | breaks[break_i].no_space_end = no_space_end; 457 | break_i += 1; 458 | no_space_end = caret; 459 | } 460 | 461 | if let Some(prev) = prev_glyph_id { 462 | if let Some(kern) = sf.face().0.tables().kern { 463 | if let Some(adv) = kern 464 | .subtables 465 | .into_iter() 466 | .filter(|st| st.horizontal && !st.variable) 467 | .find_map(|st| st.glyphs_kerning(prev.into(), id.into())) 468 | { 469 | caret += sf.dpu().i16_to_px(adv); 470 | } 471 | } 472 | } 473 | prev_glyph_id = Some(id); 474 | 475 | let position = Vec2(caret, 0.0); 476 | let glyph = Glyph { 477 | index, 478 | id, 479 | position, 480 | }; 481 | glyphs.push(glyph); 482 | 483 | caret += sf.h_advance(id); 484 | if !c.is_whitespace() { 485 | no_space_end = caret; 486 | } 487 | } 488 | 489 | glyphs.shrink_to_fit(); 490 | 491 | (glyphs, no_space_end, caret) 492 | } 493 | -------------------------------------------------------------------------------- /src/text.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License in the LICENSE-APACHE file or at: 4 | // https://www.apache.org/licenses/LICENSE-2.0 5 | 6 | //! Text object 7 | 8 | use crate::display::{Effect, MarkerPosIter, NotReady, TextDisplay}; 9 | use crate::fonts::{FontSelector, NoFontMatch}; 10 | use crate::format::FormattableText; 11 | use crate::{Align, Direction, GlyphRun, Line, Status, Vec2}; 12 | use std::num::NonZeroUsize; 13 | 14 | /// Text type-setting object (high-level API) 15 | /// 16 | /// This struct contains: 17 | /// - A [`FormattableText`] 18 | /// - A [`TextDisplay`] 19 | /// - A [`FontSelector`] 20 | /// - Font size; this defaults to 16px (the web default). 21 | /// - Text direction and alignment; by default this is inferred from the text. 22 | /// - Line-wrap width; see [`Text::set_wrap_width`]. 23 | /// - The bounds used for alignment; these [must be set][Text::set_bounds]. 24 | /// 25 | /// This struct tracks the [`TextDisplay`]'s 26 | /// [state of preparation][TextDisplay#status-of-preparation] and will perform 27 | /// steps as required. To use this struct: 28 | /// ``` 29 | /// use kas_text::{Text, Vec2}; 30 | /// use std::path::Path; 31 | /// 32 | /// let mut text = Text::new("Hello, world!"); 33 | /// text.set_bounds(Vec2(200.0, 50.0)); 34 | /// text.prepare().unwrap(); 35 | /// 36 | /// for run in text.runs(Vec2::ZERO, &[]).unwrap() { 37 | /// let (face, dpem) = (run.face_id(), run.dpem()); 38 | /// for glyph in run.glyphs() { 39 | /// println!("{face:?} - {dpem}px - {glyph:?}"); 40 | /// } 41 | /// } 42 | /// ``` 43 | #[derive(Clone, Debug)] 44 | pub struct Text { 45 | /// Bounds to use for alignment 46 | bounds: Vec2, 47 | font: FontSelector, 48 | dpem: f32, 49 | wrap_width: f32, 50 | /// Alignment (`horiz`, `vert`) 51 | /// 52 | /// By default, horizontal alignment is left or right depending on the 53 | /// text direction (see [`Self::direction`]), and vertical alignment 54 | /// is to the top. 55 | align: (Align, Align), 56 | direction: Direction, 57 | status: Status, 58 | 59 | display: TextDisplay, 60 | text: T, 61 | } 62 | 63 | impl Default for Text { 64 | #[inline] 65 | fn default() -> Self { 66 | Text::new(T::default()) 67 | } 68 | } 69 | 70 | /// Constructors and other methods requiring `T: Sized` 71 | impl Text { 72 | /// Construct from a text model 73 | /// 74 | /// This struct must be made ready for usage by calling [`Text::prepare`]. 75 | #[inline] 76 | pub fn new(text: T) -> Self { 77 | Text { 78 | bounds: Vec2::INFINITY, 79 | font: FontSelector::default(), 80 | dpem: 16.0, 81 | wrap_width: f32::INFINITY, 82 | align: Default::default(), 83 | direction: Direction::default(), 84 | status: Status::New, 85 | text, 86 | display: Default::default(), 87 | } 88 | } 89 | 90 | /// Replace the [`TextDisplay`] 91 | /// 92 | /// This may be used with [`Self::new`] to reconstruct an object which was 93 | /// disolved [`into_parts`][Self::into_parts]. 94 | #[inline] 95 | pub fn with_display(mut self, display: TextDisplay) -> Self { 96 | self.display = display; 97 | self 98 | } 99 | 100 | /// Decompose into parts 101 | #[inline] 102 | pub fn into_parts(self) -> (TextDisplay, T) { 103 | (self.display, self.text) 104 | } 105 | 106 | /// Clone the formatted text 107 | pub fn clone_text(&self) -> T 108 | where 109 | T: Clone, 110 | { 111 | self.text.clone() 112 | } 113 | 114 | /// Extract text object, discarding the rest 115 | #[inline] 116 | pub fn take_text(self) -> T { 117 | self.text 118 | } 119 | 120 | /// Access the formattable text object 121 | #[inline] 122 | pub fn text(&self) -> &T { 123 | &self.text 124 | } 125 | 126 | /// Set the text 127 | /// 128 | /// One must call [`Text::prepare`] afterwards and may wish to inspect its 129 | /// return value to check the size allocation meets requirements. 130 | pub fn set_text(&mut self, text: T) { 131 | if self.text == text { 132 | return; // no change 133 | } 134 | 135 | self.text = text; 136 | self.set_max_status(Status::New); 137 | } 138 | } 139 | 140 | /// Text, font and type-setting getters and setters 141 | impl Text { 142 | /// Length of text 143 | /// 144 | /// This is a shortcut to `self.as_str().len()`. 145 | /// 146 | /// It is valid to reference text within the range `0..text_len()`, 147 | /// even if not all text within this range will be displayed (due to runs). 148 | #[inline] 149 | pub fn str_len(&self) -> usize { 150 | self.as_str().len() 151 | } 152 | 153 | /// Access whole text as contiguous `str` 154 | /// 155 | /// It is valid to reference text within the range `0..text_len()`, 156 | /// even if not all text within this range will be displayed (due to runs). 157 | #[inline] 158 | pub fn as_str(&self) -> &str { 159 | self.text.as_str() 160 | } 161 | 162 | /// Clone the unformatted text as a `String` 163 | #[inline] 164 | pub fn clone_string(&self) -> String { 165 | self.text.as_str().to_string() 166 | } 167 | 168 | /// Get the font selector 169 | #[inline] 170 | pub fn font(&self) -> FontSelector { 171 | self.font 172 | } 173 | 174 | /// Set the font selector 175 | /// 176 | /// This font selector is used by all unformatted texts and by formatted 177 | /// texts which don't immediately replace the selector. 178 | /// 179 | /// It is necessary to [`prepare`][Self::prepare] the text after calling this. 180 | #[inline] 181 | pub fn set_font(&mut self, font: FontSelector) { 182 | if font != self.font { 183 | self.font = font; 184 | self.set_max_status(Status::New); 185 | } 186 | } 187 | 188 | /// Get the default font size (pixels) 189 | #[inline] 190 | pub fn font_size(&self) -> f32 { 191 | self.dpem 192 | } 193 | 194 | /// Set the default font size (pixels) 195 | /// 196 | /// This is a scaling factor used to convert font sizes, with units 197 | /// `pixels/Em`. Equivalently, this is the line-height in pixels. 198 | /// See [`crate::fonts`] documentation. 199 | /// 200 | /// To calculate this from text size in Points, use `dpem = dpp * pt_size` 201 | /// where the dots-per-point is usually `dpp = scale_factor * 96.0 / 72.0` 202 | /// on PC platforms, or `dpp = 1` on MacOS (or 2 for retina displays). 203 | /// 204 | /// It is necessary to [`prepare`][Self::prepare] the text after calling this. 205 | #[inline] 206 | pub fn set_font_size(&mut self, dpem: f32) { 207 | if dpem != self.dpem { 208 | self.dpem = dpem; 209 | self.set_max_status(Status::ResizeLevelRuns); 210 | } 211 | } 212 | 213 | /// Set font size 214 | /// 215 | /// This is an alternative to [`Text::set_font_size`]. It is assumed 216 | /// that 72 Points = 1 Inch and the base screen resolution is 96 DPI. 217 | /// (Note: MacOS uses a different definition where 1 Point = 1 Pixel.) 218 | #[inline] 219 | pub fn set_font_size_pt(&mut self, pt_size: f32, scale_factor: f32) { 220 | self.set_font_size(pt_size * scale_factor * (96.0 / 72.0)); 221 | } 222 | 223 | /// Get the base text direction 224 | #[inline] 225 | pub fn direction(&self) -> Direction { 226 | self.direction 227 | } 228 | 229 | /// Set the base text direction 230 | /// 231 | /// It is necessary to [`prepare`][Self::prepare] the text after calling this. 232 | #[inline] 233 | pub fn set_direction(&mut self, direction: Direction) { 234 | if direction != self.direction { 235 | self.direction = direction; 236 | self.set_max_status(Status::New); 237 | } 238 | } 239 | 240 | /// Get the text wrap width 241 | #[inline] 242 | pub fn wrap_width(&self) -> f32 { 243 | self.wrap_width 244 | } 245 | 246 | /// Set wrap width or disable line wrapping 247 | /// 248 | /// By default, this is [`f32::INFINITY`] and text lines are not wrapped. 249 | /// If set to some positive finite value, text lines will be wrapped at that 250 | /// width. 251 | /// 252 | /// Either way, explicit line-breaks such as `\n` still result in new lines. 253 | /// 254 | /// It is necessary to [`prepare`][Self::prepare] the text after calling this. 255 | #[inline] 256 | pub fn set_wrap_width(&mut self, wrap_width: f32) { 257 | debug_assert!(wrap_width >= 0.0); 258 | if wrap_width != self.wrap_width { 259 | self.wrap_width = wrap_width; 260 | self.set_max_status(Status::LevelRuns); 261 | } 262 | } 263 | 264 | /// Get text (horizontal, vertical) alignment 265 | #[inline] 266 | pub fn align(&self) -> (Align, Align) { 267 | self.align 268 | } 269 | 270 | /// Set text alignment 271 | /// 272 | /// It is necessary to [`prepare`][Self::prepare] the text after calling this. 273 | #[inline] 274 | pub fn set_align(&mut self, align: (Align, Align)) { 275 | if align != self.align { 276 | if align.0 == self.align.0 { 277 | self.set_max_status(Status::Wrapped); 278 | } else { 279 | self.set_max_status(Status::LevelRuns); 280 | } 281 | self.align = align; 282 | } 283 | } 284 | 285 | /// Get text bounds 286 | #[inline] 287 | pub fn bounds(&self) -> Vec2 { 288 | self.bounds 289 | } 290 | 291 | /// Set text bounds 292 | /// 293 | /// These are used for alignment. They are not used for wrapping; see 294 | /// instead [`Self::set_wrap_width`]. 295 | /// 296 | /// It is expected that `bounds` are finite. 297 | #[inline] 298 | pub fn set_bounds(&mut self, bounds: Vec2) { 299 | debug_assert!(bounds.is_finite()); 300 | if bounds != self.bounds { 301 | if bounds.0 != self.bounds.0 { 302 | self.set_max_status(Status::LevelRuns); 303 | } else { 304 | self.set_max_status(Status::Wrapped); 305 | } 306 | self.bounds = bounds; 307 | } 308 | } 309 | 310 | /// Get the base directionality of the text 311 | /// 312 | /// This does not require that the text is prepared. 313 | pub fn text_is_rtl(&self) -> bool { 314 | let cached_is_rtl = match self.line_is_rtl(0) { 315 | Ok(None) => Some(self.direction == Direction::Rtl), 316 | Ok(Some(is_rtl)) => Some(is_rtl), 317 | Err(NotReady) => None, 318 | }; 319 | #[cfg(not(debug_assertions))] 320 | if let Some(cached) = cached_is_rtl { 321 | return cached; 322 | } 323 | 324 | let is_rtl = self.display.text_is_rtl(self.as_str(), self.direction); 325 | if let Some(cached) = cached_is_rtl { 326 | debug_assert_eq!(cached, is_rtl); 327 | } 328 | is_rtl 329 | } 330 | 331 | /// Get the sequence of effect tokens 332 | /// 333 | /// This method has some limitations: (1) it may only return a reference to 334 | /// an existing sequence, (2) effect tokens cannot be generated dependent 335 | /// on input state, and (3) it does not incorporate color information. For 336 | /// most uses it should still be sufficient, but for other cases it may be 337 | /// preferable not to use this method (use a dummy implementation returning 338 | /// `&[]` and use inherent methods on the text object via [`Text::text`]). 339 | #[inline] 340 | pub fn effect_tokens(&self) -> &[Effect] { 341 | self.text.effect_tokens() 342 | } 343 | } 344 | 345 | /// Type-setting operations and status 346 | impl Text { 347 | /// Check whether the status is at least `status` 348 | #[inline] 349 | pub fn check_status(&self, status: Status) -> Result<(), NotReady> { 350 | if self.status >= status { 351 | Ok(()) 352 | } else { 353 | Err(NotReady) 354 | } 355 | } 356 | 357 | /// Check whether the text is fully prepared and ready for usage 358 | #[inline] 359 | pub fn is_prepared(&self) -> bool { 360 | self.status == Status::Ready 361 | } 362 | 363 | /// Adjust status to indicate a required action 364 | /// 365 | /// This is used to notify that some step of preparation may need to be 366 | /// repeated. The internally-tracked status is set to the minimum of 367 | /// `status` and its previous value. 368 | #[inline] 369 | fn set_max_status(&mut self, status: Status) { 370 | self.status = self.status.min(status); 371 | } 372 | 373 | /// Read the [`TextDisplay`], without checking status 374 | #[inline] 375 | pub fn unchecked_display(&self) -> &TextDisplay { 376 | &self.display 377 | } 378 | 379 | /// Read the [`TextDisplay`], if fully prepared 380 | #[inline] 381 | pub fn display(&self) -> Result<&TextDisplay, NotReady> { 382 | self.check_status(Status::Ready)?; 383 | Ok(self.unchecked_display()) 384 | } 385 | 386 | /// Read the [`TextDisplay`], if at least wrapped 387 | #[inline] 388 | pub fn wrapped_display(&self) -> Result<&TextDisplay, NotReady> { 389 | self.check_status(Status::Wrapped)?; 390 | Ok(self.unchecked_display()) 391 | } 392 | 393 | #[inline] 394 | fn prepare_runs(&mut self) -> Result<(), NoFontMatch> { 395 | match self.status { 396 | Status::New => { 397 | self.display 398 | .prepare_runs(&self.text, self.direction, self.font, self.dpem)? 399 | } 400 | Status::ResizeLevelRuns => self.display.resize_runs(&self.text, self.dpem), 401 | _ => (), 402 | } 403 | 404 | self.status = Status::LevelRuns; 405 | Ok(()) 406 | } 407 | 408 | /// Measure required width, up to some `max_width` 409 | /// 410 | /// This method partially prepares the [`TextDisplay`] as required. 411 | /// 412 | /// This method allows calculation of the width requirement of a text object 413 | /// without full wrapping and glyph placement. Whenever the requirement 414 | /// exceeds `max_width`, the algorithm stops early, returning `max_width`. 415 | /// 416 | /// The return value is unaffected by alignment and wrap configuration. 417 | pub fn measure_width(&mut self, max_width: f32) -> Result { 418 | self.prepare_runs()?; 419 | 420 | Ok(self.display.measure_width(max_width)) 421 | } 422 | 423 | /// Measure required vertical height, wrapping as configured 424 | /// 425 | /// Stops after `max_lines`, if provided. 426 | pub fn measure_height(&mut self, max_lines: Option) -> Result { 427 | if self.status >= Status::Wrapped { 428 | let (tl, br) = self.display.bounding_box(); 429 | return Ok(br.1 - tl.1); 430 | } 431 | 432 | self.prepare_runs()?; 433 | Ok(self.display.measure_height(self.wrap_width, max_lines)) 434 | } 435 | 436 | /// Prepare text for display, as necessary 437 | /// 438 | /// [`Self::set_bounds`] must be called before this method. 439 | /// 440 | /// Does all preparation steps necessary in order to display or query the 441 | /// layout of this text. Text is aligned within the given `bounds`. 442 | /// 443 | /// Returns `Ok(true)` on success when some action is performed, `Ok(false)` 444 | /// when the text is already prepared. 445 | pub fn prepare(&mut self) -> Result { 446 | if self.is_prepared() { 447 | return Ok(false); 448 | } else if !self.bounds.is_finite() { 449 | return Err(NotReady); 450 | } 451 | 452 | self.prepare_runs().unwrap(); 453 | debug_assert!(self.status >= Status::LevelRuns); 454 | 455 | if self.status == Status::LevelRuns { 456 | self.display 457 | .prepare_lines(self.wrap_width, self.bounds.0, self.align.0); 458 | } 459 | 460 | if self.status <= Status::Wrapped { 461 | self.display.vertically_align(self.bounds.1, self.align.1); 462 | } 463 | 464 | self.status = Status::Ready; 465 | Ok(true) 466 | } 467 | 468 | /// Get the size of the required bounding box 469 | /// 470 | /// This is the position of the upper-left and lower-right corners of a 471 | /// bounding box on content. 472 | /// Alignment and input bounds do affect the result. 473 | #[inline] 474 | pub fn bounding_box(&self) -> Result<(Vec2, Vec2), NotReady> { 475 | Ok(self.wrapped_display()?.bounding_box()) 476 | } 477 | 478 | /// Get the number of lines (after wrapping) 479 | /// 480 | /// See [`TextDisplay::num_lines`]. 481 | #[inline] 482 | pub fn num_lines(&self) -> Result { 483 | Ok(self.wrapped_display()?.num_lines()) 484 | } 485 | 486 | /// Get line properties 487 | #[inline] 488 | pub fn get_line(&self, index: usize) -> Result, NotReady> { 489 | Ok(self.wrapped_display()?.get_line(index)) 490 | } 491 | 492 | /// Iterate over line properties 493 | /// 494 | /// [Requires status][Self#status-of-preparation]: lines have been wrapped. 495 | #[inline] 496 | pub fn lines(&self) -> Result, NotReady> { 497 | Ok(self.wrapped_display()?.lines()) 498 | } 499 | 500 | /// Find the line containing text `index` 501 | /// 502 | /// See [`TextDisplay::find_line`]. 503 | #[inline] 504 | pub fn find_line( 505 | &self, 506 | index: usize, 507 | ) -> Result)>, NotReady> { 508 | Ok(self.wrapped_display()?.find_line(index)) 509 | } 510 | 511 | /// Get the directionality of the current line 512 | /// 513 | /// See [`TextDisplay::line_is_rtl`]. 514 | #[inline] 515 | pub fn line_is_rtl(&self, line: usize) -> Result, NotReady> { 516 | Ok(self.wrapped_display()?.line_is_rtl(line)) 517 | } 518 | 519 | /// Find the text index for the glyph nearest the given `pos` 520 | /// 521 | /// See [`TextDisplay::text_index_nearest`]. 522 | #[inline] 523 | pub fn text_index_nearest(&self, pos: Vec2) -> Result { 524 | Ok(self.display()?.text_index_nearest(pos)) 525 | } 526 | 527 | /// Find the text index nearest horizontal-coordinate `x` on `line` 528 | /// 529 | /// See [`TextDisplay::line_index_nearest`]. 530 | #[inline] 531 | pub fn line_index_nearest(&self, line: usize, x: f32) -> Result, NotReady> { 532 | Ok(self.wrapped_display()?.line_index_nearest(line, x)) 533 | } 534 | 535 | /// Find the starting position (top-left) of the glyph at the given index 536 | /// 537 | /// See [`TextDisplay::text_glyph_pos`]. 538 | pub fn text_glyph_pos(&self, index: usize) -> Result { 539 | Ok(self.display()?.text_glyph_pos(index)) 540 | } 541 | 542 | /// Get the number of glyphs 543 | /// 544 | /// See [`TextDisplay::num_glyphs`]. 545 | #[inline] 546 | #[cfg(feature = "num_glyphs")] 547 | pub fn num_glyphs(&self) -> Result { 548 | Ok(self.wrapped_display()?.num_glyphs()) 549 | } 550 | 551 | /// Iterate over runs of positioned glyphs 552 | /// 553 | /// All glyphs are translated by the given `offset` (this is practically 554 | /// free). 555 | /// 556 | /// An [`Effect`] sequence supports underline, strikethrough and custom 557 | /// indexing (e.g. for a color palette). Pass `&[]` if effects are not 558 | /// required. (The default effect is always [`Effect::default()`].) 559 | /// 560 | /// Runs are yielded in undefined order. The total number of 561 | /// glyphs yielded will equal [`TextDisplay::num_glyphs`]. 562 | pub fn runs<'a>( 563 | &'a self, 564 | offset: Vec2, 565 | effects: &'a [Effect], 566 | ) -> Result> + 'a, NotReady> { 567 | Ok(self.display()?.runs(offset, effects)) 568 | } 569 | 570 | /// Yield a sequence of rectangles to highlight a given text range 571 | /// 572 | /// Calls `f(top_left, bottom_right)` for each highlighting rectangle. 573 | pub fn highlight_range( 574 | &self, 575 | range: std::ops::Range, 576 | mut f: F, 577 | ) -> Result<(), NotReady> 578 | where 579 | F: FnMut(Vec2, Vec2), 580 | { 581 | Ok(self.display()?.highlight_range(range, &mut f)) 582 | } 583 | } 584 | -------------------------------------------------------------------------------- /src/display/glyph_pos.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License in the LICENSE-APACHE file or at: 4 | // https://www.apache.org/licenses/LICENSE-2.0 5 | 6 | //! Methods using positioned glyphs 7 | 8 | #![allow(clippy::collapsible_else_if)] 9 | #![allow(clippy::or_fun_call)] 10 | #![allow(clippy::never_loop)] 11 | #![allow(clippy::needless_range_loop)] 12 | 13 | use super::{Line, TextDisplay}; 14 | use crate::conv::to_usize; 15 | use crate::fonts::{self, FaceId}; 16 | use crate::{Glyph, Range, Vec2, shaper}; 17 | 18 | /// Effect formatting marker 19 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 20 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 21 | pub struct Effect { 22 | /// Index in text at which formatting becomes active 23 | /// 24 | /// (Note that we use `u32` not `usize` since it can be assumed text length 25 | /// will never exceed `u32::MAX`.) 26 | pub start: u32, 27 | /// User-specified value, e.g. index into a colour palette 28 | pub e: u16, 29 | /// Effect flags 30 | pub flags: EffectFlags, 31 | } 32 | 33 | bitflags::bitflags! { 34 | /// Text effects 35 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] 36 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 37 | pub struct EffectFlags: u16 { 38 | /// Glyph is underlined 39 | const UNDERLINE = 1 << 0; 40 | /// Glyph is crossed through by a center-line 41 | const STRIKETHROUGH = 1 << 1; 42 | } 43 | } 44 | 45 | /// Used to return the position of a glyph with associated metrics 46 | #[derive(Copy, Clone, Debug, Default, PartialEq)] 47 | pub struct MarkerPos { 48 | /// (x, y) coordinate of glyph 49 | pub pos: Vec2, 50 | /// Ascent (subtract from y to get top) 51 | pub ascent: f32, 52 | /// Descent (subtract from y to get bottom) 53 | pub descent: f32, 54 | level: u8, 55 | } 56 | 57 | impl MarkerPos { 58 | /// Returns the embedding level 59 | /// 60 | /// According to Unicode Technical Report #9, the embedding level is 61 | /// guaranteed to be between 0 and 125 (inclusive), with a default level of 62 | /// zero and where odd levels are right-to-left. 63 | #[inline] 64 | pub fn embedding_level(&self) -> u8 { 65 | self.level 66 | } 67 | 68 | /// Returns true if the cursor is left-to-right 69 | #[inline] 70 | pub fn is_ltr(&self) -> bool { 71 | self.level % 2 == 0 72 | } 73 | 74 | /// Returns true if the cursor is right-to-left 75 | #[inline] 76 | pub fn is_rtl(&self) -> bool { 77 | self.level % 2 == 1 78 | } 79 | } 80 | 81 | pub struct MarkerPosIter { 82 | v: [MarkerPos; 2], 83 | a: usize, 84 | b: usize, 85 | } 86 | 87 | impl MarkerPosIter { 88 | /// Directly access the slice of results 89 | /// 90 | /// The result excludes elements removed by iteration. 91 | pub fn as_slice(&self) -> &[MarkerPos] { 92 | &self.v[self.a..self.b] 93 | } 94 | } 95 | 96 | impl Iterator for MarkerPosIter { 97 | type Item = MarkerPos; 98 | 99 | #[inline] 100 | fn next(&mut self) -> Option { 101 | if self.a < self.b { 102 | let i = self.a; 103 | self.a = i + 1; 104 | Some(self.v[i]) 105 | } else { 106 | None 107 | } 108 | } 109 | 110 | #[inline] 111 | fn size_hint(&self) -> (usize, Option) { 112 | let len = self.b - self.a; 113 | (len, Some(len)) 114 | } 115 | } 116 | 117 | impl DoubleEndedIterator for MarkerPosIter { 118 | #[inline] 119 | fn next_back(&mut self) -> Option { 120 | if self.a < self.b { 121 | let i = self.b - 1; 122 | self.b = i; 123 | Some(self.v[i]) 124 | } else { 125 | None 126 | } 127 | } 128 | } 129 | 130 | impl ExactSizeIterator for MarkerPosIter {} 131 | 132 | /// A sequence of positioned glyphs with effects 133 | /// 134 | /// Yielded by [`TextDisplay::runs`]. 135 | pub struct GlyphRun<'a> { 136 | run: &'a shaper::GlyphRun, 137 | range: Range, 138 | offset: Vec2, 139 | effects: &'a [Effect], 140 | } 141 | 142 | impl<'a> GlyphRun<'a> { 143 | /// Get the font face used for this run 144 | #[inline] 145 | pub fn face_id(&self) -> FaceId { 146 | self.run.face_id 147 | } 148 | 149 | /// Get the font size used for this run 150 | /// 151 | /// Units are dots-per-Em (see [crate::fonts]). 152 | #[inline] 153 | pub fn dpem(&self) -> f32 { 154 | self.run.dpem 155 | } 156 | 157 | /// Get an iterator over glyphs for this run 158 | /// 159 | /// This method ignores effects; if you want those call 160 | /// [`Self::glyphs_with_effects`] instead. 161 | pub fn glyphs(&self) -> impl Iterator + '_ { 162 | self.run.glyphs[self.range.to_std()] 163 | .iter() 164 | .map(|glyph| Glyph { 165 | index: glyph.index, 166 | id: glyph.id, 167 | position: glyph.position + self.offset, 168 | }) 169 | } 170 | 171 | /// Yield glyphs and effects for this run 172 | /// 173 | /// The callback `f` receives `glyph, e` where `e` is the [`Effect::e`] 174 | /// value (defaults to 0). 175 | /// 176 | /// The callback `g` receives positioning for each underline/strike-through 177 | /// segment: `x1, x2, y_top, h` where `h` is the thickness (height). Note 178 | /// that it is possible to have `h < 1.0` and `y_top, y_top + h` to round to 179 | /// the same number; the renderer is responsible for ensuring such lines 180 | /// are actually visible. The last parameter is `e` as for `f`. 181 | /// 182 | /// Note: this is more computationally expensive than [`GlyphRun::glyphs`], 183 | /// so you may prefer to call that. Optionally one may choose to cache the 184 | /// result, though this is not really necessary. 185 | pub fn glyphs_with_effects(&self, mut f: F, mut g: G) 186 | where 187 | F: FnMut(Glyph, u16), 188 | G: FnMut(f32, f32, f32, f32, u16), 189 | { 190 | let sf = fonts::library() 191 | .get_face(self.run.face_id) 192 | .scale_by_dpu(self.run.dpu); 193 | 194 | let ltr = self.run.level.is_ltr(); 195 | 196 | let mut underline = None; 197 | let mut strikethrough = None; 198 | 199 | let mut effect_cur = usize::MAX; 200 | let mut effect_next = 0; 201 | 202 | // We iterate in left-to-right order regardless of text direction. 203 | // Start by finding the effect applicable to the left-most glyph. 204 | let left_index = self.run.glyphs[self.range.start()].index; 205 | while self 206 | .effects 207 | .get(effect_next) 208 | .map(|e| e.start <= left_index) 209 | .unwrap_or(false) 210 | { 211 | effect_cur = effect_next; 212 | effect_next += 1; 213 | } 214 | 215 | let mut next_start = self 216 | .effects 217 | .get(effect_next) 218 | .map(|e| e.start) 219 | .unwrap_or(u32::MAX); 220 | 221 | let mut fmt = self 222 | .effects 223 | .get(effect_cur) 224 | .cloned() 225 | .unwrap_or(Effect::default()); 226 | 227 | // In case an effect applies to the left-most glyph, it starts from that 228 | // glyph's x coordinate. 229 | if !fmt.flags.is_empty() { 230 | let glyph = &self.run.glyphs[self.range.start()]; 231 | let position = glyph.position + self.offset; 232 | if fmt.flags.contains(EffectFlags::UNDERLINE) { 233 | if let Some(metrics) = sf.underline_metrics() { 234 | let y_top = position.1 - metrics.position; 235 | let h = metrics.thickness; 236 | let x1 = position.0; 237 | underline = Some((x1, y_top, h, fmt.e)); 238 | } 239 | } 240 | if fmt.flags.contains(EffectFlags::STRIKETHROUGH) { 241 | if let Some(metrics) = sf.strikethrough_metrics() { 242 | let y_top = position.1 - metrics.position; 243 | let h = metrics.thickness; 244 | let x1 = position.0; 245 | strikethrough = Some((x1, y_top, h, fmt.e)); 246 | } 247 | } 248 | } 249 | 250 | // Iterate over glyphs in left-to-right order. 251 | for mut glyph in self.run.glyphs[self.range.to_std()].iter().cloned() { 252 | glyph.position += self.offset; 253 | 254 | // Does the effect change? 255 | if (ltr && next_start <= glyph.index) || (!ltr && fmt.start > glyph.index) { 256 | if ltr { 257 | // Find the next active effect 258 | loop { 259 | effect_cur = effect_next; 260 | effect_next += 1; 261 | if self 262 | .effects 263 | .get(effect_next) 264 | .map(|e| e.start > glyph.index) 265 | .unwrap_or(true) 266 | { 267 | break; 268 | } 269 | } 270 | next_start = self 271 | .effects 272 | .get(effect_next) 273 | .map(|e| e.start) 274 | .unwrap_or(u32::MAX); 275 | } else { 276 | // Find the previous active effect 277 | loop { 278 | effect_cur = effect_cur.wrapping_sub(1); 279 | if self.effects.get(effect_cur).map(|e| e.start).unwrap_or(0) <= glyph.index 280 | { 281 | break; 282 | } 283 | } 284 | } 285 | fmt = self 286 | .effects 287 | .get(effect_cur) 288 | .cloned() 289 | .unwrap_or(Effect::default()); 290 | 291 | if underline.is_some() != fmt.flags.contains(EffectFlags::UNDERLINE) { 292 | if let Some((x1, y_top, h, e)) = underline { 293 | let x2 = glyph.position.0; 294 | g(x1, x2, y_top, h, e); 295 | underline = None; 296 | } else if let Some(metrics) = sf.underline_metrics() { 297 | let y_top = glyph.position.1 - metrics.position; 298 | let h = metrics.thickness; 299 | let x1 = glyph.position.0; 300 | underline = Some((x1, y_top, h, fmt.e)); 301 | } 302 | } 303 | if strikethrough.is_some() != fmt.flags.contains(EffectFlags::STRIKETHROUGH) { 304 | if let Some((x1, y_top, h, e)) = strikethrough { 305 | let x2 = glyph.position.0; 306 | g(x1, x2, y_top, h, e); 307 | strikethrough = None; 308 | } else if let Some(metrics) = sf.strikethrough_metrics() { 309 | let y_top = glyph.position.1 - metrics.position; 310 | let h = metrics.thickness; 311 | let x1 = glyph.position.0; 312 | strikethrough = Some((x1, y_top, h, fmt.e)); 313 | } 314 | } 315 | } 316 | 317 | f(glyph, fmt.e); 318 | } 319 | 320 | // Effects end at the following glyph's start (or end of this run part) 321 | if let Some((x1, y_top, h, e)) = underline { 322 | let x2 = if self.range.end() < self.run.glyphs.len() { 323 | self.run.glyphs[self.range.end()].position.0 324 | } else { 325 | self.run.caret 326 | } + self.offset.0; 327 | g(x1, x2, y_top, h, e); 328 | } 329 | if let Some((x1, y_top, h, e)) = strikethrough { 330 | let x2 = if self.range.end() < self.run.glyphs.len() { 331 | self.run.glyphs[self.range.end()].position.0 332 | } else { 333 | self.run.caret 334 | } + self.offset.0; 335 | g(x1, x2, y_top, h, e); 336 | } 337 | } 338 | } 339 | 340 | impl TextDisplay { 341 | /// Find the starting position (top-left) of the glyph at the given index 342 | /// 343 | /// [Requires status][Self#status-of-preparation]: 344 | /// text is fully prepared for display. 345 | /// 346 | /// The index should be no greater than the text length. It is not required 347 | /// to be on a code-point boundary. Returns an iterator over matching 348 | /// positions. Length of results is guaranteed to be one of 0, 1 or 2: 349 | /// 350 | /// - 0 if the index is not included in any run of text (probably only 351 | /// possible within a multi-byte line break or beyond the text length) 352 | /// - 1 is the normal case 353 | /// - 2 if the index is at the end of one run of text *and* at the start 354 | /// of another; these positions may be the same or may not be (over 355 | /// line breaks and with bidirectional text). If only a single position 356 | /// is desired, usually the latter is preferred (via `next_back()`). 357 | /// 358 | /// The result is not guaranteed to be within [`Self::bounding_box`]. 359 | /// Depending on the use-case, the caller may need to clamp the resulting 360 | /// position. 361 | pub fn text_glyph_pos(&self, index: usize) -> MarkerPosIter { 362 | let mut v: [MarkerPos; 2] = Default::default(); 363 | let (a, mut b) = (0, 0); 364 | let mut push_result = |pos, ascent, descent, level| { 365 | v[b] = MarkerPos { 366 | pos, 367 | ascent, 368 | descent, 369 | level, 370 | }; 371 | b += 1; 372 | }; 373 | 374 | // We don't care too much about performance: use a naive search strategy 375 | 'a: for run_part in &self.wrapped_runs { 376 | if index > to_usize(run_part.text_end) { 377 | continue; 378 | } 379 | 380 | let glyph_run = &self.runs[to_usize(run_part.glyph_run)]; 381 | let sf = fonts::library() 382 | .get_face(glyph_run.face_id) 383 | .scale_by_dpu(glyph_run.dpu); 384 | 385 | // If index is at the end of a run, we potentially get two matches. 386 | if index == to_usize(run_part.text_end) { 387 | let i = if glyph_run.level.is_ltr() { 388 | run_part.glyph_range.end() 389 | } else { 390 | run_part.glyph_range.start() 391 | }; 392 | let pos = if i < glyph_run.glyphs.len() { 393 | glyph_run.glyphs[i].position 394 | } else { 395 | // NOTE: for RTL we only hit this case if glyphs.len() == 0 396 | Vec2(glyph_run.caret, 0.0) 397 | }; 398 | 399 | let pos = run_part.offset + pos; 400 | push_result(pos, sf.ascent(), sf.descent(), glyph_run.level.number()); 401 | continue; 402 | } 403 | 404 | // else: index < to_usize(run_part.text_end) 405 | let pos = 'b: loop { 406 | if glyph_run.level.is_ltr() { 407 | for glyph in glyph_run.glyphs[run_part.glyph_range.to_std()].iter().rev() { 408 | if to_usize(glyph.index) <= index { 409 | break 'b glyph.position; 410 | } 411 | } 412 | } else { 413 | for glyph in glyph_run.glyphs[run_part.glyph_range.to_std()].iter() { 414 | if to_usize(glyph.index) <= index { 415 | let mut pos = glyph.position; 416 | pos.0 += sf.h_advance(glyph.id); 417 | break 'b pos; 418 | } 419 | } 420 | } 421 | break 'a; 422 | }; 423 | 424 | let pos = run_part.offset + pos; 425 | push_result(pos, sf.ascent(), sf.descent(), glyph_run.level.number()); 426 | break; 427 | } 428 | 429 | MarkerPosIter { v, a, b } 430 | } 431 | 432 | /// Get the number of glyphs 433 | /// 434 | /// [Requires status][Self#status-of-preparation]: lines have been wrapped. 435 | /// 436 | /// This method is a simple memory-read. 437 | #[inline] 438 | #[cfg(feature = "num_glyphs")] 439 | pub fn num_glyphs(&self) -> usize { 440 | to_usize(self.num_glyphs) 441 | } 442 | 443 | /// Iterate over runs of positioned glyphs 444 | /// 445 | /// All glyphs are translated by the given `offset` (this is practically 446 | /// free). 447 | /// 448 | /// An [`Effect`] sequence supports underline, strikethrough and custom 449 | /// indexing (e.g. for a color palette). Pass `&[]` if effects are not 450 | /// required. (The default effect is always [`Effect::default()`].) 451 | /// 452 | /// Runs are yielded in undefined order. The total number of 453 | /// glyphs yielded will equal [`TextDisplay::num_glyphs`]. 454 | /// 455 | /// [Requires status][Self#status-of-preparation]: 456 | /// text is fully prepared for display. 457 | pub fn runs<'a>( 458 | &'a self, 459 | offset: Vec2, 460 | effects: &'a [Effect], 461 | ) -> impl Iterator> + 'a { 462 | self.wrapped_runs 463 | .iter() 464 | .filter(|part| !part.glyph_range.is_empty()) 465 | .map(move |part| GlyphRun { 466 | run: &self.runs[to_usize(part.glyph_run)], 467 | range: part.glyph_range, 468 | offset: offset + part.offset, 469 | effects, 470 | }) 471 | } 472 | 473 | /// Yield a sequence of rectangles to highlight a given text range 474 | /// 475 | /// [Requires status][Self#status-of-preparation]: 476 | /// text is fully prepared for display. 477 | /// 478 | /// Calls `f(top_left, bottom_right)` for each highlighting rectangle. 479 | pub fn highlight_range(&self, range: std::ops::Range, f: &mut dyn FnMut(Vec2, Vec2)) { 480 | for line in &self.lines { 481 | let line_range = line.text_range(); 482 | if line_range.end <= range.start { 483 | continue; 484 | } else if range.end <= line_range.start { 485 | break; 486 | } else if range.start <= line_range.start && line_range.end <= range.end { 487 | let tl = Vec2(self.l_bound, line.top); 488 | let br = Vec2(self.r_bound, line.bottom); 489 | f(tl, br); 490 | } else { 491 | self.highlight_line(line.clone(), range.clone(), f); 492 | } 493 | } 494 | } 495 | 496 | /// Produce highlighting rectangles within a range of runs 497 | /// 498 | /// [Requires status][Self#status-of-preparation]: 499 | /// text is fully prepared for display. 500 | /// 501 | /// Warning: runs are in logical order which does not correspond to display 502 | /// order. As a result, the order of results (on a line) is not known. 503 | fn highlight_line( 504 | &self, 505 | line: Line, 506 | range: std::ops::Range, 507 | f: &mut dyn FnMut(Vec2, Vec2), 508 | ) { 509 | let fonts = fonts::library(); 510 | 511 | let mut a; 512 | 513 | let mut i = line.run_range.start(); 514 | let line_range_end = line.run_range.end(); 515 | 'b: loop { 516 | if i >= line_range_end { 517 | return; 518 | } 519 | let run_part = &self.wrapped_runs[i]; 520 | if range.start >= to_usize(run_part.text_end) { 521 | i += 1; 522 | continue; 523 | } 524 | 525 | let glyph_run = &self.runs[to_usize(run_part.glyph_run)]; 526 | 527 | if glyph_run.level.is_ltr() { 528 | for glyph in glyph_run.glyphs[run_part.glyph_range.to_std()].iter().rev() { 529 | if to_usize(glyph.index) <= range.start { 530 | a = glyph.position.0; 531 | break 'b; 532 | } 533 | } 534 | a = 0.0; 535 | } else { 536 | let sf = fonts 537 | .get_face(glyph_run.face_id) 538 | .scale_by_dpu(glyph_run.dpu); 539 | for glyph in glyph_run.glyphs[run_part.glyph_range.to_std()].iter() { 540 | if to_usize(glyph.index) <= range.start { 541 | a = glyph.position.0; 542 | a += sf.h_advance(glyph.id); 543 | break 'b; 544 | } 545 | } 546 | a = glyph_run.caret; 547 | } 548 | break 'b; 549 | } 550 | 551 | let mut first = true; 552 | 'a: while i < line_range_end { 553 | let run_part = &self.wrapped_runs[i]; 554 | let offset = run_part.offset.0; 555 | let glyph_run = &self.runs[to_usize(run_part.glyph_run)]; 556 | 557 | if !first { 558 | a = if glyph_run.level.is_ltr() { 559 | if run_part.glyph_range.start() < glyph_run.glyphs.len() { 560 | glyph_run.glyphs[run_part.glyph_range.start()].position.0 561 | } else { 562 | 0.0 563 | } 564 | } else { 565 | if run_part.glyph_range.end() < glyph_run.glyphs.len() { 566 | glyph_run.glyphs[run_part.glyph_range.end()].position.0 567 | } else { 568 | glyph_run.caret 569 | } 570 | }; 571 | } 572 | first = false; 573 | 574 | if range.end >= to_usize(run_part.text_end) { 575 | let b; 576 | if glyph_run.level.is_ltr() { 577 | if run_part.glyph_range.end() < glyph_run.glyphs.len() { 578 | b = glyph_run.glyphs[run_part.glyph_range.end()].position.0; 579 | } else { 580 | b = glyph_run.caret; 581 | } 582 | } else { 583 | let p = if run_part.glyph_range.start() < glyph_run.glyphs.len() { 584 | glyph_run.glyphs[run_part.glyph_range.start()].position.0 585 | } else { 586 | // NOTE: for RTL we only hit this case if glyphs.len() == 0 587 | glyph_run.caret 588 | }; 589 | b = a; 590 | a = p; 591 | }; 592 | 593 | f(Vec2(a + offset, line.top), Vec2(b + offset, line.bottom)); 594 | i += 1; 595 | continue; 596 | } 597 | 598 | let sf = fonts 599 | .get_face(glyph_run.face_id) 600 | .scale_by_dpu(glyph_run.dpu); 601 | 602 | let b; 603 | 'c: loop { 604 | if glyph_run.level.is_ltr() { 605 | for glyph in glyph_run.glyphs[run_part.glyph_range.to_std()].iter().rev() { 606 | if to_usize(glyph.index) <= range.end { 607 | b = glyph.position.0; 608 | break 'c; 609 | } 610 | } 611 | } else { 612 | for glyph in glyph_run.glyphs[run_part.glyph_range.to_std()].iter() { 613 | if to_usize(glyph.index) <= range.end { 614 | b = a; 615 | a = glyph.position.0 + sf.h_advance(glyph.id); 616 | break 'c; 617 | } 618 | } 619 | } 620 | break 'a; 621 | } 622 | 623 | f(Vec2(a + offset, line.top), Vec2(b + offset, line.bottom)); 624 | break; 625 | } 626 | } 627 | } 628 | --------------------------------------------------------------------------------