diff --git a/.gitignore b/.gitignore index a5ff49ec8a..93296bc80f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ /node_modules /tmp /vendor/* +!/vendor/parley-0.11.1/ +!/vendor/parley-0.11.1/** !/vendor/v8-152.2.0/ !/vendor/v8-152.2.0/** !/vendor/deno_v8-0.3.0/ diff --git a/Cargo.lock b/Cargo.lock index 45d739d7fd..f19e78fd13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4159,8 +4159,6 @@ checksum = "4b6937eda350acc1a5d05872c3cbf99fe78619c269096e2be3d4a350058639d5" [[package]] name = "parley" version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22d2ff88bd3f7d68d1d9b09c7e6209f9a8e8c05088295140a2bcf2e9b17038c5" dependencies = [ "fontique", "harfrust", diff --git a/Cargo.toml b/Cargo.toml index 6f65509ada..cfe23ed310 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,12 +91,13 @@ members = [ "moli-http-cache", "moli-wpt-compat" ] -exclude = ["vendor/xml5ever-0.39.0", "vendor/web-audio-api-1.7.0", "vendor/usvg-0.48.1"] +exclude = ["vendor/parley-0.11.1", "vendor/xml5ever-0.39.0", "vendor/web-audio-api-1.7.0", "vendor/usvg-0.48.1"] [workspace.package] license = "MIT OR Apache-2.0" [patch.crates-io] +parley = { path = "vendor/parley-0.11.1" } usvg = { path = "vendor/usvg-0.48.1" } web-audio-api = { path = "vendor/web-audio-api-1.7.0" } xml5ever = { path = "vendor/xml5ever-0.39.0" } diff --git a/moli-layout/tests/phase4_layout_contract.rs b/moli-layout/tests/phase4_layout_contract.rs index 0e48dcfe5f..7df9064274 100644 --- a/moli-layout/tests/phase4_layout_contract.rs +++ b/moli-layout/tests/phase4_layout_contract.rs @@ -299,6 +299,155 @@ fn inline_float_content_height_includes_both_padding_and_border_edges() { assert_close(rect(&snapshot, GREEN).height, 26.0); } +#[test] +fn unbreakable_inline_text_moves_below_a_float_when_its_slot_is_too_narrow() { + let source = Source(vec![ + Node::element("root", "div", LayoutElementCategory::Generic, None, vec![1]), + Node::element( + "container", + "div", + LayoutElementCategory::Generic, + None, + vec![2, 3], + ), + Node::element( + "float", + "img", + LayoutElementCategory::Generic, + Some(LayoutReplacedKind::Image), + vec![], + ) + .with_metrics(ReplacedMetrics { + intrinsic_width: Some(16.0), + intrinsic_height: Some(16.0), + ..ReplacedMetrics::default() + }), + Node::text("text", "abc"), + ]); + let mut styles = Styles::default(); + styles.primary.insert( + 0, + sized(LayoutDisplay::Block, 200.0, 100.0, PaintColor::TRANSPARENT), + ); + styles.primary.insert( + 1, + style(LayoutDisplay::InlineBlock, GREEN) + .tap_taffy(|style| { + style.size.width = Dimension::length(10.0); + }) + .with_text_metrics(16.0, 20.0), + ); + styles.primary.insert( + 2, + sized(LayoutDisplay::Block, 16.0, 16.0, BLUE).with_float(Float::Left, Clear::None), + ); + let snapshot = render(&source, &mut styles, 200, 100); + assert_close(rect(&snapshot, GREEN).height, 36.0); +} + +#[test] +fn inline_float_wrapping_ignores_hanging_space_and_uses_the_first_fitting_band() { + for with_tall_right_float in [false, true] { + let source = Source(vec![ + Node::element( + "root", + "div", + LayoutElementCategory::Generic, + None, + vec![1, 4], + ), + Node::element( + "container", + "div", + LayoutElementCategory::Generic, + None, + if with_tall_right_float { + vec![2, 6, 3] + } else { + vec![2, 3] + }, + ), + Node::element("left", "div", LayoutElementCategory::Generic, None, vec![]), + Node::text( + "text", + if with_tall_right_float { + "abc" + } else { + "abc abc" + }, + ), + Node::element( + "reference", + "div", + LayoutElementCategory::Generic, + None, + vec![5], + ), + Node::text("reference-text", "abc"), + Node::element("right", "div", LayoutElementCategory::Generic, None, vec![]), + ]); + let mut styles = Styles::default(); + styles.primary.insert( + 0, + sized(LayoutDisplay::Block, 200.0, 100.0, PaintColor::TRANSPARENT), + ); + styles.primary.insert( + 1, + style(LayoutDisplay::InlineBlock, GREEN).with_text_metrics(16.0, 20.0), + ); + styles.primary.insert( + 2, + sized(LayoutDisplay::Block, 16.0, 16.0, BLUE).with_float(Float::Left, Clear::None), + ); + styles.primary.insert( + 4, + style(LayoutDisplay::InlineBlock, YELLOW).with_text_metrics(16.0, 20.0), + ); + styles.primary.insert( + 6, + sized(LayoutDisplay::Block, 16.0, 40.0, RED).with_float(Float::Right, Clear::None), + ); + let reference = render(&source, &mut styles, 200, 100); + let word_width = rect(&reference, YELLOW).width; + // Paint rectangles are pixel-rounded. Leave one pixel for that + // rounding while keeping the first word's trailing space hanging. + let width = word_width + if with_tall_right_float { 28.0 } else { 17.0 }; + styles.primary.insert( + 1, + style(LayoutDisplay::InlineBlock, GREEN) + .tap_taffy(|style| style.size.width = Dimension::length(width)) + .with_text_metrics(16.0, 20.0), + ); + let snapshot = render(&source, &mut styles, 200, 100); + assert_close(rect(&snapshot, GREEN).height, 40.0); + if with_tall_right_float { + assert_close(rect(&snapshot, RED).y, rect(&snapshot, BLUE).y); + } + let first_baseline = snapshot + .fragments + .iter() + .find_map(|fragment| match fragment { + PaintFragment::GlyphRun(run) => { + run.glyphs_in_surface().first().map(|glyph| glyph.y) + } + _ => None, + }) + .expect("container text"); + let short_float_bottom = rect(&snapshot, BLUE).y + 16.0; + if with_tall_right_float { + assert!( + first_baseline >= short_float_bottom + && first_baseline < rect(&snapshot, RED).y + 40.0 + ); + } else { + assert!( + first_baseline < short_float_bottom, + "hanging whitespace must not push a fitting word below the float" + ); + } + } +} + #[test] fn inline_block_ratio_height_respects_automatic_content_minimum_and_opt_outs() { let source = Source(vec![ diff --git a/vendor/parley-0.11.1/Cargo.toml b/vendor/parley-0.11.1/Cargo.toml new file mode 100644 index 0000000000..d08d05bb9e --- /dev/null +++ b/vendor/parley-0.11.1/Cargo.toml @@ -0,0 +1,173 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.88" +name = "parley" +version = "0.11.1" +build = false +exclude = ["/tests"] +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Parley provides an API for implementing rich text layout." +readme = "README.md" +keywords = [ + "text", + "layout", +] +categories = [ + "gui", + "graphics", +] +license = "Apache-2.0 OR MIT" +repository = "https://github.com/linebender/parley" + +[package.metadata.docs.rs] +all-features = true + +[features] +accesskit = ["dep:accesskit"] +complex-scripts = [] +default = ["system"] +libm = [ + "fontique/libm", + "harfrust/libm", + "peniko/libm", + "skrifa/libm", + "dep:core_maths", +] +std = [ + "fontique/std", + "harfrust/std", + "peniko/std", + "skrifa/std", + "parlance/std", +] +system = [ + "std", + "fontique/system", +] + +[lib] +name = "parley" +path = "src/lib.rs" + +[dependencies.accesskit] +version = "0.24.0" +optional = true + +[dependencies.core_maths] +version = "0.1.1" +optional = true + +[dependencies.fontique] +version = "0.11.1" +default-features = false + +[dependencies.harfrust] +version = "0.12.0" +default-features = false + +[dependencies.hashbrown] +version = "0.17.0" +features = [ + "default-hasher", + "raw-entry", +] +default-features = false + +[dependencies.icu_normalizer] +version = "2.1.1" +features = ["compiled_data"] +default-features = false + +[dependencies.icu_properties] +version = "2.1.2" +features = ["compiled_data"] +default-features = false + +[dependencies.icu_segmenter] +version = "2.1.2" +features = ["compiled_data"] +default-features = false + +[dependencies.linebender_resource_handle] +version = "0.1.1" +default-features = false + +[dependencies.parlance] +version = "0.1.0" +default-features = false + +[dependencies.parley_data] +version = "0.11.1" +features = ["baked"] +default-features = false + +[dependencies.skrifa] +version = "0.44.0" +default-features = false + +[dev-dependencies.peniko] +version = "0.6.0" +default-features = false + +[target.'cfg(not(target_os = "android"))'.dev-dependencies.oxipng] +version = "9.1.5" +features = ["freestanding"] +default-features = false + +[lints.clippy] +allow_attributes_without_reason = "warn" +cargo_common_metadata = "warn" +cast_possible_truncation = "warn" +collection_is_never_read = "warn" +dbg_macro = "warn" +debug_assert_with_mut_call = "warn" +default_trait_access = "warn" +doc_markdown = "warn" +fn_to_numeric_cast_any = "warn" +infinite_loop = "warn" +large_stack_arrays = "warn" +mismatching_type_param_order = "warn" +missing_assert_message = "warn" +missing_fields_in_debug = "warn" +negative_feature_names = "warn" +redundant_feature_names = "warn" +same_functions_in_if_condition = "warn" +semicolon_if_nothing_returned = "warn" +should_panic_without_expect = "warn" +todo = "warn" +too_many_arguments = "allow" +unseparated_literal_suffix = "warn" +use_self = "warn" +wildcard_dependencies = "warn" + +[lints.rust] +elided_lifetimes_in_paths = "warn" +keyword_idents_2024 = "forbid" +missing_debug_implementations = "warn" +missing_docs = "warn" +non_ascii_idents = "forbid" +non_local_definitions = "forbid" +trivial_numeric_casts = "warn" +unnameable_types = "warn" +unreachable_pub = "warn" +unsafe_code = "deny" +unsafe_op_in_unsafe_fn = "forbid" +unused_import_braces = "warn" +unused_lifetimes = "warn" +unused_macro_rules = "warn" +unused_qualifications = "warn" diff --git a/vendor/parley-0.11.1/LICENSE-APACHE b/vendor/parley-0.11.1/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/vendor/parley-0.11.1/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/vendor/parley-0.11.1/LICENSE-MIT b/vendor/parley-0.11.1/LICENSE-MIT new file mode 100644 index 0000000000..657288a5e3 --- /dev/null +++ b/vendor/parley-0.11.1/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright 2020 the Parley Authors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/vendor/parley-0.11.1/MOLI_PATCHES.md b/vendor/parley-0.11.1/MOLI_PATCHES.md new file mode 100644 index 0000000000..cacbfb2ea6 --- /dev/null +++ b/vendor/parley-0.11.1/MOLI_PATCHES.md @@ -0,0 +1,14 @@ +# Moli patches to Parley 0.11.1 + +Source: the `parley` 0.11.1 crates.io package, archive SHA-256 +`22d2ff88bd3f7d68d1d9b09c7e6209f9a8e8c05088295140a2bcf2e9b17038c5`. +Original source and Apache-2.0 / MIT licenses are retained. Cargo cache metadata +and the standalone lockfile are not part of this vendored dependency. + +`BreakLines::last_line_metrics()` exposes the last committed line's metrics +without changing the breaker. Moli uses its advance minus trailing whitespace +to retry an unbreakable word in a wider float exclusion band. Reading only +`LineBreakData::advance` would incorrectly include hanging whitespace. + +Regression coverage: `moli-layout/tests/phase4_layout_contract.rs`. +Remove the local patch when a compatible upstream release exposes these metrics. diff --git a/vendor/parley-0.11.1/README.md b/vendor/parley-0.11.1/README.md new file mode 100644 index 0000000000..d54898ed05 --- /dev/null +++ b/vendor/parley-0.11.1/README.md @@ -0,0 +1,55 @@ +
+ +# Parley + +**Rich text layout** + +[![Latest published parley version.](https://img.shields.io/crates/v/parley.svg)](https://crates.io/crates/parley) +[![Documentation build status.](https://img.shields.io/docsrs/parley.svg)](https://docs.rs/parley) +[![Dependency staleness status.](https://deps.rs/crate/parley/latest/status.svg)](https://deps.rs/crate/parley) +[![Linebender Zulip chat.](https://img.shields.io/badge/Linebender-%23parley-blue?logo=Zulip)](https://xi.zulipchat.com/#narrow/channel/205635-parley) +[![Apache 2.0 or MIT license.](https://img.shields.io/badge/license-Apache--2.0_OR_MIT-blue.svg)](#license) + +
+ +Parley provides an API for implementing rich text layout. +It is backed by [HarfRust](https://github.com/harfbuzz/harfrust) for text shaping. + +## Minimum supported Rust Version (MSRV) + +This version of Parley has been verified to compile with **Rust 1.88** and later. + +Future versions of Parley might increase the Rust version requirement. +It will not be treated as a breaking change and as such can even happen with small patch releases. + +
+Click here if compiling fails. + +As time has passed, some of Parley's dependencies could have released versions with a higher Rust requirement. +If you encounter a compilation issue due to a dependency and don't want to upgrade your Rust toolchain, then you could downgrade the dependency. + +```sh +# Use the problematic dependency's name and version +cargo update -p package_name --precise 0.1.1 +``` +
+ +## Community + +Discussion of Parley development happens in the [Linebender Zulip](https://xi.zulipchat.com/), specifically the [#parley channel](https://xi.zulipchat.com/#narrow/channel/205635-parley). +All public content can be read without logging in. + +Contributions are welcome by pull request. The [Rust code of conduct] applies. + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache 2.0 license, shall be licensed as noted in the [License](#license) section, without any additional terms or conditions. + +## License + +Licensed under either of + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or ) + +at your option. + +[Rust code of conduct]: https://www.rust-lang.org/policies/code-of-conduct diff --git a/vendor/parley-0.11.1/src/analysis/cluster.rs b/vendor/parley-0.11.1/src/analysis/cluster.rs new file mode 100644 index 0000000000..e0a08cea01 --- /dev/null +++ b/vendor/parley-0.11.1/src/analysis/cluster.rs @@ -0,0 +1,382 @@ +// Copyright 2025 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use alloc::vec::Vec; +use icu_normalizer::properties::Decomposed; + +use crate::analysis::AnalysisDataSources; + +/// The maximum number of characters in a single cluster. +const MAX_CLUSTER_SIZE: usize = 32; + +#[derive(Debug, Default)] +pub(crate) struct CharCluster { + pub chars: Vec, + pub is_emoji: bool, + pub map_len: u8, + pub start: u32, + pub end: u32, + pub force_normalize: bool, + comp: Form, + decomp: Form, + form: FormKind, + best_ratio: f32, +} + +impl CharCluster { + pub(crate) fn range(&self) -> SourceRange { + SourceRange { + start: self.start, + end: self.end, + } + } +} + +/// Source range of a cluster in code units. +#[derive(Copy, Clone)] +pub(crate) struct SourceRange { + pub start: u32, + pub end: u32, +} + +#[derive(Copy, Clone, Debug, Default)] +pub(crate) struct Char { + /// The character. + pub ch: char, + /// Whether the character + pub is_control_character: bool, + /// True if the character should be considered when mapping glyphs. + pub contributes_to_shaping: bool, + /// Nominal glyph identifier. + pub glyph_id: GlyphId, + /// Indexes into the list of styles for the containing text run, to find the style applicable + /// to this character. + pub style_index: u16, +} + +pub(crate) type GlyphId = u16; + +/// Whitespace content of a cluster. +#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Debug)] +#[repr(u8)] +pub(crate) enum Whitespace { + /// Not a space. + None = 0, + /// Standard space. + Space = 1, + /// Non-breaking space (U+00A0). + NoBreakSpace = 2, + /// Horizontal tab. + Tab = 3, + /// Newline (CR, LF, CRLF, LS, or PS). + Newline = 4, +} + +impl Whitespace { + /// Returns true for space or no break space. + pub(crate) fn is_space_or_nbsp(self) -> bool { + matches!(self, Self::Space | Self::NoBreakSpace) + } +} + +/// Iterative status of mapping a character cluster to nominal glyph identifiers. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub(crate) enum Status { + /// Mapping should be skipped. + Discard, + /// The best mapping so far. + Keep, + /// Complete mapping. + Complete, +} + +impl CharCluster { + pub(crate) fn clear(&mut self) { + self.chars.clear(); + self.is_emoji = false; + self.map_len = 0; + self.start = 0; + self.end = 0; + self.force_normalize = false; + self.comp.clear(); + self.decomp.clear(); + self.form = FormKind::Original; + self.best_ratio = 0.; + } + + #[inline(always)] + fn len(&self) -> usize { + self.chars.len() + } + + /// Returns the primary style index for the cluster. + #[inline(always)] + pub(crate) fn style_index(&self) -> u16 { + self.chars[0].style_index + } + + #[inline(always)] + fn contributes_to_shaping(ch: char, analysis_data_sources: &AnalysisDataSources) -> bool { + let props = analysis_data_sources.properties(ch); + crate::analysis::contributes_to_shaping(props.general_category(), props.script()) + } + + fn decomposed(&mut self, analysis_data_sources: &AnalysisDataSources) -> Option<&[Char]> { + match self.decomp.state { + FormState::Invalid => None, + FormState::None => { + self.decomp.state = FormState::Invalid; + + // Only attempt pairwise normalization (1 <-> 2 characters) + if self.chars.len() != 1 { + return None; + } + + let decomposer = analysis_data_sources.decomposing_normalizer(); + let decomp = decomposer.decompose(self.chars[0].ch); + match decomp { + Decomposed::Default | Decomposed::Singleton(_) => { + return None; + } + Decomposed::Expansion(a, b) => { + let mut copy = self.chars[0]; + copy.ch = a; + copy.contributes_to_shaping = + Self::contributes_to_shaping(a, analysis_data_sources); + self.decomp.chars[0] = copy; + + copy.ch = b; + copy.contributes_to_shaping = + Self::contributes_to_shaping(b, analysis_data_sources); + self.decomp.chars[1] = copy; + + self.decomp.len = 2; + } + } + + self.decomp.state = FormState::Valid; + self.decomp.setup(); + Some(self.decomp.chars()) + } + FormState::Valid => Some(self.decomp.chars()), + } + } + + fn composed(&mut self, analysis_data_sources: &AnalysisDataSources) -> Option<&[Char]> { + match self.comp.state { + FormState::Invalid => None, + FormState::None => { + self.comp.state = FormState::Invalid; + + // Only attempt pairwise normalization (1 <-> 2 characters) + if self.chars.len() != 2 { + return None; + } + + let composer = analysis_data_sources.composing_normalizer(); + let comp = composer.compose(self.chars[0].ch, self.chars[1].ch); + match comp { + None => {} + Some(ch) => { + let mut copy = self.chars[0]; + copy.ch = ch; + copy.contributes_to_shaping = + Self::contributes_to_shaping(ch, analysis_data_sources); + self.comp.chars[0] = copy; + self.comp.len = 1; + } + } + + self.comp.state = FormState::Valid; + self.comp.setup(); + Some(self.comp.chars()) + } + FormState::Valid => Some(self.comp.chars()), + } + } + + pub(crate) fn map( + &mut self, + f: impl Fn(char) -> GlyphId, + analysis_data_sources: &AnalysisDataSources, + ) -> Status { + let len = self.len(); + if len == 0 { + return Status::Complete; + } + let mut glyph_ids = [0_u16; MAX_CLUSTER_SIZE]; + let prev_ratio = self.best_ratio; + let mut ratio; + if self.force_normalize && self.composed(analysis_data_sources).is_some() { + ratio = self.comp.map(&f, &mut glyph_ids, self.best_ratio); + if ratio > self.best_ratio { + self.best_ratio = ratio; + self.form = FormKind::NFC; + if ratio >= 1. { + return Status::Complete; + } + } + } + ratio = Mapper { + chars: &mut self.chars[..len], + map_len: self.map_len.max(1), + has_contributing: self.map_len > 0, + } + .map(&f, &mut glyph_ids, self.best_ratio); + if ratio > self.best_ratio { + self.best_ratio = ratio; + self.form = FormKind::Original; + if ratio >= 1. { + return Status::Complete; + } + } + if self.decomposed(analysis_data_sources).is_some() { + ratio = self.decomp.map(&f, &mut glyph_ids, self.best_ratio); + if ratio > self.best_ratio { + self.best_ratio = ratio; + self.form = FormKind::NFD; + if ratio >= 1. { + return Status::Complete; + } + } + if !self.force_normalize && self.composed(analysis_data_sources).is_some() { + ratio = self.comp.map(&f, &mut glyph_ids, self.best_ratio); + if ratio > self.best_ratio { + self.best_ratio = ratio; + self.form = FormKind::NFC; + if ratio >= 1. { + return Status::Complete; + } + } + } + } + if self.best_ratio > prev_ratio { + Status::Keep + } else { + Status::Discard + } + } +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +#[allow(clippy::upper_case_acronyms)] +enum FormKind { + #[default] + Original, + NFD, + NFC, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum FormState { + None, + Valid, + Invalid, +} + +#[derive(Clone, Debug)] +pub(crate) struct Form { + chars: [Char; 2], + len: u8, + map_len: u8, + has_contributing: bool, + state: FormState, +} + +impl Default for Form { + fn default() -> Self { + Self::new() + } +} + +impl Form { + fn new() -> Self { + Self { + chars: [Char::default(), Char::default()], + len: 0, + map_len: 0, + has_contributing: false, + state: FormState::None, + } + } + + fn clear(&mut self) { + self.chars = [Char::default(), Char::default()]; + self.len = 0; + self.map_len = 0; + self.has_contributing = false; + self.state = FormState::None; + } + + #[inline(always)] + fn chars(&self) -> &[Char] { + &self.chars[..self.len as usize] + } + + #[inline(always)] + fn setup(&mut self) { + self.map_len = (self + .chars() + .iter() + .filter(|c| !c.is_control_character) + .count() as u8) + .max(1); + self.has_contributing = self.chars().iter().any(|c| c.contributes_to_shaping); + } + + #[inline(always)] + fn map( + &mut self, + f: &impl Fn(char) -> u16, + glyphs: &mut [u16; MAX_CLUSTER_SIZE], + best_ratio: f32, + ) -> f32 { + Mapper { + chars: &mut self.chars[..self.len as usize], + map_len: self.map_len, + has_contributing: self.has_contributing, + } + .map(f, glyphs, best_ratio) + } +} + +struct Mapper<'a> { + chars: &'a mut [Char], + map_len: u8, + has_contributing: bool, +} + +impl<'a> Mapper<'a> { + fn map( + &mut self, + f: &impl Fn(char) -> u16, + glyphs: &mut [u16; MAX_CLUSTER_SIZE], + best_ratio: f32, + ) -> f32 { + if self.map_len == 0 { + return 1.; + } + let mut mapped = 0; + for (c, g) in self.chars.iter().zip(glyphs.iter_mut()) { + if !c.contributes_to_shaping { + *g = f(c.ch); + if !self.has_contributing { + mapped += 1; + } + } else { + let gid = f(c.ch); + *g = gid; + if gid != 0 { + mapped += 1; + } + } + } + let ratio = mapped as f32 / self.map_len as f32; + if ratio > best_ratio { + for (ch, glyph) in self.chars.iter_mut().zip(glyphs) { + ch.glyph_id = *glyph; + } + } + ratio + } +} diff --git a/vendor/parley-0.11.1/src/analysis/mod.rs b/vendor/parley-0.11.1/src/analysis/mod.rs new file mode 100644 index 0000000000..f21d24bd6c --- /dev/null +++ b/vendor/parley-0.11.1/src/analysis/mod.rs @@ -0,0 +1,565 @@ +// Copyright 2025 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +pub(crate) mod cluster; + +use alloc::vec::Vec; +use core::marker::PhantomData; + +use crate::break_overrides::{LineBreakContext, LineBreakOverrideFn}; +use crate::resolve::StyleRun; +use crate::{Brush, LayoutContext, WordBreak}; + +use icu_normalizer::properties::{ + CanonicalComposition, CanonicalCompositionBorrowed, CanonicalDecomposition, + CanonicalDecompositionBorrowed, +}; +use icu_properties::props::{BidiMirroringGlyph, GeneralCategory, GraphemeClusterBreak, Script}; +use icu_properties::{ + CodePointMapData, CodePointMapDataBorrowed, PropertyNamesShort, PropertyNamesShortBorrowed, +}; +use icu_segmenter::options::{LineBreakOptions, LineBreakWordOption, WordBreakInvariantOptions}; +use icu_segmenter::{ + GraphemeClusterSegmenter, GraphemeClusterSegmenterBorrowed, LineSegmenter, + LineSegmenterBorrowed, WordSegmenter, WordSegmenterBorrowed, +}; +use parley_data::Properties; + +pub(crate) struct AnalysisDataSources; + +impl AnalysisDataSources { + pub(crate) fn new() -> Self { + Self + } + + #[inline(always)] + pub(crate) fn properties(&self, c: char) -> Properties { + Properties::get(c) + } + + #[inline(always)] + pub(crate) fn grapheme_segmenter(&self) -> GraphemeClusterSegmenterBorrowed<'_> { + const { GraphemeClusterSegmenter::new() } + } + + #[inline(always)] + fn word_segmenter(&self) -> WordSegmenterBorrowed<'static> { + #[cfg(feature = "complex-scripts")] + { + WordSegmenter::new_dictionary(WordBreakInvariantOptions::default()) + } + #[cfg(not(feature = "complex-scripts"))] + { + const { WordSegmenter::new_for_non_complex_scripts(WordBreakInvariantOptions::default()) } + } + } + + #[inline(always)] + fn line_segmenter(&self, word_break_strength: WordBreak) -> LineSegmenterBorrowed<'static> { + match word_break_strength { + WordBreak::Normal => { + let mut opt = LineBreakOptions::default(); + opt.word_option = Some(LineBreakWordOption::Normal); + line_segmenter_impl(opt) + } + WordBreak::BreakAll => { + let mut opt = LineBreakOptions::default(); + opt.word_option = Some(LineBreakWordOption::BreakAll); + line_segmenter_impl(opt) + } + WordBreak::KeepAll => { + let mut opt = LineBreakOptions::default(); + opt.word_option = Some(LineBreakWordOption::KeepAll); + line_segmenter_impl(opt) + } + } + } + + #[inline(always)] + fn composing_normalizer(&self) -> CanonicalCompositionBorrowed<'_> { + const { CanonicalComposition::new() } + } + + #[inline(always)] + fn decomposing_normalizer(&self) -> CanonicalDecompositionBorrowed<'_> { + const { CanonicalDecomposition::new() } + } + + #[inline(always)] + pub(crate) fn script_short_name(&self) -> PropertyNamesShortBorrowed<'static, Script> { + PropertyNamesShort::new() + } + + #[inline(always)] + fn brackets(&self) -> CodePointMapDataBorrowed<'_, BidiMirroringGlyph> { + const { CodePointMapData::new() } + } +} + +#[cfg(feature = "complex-scripts")] +#[inline(always)] +fn line_segmenter_impl(opt: LineBreakOptions<'_>) -> LineSegmenterBorrowed<'static> { + LineSegmenter::new_dictionary(opt) +} + +#[cfg(not(feature = "complex-scripts"))] +#[inline(always)] +fn line_segmenter_impl(opt: LineBreakOptions<'_>) -> LineSegmenterBorrowed<'static> { + LineSegmenter::new_for_non_complex_scripts(opt) +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub(crate) struct CharInfo { + /// The line/word breaking boundary classification of this character. + pub boundary: Boundary, + /// The Unicode script this character belongs to. + pub script: Script, + /// The grapheme cluster boundary property of this character. + pub grapheme_cluster_break: GraphemeClusterBreak, + /// The impact this character has on directionality. + pub bidi_class: icu_properties::props::BidiClass, + /// Whether or not the character is a bracket, plus mirror data if so. + pub bracket: BidiMirroringGlyph, + + flags: u8, +} + +impl CharInfo { + const VARIATION_SELECTOR_SHIFT: u8 = 0; + const REGION_INDICATOR_SHIFT: u8 = 1; + const CONTROL_SHIFT: u8 = 2; + const EMOJI_OR_PICTOGRAPH_SHIFT: u8 = 3; + const CONTRIBUTES_TO_SHAPING_SHIFT: u8 = 4; + const FORCE_NORMALIZE_SHIFT: u8 = 5; + + #[allow( + dead_code, + reason = "To be used in more complete emoji checking, in select_font" + )] + const VARIATION_SELECTOR_MASK: u8 = 1 << Self::VARIATION_SELECTOR_SHIFT; + #[allow( + dead_code, + reason = "To be used in more complete emoji checking, in select_font" + )] + const REGION_INDICATOR_MASK: u8 = 1 << Self::REGION_INDICATOR_SHIFT; + const CONTROL_MASK: u8 = 1 << Self::CONTROL_SHIFT; + const EMOJI_OR_PICTOGRAPH_MASK: u8 = 1 << Self::EMOJI_OR_PICTOGRAPH_SHIFT; + const CONTRIBUTES_TO_SHAPING_MASK: u8 = 1 << Self::CONTRIBUTES_TO_SHAPING_SHIFT; + const FORCE_NORMALIZE_MASK: u8 = 1 << Self::FORCE_NORMALIZE_SHIFT; + + fn new( + boundary: Boundary, + script: Script, + grapheme_cluster_break: GraphemeClusterBreak, + bidi_class: icu_properties::props::BidiClass, + bracket: BidiMirroringGlyph, + is_variation_selector: bool, + is_region_indicator: bool, + is_control: bool, + is_emoji_or_pictograph: bool, + contributes_to_shaping: bool, + force_normalize: bool, + ) -> Self { + Self { + boundary, + script, + grapheme_cluster_break, + bidi_class, + bracket, + flags: (is_variation_selector as u8) << Self::VARIATION_SELECTOR_SHIFT + | (is_region_indicator as u8) << Self::REGION_INDICATOR_SHIFT + | (is_control as u8) << Self::CONTROL_SHIFT + | (is_emoji_or_pictograph as u8) << Self::EMOJI_OR_PICTOGRAPH_SHIFT + | (contributes_to_shaping as u8) << Self::CONTRIBUTES_TO_SHAPING_SHIFT + | (force_normalize as u8) << Self::FORCE_NORMALIZE_SHIFT, + } + } + + #[allow( + dead_code, + reason = "To be used in more complete emoji checking, in select_font" + )] + #[inline(always)] + pub(crate) fn is_variation_selector(self) -> bool { + self.flags & Self::VARIATION_SELECTOR_MASK != 0 + } + + #[allow( + dead_code, + reason = "To be used in more complete emoji checking, in select_font" + )] + #[inline(always)] + pub(crate) fn is_region_indicator(self) -> bool { + self.flags & Self::REGION_INDICATOR_MASK != 0 + } + + #[inline(always)] + pub(crate) fn is_control(self) -> bool { + self.flags & Self::CONTROL_MASK != 0 + } + + #[inline(always)] + pub(crate) fn is_emoji_or_pictograph(self) -> bool { + self.flags & Self::EMOJI_OR_PICTOGRAPH_MASK != 0 + } + + #[inline(always)] + pub(crate) fn contributes_to_shaping(self) -> bool { + self.flags & Self::CONTRIBUTES_TO_SHAPING_MASK != 0 + } + + #[inline(always)] + pub(crate) fn force_normalize(self) -> bool { + self.flags & Self::FORCE_NORMALIZE_MASK != 0 + } +} + +/// Boundary type of a character or cluster. +#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Debug)] +#[repr(u8)] +pub(crate) enum Boundary { + /// Not a boundary. + None = 0, + /// Start of a word. + Word = 1, + /// Potential line break. + Line = 2, + /// Mandatory line break. + Mandatory = 3, +} + +pub(crate) fn analyze_text( + lcx: &mut LayoutContext, + mut text: &str, + line_break_override: Option<&LineBreakOverrideFn>, +) { + struct WordBreakSegmentIter<'a, I: Iterator, B: Brush> { + text: &'a str, + style_runs: I, + lcx: &'a LayoutContext, + char_indices: core::str::CharIndices<'a>, + current_char: (usize, char), + building_range_start: usize, + previous_word_break_style: WordBreak, + done: bool, + _phantom: PhantomData, + } + + impl<'a, I, B: Brush + 'a> WordBreakSegmentIter<'a, I, B> + where + I: Iterator, + { + fn new( + text: &'a str, + style_runs: I, + lcx: &'a LayoutContext, + first_style_run: &StyleRun, + ) -> Self { + let mut char_indices = text.char_indices(); + let current_char_len = char_indices.next().unwrap(); + let first_style = &lcx.style_table[first_style_run.style_index as usize]; + + Self { + text, + style_runs, + lcx, + char_indices, + current_char: current_char_len, + building_range_start: first_style_run.range.start, + previous_word_break_style: first_style.word_break, + done: false, + _phantom: PhantomData, + } + } + } + + impl<'a, I, B: Brush + 'a> Iterator for WordBreakSegmentIter<'a, I, B> + where + I: Iterator, + { + type Item = (&'a str, WordBreak, bool); + + fn next(&mut self) -> Option { + if self.done { + return None; + } + + for style_run in self.style_runs.by_ref() { + // Empty style ranges are disallowed. + assert!(style_run.range.start < style_run.range.end); + + let style_start_index = style_run.range.start; + let mut prev_char_index = self.current_char; + + // Find the character at the style boundary + while self.current_char.0 < style_start_index { + prev_char_index = self.current_char; + self.current_char = self.char_indices.next().unwrap(); + } + + let current_word_break_style = + self.lcx.style_table[style_run.style_index as usize].word_break; + if self.previous_word_break_style == current_word_break_style { + continue; + } + + // Produce one substring for each different word break style run + let prev_size = prev_char_index.1.len_utf8(); + let size = self.current_char.1.len_utf8(); + + let substring = &self.text[self.building_range_start..style_start_index + size]; + let result_style = self.previous_word_break_style; + + self.building_range_start = style_start_index - prev_size; + self.previous_word_break_style = current_word_break_style; + + return Some((substring, result_style, false)); + } + + // Final segment + self.done = true; + let last_substring = &self.text[self.building_range_start..self.text.len()]; + Some((last_substring, self.previous_word_break_style, true)) + } + } + + if text.is_empty() { + text = " "; + } + + // Line boundaries (word break naming refers to the line boundary determination config). + // + // This breaks text into sequences with similar line boundary config (part of style + // information). If this config is consistent for all text, we use a fast path through this. + let (first_style_run, rest_runs) = lcx + .style_runs + .split_first() + .expect("analyze_text requires at least one style run"); + + let contiguous_word_break_substrings = + WordBreakSegmentIter::new(text, rest_runs.iter(), lcx, first_style_run); + let mut global_offset = 0; + let mut line_boundary_positions: Vec = Vec::new(); + for (substring_index, (substring, word_break_strength, last)) in + contiguous_word_break_substrings.enumerate() + { + // Fast path for text with a single word-break option. + if substring_index == 0 && last { + let mut lb_iter = lcx + .analysis_data_sources + .line_segmenter(word_break_strength) + .segment_str(substring); + + let _first = lb_iter.next(); + let second = lb_iter.next(); + if second.is_none() { + continue; + } + let third = lb_iter.next(); + if third.is_none() { + continue; + } + + let iter = [second.unwrap(), third.unwrap()].into_iter().chain(lb_iter); + + line_boundary_positions.extend(iter); + // Remove the unnecessary boundary at the end added by ICU4X. + line_boundary_positions.pop(); + break; + } + + let line_boundaries_iter = lcx + .analysis_data_sources + .line_segmenter(word_break_strength) + .segment_str(substring); + + let mut substring_chars = substring.chars(); + if substring_index != 0 { + global_offset -= substring_chars.next().unwrap().len_utf8(); + } + // There will always be at least two characters if we are not taking the fast path for + // a single word break style substring. + let last_len = substring_chars.next_back().unwrap().len_utf8(); + + // Mark line boundaries (overriding word boundaries where present). + for (index, pos) in line_boundaries_iter.enumerate() { + // icu adds leading and trailing line boundaries, which we don't use. + if index == 0 || pos == substring.len() { + continue; + } + + // For all but the last substring, we ignore line boundaries caused by the last + // character, as this character is carried back from the next substring, and will be + // accounted for there. + if !last && pos == substring.len() - last_len { + continue; + } + line_boundary_positions.push(pos + global_offset); + } + + if !last { + global_offset += substring.len() - last_len; + } + } + + // Collect boundary byte positions compactly + let mut wb_iter = lcx + .analysis_data_sources + .word_segmenter() + .segment_str(text) + .peekable(); + + // Merge boundaries - line takes precedence over word + let mut lb_iter = line_boundary_positions.iter().peekable(); + let mut prev_char = None; + let mut prev_prev_char = None; + let boundary_iter = text.char_indices().map(|(byte_pos, ch)| { + // advance any stale word boundary positions + while let Some(&w) = wb_iter.peek() { + if w < byte_pos { + _ = wb_iter.next(); + } else { + break; + } + } + // advance any stale line boundary positions + while let Some(&l) = lb_iter.peek() { + if *l < byte_pos { + _ = lb_iter.next(); + } else { + break; + } + } + + let mut is_word = false; + if let Some(&w) = wb_iter.peek() { + if w == byte_pos { + is_word = true; + _ = wb_iter.next(); + } + } + let mut is_line = false; + if let Some(&l) = lb_iter.peek() { + if *l == byte_pos { + is_line = true; + _ = lb_iter.next(); + } + } + + // This leaves word boundaries intact. Consumers can only impact line boundaries. + if let (Some(prev), Some(lb_override)) = (prev_char, line_break_override) { + let forced = lb_override(LineBreakContext { + before_before: prev_prev_char, + before: prev, + after: ch, + }); + if let Some(forced) = forced { + is_line = forced; + } + } + prev_prev_char = prev_char; + prev_char = Some(ch); + + let boundary = if is_line { + Boundary::Line + } else if is_word { + Boundary::Word + } else { + Boundary::None + }; + + (boundary, ch) + }); + + let properties = |c| lcx.analysis_data_sources.properties(c); + + let mut needs_bidi_resolution = false; + + lcx.info.reserve(text.len()); + boundary_iter + // Shift line break data forward one, as line boundaries corresponding with line-breaking + // characters (like '\n') exist at an index position one higher than the respective + // character's index, but we need our iterators to align, and the rest are simply + // character-indexed. + .fold(false, |is_mandatory_linebreak, (boundary, ch)| { + let properties = properties(ch); + let script = properties.script(); + let grapheme_cluster_break = properties.grapheme_cluster_break(); + let bidi_class = properties.bidi_class(); + let general_category = properties.general_category(); + let is_emoji_or_pictograph = properties.is_emoji_or_pictograph(); + let is_variation_selector = properties.is_variation_selector(); + let is_region_indicator = properties.is_region_indicator(); + let next_mandatory_linebreak = properties.is_mandatory_linebreak(); + + let boundary = if is_mandatory_linebreak { + Boundary::Mandatory + } else { + boundary + }; + + let force_normalize = { + // "Extend" break chars should be normalized first, with two exceptions + if matches!(grapheme_cluster_break, GraphemeClusterBreak::Extend) && + ch as u32 != 0x200C && // Is not a Zero Width Non-Joiner && + !is_variation_selector + { + true + } else { + // All spacing mark break chars should be normalized first. + matches!(grapheme_cluster_break, GraphemeClusterBreak::SpacingMark) + } + }; + + needs_bidi_resolution |= crate::bidi::needs_bidi_resolution(bidi_class); + // TODO: maybe extend Properties to u64 to fit BidiMirroringGlyph + let bracket = lcx.analysis_data_sources.brackets().get(ch); + + lcx.info.push(( + CharInfo::new( + boundary, + script, + grapheme_cluster_break, + bidi_class, + bracket, + is_variation_selector, + is_region_indicator, + general_category == GeneralCategory::Control, + is_emoji_or_pictograph, + contributes_to_shaping(general_category, script), + force_normalize, + ), + 0, // Style index is populated later + )); + + next_mandatory_linebreak + }); + + if needs_bidi_resolution { + lcx.bidi.resolve( + text.chars().zip( + lcx.info + .iter() + .map(|info| (info.0.bidi_class, info.0.bracket)), + ), + None, + ); + } +} + +/// All characters contribute to shaping except: +/// - Control characters +/// - Format characters, unless they use the "Inherited" script +#[inline(always)] +pub(crate) fn contributes_to_shaping(general_category: GeneralCategory, script: Script) -> bool { + if matches!( + general_category, + GeneralCategory::Control + | GeneralCategory::LineSeparator + | GeneralCategory::ParagraphSeparator + ) { + return false; + } + + !(general_category == GeneralCategory::Format && script != Script::Inherited) +} diff --git a/vendor/parley-0.11.1/src/bidi.rs b/vendor/parley-0.11.1/src/bidi.rs new file mode 100644 index 0000000000..1229a32c6a --- /dev/null +++ b/vendor/parley-0.11.1/src/bidi.rs @@ -0,0 +1,894 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Unicode bidirectional algorithm. + +use alloc::vec::Vec; +use icu_properties::props::{BidiClass, BidiMirroringGlyph, BidiPairedBracketType}; + +/// Type alias for a bidirectional level. +pub(crate) type BidiLevel = u8; + +/// Resolver for the Unicode bidirectional algorithm. +#[derive(Clone, Default)] +pub(crate) struct BidiResolver { + base_level: BidiLevel, + levels: Vec, + initial_types: Vec, + types: Vec, + brackets: Vec<(usize, char, BidiMirroringGlyph)>, + bracket_pairs: Vec<(usize, usize)>, + runs: Vec, + indices: Vec, + flags: u16, +} + +impl BidiResolver { + /// Creates a new resolver. + pub(crate) fn new() -> Self { + Self { + base_level: 0, + levels: Vec::new(), + initial_types: Vec::new(), + types: Vec::new(), + brackets: Vec::new(), + bracket_pairs: Vec::new(), + runs: Vec::new(), + indices: Vec::new(), + flags: 0, + } + } + + /// Returns the base level of the text. + pub(crate) fn base_level(&self) -> u8 { + self.base_level + } + + /// Returns the sequence of bidi levels corresponding to all characters in the + /// paragraph. + pub(crate) fn levels(&self) -> &[BidiLevel] { + &self.levels + } + + /// Clears the resolver state. + pub(crate) fn clear(&mut self) { + self.initial_types.clear(); + self.levels.clear(); + self.types.clear(); + self.brackets.clear(); + self.bracket_pairs.clear(); + self.flags = 0; + self.base_level = 0; + } + + /// Resolves a paragraph with the specified base direction and + /// precomputed types. + pub(crate) fn resolve( + &mut self, + chars: impl Iterator, + base_level: Option, + ) { + self.clear(); + let mut needs_bidi = false; + let mut len = 0; + for (i, (ch, (t, bracket))) in chars.enumerate() { + self.initial_types.push(t); + + if bracket.paired_bracket_type != BidiPairedBracketType::None { + self.brackets.push((i, ch, bracket)); + } + + needs_bidi = needs_bidi || mask(t) & BIDI_MASK != 0; + len += 1; + } + self.base_level = match base_level { + Some(level) => level & 1, + _ => Self::default_level(&self.initial_types), + }; + if !needs_bidi && self.base_level == 0 { + self.flags |= 1; + self.levels.resize(len, self.base_level); + return; + } + self.types.extend_from_slice(&self.initial_types); + self.resolve_levels(); + self.resolve_runs(); + //self.dump_sequences(); + for i in 0..self.runs.len() { + if self.runs[i].in_sequence { + continue; + } + self.types.truncate(len); + self.indices.clear(); + let mut cur = i; + let level = self.runs[i].level; + let sos = self.runs[i].sos; + let mut eos; + loop { + let run = &self.runs[cur]; + for i in run.start..run.end { + let ty = self.types[i]; + if !is_removed_by_x9(ty) { + self.types.push(ty); + self.indices.push(i); + } + } + eos = run.eos; + cur = match run.next { + Some(i) => i, + None => break, + }; + } + self.resolve_sequence(level, sos, eos, self.indices.len()); + } + for i in 0..len { + let t = self.initial_types[i]; + if t == BidiClass::SegmentSeparator || t == BidiClass::ParagraphSeparator { + self.levels[i] = self.base_level; + for j in (0..i).rev() { + let t = self.initial_types[j]; + if is_removed_by_x9(t) { + continue; + } else if t == BidiClass::WhiteSpace + || is_isolate_initiator(t) + || t == BidiClass::PopDirectionalIsolate + { + self.levels[j] = self.base_level; + } else { + break; + } + } + } else if is_removed_by_x9(t) { + if i == 0 { + self.levels[i] = self.base_level; + } else { + self.levels[i] = self.levels[i - 1]; + } + //self.levels[i] = 0xFF; + } + } + for i in (0..len).rev() { + let t = self.initial_types[i]; + if is_removed_by_x9(t) { + continue; + } else if t == BidiClass::WhiteSpace + || is_isolate_initiator(t) + || t == BidiClass::PopDirectionalIsolate + { + //self.levels[i] = self.base_level; + } else { + break; + } + } + } + + fn default_level(types: &[BidiClass]) -> u8 { + let mut isolates = 0; + for ty in types { + let ty = *ty; + match ty { + BidiClass::RightToLeftIsolate + | BidiClass::LeftToRightIsolate + | BidiClass::FirstStrongIsolate => isolates += 1, + BidiClass::PopDirectionalIsolate if isolates > 0 => isolates -= 1, + BidiClass::LeftToRight | BidiClass::RightToLeft | BidiClass::ArabicLetter + if isolates == 0 => + { + return if ty == BidiClass::LeftToRight { 0 } else { 1 }; + } + _ => {} + } + } + 0 + } + + fn default_level_until_pdi(types: &[BidiClass]) -> u8 { + let mut isolates = 0; + for ty in types { + let ty = *ty; + match ty { + BidiClass::RightToLeftIsolate + | BidiClass::LeftToRightIsolate + | BidiClass::FirstStrongIsolate => isolates += 1, + BidiClass::PopDirectionalIsolate => { + if isolates > 0 { + isolates -= 1; + } else { + return 0; + } + } + BidiClass::LeftToRight | BidiClass::RightToLeft | BidiClass::ArabicLetter + if isolates == 0 => + { + return if ty == BidiClass::LeftToRight { 0 } else { 1 }; + } + _ => {} + } + } + 0 + } + + fn resolve_levels(&mut self) { + let base = self.base_level; + let len = self.types.len(); + self.levels.clear(); + self.levels.resize(len, 0); + let mut stack = Stack::new(); + let mut overflow_isolates = 0; + let mut overflow_embedding = 0; + let mut valid_isolates = 0; + stack.push(base, BidiClass::OtherNeutral, false); + for i in 0..len { + let t = self.types[i]; + let tmask = mask(t); + if tmask & EXPLICIT_MASK != 0 { + let is_isolate = tmask & ISOLATE_MASK != 0; + let is_rtl = if t == BidiClass::FirstStrongIsolate && i + 1 < len { + Self::default_level_until_pdi(&self.types[i + 1..]) == 1 + } else { + tmask & RTL_MASK != 0 + }; + if is_isolate { + self.levels[i] = stack.embedding_level(); + let os = stack.override_status(); + if os != BidiClass::OtherNeutral { + self.types[i] = os; + } + } + let new_level = if is_rtl { + (stack.embedding_level() + 1) | 1 + } else { + (stack.embedding_level() + 2) & !1 + }; + if new_level <= MAX_STACK as u8 && overflow_isolates == 0 && overflow_embedding == 0 + { + if is_isolate { + valid_isolates += 1; + } + stack.push( + new_level, + if t == BidiClass::LeftToRightOverride { + BidiClass::LeftToRight + } else if t == BidiClass::RightToLeftOverride { + BidiClass::RightToLeft + } else { + BidiClass::OtherNeutral + }, + is_isolate, + ); + } else if is_isolate { + overflow_isolates += 1; + } else if overflow_isolates == 0 { + overflow_embedding += 1; + } + } else if t == BidiClass::PopDirectionalIsolate { + if overflow_isolates > 0 { + overflow_isolates -= 1; + } else if valid_isolates == 0 { + // empty + } else { + overflow_embedding = 0; + while !stack.isolate_status() { + stack.pop(); + } + stack.pop(); + valid_isolates -= 1; + } + self.levels[i] = stack.embedding_level(); + if stack.override_status() != BidiClass::OtherNeutral { + self.types[i] = stack.override_status(); + } + } else if t == BidiClass::PopDirectionalFormat { + self.levels[i] = stack.embedding_level(); + if overflow_isolates > 0 { + // empty + } else if overflow_embedding > 0 { + overflow_embedding -= 1; + } else if !stack.isolate_status() && stack.depth >= 2 { + stack.pop(); + } + } else if t == BidiClass::ParagraphSeparator { + stack.depth = 1; + overflow_isolates = 0; + overflow_embedding = 0; + valid_isolates = 0; + self.levels[i] = base; + } else if t != BidiClass::BoundaryNeutral { + self.levels[i] = stack.embedding_level(); + if stack.override_status() != BidiClass::OtherNeutral { + self.types[i] = stack.override_status(); + } + } + } + } + + fn resolve_runs(&mut self) { + let len = self.types.len(); + self.runs.clear(); + let mut start = 0; + while start < len { + if !is_removed_by_x9(self.types[start]) { + break; + } + start += 1; + } + if start == len { + return; + } + let mut level = self.levels[start]; + let mut offset = 0; + for i in start + 1..len { + if is_removed_by_x9(self.types[i]) { + continue; + } + if self.levels[i] != level { + self.runs.push(Run::new(level, offset, i)); + offset = i; + level = self.levels[i]; + } + } + if offset < len { + self.runs.push(Run::new(level, offset, len)); + } + for run in &mut self.runs { + while run.start < run.end { + if is_removed_by_x9(self.types[run.start]) { + run.start += 1; + } else { + break; + } + } + while run.end > run.start { + if is_removed_by_x9(self.types[run.end - 1]) { + run.end -= 1; + } else { + break; + } + } + if run.start == run.end { + continue; + } + if self.types[run.start] == BidiClass::PopDirectionalIsolate { + run.starts_with_pdi = true; + } + let mut prev_level = self.base_level; + for i in (0..run.start).rev() { + if !is_removed_by_x9(self.types[i]) { + prev_level = self.levels[i]; + break; + } + } + run.sos = type_from_level(prev_level.max(run.level)); + if is_isolate_initiator(self.initial_types[run.end - 1]) { + run.ends_with_isolate = true; + run.eos = type_from_level(self.base_level.max(run.level)); + } else { + let mut next_level = self.base_level; + for i in run.end..len { + if !is_removed_by_x9(self.types[i]) { + next_level = self.levels[i]; + break; + } + } + run.eos = type_from_level(next_level.max(run.level)); + } + } + for i in 0..self.runs.len() { + if self.runs[i].ends_with_isolate { + let level = self.runs[i].level; + for j in i + 1..self.runs.len() { + if self.runs[j].starts_with_pdi && self.runs[j].level == level { + self.runs[i].next = Some(j); + self.runs[j].in_sequence = true; + break; + } + } + } + } + } + + #[allow(clippy::needless_range_loop)] + fn resolve_sequence(&mut self, level: u8, sos: BidiClass, eos: BidiClass, len: usize) { + if len == 0 { + return; + } + const W1_MASK: u32 = mask(BidiClass::LeftToRightIsolate) + | mask(BidiClass::RightToLeftIsolate) + | mask(BidiClass::FirstStrongIsolate) + | mask(BidiClass::PopDirectionalIsolate); + const W2_MASK: u32 = mask(BidiClass::LeftToRight) + | mask(BidiClass::RightToLeft) + | mask(BidiClass::ArabicLetter); + const W4_MASK: u32 = mask(BidiClass::EuropeanSeparator) | mask(BidiClass::CommonSeparator); + let mut prev = sos; + let mut prev_strong = prev; + let types = &mut self.types[self.initial_types.len()..]; + for i in 0..len { + let mut t = types[i]; + let tmask = mask(t); + if t == BidiClass::NonspacingMark { + // W1 + types[i] = prev; + } else { + if tmask & W1_MASK != 0 { + prev = BidiClass::OtherNeutral; + continue; + } + if t == BidiClass::EuropeanNumber { + // W2 + if prev_strong == BidiClass::ArabicLetter { + t = BidiClass::ArabicNumber; + types[i] = t; + } + } else if tmask & W2_MASK != 0 { + prev_strong = t; + // W3 + if t == BidiClass::ArabicLetter { + t = BidiClass::RightToLeft; + types[i] = t; + } + } else if tmask & W4_MASK != 0 && i < (len - 1) { + // W4 + let mut next = types[i + 1]; + if next == BidiClass::EuropeanNumber && prev_strong == BidiClass::ArabicLetter { + next = BidiClass::ArabicNumber; + } + if prev == BidiClass::EuropeanNumber && next == BidiClass::EuropeanNumber { + t = BidiClass::EuropeanNumber; + types[i] = t; + } else if t == BidiClass::CommonSeparator + && prev == BidiClass::ArabicNumber + && next == BidiClass::ArabicNumber + { + t = BidiClass::ArabicNumber; + types[i] = t; + } + } + prev = t; + } + } + // W5 + let mut i = 0; + while i < len { + if types[i] == BidiClass::EuropeanTerminator { + let limit = find_limit(types, i, BidiClass::EuropeanTerminator); + let mut t = if i == 0 { sos } else { types[i - 1] }; + if t != BidiClass::EuropeanNumber { + t = if limit == len { eos } else { types[limit] }; + } + if t == BidiClass::EuropeanNumber { + for j in i..limit { + types[j] = BidiClass::EuropeanNumber; + } + } + i = limit; + } + i += 1; + } + // W6, W7 + const W6_MASK: u32 = mask(BidiClass::EuropeanSeparator) + | mask(BidiClass::EuropeanTerminator) + | mask(BidiClass::CommonSeparator); + prev_strong = sos; + for i in 0..len { + let t = types[i]; + if mask(t) & W6_MASK != 0 { + // W6 + types[i] = BidiClass::OtherNeutral; + } else if t == BidiClass::EuropeanNumber { + // W7 + if prev_strong == BidiClass::LeftToRight { + types[i] = BidiClass::LeftToRight; + } + } else if t == BidiClass::LeftToRight || t == BidiClass::RightToLeft { + prev_strong = t; + } + } + // N0 + if !self.brackets.is_empty() { + let base_brackets = self.bracket_pairs.len(); + let mut bracket_stack = BracketStack::new(); + for i in 0..len { + if types[i] != BidiClass::OtherNeutral { + continue; + } + let index = self.indices[i]; + if let Ok(index) = self.brackets.binary_search_by(|x| x.0.cmp(&index)) { + let (_, ch, bracket) = self.brackets[index]; + match bracket.paired_bracket_type { + BidiPairedBracketType::Open => { + if bracket_stack.depth == MAX_BRACKET_STACK { + break; + } + bracket_stack.push(i, bracket.mirroring_glyph.unwrap()); + } + BidiPairedBracketType::Close => { + if let Some(open) = bracket_stack.find_and_pop(ch) { + self.bracket_pairs.push((open, i)); + } + } + _ => {} + } + } + } + if self.bracket_pairs.len() > base_brackets { + let embed_dir = if level & 1 != 0 { + BidiClass::RightToLeft + } else { + BidiClass::LeftToRight + }; + let bracket_pairs = &mut self.bracket_pairs[base_brackets..]; + bracket_pairs.sort_unstable_by_key(|a| a.0); + for pair in bracket_pairs { + let mut pair_dir = BidiClass::OtherNeutral; + for i in pair.0 + 1..pair.1 { + let dir = match types[i] { + BidiClass::EuropeanNumber + | BidiClass::ArabicNumber + | BidiClass::ArabicLetter + | BidiClass::RightToLeft => BidiClass::RightToLeft, + BidiClass::LeftToRight => BidiClass::LeftToRight, + _ => BidiClass::OtherNeutral, + }; + if dir == BidiClass::OtherNeutral { + continue; + } + pair_dir = dir; + if dir == embed_dir { + break; + } + } + if pair_dir == BidiClass::OtherNeutral { + pair.0 = self.indices[pair.0]; + pair.1 = self.indices[pair.1]; + continue; + } + if pair_dir != embed_dir { + pair_dir = sos; + for i in (0..pair.0).rev() { + let dir = match types[i] { + BidiClass::EuropeanNumber + | BidiClass::ArabicNumber + | BidiClass::ArabicLetter + | BidiClass::RightToLeft => BidiClass::RightToLeft, + BidiClass::LeftToRight => BidiClass::LeftToRight, + _ => BidiClass::OtherNeutral, + }; + if dir != BidiClass::OtherNeutral { + pair_dir = dir; + break; + } + } + if pair_dir == embed_dir || pair_dir == BidiClass::OtherNeutral { + pair_dir = embed_dir; + } + } + types[pair.0] = pair_dir; + types[pair.1] = pair_dir; + for i in pair.0 + 1..pair.1 { + let index = self.indices[i]; + if self.initial_types[index] == BidiClass::NonspacingMark { + types[i] = pair_dir; + } else { + break; + } + } + for i in pair.1 + 1..len { + let index = self.indices[i]; + if self.initial_types[index] == BidiClass::NonspacingMark { + types[i] = pair_dir; + } else { + break; + } + } + pair.0 = self.indices[pair.0]; + pair.1 = self.indices[pair.1]; + } + } + } + // N1, N2 + const N_MASK: u32 = mask(BidiClass::ParagraphSeparator) + | mask(BidiClass::SegmentSeparator) + | mask(BidiClass::WhiteSpace) + | mask(BidiClass::OtherNeutral) + | mask(BidiClass::RightToLeftIsolate) + | mask(BidiClass::LeftToRightIsolate) + | mask(BidiClass::FirstStrongIsolate) + | mask(BidiClass::PopDirectionalIsolate); + let mut i = 0; + while i < len { + let t = types[i]; + if mask(t) & N_MASK != 0 { + let offset = i; + let limit = find_limit_by_mask(types, offset, N_MASK); + let mut leading; + let mut trailing; + if offset == 0 { + leading = sos; + } else { + leading = types[offset - 1]; + if leading == BidiClass::ArabicNumber || leading == BidiClass::EuropeanNumber { + leading = BidiClass::RightToLeft; + } + } + if limit == len { + trailing = eos; + } else { + trailing = types[limit]; + if trailing == BidiClass::ArabicNumber || trailing == BidiClass::EuropeanNumber + { + trailing = BidiClass::RightToLeft; + } + } + let resolved = if leading == trailing { + // N1 + leading + } else { + // N2 + if level & 1 != 0 { + BidiClass::RightToLeft + } else { + BidiClass::LeftToRight + } + }; + for j in offset..limit { + types[j] = resolved; + } + i = limit - 1; + } + i += 1; + } + // Implicit levels + if level & 1 == 0 { + // I1 + for i in 0..len { + let index = self.indices[i]; + let t = types[i]; + if t == BidiClass::RightToLeft { + self.levels[index] = level + 1; + } else if t != BidiClass::LeftToRight { + self.levels[index] = level + 2; + } else { + self.levels[index] = level; + } + } + } else { + // I2 + for i in 0..len { + let index = self.indices[i]; + let t = types[i]; + if t != BidiClass::RightToLeft { + self.levels[index] = level + 1; + } else { + self.levels[index] = level; + } + } + } + } +} + +/// Returns a default bidi type for a level. +pub(crate) fn type_from_level(level: BidiLevel) -> BidiClass { + if level & 1 == 0 { + BidiClass::LeftToRight + } else { + BidiClass::RightToLeft + } +} + +/// Computes an ordering for a sequence of bidi runs based on levels. +pub(crate) fn _reorder(order: &mut [usize], levels: F) +where + F: Fn(usize) -> BidiLevel, +{ + let mut max_level = 0; + let mut lowest_odd_level = 255; + for (i, o) in order.iter_mut().enumerate() { + *o = i; + let level = levels(i); + if level > max_level { + max_level = level; + } + if level & 1 != 0 && level < lowest_odd_level { + lowest_odd_level = level; + } + } + let len = order.len(); + for level in (lowest_odd_level..=max_level).rev() { + let mut i = 0; + while i < len { + if levels(i) >= level { + let mut end = i + 1; + while end < len && levels(end) >= level { + end += 1; + } + let mut j = i; + let mut k = end - 1; + while j < k { + order.swap(j, k); + j += 1; + k -= 1; + } + i = end; + } + i += 1; + } + } +} + +/// Returns whether the character needs bidirectional resolution. +#[inline(always)] +pub(crate) fn needs_bidi_resolution(bidi_class: BidiClass) -> bool { + mask(bidi_class) & BIDI_MASK != 0 +} + +const OVERRIDE_MASK: u32 = mask(BidiClass::RightToLeftEmbedding) + | mask(BidiClass::LeftToRightEmbedding) + | mask(BidiClass::RightToLeftOverride) + | mask(BidiClass::LeftToRightOverride); +const ISOLATE_MASK: u32 = mask(BidiClass::RightToLeftIsolate) + | mask(BidiClass::LeftToRightIsolate) + | mask(BidiClass::FirstStrongIsolate); +const EXPLICIT_MASK: u32 = OVERRIDE_MASK | ISOLATE_MASK; +const RTL_MASK: u32 = mask(BidiClass::RightToLeftEmbedding) + | mask(BidiClass::RightToLeftOverride) + | mask(BidiClass::RightToLeftIsolate); +const REMOVED_BY_X9_MASK: u32 = + OVERRIDE_MASK | mask(BidiClass::PopDirectionalFormat) | mask(BidiClass::BoundaryNeutral); +const BIDI_MASK: u32 = EXPLICIT_MASK + | mask(BidiClass::RightToLeft) + | mask(BidiClass::ArabicLetter) + | mask(BidiClass::ArabicNumber); +const _RESET_MASK: u32 = + ISOLATE_MASK | mask(BidiClass::PopDirectionalIsolate) | mask(BidiClass::WhiteSpace); + +fn is_isolate_initiator(ty: BidiClass) -> bool { + mask(ty) & ISOLATE_MASK != 0 +} + +pub(crate) fn is_removed_by_x9(ty: BidiClass) -> bool { + mask(ty) & REMOVED_BY_X9_MASK != 0 +} + +pub(crate) fn _is_reset(ty: BidiClass) -> bool { + mask(ty) & _RESET_MASK != 0 +} + +fn find_limit(types: &[BidiClass], offset: usize, ty: BidiClass) -> usize { + let mut len = offset; + for &t in &types[offset..] { + if t != ty { + break; + } + len += 1; + } + len +} + +fn find_limit_by_mask(types: &[BidiClass], offset: usize, mask: u32) -> usize { + let mut len = offset; + for &t in &types[offset..] { + if self::mask(t) & mask == 0 { + break; + } + len += 1; + } + len +} + +#[derive(Clone)] +struct Run { + level: u8, + ends_with_isolate: bool, + starts_with_pdi: bool, + sos: BidiClass, + eos: BidiClass, + start: usize, + end: usize, + in_sequence: bool, + next: Option, +} + +impl Run { + fn new(level: u8, start: usize, end: usize) -> Self { + Self { + level, + ends_with_isolate: false, + starts_with_pdi: false, + sos: BidiClass::OtherNeutral, + eos: BidiClass::OtherNeutral, + start, + end, + in_sequence: false, + next: None, + } + } +} + +const MAX_STACK: usize = 125; + +struct Stack { + embedding_level: [u8; MAX_STACK + 1], + override_status: [BidiClass; MAX_STACK + 1], + isolate_status: [bool; MAX_STACK + 1], + depth: usize, +} + +impl Stack { + fn new() -> Self { + Self { + depth: 0, + embedding_level: [0; MAX_STACK + 1], + override_status: [BidiClass::OtherNeutral; MAX_STACK + 1], + isolate_status: [false; MAX_STACK + 1], + } + } + + fn push(&mut self, level: u8, override_status: BidiClass, isolate_status: bool) { + let d = self.depth; + self.embedding_level[d] = level; + self.override_status[d] = override_status; + self.isolate_status[d] = isolate_status; + self.depth += 1; + } + + fn pop(&mut self) { + if self.depth > 1 { + self.depth -= 1; + } + } + + fn embedding_level(&self) -> u8 { + self.embedding_level[self.depth - 1] + } + + fn override_status(&self) -> BidiClass { + self.override_status[self.depth - 1] + } + + fn isolate_status(&self) -> bool { + self.isolate_status[self.depth - 1] + } +} + +const MAX_BRACKET_STACK: usize = 63; + +struct BracketStack { + openers: [(usize, char); MAX_BRACKET_STACK], + depth: usize, +} + +impl BracketStack { + fn new() -> Self { + Self { + openers: [(0, '\0'); MAX_BRACKET_STACK], + depth: 0, + } + } + + fn push(&mut self, offset: usize, closer: char) { + self.openers[self.depth] = (offset, closer); + self.depth += 1; + } + + fn find_and_pop(&mut self, closer: char) -> Option { + if self.depth == 0 { + return None; + } + for i in (0..self.depth).rev() { + let c = self.openers[i].1; + if c == closer + || (c == '\u{232A}' && closer == '\u{3009}') + || (c == '\u{3009}' && closer == '\u{232A}') + { + self.depth = i; + return Some(self.openers[i].0); + } + } + None + } +} + +const fn mask(t: BidiClass) -> u32 { + 1 << (t.to_icu4c_value() as u32) +} diff --git a/vendor/parley-0.11.1/src/break_overrides.rs b/vendor/parley-0.11.1/src/break_overrides.rs new file mode 100644 index 0000000000..01577f4911 --- /dev/null +++ b/vendor/parley-0.11.1/src/break_overrides.rs @@ -0,0 +1,430 @@ +// Copyright 2026 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Overrides for line-break opportunities. + +use core::ops::RangeInclusive; + +/// Context for a potential line break opportunity between two adjacent code points. +#[derive(Clone, Copy, Debug)] +#[non_exhaustive] +pub struct LineBreakContext { + /// The code point before the `before` code point, if any. + pub before_before: Option, + /// The code point before the potential break. + pub before: char, + /// The code point after the potential break. + pub after: char, +} + +/// Line break opportunity override. +/// +/// Called for each adjacent pair of Unicode code points in the text, in order, +/// with a [`LineBreakContext`] describing the potential break. +/// +/// Returning: +/// - `Some(true)` : forces a line break opportunity between the pair +/// - `Some(false)` : suppresses any opportunity +/// - `None` : defers to the default (ICU) behavior +/// +/// Mandatory breaks are unaffected (e.g. `\n`). +/// +/// This is typically used to force preferential line breaking decisions when it +/// comes to ASCII punctuation like `/`, `-`, etc. For example, to prevent break +/// opportunities within "1/2". +/// +/// A separate use case is to match the line breaking behavior of existing systems +/// such as web browsers. See [`CHROMIUM_LINE_BREAK_OVERRIDE`] for a ready-made +/// override function that mirrors Chromium's behavior. +pub type LineBreakOverrideFn = dyn Fn(LineBreakContext) -> Option + Send + Sync; + +/// A line break override function mirroring Chromium's preferred line breaking behavior. +/// +/// See: +/// +/// # Differences from other browsers +/// +/// ## Compared to Safari +/// +/// Chromium (and this table) differs from Safari in 9 cases: +/// - A "-" followed by one of "!|$|)|/|:|;|?|]|}" is suppressed in Chromium, but broken in Safari. +/// +/// ## Compared to Firefox +/// +/// Firefox always defers to the default ICU behavior. +pub static CHROMIUM_LINE_BREAK_OVERRIDE: &LineBreakOverrideFn = + &(chromium_override as fn(LineBreakContext) -> Option); + +fn chromium_override(cx: LineBreakContext) -> Option { + let LineBreakContext { + before_before, + before, + after, + .. + } = cx; + // CSS "does not fully define where soft wrap opportunities occur". + // (https://www.w3.org/TR/css-text-3/#line-breaking) + // We find that Chrome always treats the position after a space sequence + // as a line break opportunity, despite this not matching UAX-14 + // (see LB13: https://www.unicode.org/reports/tr14/#LB13; we're not currently + // aware of any others). + // + // See also https://github.com/linebender/parley/pull/485, and https://github.com/linebender/parley/issues/619. + // + // See `LazyLineBreakIterator::NextBreakablePosition` + // + // + // Note that we'd need different handling in the `after == ' '` case if we ever get the equivalent of + // CSS's whitespace-collapse: break-spaces. + if before == ' ' && after != ' ' { + return Some(true); + } + // Before consulting 'before' / 'after' pair table, check for the special "-" case. + // + // Chromium doesn't allow breaking when it looks like the minus sign is for a negative + // number like "Subtract -5 from X". But, Chromium does allow breaking when the minus + // sign is part of a long URL like "AAAA-2222". + // + // See + if before == '-' && after.is_ascii_digit() { + return Some(before_before.is_some_and(|c| c.is_ascii_alphanumeric())); + } + CHROMIUM_LINE_BREAK_TABLE.lookup(before, after) +} + +/// A static table mirroring Chromium's preferred line breaking behavior for `before` / `after` +/// printable ASCII code points. +static CHROMIUM_LINE_BREAK_TABLE: AsciiLineBreakTable<5> = + AsciiLineBreakTableBuilder::chromium().build::<5>(); + +/// A line break override table for ASCII character pairs. +/// +/// See [`LineBreakOverrideFn`] for more details. +/// +/// All table operations are `const`, which means that derived tables don't +/// need a runtime construction step. +/// +/// The table is row-deduplicated. Each `before` character maps to +/// one of `N` distinct rows, so tables can be kept tiny. +/// +/// # Example +/// +/// ``` +/// # use parley::{CHROMIUM_LINE_BREAK_OVERRIDE, FontContext, LayoutContext}; +/// # let mut font_cx = FontContext::default(); +/// # let mut layout_cx: LayoutContext<[u8; 4]> = LayoutContext::new(); +/// let text = "Hello there!"; +/// let mut builder = layout_cx.ranged_builder(&mut font_cx, text, 1.0, true); +/// // Emulate Chromium: +/// builder.set_line_break_override(Some(CHROMIUM_LINE_BREAK_OVERRIDE)); +/// let layout = builder.build(text); +/// # let _ = layout; +/// ``` +#[derive(Clone)] +pub struct AsciiLineBreakTable { + /// Maps each `before` character (`0..128`) to a row in `rows`. + row_of: [u8; 128], + /// The distinct rows. + rows: [Row; N], +} + +impl AsciiLineBreakTable { + /// Look up the break override for a `(before, after)` pair. + /// + /// Return semantics copied from [`LineBreakOverrideFn`]. + pub const fn lookup(&self, before: char, after: char) -> Option { + let (b, a) = (before as u32, after as u32); + if b >= 128 || a >= 128 { + return None; + } + let row = &self.rows[self.row_of[b as usize] as usize]; + if row.overridden & (1_u128 << a) == 0 { + return None; + } + Some(row.allow & (1_u128 << a) != 0) + } +} + +/// A single deduplicated row of a [`AsciiLineBreakTable`]. +#[derive(Clone, Copy)] +struct Row { + /// Bits set means the pair has an override. + overridden: u128, + /// Bits set means a break is allowed. + allow: u128, +} + +/// A builder for an [`AsciiLineBreakTable`]. +/// +/// Construction is `const`. We create a dense `128 x 128` grid of overrides, +/// then compress it into a row-deduplicated table via [`AsciiLineBreakTableBuilder::build`]. +#[derive(Clone)] +pub struct AsciiLineBreakTableBuilder { + /// Bit `after` set in row `before` means the pair has an explicit override. + overridden: [u128; 128], + /// Bit `after` set in row `before` means a break is allowed for that pair. + allow: [u128; 128], +} + +impl AsciiLineBreakTableBuilder { + /// A builder that defers every pair to the default ICU behavior. + pub const fn new() -> Self { + Self { + overridden: [0; 128], + allow: [0; 128], + } + } + + /// Override every pair in the cartesian product of the two inclusive ASCII + /// ranges. + /// + /// # Panics + /// + /// All range bounds must be printable ASCII (`<= '\u{7f}'`). + pub const fn with_pairs( + mut self, + before: RangeInclusive, + after: RangeInclusive, + allow_break: bool, + ) -> Self { + assert!( + *before.end() as u32 <= 0x7f && *after.end() as u32 <= 0x7f, + "only printable ASCII is supported", + ); + let mut before_pos = *before.start() as u32; + while before_pos <= *before.end() as u32 { + let mut after_pos = *after.start() as u32; + while after_pos <= *after.end() as u32 { + let bit = 1_u128 << after_pos; + self.overridden[before_pos as usize] |= bit; + if allow_break { + self.allow[before_pos as usize] |= bit; + } else { + self.allow[before_pos as usize] &= !bit; + } + after_pos += 1; + } + before_pos += 1; + } + self + } + + /// Compress the dense grid into a row-deduplicated [`AsciiLineBreakTable`] + /// with at most `N` distinct rows. + /// + /// # Panics + /// + /// Panics if `N` does not exactly match the number of distinct rows required. + pub const fn build(&self) -> AsciiLineBreakTable { + // Row 0 is reserved as the "defer everything" row. + let mut rows = [Row { + overridden: 0, + allow: 0, + }; N]; + let mut len = 1_usize; + let mut row_of = [0_u8; 128]; + + let mut b = 0; + while b < 128 { + let (ov, al) = (self.overridden[b], self.allow[b]); + let mut idx = 0; + let mut found = usize::MAX; + while idx < len { + if rows[idx].overridden == ov && rows[idx].allow == al { + found = idx; + break; + } + idx += 1; + } + let r = if found != usize::MAX { + found + } else { + assert!( + len < N, + "N is too small to contain table. Repeat with a larger N." + ); + rows[len] = Row { + overridden: ov, + allow: al, + }; + len += 1; + len - 1 + }; + row_of[b] = r as u8; + b += 1; + } + + assert!( + len == N, + "N is larger than required. Repeat with a smaller N." + ); + + AsciiLineBreakTable { row_of, rows } + } + + /// See [`CHROMIUM_LINE_BREAK_TABLE`] for more details. + const fn chromium() -> Self { + // The printable ASCII range `'!'..=0x7F`. + const ALL: RangeInclusive = '!'..='\u{7f}'; + + Self::new() + .with_pairs(ALL, ALL, false) + .with_pairs(ALL, '('..='(', true) + .with_pairs(ALL, '<'..='<', true) + .with_pairs(ALL, '['..='[', true) + .with_pairs(ALL, '{'..='{', true) + .with_pairs('-'..='-', ALL, true) + .with_pairs('?'..='?', ALL, true) + .with_pairs('-'..='-', '$'..='$', false) + .with_pairs(ALL, '!'..='!', false) + .with_pairs('?'..='?', '"'..='"', false) + .with_pairs('?'..='?', '\''..='\'', false) + .with_pairs(ALL, ')'..=')', false) + .with_pairs(ALL, ','..=',', false) + .with_pairs(ALL, '.'..='.', false) + .with_pairs(ALL, '/'..='/', false) + .with_pairs('-'..='-', '0'..='9', false) + .with_pairs(ALL, ':'..=':', false) + .with_pairs(ALL, ';'..=';', false) + .with_pairs(ALL, '?'..='?', false) + .with_pairs(ALL, ']'..=']', false) + .with_pairs(ALL, '}'..='}', false) + .with_pairs('$'..='$', ALL, false) + .with_pairs('\''..='\'', ALL, false) + .with_pairs('('..='(', ALL, false) + .with_pairs('/'..='/', ALL, false) + .with_pairs('0'..='9', ALL, false) + .with_pairs('<'..='<', ALL, false) + .with_pairs('@'..='@', ALL, false) + .with_pairs('A'..='Z', ALL, false) + .with_pairs('['..='[', ALL, false) + .with_pairs('^'..='`', ALL, false) + .with_pairs('a'..='z', ALL, false) + .with_pairs('{'..='{', ALL, false) + .with_pairs('\u{7f}'..='\u{7f}', ALL, false) + } +} + +impl Default for AsciiLineBreakTableBuilder { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::AsciiLineBreakTableBuilder; + use super::CHROMIUM_LINE_BREAK_TABLE; + use super::LineBreakContext; + use super::chromium_override; + + fn cx(before_before: Option, before: char, after: char) -> LineBreakContext { + LineBreakContext { + before_before, + before, + after, + } + } + + #[test] + fn chromium_hyphen_digit_depends_on_preceding_char() { + // A break between '-' and a digit is allowed only when the character + // preceding the '-' is ASCII alphanumeric. + assert_eq!(chromium_override(cx(Some('D'), '-', '1')), Some(true)); + assert_eq!(chromium_override(cx(Some('4'), '-', '5')), Some(true)); + // Otherwise the '-' may be a minus sign, so the break is suppressed. + assert_eq!(chromium_override(cx(Some(' '), '-', '1')), Some(false)); + assert_eq!(chromium_override(cx(Some('('), '-', '1')), Some(false)); + // No preceding character (start of text) behaves like a non-alphanumeric + // context, matching Chromium's `last_last_ch == 0`. + assert_eq!(chromium_override(cx(None, '-', '1')), Some(false)); + } + + #[test] + fn chromium_ignores_uax_14_lb13() { + // Blink allows a break after a space run unconditionally. + // See comment in non-test code for more details. + for after in ['}', ')', ']', '!', '.', ',', '/', ':', ';', '?', 'b', '('] { + assert_eq!( + chromium_override(cx(None, ' ', after)), + Some(true), + "expected a break after the space, before {after:?}" + ); + } + } + + #[test] + fn chromium_hyphen_non_digit_defers_to_table() { + // '-' followed by a non-digit ignores `before_before` and uses the table. + assert_eq!(chromium_override(cx(Some('D'), '-', 'b')), Some(true)); + assert_eq!(chromium_override(cx(None, '-', 'b')), Some(true)); + // Non-ASCII after '-' defers to ICU. + assert_eq!(chromium_override(cx(Some('D'), '-', 'é')), None); + } + + #[test] + fn chromium_suppresses_ascii_punctuation_breaks() { + let t = &CHROMIUM_LINE_BREAK_TABLE; + // No break before or after a slash. + assert_eq!(t.lookup('a', '/'), Some(false)); + assert_eq!(t.lookup('/', 'b'), Some(false)); + // No break around other punctuation. + assert_eq!(t.lookup('a', '.'), Some(false)); + assert_eq!(t.lookup('a', ':'), Some(false)); + // No break between letters. + assert_eq!(t.lookup('a', 'b'), Some(false)); + } + + #[test] + fn chromium_allows_some_breaks() { + let t = &CHROMIUM_LINE_BREAK_TABLE; + // Break allowed before opening punctuation, but only when the preceding + // character's row was not later suppressed. + assert_eq!(t.lookup(')', '('), Some(true)); + assert_eq!(t.lookup(')', '<'), Some(true)); + assert_eq!(t.lookup('a', '('), Some(false)); + // Break allowed after '-' and '?' except when... + assert_eq!(t.lookup('-', 'b'), Some(true)); + assert_eq!(t.lookup('?', 'b'), Some(true)); + // ...break not allowed after '-' before a digit, or '?' before a quote. + assert_eq!(t.lookup('-', '5'), Some(false)); + assert_eq!(t.lookup('?', '"'), Some(false)); + } + + #[test] + fn non_ascii_pairs_defer_to_icu() { + let t = &CHROMIUM_LINE_BREAK_TABLE; + assert_eq!(t.lookup('a', 'é'), None); + assert_eq!(t.lookup('é', 'a'), None); + // Space (0x20) is below the printable range, so it defers too. + assert_eq!(t.lookup('a', ' '), None); + } + + #[test] + fn empty_table_defers_to_icu() { + let t = AsciiLineBreakTableBuilder::new().build::<1>(); + assert_eq!(t.lookup('a', '/'), None); + assert_eq!(t.lookup('a', '('), None); + } + + #[test] + fn dedup_matches_dense_for_chromium() { + let builder = AsciiLineBreakTableBuilder::chromium(); + for b in 0..128_u32 { + for a in 0..128_u32 { + let bit = 1_u128 << a; + let dense = if builder.overridden[b as usize] & bit == 0 { + None + } else { + Some(builder.allow[b as usize] & bit != 0) + }; + let (before, after) = (char::from_u32(b).unwrap(), char::from_u32(a).unwrap()); + assert_eq!( + CHROMIUM_LINE_BREAK_TABLE.lookup(before, after), + dense, + "mismatch at (before={b:#04x}, after={a:#04x})", + ); + } + } + } +} diff --git a/vendor/parley-0.11.1/src/builder.rs b/vendor/parley-0.11.1/src/builder.rs new file mode 100644 index 0000000000..8169197059 --- /dev/null +++ b/vendor/parley-0.11.1/src/builder.rs @@ -0,0 +1,366 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Context for layout. + +use super::FontContext; +use super::context::LayoutContext; +use super::style::{Brush, StyleProperty, TextStyle, WhiteSpaceCollapse}; + +use super::layout::Layout; + +use alloc::string::String; +use core::ops::{Bound, Range, RangeBounds}; + +use crate::InlineBoxKind; +use crate::break_overrides::LineBreakOverrideFn; +use crate::inline_box::InlineBox; +use crate::resolve::{ResolvedStyle, StyleRun, tree::ItemKind}; + +/// Builder for constructing a text layout with ranged attributes. +#[must_use] +pub struct RangedBuilder<'a, B: Brush> { + pub(crate) scale: f32, + pub(crate) quantize: bool, + pub(crate) lcx: &'a mut LayoutContext, + pub(crate) fcx: &'a mut FontContext, + pub(crate) line_break_override: Option<&'a LineBreakOverrideFn>, +} + +impl<'b, B: Brush> RangedBuilder<'b, B> { + pub fn push_default<'a>(&mut self, property: impl Into>) { + let resolved = self + .lcx + .rcx + .resolve_property(self.fcx, &property.into(), self.scale); + self.lcx.ranged_style_builder.push_default(resolved); + } + + pub fn push<'a>( + &mut self, + property: impl Into>, + range: impl RangeBounds, + ) { + let resolved = self + .lcx + .rcx + .resolve_property(self.fcx, &property.into(), self.scale); + self.lcx.ranged_style_builder.push(resolved, range); + } + + pub fn push_inline_box(&mut self, inline_box: InlineBox) { + self.lcx.inline_boxes.push(inline_box); + } + + /// Set the callback which will be called as a first provider of line breaking decisions. + /// + /// See [`LineBreakOverrideFn`] for more details. + pub fn set_line_break_override(&mut self, overrides: Option<&'b LineBreakOverrideFn>) { + self.line_break_override = overrides; + } + + pub fn build_into(self, layout: &mut Layout, text: impl AsRef) { + // Apply RangedStyleBuilder styles directly to style-table/style-run state. + self.lcx + .ranged_style_builder + .finish(&mut self.lcx.style_table, &mut self.lcx.style_runs); + + // Call generic layout builder method + build_into_layout( + layout, + self.scale, + self.quantize, + text.as_ref(), + self.lcx, + self.fcx, + self.line_break_override, + ); + } + + pub fn build(self, text: impl AsRef) -> Layout { + let mut layout = Layout::default(); + self.build_into(&mut layout, text); + layout + } +} + +/// Builder for constructing a text layout from a style table and +/// indexed style runs. +#[must_use] +pub struct StyleRunBuilder<'a, B: Brush> { + pub(crate) scale: f32, + pub(crate) quantize: bool, + pub(crate) len: usize, + pub(crate) lcx: &'a mut LayoutContext, + pub(crate) fcx: &'a mut FontContext, + pub(crate) cursor: usize, + pub(crate) line_break_override: Option<&'a LineBreakOverrideFn>, +} + +impl<'b, B: Brush> StyleRunBuilder<'b, B> { + /// Reserves additional capacity for styles and runs. + /// + /// This is an optional optimization for callers that know counts + /// up front; call it before pushing styles and runs to reduce + /// reallocations. + pub fn reserve(&mut self, additional_styles: usize, additional_runs: usize) { + self.lcx.style_table.reserve(additional_styles); + self.lcx.style_runs.reserve(additional_runs); + } + + /// Adds a fully-specified style to the shared style table and + /// returns its index. + pub fn push_style<'family, 'settings>( + &mut self, + style: TextStyle<'family, 'settings, B>, + ) -> u16 { + let resolved = self + .lcx + .rcx + .resolve_entire_style_set(self.fcx, &style, self.scale); + let style_index = self.lcx.style_table.len(); + assert!(style_index <= u16::MAX as usize, "too many styles"); + self.lcx.style_table.push(resolved); + style_index as u16 + } + + /// Adds a style run referencing an entry from the style table. + /// + /// Runs must be contiguous and non-overlapping, and must cover + /// `0..text.len()` once all runs have been added. + pub fn push_style_run(&mut self, style_index: u16, range: impl RangeBounds) { + let range = resolve_range(range, self.len); + assert!( + range.start == self.cursor, + "StyleRunBuilder expects contiguous non-overlapping runs" + ); + assert!( + range.start <= range.end, + "StyleRunBuilder expects ordered ranges" + ); + assert!( + (style_index as usize) < self.lcx.style_table.len(), + "StyleRunBuilder expects style indices that were previously added via push_style" + ); + self.lcx.style_runs.push(StyleRun { + style_index, + range: range.clone(), + }); + self.cursor = range.end; + } + + pub fn push_inline_box(&mut self, inline_box: InlineBox) { + self.lcx.inline_boxes.push(inline_box); + } + + /// Set the callback which will be called as a first provider of line breaking decisions. + /// + /// See [`LineBreakOverrideFn`] for more details. + pub fn set_line_break_override(&mut self, overrides: Option<&'b LineBreakOverrideFn>) { + self.line_break_override = overrides; + } + + pub fn build_into(self, layout: &mut Layout, text: impl AsRef) { + assert!( + self.cursor == self.len, + "StyleRunBuilder requires runs that cover the full text" + ); + build_into_layout( + layout, + self.scale, + self.quantize, + text.as_ref(), + self.lcx, + self.fcx, + self.line_break_override, + ); + } + + pub fn build(self, text: impl AsRef) -> Layout { + let mut layout = Layout::default(); + self.build_into(&mut layout, text); + layout + } +} + +/// Builder for constructing a text layout with a tree of attributes. +#[must_use] +pub struct TreeBuilder<'a, B: Brush> { + pub(crate) scale: f32, + pub(crate) quantize: bool, + pub(crate) lcx: &'a mut LayoutContext, + pub(crate) fcx: &'a mut FontContext, + pub(crate) line_break_override: Option<&'a LineBreakOverrideFn>, +} + +impl<'b, B: Brush> TreeBuilder<'b, B> { + pub fn push_style_span(&mut self, style: TextStyle<'_, '_, B>) { + let resolved = self + .lcx + .rcx + .resolve_entire_style_set(self.fcx, &style, self.scale); + self.lcx.tree_style_builder.push_style_span(resolved); + } + + pub fn push_style_modification_span<'s, 'iter>( + &mut self, + properties: impl IntoIterator>, + ) where + 's: 'iter, + B: 'iter, + { + self.lcx.tree_style_builder.push_style_modification_span( + properties + .into_iter() + .map(|p| self.lcx.rcx.resolve_property(self.fcx, p, self.scale)), + ); + } + + pub fn pop_style_span(&mut self) { + self.lcx.tree_style_builder.pop_style_span(); + } + + pub fn push_text(&mut self, text: &str) { + self.lcx.tree_style_builder.push_text(text); + } + + pub fn push_inline_box(&mut self, mut inline_box: InlineBox) { + if inline_box.kind == InlineBoxKind::InFlow { + self.lcx.tree_style_builder.push_uncommitted_text(false); + self.lcx.tree_style_builder.set_is_span_first(false); + self.lcx + .tree_style_builder + .set_last_item_kind(ItemKind::InlineBox); + } + + // TODO: arrange type better here to factor out the index + inline_box.index = self.lcx.tree_style_builder.current_text_len(); + self.lcx.inline_boxes.push(inline_box); + } + + pub fn set_white_space_mode(&mut self, white_space_collapse: WhiteSpaceCollapse) { + self.lcx + .tree_style_builder + .set_white_space_mode(white_space_collapse); + } + + /// Set the callback which will be called as a first provider of line breaking decisions. + /// + /// See [`LineBreakOverrideFn`] for more details. + pub fn set_line_break_override(&mut self, overrides: Option<&'b LineBreakOverrideFn>) { + self.line_break_override = overrides; + } + + #[inline] + pub fn build_into(self, layout: &mut Layout) -> String { + // Apply TreeStyleBuilder styles to LayoutContext. + let text = self + .lcx + .tree_style_builder + .finish(&mut self.lcx.style_table, &mut self.lcx.style_runs); + + // Call generic layout builder method + build_into_layout( + layout, + self.scale, + self.quantize, + &text, + self.lcx, + self.fcx, + self.line_break_override, + ); + + text + } + + #[inline] + pub fn build(self) -> (Layout, String) { + let mut layout = Layout::default(); + let text = self.build_into(&mut layout); + (layout, text) + } +} + +fn build_into_layout( + layout: &mut Layout, + scale: f32, + quantize: bool, + text: &str, + lcx: &mut LayoutContext, + fcx: &mut FontContext, + line_break_override: Option<&LineBreakOverrideFn>, +) { + if text.is_empty() && lcx.style_runs.is_empty() { + lcx.style_table.push(ResolvedStyle::default()); + lcx.style_runs.push(StyleRun { + style_index: 0, + range: 0..0, + }); + } + assert!( + !lcx.style_runs.is_empty(), + "at least one style run is required" + ); + + crate::analysis::analyze_text(lcx, text, line_break_override); + + layout.data.clear(); + layout.data.scale = scale; + layout.data.quantize = quantize; + layout.data.base_level = lcx.bidi.base_level(); + layout.data.text_len = text.len(); + + let mut char_index = 0; + for style_run in &lcx.style_runs { + for _ in text[style_run.range.clone()].chars() { + lcx.info[char_index].1 = style_run.style_index; + char_index += 1; + } + } + + // Copy the visual styles into the layout + layout + .data + .styles + .extend(lcx.style_table.iter().map(|s| s.as_layout_style())); + + // Sort the inline boxes as subsequent code assumes that they are in text index order. + // Note: It's important that this is a stable sort to allow users to control the order of contiguous inline boxes + lcx.inline_boxes.sort_by_key(|b| b.index); + + { + let query = fcx.collection.query(&mut fcx.source_cache); + super::shape::shape_text( + &lcx.rcx, + query, + &lcx.style_table, + &lcx.inline_boxes, + &lcx.info, + lcx.bidi.levels(), + &mut lcx.scx, + text, + layout, + &lcx.analysis_data_sources, + ); + } + + // Move inline boxes into the layout + layout.data.inline_boxes.clear(); + core::mem::swap(&mut layout.data.inline_boxes, &mut lcx.inline_boxes); + + layout.data.finish(); +} + +fn resolve_range(range: impl RangeBounds, len: usize) -> Range { + let start = match range.start_bound() { + Bound::Unbounded => 0, + Bound::Included(n) => *n, + Bound::Excluded(n) => *n + 1, + }; + let end = match range.end_bound() { + Bound::Unbounded => len, + Bound::Included(n) => *n + 1, + Bound::Excluded(n) => *n, + }; + start.min(len)..end.min(len) +} diff --git a/vendor/parley-0.11.1/src/context.rs b/vendor/parley-0.11.1/src/context.rs new file mode 100644 index 0000000000..277e2d285e --- /dev/null +++ b/vendor/parley-0.11.1/src/context.rs @@ -0,0 +1,204 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Context for layout. + +use alloc::{vec, vec::Vec}; + +use super::FontContext; +use super::builder::{RangedBuilder, StyleRunBuilder}; +use super::resolve::tree::TreeStyleBuilder; +use super::resolve::{RangedStyleBuilder, ResolveContext, ResolvedStyle, StyleRun}; +use super::style::{Brush, TextStyle}; + +use crate::analysis::{AnalysisDataSources, CharInfo}; +use crate::bidi::BidiResolver; +use crate::builder::TreeBuilder; +use crate::inline_box::InlineBox; +use crate::shape::ShapeContext; + +/// Shared scratch space used when constructing text layouts. +/// +/// This type is designed to be a global resource with only one per-application (or per-thread). +pub struct LayoutContext { + pub(crate) rcx: ResolveContext, + pub(crate) style_table: Vec>, + pub(crate) style_runs: Vec, + pub(crate) inline_boxes: Vec, + pub(crate) bidi: BidiResolver, + + // Reusable style builders (to amortise allocations) + pub(crate) ranged_style_builder: RangedStyleBuilder, + pub(crate) tree_style_builder: TreeStyleBuilder, + + // u16: style index for character + pub(crate) info: Vec<(CharInfo, u16)>, + pub(crate) scx: ShapeContext, + + // Unicode analysis data sources (provided by icu) + pub(crate) analysis_data_sources: AnalysisDataSources, +} + +impl LayoutContext { + pub fn new() -> Self { + Self { + rcx: ResolveContext::default(), + style_table: vec![], + style_runs: vec![], + inline_boxes: vec![], + bidi: BidiResolver::new(), + ranged_style_builder: RangedStyleBuilder::default(), + tree_style_builder: TreeStyleBuilder::default(), + info: vec![], + analysis_data_sources: AnalysisDataSources::new(), + scx: ShapeContext::default(), + } + } + + fn resolve_style_set( + &mut self, + font_ctx: &mut FontContext, + scale: f32, + raw_style: &TextStyle<'_, '_, B>, + ) -> ResolvedStyle { + self.rcx + .resolve_entire_style_set(font_ctx, raw_style, scale) + } + + /// Create a ranged style layout builder. + /// + /// Set `quantize` as `true` to have the layout coordinates aligned to pixel boundaries. + /// That is the easiest way to avoid blurry text and to receive ready-to-paint layout metrics. + /// + /// For advanced rendering use cases you can set `quantize` as `false` and receive + /// fractional coordinates. This ensures the most accurate results if you want to perform + /// some post-processing on the coordinates before painting. To avoid blurry text you will + /// still need to quantize the coordinates just before painting. + /// + /// Your should round at least the following: + /// * Glyph run baseline + /// * Inline box baseline + /// - `box.y = (box.y + box.height).round() - box.height` + /// * Selection geometry's `y0` & `y1` + /// * Cursor geometry's `y0` & `y1` + /// + /// Keep in mind that for the simple `f32::round` to be effective, + /// you need to first ensure the coordinates are in physical pixel space. + pub fn ranged_builder<'a>( + &'a mut self, + fcx: &'a mut FontContext, + text: &'a str, + scale: f32, + quantize: bool, + ) -> RangedBuilder<'a, B> { + self.begin(); + + let resolved_root_style = self.resolve_style_set(fcx, scale, &TextStyle::default()); + self.ranged_style_builder + .begin(resolved_root_style, text.len()); + + fcx.source_cache.prune(128, false); + + RangedBuilder { + scale, + quantize, + lcx: self, + fcx, + line_break_override: None, + } + } + + /// Create a builder for constructing a layout from indexed style runs. + /// + /// Unlike [`Self::ranged_builder`], this builder expects callers to provide: + /// - a style table of fully specified [`TextStyle`] values (via [`StyleRunBuilder::push_style`]) + /// - a complete sequence of **contiguous**, **non-overlapping** spans that cover + /// `0..text.len()` and reference style indices (via [`StyleRunBuilder::push_style_run`]) + /// + /// Parley then skips its internal range-splitting logic. + pub fn style_run_builder<'a>( + &'a mut self, + fcx: &'a mut FontContext, + text: &'a str, + scale: f32, + quantize: bool, + ) -> StyleRunBuilder<'a, B> { + self.begin(); + + fcx.source_cache.prune(128, false); + + StyleRunBuilder { + scale, + quantize, + len: text.len(), + lcx: self, + fcx, + cursor: 0, + line_break_override: None, + } + } + + /// Create a tree style layout builder. + /// + /// Set `quantize` as `true` to have the layout coordinates aligned to pixel boundaries. + /// That is the easiest way to avoid blurry text and to receive ready-to-paint layout metrics. + /// + /// For advanced rendering use cases you can set `quantize` as `false` and receive + /// fractional coordinates. This ensures the most accurate results if you want to perform + /// some post-processing on the coordinates before painting. To avoid blurry text you will + /// still need to quantize the coordinates just before painting. + /// + /// Your should round at least the following: + /// * Glyph run baseline + /// * Inline box baseline + /// - `box.y = (box.y + box.height).round() - box.height` + /// * Selection geometry's `y0` & `y1` + /// * Cursor geometry's `y0` & `y1` + /// + /// Keep in mind that for the simple `f32::round` to be effective, + /// you need to first ensure the coordinates are in physical pixel space. + pub fn tree_builder<'a>( + &'a mut self, + fcx: &'a mut FontContext, + scale: f32, + quantize: bool, + root_style: &TextStyle<'_, '_, B>, + ) -> TreeBuilder<'a, B> { + self.begin(); + + let resolved_root_style = self.resolve_style_set(fcx, scale, root_style); + self.tree_style_builder.begin(resolved_root_style); + + fcx.source_cache.prune(128, false); + + TreeBuilder { + scale, + quantize, + lcx: self, + fcx, + line_break_override: None, + } + } + + fn begin(&mut self) { + self.rcx.clear(); + self.style_table.clear(); + self.style_runs.clear(); + self.inline_boxes.clear(); + self.info.clear(); + self.bidi.clear(); + } +} + +impl Default for LayoutContext { + fn default() -> Self { + Self::new() + } +} + +impl Clone for LayoutContext { + fn clone(&self) -> Self { + // None of the internal state is visible so just return a new instance. + Self::new() + } +} diff --git a/vendor/parley-0.11.1/src/convert.rs b/vendor/parley-0.11.1/src/convert.rs new file mode 100644 index 0000000000..da0aaf55b3 --- /dev/null +++ b/vendor/parley-0.11.1/src/convert.rs @@ -0,0 +1,23 @@ +// Copyright 2024 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use crate::analysis::AnalysisDataSources; + +use icu_properties::props::Script; + +pub(crate) fn script_to_fontique( + script: Script, + analysis_data_sources: &AnalysisDataSources, +) -> fontique::Script { + analysis_data_sources + .script_short_name() + .get(script) + .unwrap_or("Zzzz") + .parse() + .unwrap_or(fontique::Script::UNKNOWN) +} + +pub(crate) fn script_to_harfrust(script: fontique::Script) -> harfrust::Script { + harfrust::Script::from_iso15924_tag(harfrust::Tag::new(&script.to_bytes())) + .unwrap_or(harfrust::script::UNKNOWN) +} diff --git a/vendor/parley-0.11.1/src/editing/cursor.rs b/vendor/parley-0.11.1/src/editing/cursor.rs new file mode 100644 index 0000000000..7a91caee74 --- /dev/null +++ b/vendor/parley-0.11.1/src/editing/cursor.rs @@ -0,0 +1,466 @@ +// Copyright 2025 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use crate::BoundingBox; +#[cfg(feature = "accesskit")] +use crate::analysis::cluster::Whitespace; +use crate::layout::{Affinity, BreakReason, Cluster, ClusterSide, Layout, Line}; +#[cfg(feature = "accesskit")] +use crate::layout::{ClusterPath, LayoutAccessibility}; +use crate::style::Brush; +#[cfg(feature = "accesskit")] +use accesskit::TextPosition; + +/// Defines a position with a text layout. +#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)] +pub struct Cursor { + pub(crate) index: usize, + pub(crate) affinity: Affinity, +} + +impl Cursor { + /// Creates a new cursor from the given byte index and affinity. + pub fn from_byte_index(layout: &Layout, index: usize, affinity: Affinity) -> Self { + if let Some(cluster) = Cluster::from_byte_index(layout, index) { + let index = cluster.text_range().start; + Self { + index, + affinity: if index != 0 { + affinity + } else { + // There is no Upstream cluster of the 0 position so we force Downstream affinity. + Affinity::Downstream + }, + } + } else { + Self { + index: layout.data.text_len, + affinity: Affinity::Upstream, + } + } + } + + /// Creates a new cursor from the given coordinates. + pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { + let (index, affinity) = if let Some((cluster, side)) = Cluster::from_point(layout, x, y) { + let is_leading = side == ClusterSide::Left; + if cluster.is_rtl() { + if is_leading { + (cluster.text_range().end, Affinity::Upstream) + } else { + (cluster.text_range().start, Affinity::Downstream) + } + } else { + // We never want to position the cursor _after_ a hard + // line since that cursor appears visually at the start + // of the next line + if is_leading || cluster.is_line_break() == Some(BreakReason::Explicit) { + (cluster.text_range().start, Affinity::Downstream) + } else { + (cluster.text_range().end, Affinity::Upstream) + } + } + } else { + (layout.data.text_len, Affinity::Downstream) + }; + Self { index, affinity } + } + + #[cfg(feature = "accesskit")] + pub fn from_access_position( + pos: &TextPosition, + layout: &Layout, + layout_access: &LayoutAccessibility, + ) -> Option { + let span_path = layout_access.span_paths_by_access_id.get(&pos.node)?; + let run = span_path.run(layout)?; + let index = run + .get(span_path.logical_index() + pos.character_index) + .map(|cluster| cluster.text_range().start) + .unwrap_or(layout.data.text_len); + Some(Self::from_byte_index(layout, index, Affinity::Downstream)) + } + + pub(crate) fn from_cluster( + layout: &Layout, + cluster: Cluster<'_, B>, + moving_right: bool, + ) -> Self { + Self::from_byte_index( + layout, + cluster.text_range().start, + affinity_for_dir(cluster.is_rtl(), moving_right), + ) + } + + /// Returns the logical text index of the cursor. + pub fn index(&self) -> usize { + self.index + } + + /// Returns the affinity of the cursor. + /// + /// This defines the direction from which the cursor entered its current + /// position and affects the visual location of the rendered cursor. + pub fn affinity(&self) -> Affinity { + self.affinity + } + + /// Returns a new cursor that is guaranteed to be within the bounds of the + /// given layout. + #[must_use] + pub fn refresh(&self, layout: &Layout) -> Self { + Self::from_byte_index(layout, self.index, self.affinity) + } + + /// Returns a new cursor that is positioned at the previous cluster boundary + /// in visual order. + #[must_use] + pub fn previous_visual(&self, layout: &Layout) -> Self { + let [left, right] = self.visual_clusters(layout); + if let (Some(left), Some(right)) = (&left, &right) { + if left.is_soft_line_break() { + if left.is_rtl() && self.affinity == Affinity::Upstream { + let index = if right.is_rtl() { + left.text_range().start + } else { + left.text_range().end + }; + return Self::from_byte_index(layout, index, Affinity::Downstream); + } else if !left.is_rtl() && self.affinity == Affinity::Downstream { + let index = if right.is_rtl() { + right.text_range().end + } else { + right.text_range().start + }; + return Self::from_byte_index(layout, index, Affinity::Upstream); + } + } + } + if let Some(left) = left { + let index = if left.is_rtl() { + left.text_range().end + } else { + left.text_range().start + }; + return Self::from_byte_index(layout, index, affinity_for_dir(left.is_rtl(), false)); + } + *self + } + + /// Returns a new cursor that is positioned at the next cluster boundary + /// in visual order. + #[must_use] + pub fn next_visual(&self, layout: &Layout) -> Self { + let [left, right] = self.visual_clusters(layout); + if let (Some(left), Some(right)) = (&left, &right) { + if left.is_soft_line_break() { + if left.is_rtl() && self.affinity == Affinity::Downstream { + let index = if right.is_rtl() { + right.text_range().end + } else { + right.text_range().start + }; + return Self::from_byte_index(layout, index, Affinity::Upstream); + } else if !left.is_rtl() && self.affinity == Affinity::Upstream { + let index = if right.is_rtl() { + right.text_range().end + } else { + right.text_range().start + }; + return Self::from_byte_index(layout, index, Affinity::Downstream); + } + } + let index = if right.is_rtl() { + right.text_range().start + } else { + right.text_range().end + }; + return Self::from_byte_index(layout, index, affinity_for_dir(right.is_rtl(), true)); + } + if let Some(right) = right { + let index = if right.is_rtl() { + right.text_range().start + } else { + right.text_range().end + }; + return Self::from_byte_index(layout, index, affinity_for_dir(right.is_rtl(), true)); + } + *self + } + + /// Returns a new cursor that is positioned at the next word boundary + /// in visual order. + #[must_use] + pub fn next_visual_word(&self, layout: &Layout) -> Self { + let mut cur = *self; + loop { + let next = cur.next_visual(layout); + if next == cur { + break; + } + cur = next; + let [Some(left), Some(right)] = cur.visual_clusters(layout) else { + break; + }; + if left.is_rtl() { + if left.is_word_boundary() && !left.is_space_or_nbsp() { + break; + } + } else if right.is_word_boundary() && !left.is_space_or_nbsp() { + break; + } + } + cur + } + + /// Returns a new cursor that is positioned at the previous word boundary + /// in visual order. + #[must_use] + pub fn previous_visual_word(&self, layout: &Layout) -> Self { + let mut cur = *self; + loop { + let next = cur.previous_visual(layout); + if next == cur { + break; + } + cur = next; + let [Some(left), Some(right)] = cur.visual_clusters(layout) else { + break; + }; + if left.is_rtl() { + if left.is_word_boundary() + && (left.is_space_or_nbsp() + || (right.is_word_boundary() && !right.is_space_or_nbsp())) + { + break; + } + } else if right.is_word_boundary() && !right.is_space_or_nbsp() { + break; + } + } + cur + } + + /// Returns a new cursor that is positioned at the next word boundary + /// in logical order. + #[must_use] + pub fn next_logical_word(&self, layout: &Layout) -> Self { + let [left, right] = self.logical_clusters(layout); + if let Some(cluster) = right.or(left) { + let start = cluster.clone(); + let cluster = cluster.next_logical_word().unwrap_or(cluster); + if cluster.path == start.path { + return Self::from_byte_index(layout, usize::MAX, Affinity::Downstream); + } + return Self::from_cluster(layout, cluster, true); + } + *self + } + + /// Returns a new cursor that is positioned at the previous word boundary + /// in logical order. + #[must_use] + pub fn previous_logical_word(&self, layout: &Layout) -> Self { + let [left, right] = self.logical_clusters(layout); + if let Some(cluster) = left.or(right) { + let cluster = cluster.previous_logical_word().unwrap_or(cluster); + return Self::from_cluster(layout, cluster, true); + } + *self + } + + /// Returns a rectangle that represents the visual geometry of the cursor + /// in layout space. + /// + /// The `width` parameter defines the width of the resulting rectangle. + pub fn geometry(&self, layout: &Layout, width: f32) -> BoundingBox { + match self.visual_clusters(layout) { + [Some(left), Some(right)] => { + if left.is_end_of_line() { + if left.is_soft_line_break() { + let (cluster, at_end) = if left.is_rtl() + && self.affinity == Affinity::Downstream + || !left.is_rtl() && self.affinity == Affinity::Upstream + { + (left, true) + } else { + (right, false) + }; + cursor_rect(&cluster, at_end, width) + } else { + cursor_rect(&right, false, width) + } + } else { + cursor_rect(&left, true, width) + } + } + [Some(left), None] if left.is_hard_line_break() => last_line_cursor_rect(layout, width), + [Some(left), _] => cursor_rect(&left, true, width), + [_, Some(right)] => cursor_rect(&right, false, width), + _ => last_line_cursor_rect(layout, width), + } + } + + /// Returns the pair of clusters that logically bound the cursor + /// position. + /// + /// The order in the array is upstream followed by downstream. + pub fn logical_clusters<'a, B: Brush>( + &self, + layout: &'a Layout, + ) -> [Option>; 2] { + let upstream = self + .index + .checked_sub(1) + .and_then(|index| Cluster::from_byte_index(layout, index)); + let downstream = Cluster::from_byte_index(layout, self.index); + [upstream, downstream] + } + + /// Returns the pair of clusters that visually bound the cursor + /// position. + /// + /// The order in the array is left followed by right. + pub fn visual_clusters<'a, B: Brush>( + &self, + layout: &'a Layout, + ) -> [Option>; 2] { + if self.affinity == Affinity::Upstream { + if let Some(cluster) = self.upstream_cluster(layout) { + if cluster.is_rtl() { + [cluster.previous_visual(), Some(cluster)] + } else { + [Some(cluster.clone()), cluster.next_visual()] + } + } else if let Some(cluster) = self.downstream_cluster(layout) { + if cluster.is_rtl() { + [None, Some(cluster)] + } else { + [Some(cluster), None] + } + } else { + [None, None] + } + } else if let Some(cluster) = self.downstream_cluster(layout) { + if cluster.is_rtl() { + [Some(cluster.clone()), cluster.next_visual()] + } else { + [cluster.previous_visual(), Some(cluster)] + } + } else if let Some(cluster) = self.upstream_cluster(layout) { + if cluster.is_rtl() { + [None, Some(cluster)] + } else { + [Some(cluster), None] + } + } else { + [None, None] + } + } + + pub(crate) fn line(self, layout: &Layout) -> Option<(usize, Line<'_, B>)> { + let geometry = self.geometry(layout, 0.0); + layout.line_for_offset(geometry.y0 as f32) + } + + pub(crate) fn upstream_cluster(self, layout: &Layout) -> Option> { + self.index + .checked_sub(1) + .and_then(|index| Cluster::from_byte_index(layout, index)) + } + + pub(crate) fn downstream_cluster(self, layout: &Layout) -> Option> { + Cluster::from_byte_index(layout, self.index) + } + + #[cfg(feature = "accesskit")] + pub fn to_access_position( + &self, + layout: &Layout, + layout_access: &LayoutAccessibility, + ) -> Option { + if layout.data.text_len == 0 { + // If the text is empty, just return the first node with a + // character index of 0. + return Some(TextPosition { + node: *layout_access + .access_ids_by_span_path + .get(&ClusterPath::new(0, 0, 0))?, + character_index: 0, + }); + } + // Prefer the downstream cluster except at the end of the text + // where we'll choose the upstream cluster and add 1 to the + // character index. + let (offset, path) = self + .downstream_cluster(layout) + .map(|cluster| (0, cluster.path)) + .or_else(|| { + self.upstream_cluster(layout) + .map(|cluster| (1, cluster.path)) + })?; + // If we're at the end of the layout and the layout ends with a newline + // then make sure we use the "phantom" run at the end so that + // AccessKit has correct visual geometry for the cursor. + let (span_path, character_index) = if self.index == layout.data.text_len + && layout + .data + .clusters + .last() + .map(|cluster| cluster.info.whitespace() == Whitespace::Newline) + .unwrap_or_default() + { + (ClusterPath::new(path.line_index + 1, 0, 0), 0) + } else { + let span_path = layout_access.span_paths_by_cluster_path.get(&path).unwrap(); + ( + *span_path, + path.logical_index() - span_path.logical_index() + offset, + ) + }; + let id = layout_access.access_ids_by_span_path.get(&span_path)?; + Some(TextPosition { + node: *id, + character_index, + }) + } +} + +// --- + +fn affinity_for_dir(is_rtl: bool, moving_right: bool) -> Affinity { + match (is_rtl, moving_right) { + (true, true) | (false, false) => Affinity::Downstream, + _ => Affinity::Upstream, + } +} + +fn cursor_rect(cluster: &Cluster<'_, B>, at_end: bool, size: f32) -> BoundingBox { + let mut line_x = cluster.visual_offset().unwrap_or_default(); + if at_end { + line_x += cluster.advance(); + } + let line = cluster.line(); + let metrics = line.metrics(); + BoundingBox::new( + line_x as f64, + metrics.block_min_coord as f64, + (line_x + size) as f64, + metrics.block_max_coord as f64, + ) +} + +fn last_line_cursor_rect(layout: &Layout, size: f32) -> BoundingBox { + if let Some(line) = layout.get(layout.len().saturating_sub(1)) { + let metrics = line.metrics(); + BoundingBox::new( + metrics.offset as f64, + metrics.block_min_coord as f64, + (metrics.offset + size) as f64, + metrics.block_max_coord as f64, + ) + } else { + BoundingBox::default() + } +} diff --git a/vendor/parley-0.11.1/src/editing/editor.rs b/vendor/parley-0.11.1/src/editing/editor.rs new file mode 100644 index 0000000000..9582ef628a --- /dev/null +++ b/vendor/parley-0.11.1/src/editing/editor.rs @@ -0,0 +1,1286 @@ +// Copyright 2024 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! A simple plain text editor and related types. + +use alloc::{borrow::ToOwned, string::String, vec::Vec}; +use core::{ + cmp::PartialEq, + default::Default, + fmt::{Debug, Display}, + num::NonZeroUsize, + ops::Range, +}; + +use crate::editing::{Cursor, Selection}; +use crate::layout::{Affinity, Alignment, AlignmentOptions, Layout}; +use crate::style::Brush; +use crate::{BoundingBox, FontContext, LayoutContext, StyleProperty, StyleSet}; + +#[cfg(feature = "accesskit")] +use crate::layout::LayoutAccessibility; +#[cfg(feature = "accesskit")] +use accesskit::{Node, NodeId, TreeUpdate}; + +/// Opaque representation of a generation. +/// +/// Obtained from [`PlainEditor::generation`]. +// Overflow handling: the generations are only compared, +// so wrapping is fine. This could only fail if exactly +// `u32::MAX` generations happen between drawing +// operations. This is implausible and so can be ignored. +#[derive(PartialEq, Eq, Default, Clone, Copy, Debug)] +pub struct Generation(u32); + +impl Generation { + /// Make it not what it currently is. + pub(crate) fn nudge(&mut self) { + self.0 = self.0.wrapping_add(1); + } +} + +/// A string which is potentially discontiguous in memory. +/// +/// This is returned by [`PlainEditor::text`], as the IME preedit +/// area needs to be efficiently excluded from its return value. +#[derive(Debug, Clone, Copy)] +pub struct SplitString<'source>([&'source str; 2]); + +impl<'source> SplitString<'source> { + /// Get the characters of this string. + pub fn chars(self) -> impl Iterator + 'source { + self.into_iter().flat_map(str::chars) + } +} + +impl PartialEq<&'_ str> for SplitString<'_> { + fn eq(&self, other: &&'_ str) -> bool { + let [a, b] = self.0; + let mid = a.len(); + match other.split_at_checked(mid) { + Some((a_1, b_1)) => a_1 == a && b_1 == b, + None => false, + } + } +} +// We intentionally choose not to: +// impl PartialEq for SplitString<'_> {} +// for simplicity, as the impl wouldn't be useful and is non-trivial + +impl Display for SplitString<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let [a, b] = self.0; + write!(f, "{a}{b}") + } +} + +/// Iterate through the source strings. +impl<'source> IntoIterator for SplitString<'source> { + type Item = &'source str; + type IntoIter = <[&'source str; 2] as IntoIterator>::IntoIter; + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +/// Basic plain text editor with a single style applied to the entire text. +/// +/// Internally, this is a wrapper around a string buffer and its corresponding [`Layout`], +/// which is kept up-to-date as needed. +/// This layout is invalidated by a number. +#[derive(Clone, Debug)] +pub struct PlainEditor +where + T: Brush + Clone + Debug + PartialEq + Default, +{ + layout: Layout, + buffer: String, + default_style: StyleSet, + #[cfg(feature = "accesskit")] + layout_access: LayoutAccessibility, + selection: Selection, + /// Byte offsets of IME composing preedit text in the text buffer. + /// `None` if the IME is not currently composing. + compose: Option>, + /// Whether the cursor should be shown. The IME can request to hide the cursor. + show_cursor: bool, + width: Option, + font_size: f32, + scale: f32, + quantize: bool, + // Simple tracking of when the layout needs to be updated + // before it can be used for `Selection` calculations or + // for drawing. + // Not all operations on `PlainEditor` need to operate on a + // clean layout, and not all operations trigger a layout. + layout_dirty: bool, + // TODO: We could avoid redoing the full text layout if only + // linebreaking or alignment were changed. + // linebreak_dirty: bool, + // alignment_dirty: bool, + alignment: Alignment, + generation: Generation, +} + +impl PlainEditor +where + T: Brush, +{ + /// Create a new editor, with default font size `font_size`. + pub fn new(font_size: f32) -> Self { + Self { + default_style: StyleSet::new(font_size), + buffer: String::default(), + layout: Layout::default(), + #[cfg(feature = "accesskit")] + layout_access: LayoutAccessibility::default(), + selection: Selection::default(), + compose: None, + show_cursor: true, + width: None, + font_size, + scale: 1.0, + quantize: true, + layout_dirty: true, + alignment: Alignment::Start, + // We don't use the `default` value to start with, as our consumers + // will choose to use that as their initial value, but will probably need + // to redraw if they haven't already. + generation: Generation(1), + } + } +} + +/// A short-lived wrapper around [`PlainEditor`]. +/// +/// This can perform operations which require the editor's layout to +/// be up-to-date by refreshing it as necessary. +pub struct PlainEditorDriver<'a, T> +where + T: Brush + Clone + Debug + PartialEq + Default, +{ + pub editor: &'a mut PlainEditor, + pub font_cx: &'a mut FontContext, + pub layout_cx: &'a mut LayoutContext, +} + +impl PlainEditorDriver<'_, T> +where + T: Brush + Clone + Debug + PartialEq + Default, +{ + // --- MARK: Forced relayout --- + /// Insert at cursor, or replace selection. + pub fn insert_or_replace_selection(&mut self, s: &str) { + self.editor + .replace_selection(self.font_cx, self.layout_cx, s); + } + + /// Delete the selection. + pub fn delete_selection(&mut self) { + self.insert_or_replace_selection(""); + } + + /// Delete the specified numbers of bytes before the selection. + /// The selection is moved to the left by that number of bytes + /// but otherwise unchanged. + /// + /// The deleted range is clamped to the start of the buffer. + /// No-op if the start of the range is not a char boundary. + pub fn delete_bytes_before_selection(&mut self, len: NonZeroUsize) { + let old_selection = self.editor.selection; + let selection_range = old_selection.text_range(); + let range = selection_range.start.saturating_sub(len.get())..selection_range.start; + if range.is_empty() || !self.editor.buffer.is_char_boundary(range.start) { + return; + } + self.editor.buffer.replace_range(range.clone(), ""); + self.editor + .update_compose_for_replaced_range(range.clone(), 0); + self.update_layout(); + let old_anchor = old_selection.anchor(); + let old_focus = old_selection.focus(); + // When doing the equivalent of a backspace on a collapsed selection, + // always use downstream affinity, as `backdelete` does. + let (anchor_affinity, focus_affinity) = if old_selection.is_collapsed() { + (Affinity::Downstream, Affinity::Downstream) + } else { + (old_anchor.affinity(), old_focus.affinity()) + }; + self.editor.set_selection(Selection::new( + Cursor::from_byte_index( + &self.editor.layout, + old_anchor.index() - range.len(), + anchor_affinity, + ), + Cursor::from_byte_index( + &self.editor.layout, + old_focus.index() - range.len(), + focus_affinity, + ), + )); + } + + /// Delete the specified numbers of bytes after the selection. + /// The selection is unchanged. + /// + /// The deleted range is clamped to the end of the buffer. + /// No-op if the end of the range is not a char boundary. + pub fn delete_bytes_after_selection(&mut self, len: NonZeroUsize) { + let selection_range = self.editor.selection.text_range(); + let range = selection_range.end + ..selection_range + .end + .saturating_add(len.get()) + .min(self.editor.buffer.len()); + if range.is_empty() || !self.editor.buffer.is_char_boundary(range.end) { + return; + } + self.editor.buffer.replace_range(range.clone(), ""); + self.editor.update_compose_for_replaced_range(range, 0); + self.update_layout(); + } + + /// Delete the selection or the next cluster (typical ‘delete’ behavior). + pub fn delete(&mut self) { + if self.editor.selection.is_collapsed() { + // Upstream cluster range + if let Some(range) = self + .editor + .selection + .focus() + .logical_clusters(&self.editor.layout)[1] + .as_ref() + .map(|cluster| cluster.text_range()) + .and_then(|range| (!range.is_empty()).then_some(range)) + { + self.editor.buffer.replace_range(range.clone(), ""); + self.editor.update_compose_for_replaced_range(range, 0); + self.update_layout(); + } + } else { + self.delete_selection(); + } + } + + /// Delete the selection or up to the next word boundary (typical ‘ctrl + delete’ behavior). + pub fn delete_word(&mut self) { + if self.editor.selection.is_collapsed() { + let focus = self.editor.selection.focus(); + let start = focus.index(); + let end = focus.next_logical_word(&self.editor.layout).index(); + if self.editor.buffer.get(start..end).is_some() { + self.editor.buffer.replace_range(start..end, ""); + self.editor.update_compose_for_replaced_range(start..end, 0); + self.update_layout(); + self.editor.set_selection( + Cursor::from_byte_index(&self.editor.layout, start, Affinity::Downstream) + .into(), + ); + } + } else { + self.delete_selection(); + } + } + + /// Delete the selection or the previous cluster (typical ‘backspace’ behavior). + pub fn backdelete(&mut self) { + if self.editor.selection.is_collapsed() { + // Upstream cluster + if let Some(cluster) = self + .editor + .selection + .focus() + .logical_clusters(&self.editor.layout)[0] + .clone() + { + let range = cluster.text_range(); + let end = range.end; + let start = if cluster.is_hard_line_break() || cluster.is_emoji() { + // For newline sequences and emoji, delete the previous cluster + range.start + } else { + // Otherwise, delete the previous character + let Some((start, _)) = self + .editor + .buffer + .get(..end) + .and_then(|str| str.char_indices().next_back()) + else { + return; + }; + start + }; + self.editor.buffer.replace_range(start..end, ""); + self.editor.update_compose_for_replaced_range(start..end, 0); + self.update_layout(); + self.editor.set_selection( + Cursor::from_byte_index(&self.editor.layout, start, Affinity::Downstream) + .into(), + ); + } + } else { + self.delete_selection(); + } + } + + /// Delete the selection or back to the previous word boundary (typical ‘ctrl + backspace’ behavior). + pub fn backdelete_word(&mut self) { + if self.editor.selection.is_collapsed() { + let focus = self.editor.selection.focus(); + let end = focus.index(); + let start = focus.previous_logical_word(&self.editor.layout).index(); + if self.editor.buffer.get(start..end).is_some() { + self.editor.buffer.replace_range(start..end, ""); + self.editor.update_compose_for_replaced_range(start..end, 0); + self.update_layout(); + self.editor.set_selection( + Cursor::from_byte_index(&self.editor.layout, start, Affinity::Downstream) + .into(), + ); + } + } else { + self.delete_selection(); + } + } + + // --- MARK: IME --- + /// Set the IME preedit composing text. + /// + /// This starts composing. Composing is reset by calling [`clear_compose`](Self::clear_compose). + /// Alternatively, the preedit text can be committed by calling [`finish_compose`](Self::finish_compose). + /// + /// The selection and preedit region can be manipulated independently while composing + /// is active. + /// + /// The preedit text replaces the current selection if this call starts composing. + /// + /// The selection is updated based on `cursor`, which contains the byte offsets relative to the + /// start of the preedit text. If `cursor` is `None`, the selection and caret are hidden. + pub fn set_compose(&mut self, text: &str, cursor: Option<(usize, usize)>) { + debug_assert!(!text.is_empty()); + debug_assert!(cursor.map(|cursor| cursor.1 <= text.len()).unwrap_or(true)); + + let start = if let Some(preedit_range) = &self.editor.compose { + self.editor + .buffer + .replace_range(preedit_range.clone(), text); + preedit_range.start + } else { + if self.editor.selection.is_collapsed() { + self.editor + .buffer + .insert_str(self.editor.selection.text_range().start, text); + } else { + self.editor + .buffer + .replace_range(self.editor.selection.text_range(), text); + } + self.editor.selection.text_range().start + }; + self.editor.compose = Some(start..start + text.len()); + self.editor.show_cursor = cursor.is_some(); + self.update_layout(); + + // Select the location indicated by the IME. If `cursor` is none, collapse the selection to + // a caret at the start of the preedit text. As `self.editor.show_cursor` is `false`, it + // won't show up. + let cursor = cursor.unwrap_or((0, 0)); + self.editor.set_selection(Selection::new( + self.editor.cursor_at(start + cursor.0), + self.editor.cursor_at(start + cursor.1), + )); + } + + /// Set the preedit range to a range of byte indices. + /// This leaves the selection and cursor unchanged. + /// + /// No-op if either index is not a char boundary. + pub fn set_compose_byte_range(&mut self, start: usize, end: usize) { + if self.editor.buffer.is_char_boundary(start) && self.editor.buffer.is_char_boundary(end) { + self.editor.compose = Some(start..end); + self.update_layout(); + } + } + + /// Stop IME composing. + /// + /// This removes the IME preedit text, shows the cursor if it was hidden, + /// and moves the cursor to the start of the former preedit region. + pub fn clear_compose(&mut self) { + if let Some(preedit_range) = self.editor.compose.take() { + self.editor.buffer.replace_range(preedit_range.clone(), ""); + self.editor.show_cursor = true; + self.update_layout(); + + self.editor + .set_selection(self.editor.cursor_at(preedit_range.start).into()); + } + } + + /// Commit the IME preedit text, if any. + /// + /// This doesn't change the selection, but shows the cursor if + /// it was hidden. + pub fn finish_compose(&mut self) { + if self.editor.compose.take().is_some() { + self.editor.show_cursor = true; + self.update_layout(); + } + } + + // --- MARK: Cursor Movement --- + /// Move the cursor to the cluster boundary nearest this point in the layout. + pub fn move_to_point(&mut self, x: f32, y: f32) { + self.refresh_layout(); + self.editor + .set_selection(Selection::from_point(&self.editor.layout, x, y)); + } + + /// Move the cursor to a byte index. + /// + /// No-op if index is not a char boundary. + pub fn move_to_byte(&mut self, index: usize) { + if self.editor.buffer.is_char_boundary(index) { + self.refresh_layout(); + self.editor + .set_selection(self.editor.cursor_at(index).into()); + } + } + + /// Move the cursor to the start of the buffer. + pub fn move_to_text_start(&mut self) { + self.refresh_layout(); + self.editor.set_selection(self.editor.selection.move_lines( + &self.editor.layout, + isize::MIN, + false, + )); + } + + /// Move the cursor to just after the previous hard line break (such as `\n`). + pub fn move_to_hard_line_start(&mut self) { + self.refresh_layout(); + self.editor.set_selection( + self.editor + .selection + .hard_line_start(&self.editor.layout, false), + ); + } + + /// Move the cursor to the start of the physical line. + pub fn move_to_line_start(&mut self) { + self.refresh_layout(); + self.editor + .set_selection(self.editor.selection.line_start(&self.editor.layout, false)); + } + + /// Move the cursor to the end of the buffer. + pub fn move_to_text_end(&mut self) { + self.refresh_layout(); + self.editor.set_selection(self.editor.selection.move_lines( + &self.editor.layout, + isize::MAX, + false, + )); + } + + /// Move the cursor to just before the next hard line break (such as `\n`). + pub fn move_to_hard_line_end(&mut self) { + self.refresh_layout(); + self.editor.set_selection( + self.editor + .selection + .hard_line_end(&self.editor.layout, false), + ); + } + + /// Move the cursor to the end of the physical line. + pub fn move_to_line_end(&mut self) { + self.refresh_layout(); + self.editor + .set_selection(self.editor.selection.line_end(&self.editor.layout, false)); + } + + /// Move up to the closest physical cluster boundary on the previous line, preserving the horizontal position for repeated movements. + pub fn move_up(&mut self) { + self.refresh_layout(); + self.editor.set_selection( + self.editor + .selection + .previous_line(&self.editor.layout, false), + ); + } + + /// Move down to the closest physical cluster boundary on the next line, preserving the horizontal position for repeated movements. + pub fn move_down(&mut self) { + self.refresh_layout(); + self.editor + .set_selection(self.editor.selection.next_line(&self.editor.layout, false)); + } + + /// Move to the next cluster left in visual order. + pub fn move_left(&mut self) { + self.refresh_layout(); + self.editor.set_selection( + self.editor + .selection + .previous_visual(&self.editor.layout, false), + ); + } + + /// Move to the next cluster right in visual order. + pub fn move_right(&mut self) { + self.refresh_layout(); + self.editor.set_selection( + self.editor + .selection + .next_visual(&self.editor.layout, false), + ); + } + + /// Move to the next word boundary left. + pub fn move_word_left(&mut self) { + self.refresh_layout(); + self.editor.set_selection( + self.editor + .selection + .previous_visual_word(&self.editor.layout, false), + ); + } + + /// Move to the next word boundary right. + pub fn move_word_right(&mut self) { + self.refresh_layout(); + self.editor.set_selection( + self.editor + .selection + .next_visual_word(&self.editor.layout, false), + ); + } + + /// Select the whole buffer. + pub fn select_all(&mut self) { + self.refresh_layout(); + self.editor.set_selection( + Selection::from_byte_index(&self.editor.layout, 0_usize, Affinity::default()) + .move_lines(&self.editor.layout, isize::MAX, true), + ); + } + + /// Collapse selection into caret. + pub fn collapse_selection(&mut self) { + self.editor.set_selection(self.editor.selection.collapse()); + } + + /// Move the selection focus point to the start of the buffer. + pub fn select_to_text_start(&mut self) { + self.refresh_layout(); + self.editor.set_selection(self.editor.selection.move_lines( + &self.editor.layout, + isize::MIN, + true, + )); + } + + /// Move the selection focus point to just after the previous hard line break (such as `\n`). + pub fn select_to_hard_line_start(&mut self) { + self.refresh_layout(); + self.editor.set_selection( + self.editor + .selection + .hard_line_start(&self.editor.layout, true), + ); + } + + /// Move the selection focus point to the start of the physical line. + pub fn select_to_line_start(&mut self) { + self.refresh_layout(); + self.editor + .set_selection(self.editor.selection.line_start(&self.editor.layout, true)); + } + + /// Move the selection focus point to the end of the buffer. + pub fn select_to_text_end(&mut self) { + self.refresh_layout(); + self.editor.set_selection(self.editor.selection.move_lines( + &self.editor.layout, + isize::MAX, + true, + )); + } + + /// Move the selection focus point to just before the next hard line break (such as `\n`). + pub fn select_to_hard_line_end(&mut self) { + self.refresh_layout(); + self.editor.set_selection( + self.editor + .selection + .hard_line_end(&self.editor.layout, true), + ); + } + + /// Move the selection focus point to the end of the physical line. + pub fn select_to_line_end(&mut self) { + self.refresh_layout(); + self.editor + .set_selection(self.editor.selection.line_end(&self.editor.layout, true)); + } + + /// Move the selection focus point up to the nearest cluster boundary on the previous line, preserving the horizontal position for repeated movements. + pub fn select_up(&mut self) { + self.refresh_layout(); + self.editor.set_selection( + self.editor + .selection + .previous_line(&self.editor.layout, true), + ); + } + + /// Move the selection focus point down to the nearest cluster boundary on the next line, preserving the horizontal position for repeated movements. + pub fn select_down(&mut self) { + self.refresh_layout(); + self.editor + .set_selection(self.editor.selection.next_line(&self.editor.layout, true)); + } + + /// Move the selection focus point to the next cluster left in visual order. + pub fn select_left(&mut self) { + self.refresh_layout(); + self.editor.set_selection( + self.editor + .selection + .previous_visual(&self.editor.layout, true), + ); + } + + /// Move the selection focus point to the next cluster right in visual order. + pub fn select_right(&mut self) { + self.refresh_layout(); + self.editor + .set_selection(self.editor.selection.next_visual(&self.editor.layout, true)); + } + + /// Move the selection focus point to the next word boundary left. + pub fn select_word_left(&mut self) { + self.refresh_layout(); + self.editor.set_selection( + self.editor + .selection + .previous_visual_word(&self.editor.layout, true), + ); + } + + /// Move the selection focus point to the next word boundary right. + pub fn select_word_right(&mut self) { + self.refresh_layout(); + self.editor.set_selection( + self.editor + .selection + .next_visual_word(&self.editor.layout, true), + ); + } + + /// Select the word at the point. + pub fn select_word_at_point(&mut self, x: f32, y: f32) { + self.refresh_layout(); + self.editor + .set_selection(Selection::word_from_point(&self.editor.layout, x, y)); + } + + /// Select the physical line at the point. + /// + /// Note that this metehod determines line breaks for any reason, including due to word wrapping. + /// To select the text between explicit newlines, use [`select_hard_line_at_point`](Self::select_hard_line_at_point). + /// In most text editing cases, this is the preferred behaviour. + pub fn select_line_at_point(&mut self, x: f32, y: f32) { + self.refresh_layout(); + let line = Selection::line_from_point(&self.editor.layout, x, y); + self.editor.set_selection(line); + } + + /// Select the "logical" line at the point. + /// + /// The logical line is defined by line break characters, such as `\n`, rather than due to soft-wrapping. + pub fn select_hard_line_at_point(&mut self, x: f32, y: f32) { + self.refresh_layout(); + let hard_line = Selection::hard_line_from_point(&self.editor.layout, x, y); + self.editor.set_selection(hard_line); + } + + /// Move the selection focus point to the cluster boundary closest to point. + /// + /// If the initial selection was created from a word or line, then the new + /// selection will be extended at the same granularity. + pub fn extend_selection_to_point(&mut self, x: f32, y: f32) { + self.refresh_layout(); + // FIXME: This is usually the wrong way to handle selection extension for mouse moves, but not a regression. + self.editor.set_selection( + self.editor + .selection + .extend_to_point(&self.editor.layout, x, y), + ); + } + + /// Move the selection focus point to the cluster boundary closest to point. + pub fn shift_click_extension(&mut self, x: f32, y: f32) { + self.refresh_layout(); + self.editor + .set_selection( + self.editor + .selection + .shift_click_extension(&self.editor.layout, x, y), + ); + } + + /// Move the selection focus point to a byte index. + /// + /// No-op if index is not a char boundary. + pub fn extend_selection_to_byte(&mut self, index: usize) { + if self.editor.buffer.is_char_boundary(index) { + self.refresh_layout(); + self.editor + .set_selection(self.editor.selection.extend(self.editor.cursor_at(index))); + } + } + + /// Select a range of byte indices. + /// + /// No-op if either index is not a char boundary. + pub fn select_byte_range(&mut self, start: usize, end: usize) { + if self.editor.buffer.is_char_boundary(start) && self.editor.buffer.is_char_boundary(end) { + self.refresh_layout(); + self.editor.set_selection(Selection::new( + self.editor.cursor_at(start), + self.editor.cursor_at(end), + )); + } + } + + #[cfg(feature = "accesskit")] + /// Select inside the editor based on the selection provided by accesskit. + pub fn select_from_accesskit(&mut self, selection: &accesskit::TextSelection) { + self.refresh_layout(); + if let Some(selection) = Selection::from_access_selection( + selection, + &self.editor.layout, + &self.editor.layout_access, + ) { + self.editor.set_selection(selection); + } + } + + // --- MARK: Rendering --- + #[cfg(feature = "accesskit")] + /// Perform an accessibility update. + pub fn accessibility( + &mut self, + update: &mut TreeUpdate, + node: &mut Node, + next_node_id: impl FnMut() -> NodeId, + x_offset: f64, + y_offset: f64, + set_brush_properties: impl Fn(&mut Node, &crate::Style), + ) -> Option<()> { + self.refresh_layout(); + self.editor.accessibility_unchecked( + update, + node, + next_node_id, + x_offset, + y_offset, + set_brush_properties, + ); + Some(()) + } + + /// Get the up-to-date layout for this driver. + pub fn layout(&mut self) -> &Layout { + self.editor.layout(self.font_cx, self.layout_cx) + } + + // --- MARK: Internal helpers--- + /// Update the layout if needed. + pub fn refresh_layout(&mut self) { + self.editor.refresh_layout(self.font_cx, self.layout_cx); + } + + /// Update the layout unconditionally. + fn update_layout(&mut self) { + self.editor.update_layout(self.font_cx, self.layout_cx); + } +} + +impl PlainEditor +where + T: Brush + Clone + Debug + PartialEq + Default, +{ + /// Run a series of [`PlainEditorDriver`] methods. + /// + /// This type is only used to simplify methods which require both + /// the editor and the provided contexts. + pub fn driver<'drv>( + &'drv mut self, + font_cx: &'drv mut FontContext, + layout_cx: &'drv mut LayoutContext, + ) -> PlainEditorDriver<'drv, T> { + PlainEditorDriver { + editor: self, + font_cx, + layout_cx, + } + } + + /// Borrow the current selection. The indices returned by functions + /// such as [`Selection::text_range`] refer to the raw text buffer, + /// including the IME preedit region, which can be accessed via + /// [`PlainEditor::raw_text`]. + pub fn raw_selection(&self) -> &Selection { + &self.selection + } + + /// Borrow the current IME preedit range, if any. These indices refer + /// to the raw text buffer, which can be accessed via [`PlainEditor::raw_text`]. + pub fn raw_compose(&self) -> &Option> { + &self.compose + } + + /// If the current selection is not collapsed, returns the text content of + /// that selection. + pub fn selected_text(&self) -> Option<&str> { + if self.is_composing() { + return None; + } + if !self.selection.is_collapsed() { + self.buffer.get(self.selection.text_range()) + } else { + None + } + } + + /// Get rectangles, and their corresponding line indices, representing the selected portions of + /// text. + pub fn selection_geometry(&self) -> Vec<(BoundingBox, usize)> { + // We do not check `self.show_cursor` here, as the IME handling code collapses the + // selection to a caret in that case. + self.selection.geometry(&self.layout) + } + + /// Invoke a callback with each rectangle representing the selected portions of text, and the + /// indices of the lines to which they belong. + pub fn selection_geometry_with(&self, f: impl FnMut(BoundingBox, usize)) { + // We do not check `self.show_cursor` here, as the IME handling code collapses the + // selection to a caret in that case. + self.selection.geometry_with(&self.layout, f); + } + + /// Get a rectangle representing the current caret cursor position. + /// + /// There is not always a caret. For example, the IME may have indicated the caret should be + /// hidden. + pub fn cursor_geometry(&self, size: f32) -> Option { + self.show_cursor + .then(|| self.selection.focus().geometry(&self.layout, size)) + } + + /// Get a rectangle bounding the text the user is currently editing. + /// + /// This is useful for suggesting an exclusion area to the platform for, e.g., IME candidate + /// box placement. This bounds the area of the preedit text if present, otherwise it bounds the + /// selection on the focused line. + pub fn ime_cursor_area(&self) -> BoundingBox { + let (area, focus) = if let Some(preedit_range) = &self.compose { + let selection = Selection::new( + self.cursor_at(preedit_range.start), + self.cursor_at(preedit_range.end), + ); + + // Bound the entire preedit text. + let mut area = None; + selection.geometry_with(&self.layout, |rect, _| { + let area = area.get_or_insert(rect); + *area = area.union(rect); + }); + + ( + area.unwrap_or_else(|| selection.focus().geometry(&self.layout, 0.)), + selection.focus(), + ) + } else { + // Bound the selected parts of the focused line only. + let focus = self.selection.focus().geometry(&self.layout, 0.); + let mut area = focus; + self.selection.geometry_with(&self.layout, |rect, _| { + if rect.y0 == focus.y0 { + area = area.union(rect); + } + }); + + (area, self.selection.focus()) + }; + + // Ensure some context is captured even for tiny or collapsed selections by including a + // region surrounding the selection. Doing this unconditionally, the IME candidate box + // usually does not need to jump around when composing starts or the preedit is added to. + let [upstream, downstream] = focus.logical_clusters(&self.layout); + let font_size = downstream + .or(upstream) + .map(|cluster| cluster.run().font_size()) + .unwrap_or(self.font_size * self.scale); + // Using 0.6 as an estimate of the average advance + let inflate = 3. * 0.6 * font_size as f64; + let editor_width = self.width.map(f64::from).unwrap_or(f64::INFINITY); + BoundingBox { + x0: (area.x0 - inflate).max(0.), + x1: (area.x1 + inflate).min(editor_width), + y0: area.y0, + y1: area.y1, + } + } + + /// Borrow the text content of the buffer. + /// + /// The return value is a `SplitString` because it + /// excludes the IME preedit region. + pub fn text(&self) -> SplitString<'_> { + if let Some(preedit_range) = &self.compose { + SplitString([ + &self.buffer[..preedit_range.start], + &self.buffer[preedit_range.end..], + ]) + } else { + SplitString([&self.buffer, ""]) + } + } + + /// Borrow the text content of the buffer, including the IME preedit + /// region if any. + /// + /// Application authors should generally prefer [`text`](Self::text). That method excludes the + /// IME preedit contents, which are not meaningful for applications to access; the + /// in-progress IME content is not itself what the user intends to write. + pub fn raw_text(&self) -> &str { + &self.buffer + } + + /// Get the current `Generation` of the layout, to decide whether to draw. + /// + /// You should store the generation the editor was at when you last drew it, and then redraw + /// when the generation is different (`Generation` is [`PartialEq`], so supports the equality `==` operation). + pub fn generation(&self) -> Generation { + self.generation + } + + /// Replace the whole text buffer. + pub fn set_text(&mut self, is: &str) { + self.buffer.clear(); + self.buffer.push_str(is); + self.layout_dirty = true; + self.compose = None; + } + + /// Set the width of the layout. + pub fn set_width(&mut self, width: Option) { + self.width = width; + self.layout_dirty = true; + } + + /// Set the alignment of the layout. + pub fn set_alignment(&mut self, alignment: Alignment) { + self.alignment = alignment; + self.layout_dirty = true; + } + + /// Set the scale for the layout. + pub fn set_scale(&mut self, scale: f32) { + self.scale = scale; + self.layout_dirty = true; + } + + /// Get the current scale for the layout. + pub fn get_scale(&self) -> f32 { + self.scale + } + + pub fn get_font_size(&self) -> f32 { + self.font_size + } + + /// Set whether to quantize the layout coordinates. + /// + /// Set `quantize` as `true` to have the layout coordinates aligned to pixel boundaries. + /// That is the easiest way to avoid blurry text and to receive ready-to-paint layout metrics. + /// + /// For advanced rendering use cases you can set `quantize` as `false` and receive + /// fractional coordinates. This ensures the most accurate results if you want to perform + /// some post-processing on the coordinates before painting. To avoid blurry text you will + /// still need to quantize the coordinates just before painting. + /// + /// Your should round at least the following: + /// * Glyph run baseline + /// * Inline box baseline + /// - `box.y = (box.y + box.height).round() - box.height` + /// * Selection geometry's `y0` & `y1` + /// * Cursor geometry's `y0` & `y1` + /// + /// Keep in mind that for the simple `f32::round` to be effective, + /// you need to first ensure the coordinates are in physical pixel space. + pub fn set_quantize(&mut self, quantize: bool) { + self.quantize = quantize; + self.layout_dirty = true; + } + + /// Modify the styles provided for this editor. + pub fn edit_styles(&mut self) -> &mut StyleSet { + self.layout_dirty = true; + &mut self.default_style + } + + /// Get the current default styles for this editor. + pub fn get_styles(&self) -> &StyleSet { + &self.default_style + } + + /// Whether the editor is currently in IME composing mode. + pub fn is_composing(&self) -> bool { + self.compose.is_some() + } + + /// Get the full read-only details from the layout, which will be updated if necessary. + /// + /// If the required contexts are not available, then [`refresh_layout`](Self::refresh_layout) can + /// be called in a scope when they are available, and [`try_layout`](Self::try_layout) can + /// be used instead. + pub fn layout( + &mut self, + font_cx: &mut FontContext, + layout_cx: &mut LayoutContext, + ) -> &Layout { + self.refresh_layout(font_cx, layout_cx); + &self.layout + } + + // --- MARK: Raw APIs --- + /// Get the full read-only details from the layout, if valid. + /// + /// Returns `None` if the layout is not up-to-date. + /// You can call [`refresh_layout`](Self::refresh_layout) before using this method, + /// to ensure that the layout is up-to-date. + /// + /// The [`layout`](Self::layout) method should generally be preferred. + pub fn try_layout(&self) -> Option<&Layout> { + if self.layout_dirty { + None + } else { + Some(&self.layout) + } + } + + #[cfg(feature = "accesskit")] + #[inline] + /// Perform an accessibility update if the layout is valid. + /// + /// Returns `None` if the layout is not up-to-date. + /// You can call [`refresh_layout`](Self::refresh_layout) before using this method, + /// to ensure that the layout is up-to-date. + /// The [`accessibility`](PlainEditorDriver::accessibility) method on the driver type + /// should be preferred if the contexts are available, which will do this automatically. + pub fn try_accessibility( + &mut self, + update: &mut TreeUpdate, + node: &mut Node, + next_node_id: impl FnMut() -> NodeId, + x_offset: f64, + y_offset: f64, + set_brush_properties: impl Fn(&mut Node, &crate::Style), + ) -> Option<()> { + if self.layout_dirty { + return None; + } + self.accessibility_unchecked( + update, + node, + next_node_id, + x_offset, + y_offset, + set_brush_properties, + ); + Some(()) + } + + /// Update the layout if it is dirty. + /// + /// This should only be used alongside [`try_layout`](Self::try_layout) + /// or [`try_accessibility`](Self::try_accessibility), if those will be + /// called in a scope where the contexts are not available. + pub fn refresh_layout(&mut self, font_cx: &mut FontContext, layout_cx: &mut LayoutContext) { + if self.layout_dirty { + self.update_layout(font_cx, layout_cx); + } + } + + // --- MARK: Internal Helpers --- + /// Make a cursor at a given byte index. + fn cursor_at(&self, index: usize) -> Cursor { + // TODO: Do we need to be non-dirty? + // FIXME: `Selection` should make this easier + if index >= self.buffer.len() { + Cursor::from_byte_index(&self.layout, self.buffer.len(), Affinity::Upstream) + } else { + Cursor::from_byte_index(&self.layout, index, Affinity::Downstream) + } + } + + fn update_compose_for_replaced_range(&mut self, old_range: Range, new_len: usize) { + if new_len == old_range.len() { + return; + } + let Some(compose) = &mut self.compose else { + return; + }; + if compose.end <= old_range.start { + return; + } + if compose.start >= old_range.end { + if new_len > old_range.len() { + let diff = new_len - old_range.len(); + *compose = compose.start + diff..compose.end + diff; + } else { + let diff = old_range.len() - new_len; + *compose = compose.start - diff..compose.end - diff; + } + return; + } + if new_len < old_range.len() { + if compose.start >= (old_range.start + new_len) { + self.compose = None; + return; + } + compose.end = compose.end.min(old_range.start + new_len); + } + } + + fn replace_selection( + &mut self, + font_cx: &mut FontContext, + layout_cx: &mut LayoutContext, + s: &str, + ) { + let range = self.selection.text_range(); + let start = range.start; + if self.selection.is_collapsed() { + self.buffer.insert_str(start, s); + } else { + self.buffer.replace_range(range.clone(), s); + } + self.update_compose_for_replaced_range(range, s.len()); + + self.update_layout(font_cx, layout_cx); + let new_index = start.saturating_add(s.len()); + let affinity = if s.ends_with(['\n', '\r', '\u{2028}', '\u{2029}']) { + Affinity::Downstream + } else { + Affinity::Upstream + }; + self.set_selection(Cursor::from_byte_index(&self.layout, new_index, affinity).into()); + } + + /// Update the selection, and nudge the `Generation` if something other than `h_pos` changed. + fn set_selection(&mut self, new_sel: Selection) { + if new_sel.focus() != self.selection.focus() || new_sel.anchor() != self.selection.anchor() + { + self.generation.nudge(); + } + + // This debug code is quite useful when diagnosing selection problems. + #[cfg(feature = "std")] + #[allow(clippy::print_stderr)] // reason = "unreachable debug code" + if false { + use std::{eprint, eprintln}; + + let focus = new_sel.focus(); + let cluster = focus.logical_clusters(&self.layout); + let dbg = ( + cluster[0].as_ref().map(|c| &self.buffer[c.text_range()]), + focus.index(), + focus.affinity(), + cluster[1].as_ref().map(|c| &self.buffer[c.text_range()]), + ); + eprint!("{dbg:?}"); + let cluster = focus.visual_clusters(&self.layout); + let dbg = ( + cluster[0].as_ref().map(|c| &self.buffer[c.text_range()]), + cluster[0] + .as_ref() + .map(|c| if c.is_word_boundary() { " W" } else { "" }) + .unwrap_or_default(), + focus.index(), + focus.affinity(), + cluster[1].as_ref().map(|c| &self.buffer[c.text_range()]), + cluster[1] + .as_ref() + .map(|c| if c.is_word_boundary() { " W" } else { "" }) + .unwrap_or_default(), + ); + eprintln!(" | visual: {dbg:?}"); + } + self.selection = new_sel; + } + /// Update the layout. + fn update_layout(&mut self, font_cx: &mut FontContext, layout_cx: &mut LayoutContext) { + let mut builder = + layout_cx.ranged_builder(font_cx, &self.buffer, self.scale, self.quantize); + for prop in self.default_style.inner().values() { + builder.push_default(prop.to_owned()); + } + if let Some(preedit_range) = &self.compose { + builder.push(StyleProperty::Underline(true), preedit_range.clone()); + } + self.layout = builder.build(&self.buffer); + self.layout.break_all_lines(self.width); + self.layout + .align(self.alignment, AlignmentOptions::default()); + self.selection = self.selection.refresh(&self.layout); + self.layout_dirty = false; + self.generation.nudge(); + } + + #[cfg(feature = "accesskit")] + /// Perform an accessibility update, assuming that the layout is valid. + /// + /// The wrapper [`accessibility`](PlainEditorDriver::accessibility) on the driver type should + /// be preferred. + /// + /// You should always call [`refresh_layout`](Self::refresh_layout) before using this method, + /// with no other modifying method calls in between. + fn accessibility_unchecked( + &mut self, + update: &mut TreeUpdate, + node: &mut Node, + next_node_id: impl FnMut() -> NodeId, + x_offset: f64, + y_offset: f64, + set_brush_properties: impl Fn(&mut Node, &crate::Style), + ) { + self.layout_access.build_nodes( + &self.buffer, + &self.layout, + update, + node, + next_node_id, + x_offset, + y_offset, + set_brush_properties, + ); + if self.show_cursor { + if let Some(selection) = self + .selection + .to_access_selection(&self.layout, &self.layout_access) + { + node.set_text_selection(selection); + } + } else { + node.clear_text_selection(); + } + node.add_action(accesskit::Action::SetTextSelection); + } +} diff --git a/vendor/parley-0.11.1/src/editing/mod.rs b/vendor/parley-0.11.1/src/editing/mod.rs new file mode 100644 index 0000000000..95a5959d44 --- /dev/null +++ b/vendor/parley-0.11.1/src/editing/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2025 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +mod cursor; +mod editor; +mod selection; + +pub use self::cursor::*; +pub use self::editor::*; +pub use self::selection::*; diff --git a/vendor/parley-0.11.1/src/editing/selection.rs b/vendor/parley-0.11.1/src/editing/selection.rs new file mode 100644 index 0000000000..ba4fcddff9 --- /dev/null +++ b/vendor/parley-0.11.1/src/editing/selection.rs @@ -0,0 +1,672 @@ +// Copyright 2025 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use crate::BoundingBox; +use crate::editing::Cursor; +#[cfg(feature = "accesskit")] +use crate::layout::LayoutAccessibility; +use crate::layout::{Affinity, BreakReason, Cluster, Layout, LineItem}; +use crate::style::Brush; + +use alloc::vec::Vec; +use core::ops::Range; + +/// Defines a range within a text layout. +#[derive(Copy, Clone, Default, Debug)] +pub struct Selection { + anchor: Cursor, + focus: Cursor, + anchor_base: AnchorBase, + h_pos: Option, +} + +#[derive(Copy, Clone, Default, Debug)] +enum AnchorBase { + #[default] + Cluster, + Word(Cursor, Cursor), + Line(Cursor, Cursor), +} + +impl Selection { + /// Creates a new selection from the given anchor and focus cursors. + pub fn new(anchor: Cursor, focus: Cursor) -> Self { + Self { + anchor, + focus, + anchor_base: AnchorBase::default(), + h_pos: None, + } + } + + /// Creates a new collapsed selection from the given byte index and + /// affinity. + pub fn from_byte_index(layout: &Layout, index: usize, affinity: Affinity) -> Self { + Cursor::from_byte_index(layout, index, affinity).into() + } + + /// Creates a new collapsed selection from the given point. + pub fn from_point(layout: &Layout, x: f32, y: f32) -> Self { + Cursor::from_point(layout, x, y).into() + } + + /// Creates a new selection bounding the word at the given coordinates. + pub fn word_from_point(layout: &Layout, x: f32, y: f32) -> Self { + if let Some((mut cluster, _)) = Cluster::from_point(layout, x, y) { + if !cluster.is_word_boundary() { + if let Some(prev) = cluster.previous_logical_word() { + cluster = prev; + } + } + let anchor = Cursor::from_cluster(layout, cluster.clone(), !cluster.is_rtl()); + let focus = anchor.next_logical_word(layout); + Self { + anchor, + focus, + anchor_base: AnchorBase::Word(anchor, focus), + h_pos: None, + } + } else { + Cursor::from_byte_index(layout, layout.data.text_len, Affinity::Upstream).into() + } + } + + /// Creates a new selection bounding the line at the given coordinates. + pub fn line_from_point(layout: &Layout, x: f32, y: f32) -> Self { + let Self { anchor, focus, .. } = Self::from_point(layout, x, y) + .line_start(layout, false) + .line_end(layout, true); + Self { + anchor, + focus, + anchor_base: AnchorBase::Line(anchor, focus), + h_pos: None, + } + } + + /// Creates a new selection bounding the "logical" line at the given coordinates. + /// + /// That is, the line as defined by line break characters, rather than due to soft-wrapping. + pub fn hard_line_from_point(layout: &Layout, x: f32, y: f32) -> Self { + let Self { anchor, focus, .. } = Self::from_point(layout, x, y) + .hard_line_start(layout, false) + .hard_line_end(layout, true); + Self { + anchor, + focus, + anchor_base: AnchorBase::Line(anchor, focus), + h_pos: None, + } + } + + #[cfg(feature = "accesskit")] + pub fn from_access_selection( + selection: &accesskit::TextSelection, + layout: &Layout, + layout_access: &LayoutAccessibility, + ) -> Option { + let anchor = Cursor::from_access_position(&selection.anchor, layout, layout_access)?; + let focus = Cursor::from_access_position(&selection.focus, layout, layout_access)?; + Some(Self::new(anchor, focus)) + } + + /// Returns `true` if the anchor and focus of the selection are the same. + /// + /// This means that the selection represents a single position rather than + /// a range. + pub fn is_collapsed(&self) -> bool { + self.anchor.index == self.focus.index + } + + /// Returns the anchor of the selection. + /// + /// In a non-collapsed selection, this indicates where the selection was + /// initiated. + pub fn anchor(&self) -> Cursor { + self.anchor + } + + /// Returns the focus of the selection. + /// + /// In a non-collapsed selection, this indicates the current position. + pub fn focus(&self) -> Cursor { + self.focus + } + + /// Returns a new collapsed selection at the position of the current + /// focus. + #[must_use] + pub fn collapse(&self) -> Self { + self.focus.into() + } + + /// Returns a new selection that is guaranteed to be within the bounds of + /// the given layout. + #[must_use] + pub fn refresh(&self, layout: &Layout) -> Self { + let anchor = self.anchor.refresh(layout); + let focus = self.focus.refresh(layout); + let anchor_base = match self.anchor_base { + AnchorBase::Cluster => AnchorBase::Cluster, + AnchorBase::Word(start, end) => { + AnchorBase::Word(start.refresh(layout), end.refresh(layout)) + } + AnchorBase::Line(start, end) => { + AnchorBase::Line(start.refresh(layout), end.refresh(layout)) + } + }; + let h_pos = self.h_pos; + Self { + anchor, + focus, + anchor_base, + h_pos, + } + } + + /// Returns the underlying text range of the selection. + pub fn text_range(&self) -> Range { + let start = self.anchor.index().min(self.focus.index()); + let end = self.focus.index().max(self.anchor.index()); + start..end + } + + /// Returns a new selection with the focus at the next cluster in visual + /// order. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. + #[must_use] + pub fn next_visual(&self, layout: &Layout, extend: bool) -> Self { + if !self.is_collapsed() && !extend { + let anchor_geom = self.anchor.geometry(layout, 0.0); + let focus_geom = self.focus.geometry(layout, 0.0); + let new_focus = if (anchor_geom.y0, anchor_geom.x0) > (focus_geom.y0, focus_geom.x0) { + self.anchor + } else { + self.focus + }; + new_focus.into() + } else { + self.maybe_extend(self.focus.next_visual(layout), extend) + } + } + + /// Returns a new selection with the focus at the previous cluster in visual + /// order. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. + #[must_use] + pub fn previous_visual(&self, layout: &Layout, extend: bool) -> Self { + if !self.is_collapsed() && !extend { + let anchor_geom = self.anchor.geometry(layout, 0.0); + let focus_geom = self.focus.geometry(layout, 0.0); + let new_focus = if (anchor_geom.y0, anchor_geom.x0) < (focus_geom.y0, focus_geom.x0) { + self.anchor + } else { + self.focus + }; + new_focus.into() + } else { + self.maybe_extend(self.focus.previous_visual(layout), extend) + } + } + + /// Returns a new selection with the focus moved to the next word in visual + /// order. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. + #[must_use] + pub fn next_visual_word(&self, layout: &Layout, extend: bool) -> Self { + self.maybe_extend(self.focus.next_visual_word(layout), extend) + } + + /// Returns a new selection with the focus moved to the previous word in + /// visual order. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. + #[must_use] + pub fn previous_visual_word(&self, layout: &Layout, extend: bool) -> Self { + self.maybe_extend(self.focus.previous_visual_word(layout), extend) + } + + /// Returns a new selection with the focus moved to the next line. The + /// current horizontal position will be maintained. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. + #[must_use] + pub fn next_line(&self, layout: &Layout, extend: bool) -> Self { + self.move_lines(layout, 1, extend) + } + + /// Returns a new selection with the focus moved to the previous line. The + /// current horizontal position will be maintained. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. + #[must_use] + pub fn previous_line(&self, layout: &Layout, extend: bool) -> Self { + self.move_lines(layout, -1, extend) + } + + /// Returns a new selection with the focus moved the specified number of + /// lines. + /// + /// The sign of the `delta` parameter determines the direction to move with + /// negative values moving toward previous lines and positive ones moving + /// toward next lines. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. + #[must_use] + pub fn move_lines(&self, layout: &Layout, delta: isize, extend: bool) -> Self { + if delta == 0 { + return *self; + } + let line_limit = layout.len().saturating_sub(1); + let geometry = self.focus.geometry(layout, 0.0); + let line_index = layout + .line_for_offset(geometry.y0 as f32) + .map(|(ix, _)| ix) + .unwrap_or(line_limit); + let new_line_index = line_index.saturating_add_signed(delta); + if delta < 0 && line_index.checked_add_signed(delta).is_none() { + return self + .move_to_line(layout, 0, extend) + .line_start(layout, extend); + } else if delta > 0 && new_line_index > line_limit { + return self + .move_to_line(layout, line_limit, extend) + .line_end(layout, extend); + } + self.move_to_line(layout, new_line_index, extend) + } + + #[must_use] + fn move_to_line(&self, layout: &Layout, line_index: usize, extend: bool) -> Self { + let Some(line) = layout.get(line_index) else { + return *self; + }; + let h_pos = self + .h_pos + .unwrap_or_else(|| self.focus.geometry(layout, 0.0).x0 as f32); + let y = line.metrics().block_max_coord - line.metrics().ascent * 0.5; + let new_focus = Cursor::from_point(layout, h_pos, y); + let h_pos = Some(h_pos); + if extend { + Self { + anchor: self.anchor, + focus: new_focus, + h_pos, + ..Default::default() + } + } else { + Self { + anchor: new_focus, + focus: new_focus, + h_pos, + ..Default::default() + } + } + } + + /// Returns a new selection with the focus moved to the start of the + /// current line. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. + #[must_use] + pub fn line_start(&self, layout: &Layout, extend: bool) -> Self { + if let Some((_, line)) = self.focus.line(layout) { + self.maybe_extend( + Cursor::from_byte_index(layout, line.text_range().start, Affinity::Downstream), + extend, + ) + } else { + *self + } + } + + /// Returns a new selection with the focus moved to just after the previous hard line break. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. + #[must_use] + pub fn hard_line_start(&self, layout: &Layout, extend: bool) -> Self { + if let Some((mut hard_line_start_index, line)) = self.focus.line(layout) { + let mut result_byte_index = line.text_range().start; + loop { + if hard_line_start_index == 0 { + break; + } + let prev_index = hard_line_start_index - 1; + let Some(line) = layout.get(prev_index) else { + unreachable!( + "{hard_line_start_index} is a valid line in the layout, but {prev_index} isn't, despite the latter being smaller.\n\ + The layout has {} lines.", + layout.len() + ); + }; + if matches!(line.break_reason(), BreakReason::Explicit) { + // The start of the line 'hard_line_start_index' is the target point. + break; + } + result_byte_index = line.text_range().start; + hard_line_start_index = prev_index; + } + self.maybe_extend( + Cursor::from_byte_index(layout, result_byte_index, Affinity::Downstream), + extend, + ) + } else { + *self + } + } + + /// Returns a new selection with the focus moved to the end of the + /// current line. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. + #[must_use] + pub fn line_end(&self, layout: &Layout, extend: bool) -> Self { + if let Some((_, line)) = self.focus.line(layout) { + let (index, affinity) = (line.break_reason() == BreakReason::Explicit) + .then(|| { + Cluster::from_byte_index(layout, line.text_range().end - 1) + .map(|cluster| (cluster.text_range().start, Affinity::Downstream)) + }) + .flatten() + .unwrap_or_else(|| (line.text_range().end, Affinity::Upstream)); + self.maybe_extend(Cursor::from_byte_index(layout, index, affinity), extend) + } else { + *self + } + } + + /// Returns a new selection with the focus moved to just before the next hard line break. + /// + /// If `extend` is `true` then the current anchor will be retained, + /// otherwise the new selection will be collapsed. + #[must_use] + pub fn hard_line_end(&self, layout: &Layout, extend: bool) -> Self { + if let Some((mut hard_line_end_index, line)) = self.focus.line(layout) { + let mut result_byte_index = line.text_range().end; + // If we're already on the last line of the hard line, use that. + if !matches!(line.break_reason(), BreakReason::Explicit) { + // Otherwise, check if any of the following lines are the last line of the hard line. + loop { + let next_index = hard_line_end_index + 1; + if let Some(line) = layout.get(next_index) { + result_byte_index = line.text_range().end; + hard_line_end_index = next_index; + if matches!(line.break_reason(), BreakReason::Explicit) { + // result_byte_index is the last byte of the previous line, so is the value we need + break; + } + } else { + // We hit the end of text. Select to the end of the "final" line, which was not an EOF. + return self.maybe_extend( + Cursor::from_byte_index(layout, result_byte_index, Affinity::Upstream), + extend, + ); + } + } + } + + // We want to select to "before" the newline character in the hard line, so we have downstream affinity on the boundary before it. + self.maybe_extend( + Cursor::from_byte_index(layout, result_byte_index - 1, Affinity::Downstream), + extend, + ) + } else { + *self + } + } + + /// Returns a new selection with the focus extended to the given point. + /// + /// If the initial selection was created from a word or line, then the new + /// selection will be extended at the same granularity. + #[must_use] + pub fn extend_to_point(&self, layout: &Layout, x: f32, y: f32) -> Self { + match self.anchor_base { + AnchorBase::Cluster => Self::new(self.anchor, Cursor::from_point(layout, x, y)), + AnchorBase::Word(start, end) => { + let target = Self::word_from_point(layout, x, y); + let [anchor, focus] = extend_selection(target, [start, end]); + Self { + anchor, + focus, + anchor_base: self.anchor_base, + h_pos: None, + } + } + AnchorBase::Line(start, end) => { + let target = Self::line_from_point(layout, x, y); + let [anchor, focus] = extend_selection(target, [start, end]); + Self { + anchor, + focus, + anchor_base: self.anchor_base, + h_pos: None, + } + } + } + } + + /// Returns a new selection with the focus extended to the given point. + #[must_use] + pub fn shift_click_extension(&self, layout: &Layout, x: f32, y: f32) -> Self { + let target = Cursor::from_point(layout, x, y); + match self.anchor_base { + AnchorBase::Cluster => Self::new(self.anchor, target), + AnchorBase::Word(start, end) | AnchorBase::Line(start, end) => { + // Place the focus where the user just clicked, and the anchor on the "far" side of the anchorbase. + let anchor = if target.index < start.index { + end + } else { + start + }; + Self { + anchor, + focus: target, + anchor_base: self.anchor_base, + h_pos: None, + } + } + } + } + + /// Returns a new selection with the current anchor and the focus set to + /// the given value. + #[must_use] + pub fn extend(&self, focus: Cursor) -> Self { + Self::new(self.anchor, focus) + } + + /// Returns a vector containing the rectangles which represent the visual + /// geometry of this selection for the given layout, and the indices of the + /// lines to which they belong. + /// + /// This is a convenience method built on [`geometry_with`](Self::geometry_with). + pub fn geometry(&self, layout: &Layout) -> Vec<(BoundingBox, usize)> { + let mut rects = Vec::new(); + self.geometry_with(layout, |rect, line_idx| rects.push((rect, line_idx))); + rects + } + + /// Invokes `f` with the sequence of rectangles which represent the visual + /// geometry of this selection for the given layout, and the indices of the + /// lines to which they belong. + /// + /// This avoids allocation if the intent is to render the rectangles + /// immediately. + pub fn geometry_with( + &self, + layout: &Layout, + mut f: impl FnMut(BoundingBox, usize), + ) { + const NEWLINE_WHITESPACE_WIDTH_RATIO: f64 = 0.25; + if self.is_collapsed() { + return; + } + let mut start = self.anchor; + let mut end = self.focus; + if start.index > end.index { + core::mem::swap(&mut start, &mut end); + } + let text_range = start.index..end.index; + let line_start_ix = start.line(layout).map(|(ix, _)| ix).unwrap_or(0); + let line_end_ix = end + .line(layout) + .map(|(ix, _)| ix) + .unwrap_or(layout.len() + 1); + for line_ix in line_start_ix..=line_end_ix { + let Some(line) = layout.get(line_ix) else { + continue; + }; + let metrics = line.metrics(); + let line_min = metrics.block_min_coord as f64; + let line_max = metrics.block_max_coord as f64; + // Trailing whitespace to indicate that the newline character at the + // end of this line is selected. It's based on the ascent and + // descent so it doesn't change with the line height. + // + // TODO: the width of this whitespace should be the width of a space + // (U+0020) character. + let newline_whitespace = if line.break_reason() == BreakReason::Explicit { + (metrics.ascent as f64 + metrics.descent as f64) * NEWLINE_WHITESPACE_WIDTH_RATIO + } else { + 0.0 + }; + if line_ix == line_start_ix || line_ix == line_end_ix { + // We only need to run the expensive logic on the first and + // last lines + let mut start_x = metrics.offset as f64 + metrics.inline_min_coord as f64; + let mut cur_x = start_x; + let mut cluster_count = 0; + let mut box_advance = 0.0; + let mut have_seen_any_runs = false; + for item in line.items_nonpositioned() { + match item { + LineItem::Run(run) => { + have_seen_any_runs = true; + for cluster in run.visual_clusters() { + let advance = cluster.advance() as f64 + box_advance; + box_advance = 0.0; + if text_range.contains(&cluster.text_range().start) { + cluster_count += 1; + cur_x += advance; + } else { + if cur_x != start_x { + f( + BoundingBox::new(start_x, line_min, cur_x, line_max), + line_ix, + ); + } + cur_x += advance; + start_x = cur_x; + } + } + } + LineItem::InlineBox(inline_box) => { + box_advance += inline_box.width as f64; + // HACK: Don't display selections for inline boxes + // if they're the first thing in the line. This + // makes the selection match the cursor position. + if !have_seen_any_runs { + cur_x += box_advance; + box_advance = 0.0; + start_x = cur_x; + } + } + } + } + let mut end_x = cur_x; + if line_ix != line_end_ix || (cluster_count != 0 && metrics.advance == 0.0) { + end_x += newline_whitespace; + } + if end_x != start_x { + f( + BoundingBox::new(start_x, line_min, end_x, line_max), + line_ix, + ); + } + } else { + let x = metrics.offset as f64 + metrics.inline_min_coord as f64; + let width = metrics.advance as f64; + f( + BoundingBox::new(x, line_min, x + width + newline_whitespace, line_max), + line_ix, + ); + } + } + } + + pub(crate) fn maybe_extend(&self, focus: Cursor, extend: bool) -> Self { + if extend { + Self::new(self.anchor, focus) + } else { + focus.into() + } + } + + #[cfg(feature = "accesskit")] + pub fn to_access_selection( + &self, + layout: &Layout, + layout_access: &LayoutAccessibility, + ) -> Option { + let anchor = self.anchor.to_access_position(layout, layout_access)?; + let focus = self.focus.to_access_position(layout, layout_access)?; + Some(accesskit::TextSelection { anchor, focus }) + } +} + +impl PartialEq for Selection { + fn eq(&self, other: &Self) -> bool { + self.anchor == other.anchor && self.focus == other.focus + } +} + +impl Eq for Selection {} + +impl From for Selection { + fn from(value: Cursor) -> Self { + Self::new(value, value) + } +} + +/// Given the anchor base and the target selection, returns the anchor and focus of the resulting selection extension. +fn extend_selection(target_selection: Selection, anchor_base: [Cursor; 2]) -> [Cursor; 2] { + let extending_to_the_right = target_selection.anchor.index >= anchor_base[0].index; + + let cursors = [ + target_selection.anchor, + target_selection.focus, + anchor_base[0], + anchor_base[1], + ]; + + let mut min = cursors[0]; + let mut max = cursors[0]; + for pos in cursors { + if pos.index < min.index { + min = pos; + } + if pos.index > max.index { + max = pos; + } + } + + if extending_to_the_right { + [min, max] + } else { + [max, min] + } +} diff --git a/vendor/parley-0.11.1/src/font.rs b/vendor/parley-0.11.1/src/font.rs new file mode 100644 index 0000000000..39abebce91 --- /dev/null +++ b/vendor/parley-0.11.1/src/font.rs @@ -0,0 +1,22 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use fontique::Collection; + +use fontique::SourceCache; + +/// A font database/cache (wrapper around a Fontique [`Collection`] and [`SourceCache`]). +/// +/// This type is designed to be a global resource with only one per-application (or per-thread). +#[derive(Default, Clone)] +pub struct FontContext { + pub collection: Collection, + pub source_cache: SourceCache, +} + +impl FontContext { + /// Create a new `FontContext`, discovering system fonts if available. + pub fn new() -> Self { + Self::default() + } +} diff --git a/vendor/parley-0.11.1/src/inline_box.rs b/vendor/parley-0.11.1/src/inline_box.rs new file mode 100644 index 0000000000..e7324bda64 --- /dev/null +++ b/vendor/parley-0.11.1/src/inline_box.rs @@ -0,0 +1,40 @@ +// Copyright 2024 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +/// A box to be laid out inline with text +#[derive(PartialEq, Debug, Clone)] +pub struct InlineBox { + /// User-specified identifier for the box, which can be used by the user to determine which box in + /// parley's output corresponds to which box in its input. + pub id: u64, + /// Whether the box is in-flow (takes up space in the layout) or out-of-flow (e.g. absolutely positioned or floated) + pub kind: InlineBoxKind, + /// The byte offset into the underlying text string at which the box should be placed. + /// This must not be within a Unicode code point. + pub index: usize, + /// The width of the box in pixels + pub width: f32, + /// The height of the box in pixels + pub height: f32, +} + +/// Whether a box is in-flow (takes up space in the layout) or out-of-flow (e.g. absolutely positioned) +/// or custom-out-of-flow (line-breaking should yield control flow) +#[derive(PartialEq, Debug, Clone, Copy)] +pub enum InlineBoxKind { + /// `InFlow` boxes take up space in the layout and flow in line with text + /// + /// They correspond to `display: inline-block` boxes in CSS. + InFlow, + /// `OutOfFlow` boxes are assigned a position as if they were a zero-sized inline box, but + /// do not take up space in the layout. + /// + /// They correspond to `position: absolute` boxes in CSS. + OutOfFlow, + /// `CustomOutOfFlow` boxes also do not take up space in the layout, but they are not assigned a position + /// by Parley. When they are encountered, control flow is yielded back to the caller who is then responsible + /// for laying out the box. + /// + /// They can be used to implement advanced layout modes such as CSS's `float` + CustomOutOfFlow, +} diff --git a/vendor/parley-0.11.1/src/layout/accessibility.rs b/vendor/parley-0.11.1/src/layout/accessibility.rs new file mode 100644 index 0000000000..88f2960a38 --- /dev/null +++ b/vendor/parley-0.11.1/src/layout/accessibility.rs @@ -0,0 +1,307 @@ +// Copyright 2025 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; + +use accesskit::{Node, NodeId, Role, TextAlign, TextDirection, TreeUpdate}; +use hashbrown::{HashMap, HashSet}; +use skrifa::{ + FontRef, + raw::{TableProvider, types::NameId}, +}; + +use crate::style::Brush; +use crate::{Alignment, ClusterPath, FontStyle, Layout, LineMetrics, Run, Style}; + +fn link_spans(prev_id: NodeId, prev: &mut Node, next_id: NodeId, next: &mut Node) { + prev.set_next_on_line(next_id); + next.set_previous_on_line(prev_id); +} + +fn finish_span( + node: &mut Node, + x_offset: f64, + y_offset: f64, + metrics: &LineMetrics, + run_offset: f32, + span_offset: f32, + span_advance: f32, + span_text: String, + character_lengths: Vec, + character_positions: Vec, + character_widths: Vec, + word_starts: Vec, +) { + node.set_bounds(accesskit::Rect { + x0: x_offset + (run_offset + span_offset) as f64, + y0: y_offset + metrics.block_min_coord as f64, + x1: x_offset + (run_offset + span_offset + span_advance) as f64, + y1: y_offset + metrics.block_max_coord as f64, + }); + node.set_value(span_text); + node.set_character_lengths(character_lengths); + node.set_character_positions(character_positions); + node.set_character_widths(character_widths); + node.set_word_starts(word_starts); +} + +fn add_span(update: &mut TreeUpdate, parent_node: &mut Node, id: NodeId, node: Node) { + update.nodes.push((id, node)); + parent_node.push_child(id); +} + +#[derive(Clone, Default, Debug)] +pub struct LayoutAccessibility { + // We define a span as a sequence of clusters, in logical order, that all + // have an identical style. For each span we create an AccessKit node + // with the `TextRun` role, and these nodes are in logical order. + // The following two fields maintain a two-way mapping between spans + // and AccessKit node IDs, where each span is identified by the path to + // its first cluster, or a span path for short. These maps are maintained by + // `LayoutAccess::build_nodes`, which ensures that removed spans are removed + // from the maps on the next accessibility pass. + pub(crate) access_ids_by_span_path: HashMap, + pub(crate) span_paths_by_access_id: HashMap, + // Map from cluster path to span path. This allows `Cursor::to_access_position` + // to complete in O(1), rather than worst-case O(n) where n is the length + // of the run. It also means that the logic for when to start a new span, + // including the limitation on the number of characters per span, + // only needs to live in `LayoutAccess::build_nodes`. + pub(crate) span_paths_by_cluster_path: HashMap, +} + +impl LayoutAccessibility { + fn span_id_and_node( + &mut self, + next_node_id: &mut impl FnMut() -> NodeId, + ids: &mut HashSet, + run: &Run<'_, B>, + span_path: ClusterPath, + ) -> (NodeId, Node) { + // If we encountered this same span path in the previous + // accessibility pass, reuse the same AccessKit ID. Otherwise, + // allocate a new one. This enables stable node IDs when merely + // updating the content of existing spans. + let id = self + .access_ids_by_span_path + .get(&span_path) + .copied() + .unwrap_or_else(|| { + let id = (*next_node_id)(); + self.access_ids_by_span_path.insert(span_path, id); + self.span_paths_by_access_id.insert(id, span_path); + id + }); + ids.insert(id); + let mut node = Node::new(Role::TextRun); + node.set_text_direction(if run.is_rtl() { + TextDirection::RightToLeft + } else { + TextDirection::LeftToRight + }); + + let font = run.font(); + if let Ok(font_ref) = FontRef::from_index(font.data.as_ref(), font.index) { + if let Ok(name) = font_ref.name() { + for n in name.name_record().iter() { + if n.name_id.get() == NameId::FAMILY_NAME { + if let Ok(string) = n.string(name.string_data()) { + node.set_font_family(string.to_string()); + } + break; + } + } + } + } + node.set_font_size(run.font_size()); + let attrs = run.font_attrs(); + node.set_font_weight(attrs.weight.value()); + if matches!(attrs.style, FontStyle::Italic) { + node.set_italic(); + } + if let Some(align) = run.layout.data.alignment { + node.set_text_align(match align { + Alignment::Start => { + if run.is_rtl() { + TextAlign::Right + } else { + TextAlign::Left + } + } + Alignment::End => { + if run.is_rtl() { + TextAlign::Left + } else { + TextAlign::Right + } + } + Alignment::Left => TextAlign::Left, + Alignment::Center => TextAlign::Center, + Alignment::Right => TextAlign::Right, + Alignment::Justify => TextAlign::Justify, + }); + } + + (id, node) + } + + #[allow(clippy::too_many_arguments)] + pub fn build_nodes( + &mut self, + text: &str, + layout: &Layout, + update: &mut TreeUpdate, + parent_node: &mut Node, + mut next_node_id: impl FnMut() -> NodeId, + x_offset: f64, + y_offset: f64, + set_brush_properties: impl Fn(&mut Node, &Style), + ) { + self.span_paths_by_cluster_path.clear(); + // Build a set of node IDs for the runs encountered in this pass. + let mut ids = HashSet::::new(); + // Reuse scratch space for storing a sorted list of runs. + let mut runs = Vec::new(); + + for (line_index, line) in layout.lines().enumerate() { + let metrics = line.metrics(); + // Defer adding each run node until we reach either the next run + // or the end of the line. That way, we can set relations between + // runs in a line and do anything special that might be required + // for the last run in a line. + let mut last_node: Option<(NodeId, Node)> = None; + + // Iterate over the runs from left to right, computing their offsets, + // then sort them into text order. + runs.clear(); + runs.reserve(line.len()); + { + let mut run_offset = metrics.offset; + for run in line.runs() { + let advance = run.advance(); + runs.push((run, run_offset)); + run_offset += advance; + } + } + runs.sort_by_key(|(r, _)| r.text_range().start); + + for (run, run_offset) in runs.drain(..) { + let mut span_path = ClusterPath::new(line_index as u32, run.index() as u32, 0); + let (mut id, mut node) = + self.span_id_and_node(&mut next_node_id, &mut ids, &run, span_path); + + if let Some((last_id, mut last_node)) = last_node.take() { + link_spans(last_id, &mut last_node, id, &mut node); + add_span(update, parent_node, last_id, last_node); + } + + let mut prev_style_index: Option = None; + let mut span_text = String::new(); + let mut character_lengths = Vec::new(); + let mut span_offset = 0.0; + let mut span_advance = 0.0; + let mut character_positions = Vec::new(); + let mut character_widths = Vec::new(); + let mut word_starts = Vec::new(); + + for cluster in run.clusters() { + let style_index = cluster.data.style_index; + if let Some(prev_index) = prev_style_index { + // Limit spans to 256 characters because `word_starts` + // consists of `u8`s. + if prev_index != style_index || character_lengths.len() >= 256 { + prev_style_index = None; + finish_span( + &mut node, + x_offset, + y_offset, + metrics, + run_offset, + span_offset, + span_advance, + span_text.clone(), + character_lengths.clone(), + character_positions.clone(), + character_widths.clone(), + word_starts.clone(), + ); + span_offset += span_advance; + span_advance = 0.0; + span_text.clear(); + character_lengths.clear(); + character_positions.clear(); + character_widths.clear(); + word_starts.clear(); + (id, node) = { + let (old_id, mut old_node) = (id, node); + span_path = cluster.path(); + let (new_id, mut new_node) = self.span_id_and_node( + &mut next_node_id, + &mut ids, + &run, + span_path, + ); + link_spans(old_id, &mut old_node, new_id, &mut new_node); + add_span(update, parent_node, old_id, old_node); + (new_id, new_node) + }; + } + } + + if prev_style_index.is_none() { + prev_style_index = Some(style_index); + let style = cluster.first_style(); + set_brush_properties(&mut node, style); + if let Some(locale) = &style.locale { + node.set_language(locale.as_str()); + } + } + + let cluster_text = &text[cluster.text_range()]; + span_text.push_str(cluster_text); + if cluster.is_word_boundary() && !cluster.is_space_or_nbsp() { + word_starts.push(character_lengths.len() as _); + } + character_lengths.push(cluster_text.len() as _); + character_positions.push(span_advance); + character_widths.push(cluster.advance()); + span_advance += cluster.advance(); + self.span_paths_by_cluster_path + .insert(cluster.path(), span_path); + } + + finish_span( + &mut node, + x_offset, + y_offset, + metrics, + run_offset, + span_offset, + span_advance, + span_text, + character_lengths, + character_positions, + character_widths, + word_starts, + ); + last_node = Some((id, node)); + } + + if let Some((id, node)) = last_node { + add_span(update, parent_node, id, node); + } + } + + // Remove mappings for spans that no longer exist. + self.span_paths_by_access_id.retain(|access_id, span_path| { + let keep = ids.contains(access_id); + if !keep { + self.access_ids_by_span_path.remove(span_path); + } + keep + }); + } +} diff --git a/vendor/parley-0.11.1/src/layout/alignment.rs b/vendor/parley-0.11.1/src/layout/alignment.rs new file mode 100644 index 0000000000..26aa1144c5 --- /dev/null +++ b/vendor/parley-0.11.1/src/layout/alignment.rs @@ -0,0 +1,193 @@ +// Copyright 2024 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use super::{ + BreakReason, + data::{ClusterData, LineItemData}, +}; +use crate::data::LayoutData; +use crate::style::Brush; + +/// Alignment of a layout. +#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)] +#[repr(u8)] +pub enum Alignment { + /// This is [`Alignment::Left`] for LTR text and [`Alignment::Right`] for RTL text. + #[default] + Start, + /// This is [`Alignment::Right`] for LTR text and [`Alignment::Left`] for RTL text. + End, + /// Align content to the left edge. + /// + /// For alignment that should be aware of text direction, use [`Alignment::Start`] or + /// [`Alignment::End`] instead. + Left, + /// Align each line centered within the container. + Center, + /// Align content to the right edge. + /// + /// For alignment that should be aware of text direction, use [`Alignment::Start`] or + /// [`Alignment::End`] instead. + Right, + /// Justify each line by spacing out content, except for the last line. + Justify, +} + +/// Additional options to fine tune alignment +#[derive(Debug, Clone, Copy)] +pub struct AlignmentOptions { + /// If set to `true`, "end" and "center" alignment will apply even if the line contents are + /// wider than the alignment width. If it is set to `false`, all overflowing lines will be + /// [`Alignment::Start`] aligned. + pub align_when_overflowing: bool, +} + +#[expect( + clippy::derivable_impls, + reason = "Make default values explicit rather than relying on the implicit default value of bool" +)] +impl Default for AlignmentOptions { + fn default() -> Self { + Self { + align_when_overflowing: false, + } + } +} + +/// Align the layout. +/// +/// If [`Alignment::Justify`] is requested, clusters' [`ClusterData::advance`] will be adjusted. +/// Prior to re-line-breaking or re-aligning, [`unjustify`] has to be called. +pub(crate) fn align( + layout: &mut LayoutData, + alignment: Alignment, + options: AlignmentOptions, +) { + #[cfg(feature = "accesskit")] + { + layout.alignment = Some(alignment); + } + layout.is_aligned_justified = alignment == Alignment::Justify; + + align_impl::<_, false>(layout, alignment, options); +} + +/// Removes previous justification applied to clusters. +/// +/// This is part of resetting state in preparation for re-line-breaking or re-aligning the same +/// layout. +pub(crate) fn unjustify(layout: &mut LayoutData) { + if layout.is_aligned_justified { + align_impl::<_, true>(layout, Alignment::Justify, AlignmentOptions::default()); + layout.is_aligned_justified = false; + } +} + +/// The actual alignment implementation. +/// +/// This is const-generic over `UNDO_JUSTIFICATION`: justified alignment adjusts clusters' +/// [`ClusterData::advance`], and this mutation has to be undone for re-line-breaking or +/// re-aligning. `UNDO_JUSTIFICATION` indicates whether the adjustment has to be applied, or +/// undone. +/// +/// Writing a separate function for undoing justification would be faster, but not by much, and +/// doing it this way we are sure the calculations performed are equivalent. +fn align_impl( + layout: &mut LayoutData, + alignment: Alignment, + options: AlignmentOptions, +) { + // Whether the text base direction is right-to-left. + let is_rtl = layout.base_level & 1 == 1; + + // Apply alignment to line items + for line in &mut layout.lines { + let indent = line.indent; + + if is_rtl { + // In RTL text, trailing whitespace is on the left. As we hang that whitespace, offset + // the line to the left. Note: indent is not subtracted here because `free_space` below + // already accounts for it. + line.metrics.offset = -line.metrics.trailing_whitespace; + } else { + line.metrics.offset = indent; + } + + // Compute free space. + let line_width = line.metrics.inline_max_coord - line.metrics.inline_min_coord; + let free_space = + line_width - indent - line.metrics.advance + line.metrics.trailing_whitespace; + + if !options.align_when_overflowing && free_space <= 0.0 { + if is_rtl { + // In RTL text, right-align on overflow. + line.metrics.offset += free_space; + } + continue; + } + + match (alignment, is_rtl) { + (Alignment::Left, _) | (Alignment::Start, false) | (Alignment::End, true) => { + // Do nothing + } + (Alignment::Right, _) | (Alignment::Start, true) | (Alignment::End, false) => { + line.metrics.offset += free_space; + } + (Alignment::Center, _) => { + line.metrics.offset += free_space * 0.5; + } + (Alignment::Justify, _) => { + // Justified alignment doesn't have any effect if free_space is negative or zero + if free_space <= 0.0 { + continue; + } + + // Justified alignment doesn't apply to the last line of a paragraph + // (`BreakReason::None`), (`BreakReason::Explicit`) or if there are no whitespace + // gaps to adjust. In that case, start-align, i.e., left-align for LTR text and + // right-align for RTL text. + if matches!(line.break_reason, BreakReason::None | BreakReason::Explicit) + || line.num_spaces == 0 + { + if is_rtl { + line.metrics.offset += free_space; + } + continue; + } + + let adjustment = + free_space / line.num_spaces as f32 * if UNDO_JUSTIFICATION { -1. } else { 1. }; + let mut applied = 0; + // Iterate over text runs in the line and clusters in the text run + // - Iterate forwards for even bidi levels (which represent LTR runs) + // - Iterate backwards for odd bidi levels (which represent RTL runs) + let line_items: &mut dyn Iterator = if is_rtl { + &mut layout.line_items[line.item_range.clone()].iter().rev() + } else { + &mut layout.line_items[line.item_range.clone()].iter() + }; + line_items + .filter(|item| item.is_text_run()) + .for_each(|line_item| { + let clusters = &mut layout.clusters[line_item.cluster_range.clone()]; + let line_item_is_rtl = line_item.bidi_level & 1 != 0; + let clusters: &mut dyn Iterator = + if line_item_is_rtl { + &mut clusters.iter_mut().rev() + } else { + &mut clusters.iter_mut() + }; + clusters.for_each(|cluster| { + if applied == line.num_spaces { + return; + } + if cluster.info.whitespace().is_space_or_nbsp() { + cluster.advance += adjustment; + applied += 1; + } + }); + }); + } + } + } +} diff --git a/vendor/parley-0.11.1/src/layout/cluster.rs b/vendor/parley-0.11.1/src/layout/cluster.rs new file mode 100644 index 0000000000..4d8ec92e12 --- /dev/null +++ b/vendor/parley-0.11.1/src/layout/cluster.rs @@ -0,0 +1,604 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use crate::analysis::cluster::Whitespace; +use crate::layout::Style; +use crate::layout::data::BreakReason; +use crate::layout::data::ClusterData; +use crate::layout::glyph::Glyph; +use crate::layout::layout::Layout; +use crate::layout::line::{Line, LineItem}; +use crate::layout::run::Run; +use crate::style::Brush; +use core::ops::Range; + +/// Atomic unit of text. +#[derive(Copy, Clone)] +pub struct Cluster<'a, B: Brush> { + pub(crate) path: ClusterPath, + pub(crate) run: Run<'a, B>, + pub(crate) data: &'a ClusterData, +} + +/// Defines the visual side of the cluster for hit testing. +/// +/// See [`Cluster::from_point`]. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum ClusterSide { + /// Cluster was hit on the left half. + Left, + /// Cluster was hit on the right half. + Right, +} + +impl<'a, B: Brush> Cluster<'a, B> { + /// Returns the cluster for the given layout and byte index. + pub fn from_byte_index(layout: &'a Layout, byte_index: usize) -> Option { + let mut path = ClusterPath::default(); + if let Some((line_index, line)) = layout.line_for_byte_index(byte_index) { + path.line_index = line_index as u32; + for run in line.runs() { + path.run_index = run.index; + if !run.text_range().contains(&byte_index) { + continue; + } + for (cluster_index, cluster) in run.clusters().enumerate() { + path.logical_index = cluster_index as u32; + if cluster.text_range().contains(&byte_index) { + return path.cluster(layout); + } + } + } + } + None + } + + /// Returns the cluster and side which is at the specified position in the given layout. If no cluster is + /// under the specified point then None will be returned. + /// + /// This is usually the expected behaviour when hit-testing clusters for "hover" or "click" functionality. + pub fn from_point_exact(layout: &'a Layout, x: f32, y: f32) -> Option<(Self, ClusterSide)> { + Cluster::from_point_impl(layout, x, y, true) + } + + /// Returns the cluster and side which is at the specified position in the given layout. If no cluster is + /// under the specified point but the point is within the overall layout area then it will return the nearest. + /// + /// This is usually the expected behaviour when hit-testing clusers for text selection or caret positioning. + pub fn from_point(layout: &'a Layout, x: f32, y: f32) -> Option<(Self, ClusterSide)> { + Cluster::from_point_impl(layout, x, y, false) + } + + /// Returns the cluster and side for the given layout and point. + fn from_point_impl( + layout: &'a Layout, + x: f32, + y: f32, + exact: bool, + ) -> Option<(Self, ClusterSide)> { + let mut path = ClusterPath::default(); + if let Some((line_index, line)) = layout.line_for_offset(y) { + path.line_index = line_index as u32; + let mut offset = line.metrics().offset + line.metrics().inline_min_coord; + let last_run_index = line.len().saturating_sub(1); + for item in line.items_nonpositioned() { + match item { + LineItem::Run(run) => { + let is_last_run = run.index as usize == last_run_index; + let run_advance = run.advance(); + path.run_index = run.index; + path.logical_index = 0; + if x > offset + run_advance && (exact || !is_last_run) { + offset += run_advance; + continue; + } + let last_cluster_index = run.cluster_range().len().saturating_sub(1); + for (visual_index, cluster) in run.visual_clusters().enumerate() { + let is_last_cluster = is_last_run && visual_index == last_cluster_index; + path.logical_index = + run.visual_to_logical(visual_index).unwrap_or_default() as u32; + let cluster_advance = cluster.advance(); + let edge = offset; + offset += cluster_advance; + if x > offset && (exact || !is_last_cluster) { + continue; + } + if x < edge && exact { + continue; + } + let side = if x <= edge + cluster_advance * 0.5 { + ClusterSide::Left + } else { + ClusterSide::Right + }; + return Some((path.cluster(layout)?, side)); + } + } + LineItem::InlineBox(inline_box) => { + offset += inline_box.width; + } + } + } + } + if y <= 0.0 && !exact { + Some((path.cluster(layout)?, ClusterSide::Left)) + } else { + None + } + } + + /// Returns the line that contains the cluster. + pub fn line(&self) -> Line<'a, B> { + self.run.layout.get(self.run.line_index as usize).unwrap() + } + + /// Returns the run that contains the cluster. + pub fn run(&self) -> Run<'a, B> { + self.run.clone() + } + + /// Returns the path that contains the set of indices to reach the cluster + /// from a layout. + pub fn path(&self) -> ClusterPath { + self.path + } + + /// Returns the range of text that defines the cluster. + pub fn text_range(&self) -> Range { + self.data.text_range(self.run.data) + } + + /// Returns the first style that applies to the cluster. If the cluster contains multiple glyphs + /// then this style may not apply to all glyphs in the cluster (see the `DIVERGENT_STYLES` flag) + pub fn first_style(&self) -> &Style { + &self.run.layout.styles()[usize::from(self.data.style_index)] + } + + /// Returns the advance of the cluster. + pub fn advance(&self) -> f32 { + self.data.advance + } + + /// Returns `true` if this is a right-to-left cluster. + pub fn is_rtl(&self) -> bool { + self.run.is_rtl() + } + + /// Returns `true` if the cluster is the beginning of a ligature. + pub fn is_ligature_start(&self) -> bool { + self.data.is_ligature_start() + } + + /// Returns `true` if the cluster is a ligature continuation. + pub fn is_ligature_continuation(&self) -> bool { + self.data.is_ligature_component() + } + + /// Returns `true` if the cluster is a word boundary. + pub fn is_word_boundary(&self) -> bool { + self.data.info.is_boundary() + } + + /// Returns `true` if the cluster is a soft line break. + pub fn is_soft_line_break(&self) -> bool { + self.is_end_of_line() + && matches!( + self.line().data.break_reason, + BreakReason::Regular | BreakReason::Emergency + ) + } + + /// Returns `true` if the cluster is a hard line break. + pub fn is_hard_line_break(&self) -> bool { + self.data.info.whitespace() == Whitespace::Newline + } + + /// Returns `true` if the cluster is a space or no-break space. + pub fn is_space_or_nbsp(&self) -> bool { + self.data.info.whitespace().is_space_or_nbsp() + } + + /// Returns `true` if the cluster is an emoji sequence. + pub fn is_emoji(&self) -> bool { + self.data.info.is_emoji() + } + + /// Returns an iterator over the glyphs in the cluster. + pub fn glyphs(&self) -> impl Iterator + 'a + Clone { + if self.data.glyph_len == 0xFF { + GlyphIter::Single(Some(Glyph { + id: self.data.glyph_offset, + style_index: self.data.style_index, + x: 0., + y: 0., + advance: self.data.advance, + })) + } else { + let start = self.run.data.glyph_start + self.data.glyph_offset as usize; + GlyphIter::Slice( + self.run.layout.data.glyphs[start..start + self.data.glyph_len as usize].iter(), + ) + } + } + + /// Returns `true` if this cluster is at the beginning of a line. + pub fn is_start_of_line(&self) -> bool { + self.path.run_index == 0 && self.run.logical_to_visual(self.path.logical_index()) == Some(0) + } + + /// Returns `true` if this cluster is at the end of a line. + pub fn is_end_of_line(&self) -> bool { + self.line().len().saturating_sub(1) == self.path.run_index() + && self.run.logical_to_visual(self.path.logical_index()) + == Some(self.run.cluster_range().len().saturating_sub(1)) + } + + /// If the cluster as at the end of the line, returns the reason + /// for the line break. + pub fn is_line_break(&self) -> Option { + if self.is_end_of_line() { + Some(self.line().data.break_reason) + } else { + None + } + } + + /// Returns the cluster that follows this one in logical order. + pub fn next_logical(&self) -> Option { + if self.path.logical_index() + 1 < self.run.cluster_range().len() { + // Fast path: next cluster is in the same run + ClusterPath { + line_index: self.path.line_index, + run_index: self.path.run_index, + logical_index: self.path.logical_index + 1, + } + .cluster(self.run.layout) + } else { + let index = self.text_range().end; + if index >= self.run.layout.data.text_len { + return None; + } + // We have to search for the cluster containing our end index + Self::from_byte_index(self.run.layout, index) + } + } + + /// Returns the cluster that precedes this one in logical order. + pub fn previous_logical(&self) -> Option { + if self.path.logical_index > 0 { + // Fast path: previous cluster is in the same run + ClusterPath { + line_index: self.path.line_index, + run_index: self.path.run_index, + logical_index: self.path.logical_index - 1, + } + .cluster(self.run.layout) + } else { + Self::from_byte_index(self.run.layout, self.text_range().start.checked_sub(1)?) + } + } + + /// Returns the cluster that follows this one in visual order. + pub fn next_visual(&self) -> Option { + let layout = self.run.layout; + let run = self.run.clone(); + let visual_index = run.logical_to_visual(self.path.logical_index())?; + if let Some(cluster_index) = run.visual_to_logical(visual_index + 1) { + // Fast path: next visual cluster is in the same run + run.get(cluster_index) + } else { + // We just want to find the first line/run following this one that + // contains any cluster. + let mut run_index = self.path.run_index() + 1; + for line_index in self.path.line_index()..layout.len() { + let line = layout.get(line_index)?; + for run_index in run_index..line.len() { + if let Some(run) = line.item(run_index).and_then(|item| item.run()) { + if !run.cluster_range().is_empty() { + return ClusterPath { + line_index: line_index as u32, + run_index: run_index as u32, + logical_index: run.visual_to_logical(0)? as u32, + } + .cluster(layout); + } + } + } + // Restart at first run on next line + run_index = 0; + } + None + } + } + + /// Returns the cluster that precedes this one in visual order. + pub fn previous_visual(&self) -> Option { + let visual_index = self.run.logical_to_visual(self.path.logical_index())?; + if let Some(cluster_index) = visual_index + .checked_sub(1) + .and_then(|visual_index| self.run.visual_to_logical(visual_index)) + { + // Fast path: previous visual cluster is in the same run + ClusterPath { + line_index: self.path.line_index, + run_index: self.path.run_index, + logical_index: cluster_index as u32, + } + .cluster(self.run.layout) + } else { + // We just want to find the first line/run preceding this one that + // contains any cluster. + let layout = self.run.layout; + let mut run_index = Some(self.path.run_index()); + for line_index in (0..=self.path.line_index()).rev() { + let line = layout.get(line_index)?; + let first_run = run_index.unwrap_or(line.len()); + for run_index in (0..first_run).rev() { + if let Some(run) = line.item(run_index).and_then(|item| item.run()) { + let range = run.cluster_range(); + if !range.is_empty() { + return ClusterPath { + line_index: line_index as u32, + run_index: run_index as u32, + logical_index: run.visual_to_logical(range.len() - 1)? as u32, + } + .cluster(layout); + } + } + } + run_index = None; + } + None + } + } + + /// Returns the next cluster that is marked as a word boundary. + pub fn next_logical_word(&self) -> Option { + let mut cluster = self.clone(); + while let Some(next) = cluster.next_logical() { + if next.is_word_boundary() { + return Some(next); + } + cluster = next; + } + None + } + + /// Returns the next cluster that is marked as a word boundary. + pub fn next_visual_word(&self) -> Option { + let mut cluster = self.clone(); + while let Some(next) = cluster.next_visual() { + if next.is_word_boundary() { + return Some(next); + } + cluster = next; + } + None + } + + /// Returns the previous cluster that is marked as a word boundary. + pub fn previous_logical_word(&self) -> Option { + let mut cluster = self.clone(); + while let Some(prev) = cluster.previous_logical() { + if prev.is_word_boundary() { + return Some(prev); + } + cluster = prev; + } + None + } + + /// Returns the previous cluster that is marked as a word boundary. + pub fn previous_visual_word(&self) -> Option { + let mut cluster = self.clone(); + while let Some(prev) = cluster.previous_visual() { + if prev.is_word_boundary() { + return Some(prev); + } + cluster = prev; + } + None + } + + /// Returns the visual offset of this cluster along direction of text flow. + /// + /// This cost of this function is roughly linear in the number of clusters + /// on the containing line. + pub fn visual_offset(&self) -> Option { + let line = self.path.line(self.run.layout)?; + let mut offset = line.metrics().offset; + for run_index in 0..=self.path.run_index() { + let item = line.item(run_index)?; + match item { + LineItem::Run(run) => { + if run_index != self.path.run_index() { + offset += run.advance(); + } else { + let visual_index = run.logical_to_visual(self.path.logical_index())?; + for cluster in run.visual_clusters().take(visual_index) { + offset += cluster.advance(); + } + } + } + LineItem::InlineBox(inline_box) => { + offset += inline_box.width; + } + } + } + Some(offset) + } + + pub(crate) fn info(&self) -> &super::data::ClusterInfo { + &self.data.info + } + + /// Returns the text length of the cluster in bytes. + /// + /// This is only used for tests, and is *not* part of the public API. + #[doc(hidden)] + pub fn text_len(&self) -> u8 { + self.data.text_len + } + + /// Returns this cluster's original character. + /// + /// This is only used for tests, and is *not* part of the public API. + #[doc(hidden)] + pub fn source_char(&self) -> char { + self.data.info.source_char() + } +} + +/// Determines how a cursor attaches to a cluster. +#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)] +pub enum Affinity { + /// Cursor is attached to the character that is logically following in the + /// text stream. + #[default] + Downstream = 0, + /// Cursor is attached to the character that is logically preceding in the + /// text stream. + Upstream = 1, +} + +impl Affinity { + #[must_use] + pub fn invert(&self) -> Self { + match self { + Self::Downstream => Self::Upstream, + Self::Upstream => Self::Downstream, + } + } +} + +/// Index based path to a cluster. +#[derive(Copy, Clone, PartialEq, Eq, Hash, Default, Debug)] +pub struct ClusterPath { + pub(crate) line_index: u32, + pub(crate) run_index: u32, + pub(crate) logical_index: u32, +} + +impl ClusterPath { + pub(crate) fn new(line_index: u32, run_index: u32, logical_index: u32) -> Self { + Self { + line_index, + run_index, + logical_index, + } + } + + /// Returns the index of the line containing this cluster. + pub fn line_index(&self) -> usize { + self.line_index as usize + } + + /// Returns the index of the run (within the owning line) containing this + /// cluster. + pub fn run_index(&self) -> usize { + self.run_index as usize + } + + /// Returns the logical index of the cluster within the owning run. + pub fn logical_index(&self) -> usize { + self.logical_index as usize + } + + /// Returns the line for this path and the specified layout. + pub fn line<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { + layout.get(self.line_index()) + } + + /// Returns the run for this path and the specified layout. + pub fn run<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { + self.line(layout)?.item(self.run_index())?.run() + } + + /// Returns the cluster for this path and the specified layout. + pub fn cluster<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { + self.run(layout)?.get(self.logical_index()) + } +} + +#[derive(Clone)] +enum GlyphIter<'a> { + Single(Option), + Slice(core::slice::Iter<'a, Glyph>), +} + +impl Iterator for GlyphIter<'_> { + type Item = Glyph; + + fn next(&mut self) -> Option { + match self { + Self::Single(glyph) => glyph.take(), + Self::Slice(iter) => { + let glyph = *iter.next()?; + Some(glyph) + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::{ + Alignment, AlignmentOptions, Cluster, FontContext, Layout, LayoutContext, + PositionedLayoutItem, StyleProperty, + }; + + type Brush = (); + + fn create_unaligned_layout() -> Layout { + let mut layout_ctx = LayoutContext::new(); + // TODO: Use a test font + let mut font_ctx = FontContext::new(); + let text = "Parley exists"; + let mut builder = layout_ctx.ranged_builder(&mut font_ctx, text, 1.0, true); + builder.push_default(StyleProperty::FontSize(10.)); + let mut layout = builder.build(text); + layout.break_all_lines(None); + layout + } + + fn cluster_from_position_with_alignment(alignment: Alignment) { + let mut layout = create_unaligned_layout(); + layout.align(alignment, AlignmentOptions::default()); + assert_eq!( + layout.len(), + 1, + "Text doesn't contain any newlines, and there's no max advance" + ); + let line = layout.get(0).unwrap(); + + let mut test_count = 0; + for item in line.items() { + let PositionedLayoutItem::GlyphRun(run) = item else { + unreachable!("No inline boxes set up"); + }; + for glyph in run.positioned_glyphs() { + test_count += 1; + let cluster = Cluster::from_point(&layout, glyph.x + 0.1, glyph.y).unwrap(); + assert_eq!(cluster.0.glyphs().next().unwrap().id, glyph.id); + } + } + assert!(test_count > 5); + } + + #[test] + fn cluster_from_position_start_alignment() { + cluster_from_position_with_alignment(Alignment::Start); + } + #[test] + fn cluster_from_position_center_alignment() { + cluster_from_position_with_alignment(Alignment::Center); + } + #[test] + fn cluster_from_position_end_alignment() { + cluster_from_position_with_alignment(Alignment::End); + } + #[test] + fn cluster_from_position_justified_alignment() { + cluster_from_position_with_alignment(Alignment::Justify); + } +} diff --git a/vendor/parley-0.11.1/src/layout/data.rs b/vendor/parley-0.11.1/src/layout/data.rs new file mode 100644 index 0000000000..19ba8e615b --- /dev/null +++ b/vendor/parley-0.11.1/src/layout/data.rs @@ -0,0 +1,962 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use crate::inline_box::InlineBox; +use crate::layout::{ContentWidths, Glyph, LineMetrics, RunMetrics, Style}; +use crate::style::Brush; +use crate::util::nearly_zero; +use crate::{FontData, IndentOptions, InlineBoxKind, LineHeight, OverflowWrap, TextWrapMode}; +use core::ops::Range; + +use alloc::vec::Vec; + +use crate::analysis::cluster::Whitespace; +use crate::analysis::{Boundary, CharInfo}; + +#[derive(Copy, Clone, Debug, PartialEq)] +pub(crate) struct ClusterData { + pub(crate) info: ClusterInfo, + /// Cluster flags (see impl methods for details). + pub(crate) flags: u16, + /// Style index for this cluster. + pub(crate) style_index: u16, + /// Number of glyphs in this cluster (0xFF = single glyph stored inline) + pub(crate) glyph_len: u8, + /// Number of text bytes in this cluster + pub(crate) text_len: u8, + /// If `glyph_len == 0xFF`, then `glyph_offset` is a glyph identifier, + /// otherwise, it's an offset into the glyph array with the base + /// taken from the owning run. + pub(crate) glyph_offset: u32, + /// Offset into the text for this cluster + pub(crate) text_offset: u16, + /// Advance width for this cluster + pub(crate) advance: f32, +} + +impl ClusterData { + pub(crate) const LIGATURE_START: u16 = 1; + pub(crate) const LIGATURE_COMPONENT: u16 = 2; + + #[inline(always)] + pub(crate) fn is_ligature_start(self) -> bool { + self.flags & Self::LIGATURE_START != 0 + } + + #[inline(always)] + pub(crate) fn is_ligature_component(self) -> bool { + self.flags & Self::LIGATURE_COMPONENT != 0 + } + + #[inline(always)] + pub(crate) fn text_range(self, run: &RunData) -> Range { + let start = run.text_range.start + self.text_offset as usize; + start..start + self.text_len as usize + } +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub(crate) struct ClusterInfo { + boundary: Boundary, + source_char: char, +} + +impl ClusterInfo { + pub(crate) fn new(boundary: Boundary, source_char: char) -> Self { + Self { + boundary, + source_char, + } + } + + // Returns the boundary type of the cluster. + pub(crate) fn boundary(self) -> Boundary { + self.boundary + } + + // Returns the whitespace type of the cluster. + pub(crate) fn whitespace(self) -> Whitespace { + to_whitespace(self.source_char) + } + + /// Returns if the cluster is a line boundary. + pub(crate) fn is_boundary(self) -> bool { + self.boundary != Boundary::None + } + + /// Returns if the cluster is an emoji. + pub(crate) fn is_emoji(self) -> bool { + // TODO: Defer to ICU4X properties (see: https://docs.rs/icu/latest/icu/properties/props/struct.Emoji.html). + matches!(self.source_char as u32, 0x1F600..=0x1F64F | 0x1F300..=0x1F5FF | 0x1F680..=0x1F6FF | 0x2600..=0x26FF | 0x2700..=0x27BF) + } + + /// Returns if the cluster is any whitespace. + pub(crate) fn is_whitespace(self) -> bool { + self.source_char.is_whitespace() + } + + /// Returns the cluster's original character. + pub(crate) fn source_char(self) -> char { + self.source_char + } +} + +const fn to_whitespace(c: char) -> Whitespace { + const LINE_SEPARATOR: char = '\u{2028}'; + const PARAGRAPH_SEPARATOR: char = '\u{2029}'; + + match c { + ' ' => Whitespace::Space, + '\t' => Whitespace::Tab, + '\n' | '\r' | LINE_SEPARATOR | PARAGRAPH_SEPARATOR => Whitespace::Newline, + '\u{00A0}' => Whitespace::NoBreakSpace, + _ => Whitespace::None, + } +} + +/// `HarfRust`-based run data +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct RunData { + /// Index of the font for the run. + pub(crate) font_index: usize, + /// Font size. + pub(crate) font_size: f32, + /// Font attributes, needed for accessibility. + pub(crate) font_attrs: fontique::Attributes, + /// Synthesis for rendering (contains variation settings) + pub(crate) synthesis: fontique::Synthesis, + /// Range of normalized coordinates in the layout data. + pub(crate) coords_range: Range, + /// Range of the source text. + pub(crate) text_range: Range, + /// Bidi level for the run. + pub(crate) bidi_level: u8, + /// Range of clusters. + pub(crate) cluster_range: Range, + /// Base for glyph indices. + pub(crate) glyph_start: usize, + /// Metrics for the run. + pub(crate) metrics: RunMetrics, + /// Additional word spacing. + pub(crate) word_spacing: f32, + /// Additional letter spacing. + pub(crate) letter_spacing: f32, + /// Total advance of the run. + pub(crate) advance: f32, +} + +#[derive(Copy, Clone, Default, PartialEq, Debug)] +pub enum BreakReason { + #[default] + None, + Regular, + Explicit, + Emergency, +} + +#[derive(Clone, Default, Debug, PartialEq)] +pub(crate) struct LineData { + /// Range of the source text. + pub(crate) text_range: Range, + /// Range of line items. + pub(crate) item_range: Range, + /// Metrics for the line. + pub(crate) metrics: LineMetrics, + /// The cause of the line break. + pub(crate) break_reason: BreakReason, + /// Maximum advance for the line. + pub(crate) max_advance: f32, + /// Number of justified clusters on the line. + pub(crate) num_spaces: usize, + /// Text indent applied to this line. + pub(crate) indent: f32, +} + +impl LineData { + pub(crate) fn size(&self) -> f32 { + self.metrics.ascent + self.metrics.descent + self.metrics.leading + } +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct LineItemData { + /// Whether the item is a run or an inline box + pub(crate) kind: LayoutItemKind, + /// The index of the run or inline box in the runs or `inline_boxes` vec + pub(crate) index: usize, + /// Bidi level for the item (used for reordering) + pub(crate) bidi_level: u8, + /// Advance (size in direction of text flow) for the run. + pub(crate) advance: f32, + + // Fields that only apply to text runs (Ignored for boxes) + // TODO: factor this out? + /// True if the run is composed entirely of whitespace. + pub(crate) is_whitespace: bool, + /// True if the run ends in whitespace. + pub(crate) has_trailing_whitespace: bool, + /// Range of the source text. + pub(crate) text_range: Range, + /// Range of clusters. + pub(crate) cluster_range: Range, +} + +impl LineItemData { + pub(crate) fn is_text_run(&self) -> bool { + self.kind == LayoutItemKind::TextRun + } + + #[inline(always)] + pub(crate) fn is_rtl(&self) -> bool { + self.bidi_level & 1 != 0 + } + + /// If the item is a text run + /// - Determine if it consists entirely of whitespace (`is_whitespace` property) + /// - Determine if it has trailing whitespace (`has_trailing_whitespace` property) + pub(crate) fn compute_whitespace_properties(&mut self, layout_data: &LayoutData) { + // Skip items which are not text runs + if self.kind != LayoutItemKind::TextRun { + return; + } + + self.is_whitespace = true; + if self.is_rtl() { + // RTL runs check for "trailing" whitespace at the front. + for cluster in layout_data.clusters[self.cluster_range.clone()].iter() { + if cluster.info.is_whitespace() { + self.has_trailing_whitespace = true; + } else { + self.is_whitespace = false; + break; + } + } + } else { + for cluster in layout_data.clusters[self.cluster_range.clone()] + .iter() + .rev() + { + if cluster.info.is_whitespace() { + self.has_trailing_whitespace = true; + } else { + self.is_whitespace = false; + break; + } + } + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum LayoutItemKind { + TextRun, + InlineBox, +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct LayoutItem { + /// Whether the item is a run or an inline box + pub(crate) kind: LayoutItemKind, + /// The index of the run or inline box in the runs or `inline_boxes` vec + pub(crate) index: usize, + /// Bidi level for the item (used for reordering) + pub(crate) bidi_level: u8, +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct LayoutData { + // General settings (directly from the "builder") + /// The display scale factor + pub(crate) scale: f32, + /// Whether metrics should be quantized to pixel boundaries + pub(crate) quantize: bool, + /// The `BiDi` base level + pub(crate) base_level: u8, + /// The length of the text in the layout + pub(crate) text_len: usize, + + // Output of style resolution (input to line breaking) + pub(crate) styles: Vec>, + pub(crate) inline_boxes: Vec, + + // Output of shaping (input to line breaking) + pub(crate) fonts: Vec, + pub(crate) coords: Vec, + pub(crate) runs: Vec, + pub(crate) items: Vec, + pub(crate) clusters: Vec, + pub(crate) glyphs: Vec, + + // Output of line breaking + /// The lines in the + pub(crate) lines: Vec, + /// Items within each line + pub(crate) line_items: Vec, + /// The width constraint that was used to line break the layout + pub(crate) layout_max_advance: f32, + /// The computed width of the layout excluding trailing whitespace + pub(crate) width: f32, + /// The computed width of the layout including trailing whitespace + pub(crate) full_width: f32, + /// The computed height of the layout + pub(crate) height: f32, + + // Output of alignment + #[cfg(feature = "accesskit")] + /// Directly store the alignment if accessibility is enabled so we can + /// set the corresponding AccessKit property. + pub(crate) alignment: Option, + /// Whether the layout is aligned with [`crate::Alignment::Justify`]. + pub(crate) is_aligned_justified: bool, + /// The text-indent amount in layout units. + pub(crate) indent_amount: f32, + /// Options controlling text-indent behavior (each-line, hanging). + pub(crate) indent_options: IndentOptions, +} + +impl Default for LayoutData { + fn default() -> Self { + Self { + scale: 1., + quantize: true, + base_level: 0, + text_len: 0, + width: 0., + full_width: 0., + height: 0., + fonts: Vec::new(), + coords: Vec::new(), + styles: Vec::new(), + inline_boxes: Vec::new(), + runs: Vec::new(), + items: Vec::new(), + clusters: Vec::new(), + glyphs: Vec::new(), + lines: Vec::new(), + line_items: Vec::new(), + #[cfg(feature = "accesskit")] + alignment: None, + is_aligned_justified: false, + layout_max_advance: 0.0, + indent_amount: 0.0, + indent_options: IndentOptions::default(), + } + } +} + +impl LayoutData { + pub(crate) fn clear(&mut self) { + self.scale = 1.; + self.quantize = true; + self.base_level = 0; + self.text_len = 0; + self.width = 0.; + self.full_width = 0.; + self.height = 0.; + self.fonts.clear(); + self.coords.clear(); + self.styles.clear(); + self.inline_boxes.clear(); + self.runs.clear(); + self.items.clear(); + self.clusters.clear(); + self.glyphs.clear(); + self.lines.clear(); + self.line_items.clear(); + } + + /// Push an inline box to the list of items + pub(crate) fn push_inline_box(&mut self, index: usize) { + // Give the box the same bidi level as the preceding text run + // (or else default to 0 if there is not yet a text run) + let bidi_level = self.runs.last().map(|r| r.bidi_level).unwrap_or(0); + + self.items.push(LayoutItem { + kind: LayoutItemKind::InlineBox, + index, + bidi_level, + }); + } + #[allow(clippy::too_many_arguments)] + pub(crate) fn push_run( + &mut self, + font: FontData, + font_size: f32, + font_attrs: fontique::Attributes, + synthesis: fontique::Synthesis, + glyph_buffer: &harfrust::GlyphBuffer, + bidi_level: u8, + style_index: u16, + word_spacing: f32, + letter_spacing: f32, + source_text: &str, + char_infos: &[(CharInfo, u16)], // From text analysis + text_range: Range, // The text range this run covers + coords: &[harfrust::NormalizedCoord], + ) { + let coords_start = self.coords.len(); + self.coords.extend(coords.iter().map(|c| c.to_bits())); + let coords_end = self.coords.len(); + + let font_index = self + .fonts + .iter() + .position(|f| *f == font) + .unwrap_or_else(|| { + let index = self.fonts.len(); + self.fonts.push(font); + index + }); + + let metrics = { + let font = &self.fonts[font_index]; + let font_ref = skrifa::FontRef::from_index(font.data.as_ref(), font.index).unwrap(); + skrifa::metrics::Metrics::new(&font_ref, skrifa::prelude::Size::new(font_size), coords) + }; + let units_per_em = metrics.units_per_em as f32; + + let metrics = { + let (underline_offset, underline_size) = if let Some(underline) = metrics.underline { + (underline.offset, underline.thickness) + } else { + // Default values from Harfbuzz: https://github.com/harfbuzz/harfbuzz/blob/00492ec7df0038f41f78d43d477c183e4e4c506e/src/hb-ot-metrics.cc#L334 + let default = units_per_em / 18.0; + (default, default) + }; + let (strikethrough_offset, strikethrough_size) = + if let Some(strikeout) = metrics.strikeout { + (strikeout.offset, strikeout.thickness) + } else { + // Default values from HarfBuzz: https://github.com/harfbuzz/harfbuzz/blob/00492ec7df0038f41f78d43d477c183e4e4c506e/src/hb-ot-metrics.cc#L334-L347 + (metrics.ascent / 2.0, units_per_em / 18.0) + }; + + // Compute line height + let style = &self.styles[style_index as usize]; + let line_height = match style.line_height { + LineHeight::Absolute(value) => value, + LineHeight::FontSizeRelative(value) => value * font_size, + LineHeight::MetricsRelative(value) => { + (metrics.ascent - metrics.descent + metrics.leading) * value + } + }; + + RunMetrics { + ascent: metrics.ascent, + descent: -metrics.descent, + leading: metrics.leading, + underline_offset, + underline_size, + strikethrough_offset, + strikethrough_size, + line_height, + x_height: metrics.x_height, + cap_height: metrics.cap_height, + } + }; + + let cluster_range = self.clusters.len()..self.clusters.len(); + + let mut run = RunData { + font_index, + font_size, + font_attrs, + synthesis, + coords_range: coords_start..coords_end, + text_range, + bidi_level, + cluster_range, + glyph_start: self.glyphs.len(), + metrics, + word_spacing, + letter_spacing, + advance: 0., + }; + + // `HarfRust` returns glyphs in visual order, so we need to process them as such while + // maintaining logical ordering of clusters. + + let glyph_infos = glyph_buffer.glyph_infos(); + if glyph_infos.is_empty() { + return; + } + let glyph_positions = glyph_buffer.glyph_positions(); + let scale_factor = font_size / units_per_em; + let cluster_range_start = self.clusters.len(); + let is_rtl = bidi_level & 1 == 1; + if !is_rtl { + run.advance = process_clusters( + Direction::Ltr, + &mut self.clusters, + &mut self.glyphs, + scale_factor, + glyph_infos, + glyph_positions, + char_infos, + source_text.char_indices(), + ); + } else { + run.advance = process_clusters( + Direction::Rtl, + &mut self.clusters, + &mut self.glyphs, + scale_factor, + glyph_infos, + glyph_positions, + char_infos, + source_text.char_indices().rev(), + ); + // Reverse clusters into logical order for RTL + let clusters_len = self.clusters.len(); + self.clusters[cluster_range_start..clusters_len].reverse(); + } + + run.cluster_range = cluster_range_start..self.clusters.len(); + if !run.cluster_range.is_empty() { + self.runs.push(run); + self.items.push(LayoutItem { + kind: LayoutItemKind::TextRun, + index: self.runs.len() - 1, + bidi_level, + }); + } + } + + pub(crate) fn finish(&mut self) { + for run in &self.runs { + let word = run.word_spacing; + let letter = run.letter_spacing; + if nearly_zero(word) && nearly_zero(letter) { + continue; + } + let clusters = &mut self.clusters[run.cluster_range.clone()]; + for cluster in clusters { + let mut spacing = letter; + if !nearly_zero(word) && cluster.info.whitespace().is_space_or_nbsp() { + spacing += word; + } + if !nearly_zero(spacing) { + cluster.advance += spacing; + if cluster.glyph_len != 0xFF { + let start = run.glyph_start + cluster.glyph_offset as usize; + let end = start + cluster.glyph_len as usize; + let glyphs = &mut self.glyphs[start..end]; + if let Some(last) = glyphs.last_mut() { + last.advance += spacing; + } + } + } + } + } + } + + // TODO: this method does not handle mixed direction text at all. + pub(crate) fn calculate_content_widths(&self) -> ContentWidths { + fn whitespace_advance(cluster: Option<&ClusterData>) -> f32 { + cluster + .filter(|cluster| cluster.info.whitespace().is_space_or_nbsp()) + .map_or(0.0, |cluster| cluster.advance) + } + + let mut min_width = 0.0_f32; + let mut max_width = 0.0_f32; + + let mut running_min_width = 0.0; + let mut running_max_width = 0.0; + let mut text_wrap_mode = TextWrapMode::Wrap; + let mut prev_cluster: Option<&ClusterData> = None; + let is_rtl = self.base_level & 1 == 1; + for item in &self.items { + match item.kind { + LayoutItemKind::TextRun => { + let run = &self.runs[item.index]; + let clusters = &self.clusters[run.cluster_range.clone()]; + if is_rtl { + prev_cluster = clusters.first(); + } + for cluster in clusters { + let boundary = cluster.info.boundary(); + let style = &self.styles[cluster.style_index as usize]; + let prev_text_wrap_mode = text_wrap_mode; + text_wrap_mode = style.text_wrap_mode; + if boundary == Boundary::Mandatory + || (prev_text_wrap_mode == TextWrapMode::Wrap + && (boundary == Boundary::Line + || style.overflow_wrap == OverflowWrap::Anywhere)) + { + let trailing_whitespace = whitespace_advance(prev_cluster); + min_width = min_width.max(running_min_width - trailing_whitespace); + running_min_width = 0.0; + if boundary == Boundary::Mandatory { + max_width = max_width.max(running_max_width - trailing_whitespace); + running_max_width = 0.0; + } + } + running_min_width += cluster.advance; + running_max_width += cluster.advance; + if !is_rtl { + prev_cluster = Some(cluster); + } + } + let trailing_whitespace = whitespace_advance(prev_cluster); + min_width = min_width.max(running_min_width - trailing_whitespace); + } + LayoutItemKind::InlineBox => { + let ibox = &self.inline_boxes[item.index]; + if ibox.kind == InlineBoxKind::InFlow { + running_max_width += ibox.width; + if text_wrap_mode == TextWrapMode::Wrap { + let trailing_whitespace = whitespace_advance(prev_cluster); + min_width = min_width.max(running_min_width - trailing_whitespace); + min_width = min_width.max(ibox.width); + running_min_width = 0.0; + } else { + running_min_width += ibox.width; + } + } + prev_cluster = None; + } + } + let trailing_whitespace = whitespace_advance(prev_cluster); + max_width = max_width.max(running_max_width - trailing_whitespace); + } + + let trailing_whitespace = whitespace_advance(prev_cluster); + min_width = min_width.max(running_min_width - trailing_whitespace); + + ContentWidths { + min: min_width, + max: max_width, + } + } +} + +/// Processes shaped glyphs from `HarfRust` and converts them into `ClusterData` and `Glyph`. +/// +/// # Parameters +/// +/// ## Output Parameters (mutated by this function): +/// * `clusters` - Vector where new `ClusterData` entries will be pushed. +/// * `glyphs` - Vector where new `Glyph` entries will be pushed. Note: single-glyph clusters +/// with zero offsets may be inlined directly into `ClusterData`. +/// +/// ## Input Parameters: +/// * `direction` - Direction of the text. +/// * `scale_factor` - Scaling factor used to convert font units to the target size. +/// * `glyph_infos` - `HarfRust` glyph information in visual order. +/// * `glyph_positions` - `HarfRust` glyph positioning data in visual order. +/// * `char_infos` - Character information from text analysis, indexed by cluster ID. +/// * `char_indices_iter` - Iterator over (`byte_offset`, `char`) pairs from the source text. +/// Should be in logical order (forward for LTR, reverse for RTL). +fn process_clusters>( + direction: Direction, + clusters: &mut Vec, + glyphs: &mut Vec, + scale_factor: f32, + glyph_infos: &[harfrust::GlyphInfo], + glyph_positions: &[harfrust::GlyphPosition], + char_infos: &[(CharInfo, u16)], + char_indices_iter: I, +) -> f32 { + let mut char_indices_iter = char_indices_iter.peekable(); + let mut cluster_start_char = char_indices_iter.next().unwrap(); + let mut total_glyphs: u32 = 0; + let mut cluster_glyph_offset: u32 = 0; + let start_cluster_id = glyph_infos.first().unwrap().cluster; + let mut cluster_id = start_cluster_id; + let mut char_info = char_infos[cluster_id as usize]; + let mut run_advance = 0.0; + let mut cluster_advance = 0.0; + // If the current cluster might be a single-glyph, zero-offset cluster, we defer + // pushing the first glyph to `glyphs` because it might be inlined into `ClusterData`. + let mut pending_inline_glyph: Option = None; + + // The mental model for understanding this function is best grasped by first reading + // the HarfBuzz docs on [clusters](https://harfbuzz.github.io/working-with-harfbuzz-clusters.html). + // + // `num_components` is the number of characters in the current cluster. Since source text's characters + // were inserted into `HarfRust`'s buffer using their logical indices as the cluster ID, `HarfRust` will + // assign the first character's cluster ID (in logical order) to the merged cluster because the minimum + // ID is selected for [merging](https://github.com/harfbuzz/harfrust/blob/a38025fb336230b492366740c86021bb406bcd0d/src/hb/buffer.rs#L920-L924). + // + // So, the number of components in a given cluster is dependent on `direction`. + // - In LTR, `num_components` is the difference between the next cluster and the current cluster. + // - In RTL, `num_components` is the difference between the last cluster and the current cluster. + // This is because we must compare the current cluster to its next larger ID (in other words, the next + // logical index, which is visually downstream in LTR and visually upstream in RTL). + // + // For example, consider the LTR text for "afi" where "fi" form a ligature. + // Initial cluster values: 0, 1, 2 (logical + visual order) + // `HarfRust` assignation: 0, 1, 1 + // Cluster count: 2 + // `num_components`: (1 - 0 =) 1, (3 - 1 =) 2 + // + // Now consider the RTL text for "حداً". + // Initial cluster values: 0, 1, 2, 3 (logical, or in-memory, order) + // Reversed cluster values: 3, 2, 1, 0 (visual order - the return order of `HarfRust` for RTL) + // `HarfRust` assignation: 3, 2, 0, 0 + // Cluster count: 3 + // `num_components`: (4 - 3 =) 1, (3 - 2 =) 1, (2 - 0 =) 2 + let num_components = + |next_cluster: u32, current_cluster: u32, last_cluster: u32| match direction { + Direction::Ltr => next_cluster - current_cluster, + Direction::Rtl => last_cluster - current_cluster, + }; + let mut last_cluster_id: u32 = match direction { + Direction::Ltr => 0, + Direction::Rtl => char_infos.len() as u32, + }; + + for (glyph_info, glyph_pos) in glyph_infos.iter().zip(glyph_positions.iter()) { + // Flush previous cluster if we've reached a new cluster + if cluster_id != glyph_info.cluster { + run_advance += cluster_advance; + let num_components = num_components(glyph_info.cluster, cluster_id, last_cluster_id); + cluster_advance /= num_components as f32; + let is_newline = to_whitespace(cluster_start_char.1) == Whitespace::Newline; + let cluster_type = if num_components > 1 { + debug_assert!(!is_newline); + ClusterType::LigatureStart + } else if is_newline { + ClusterType::Newline + } else { + ClusterType::Regular + }; + + let inline_glyph_id = if matches!(cluster_type, ClusterType::Regular) { + pending_inline_glyph.take().map(|g| g.id) + } else { + // This isn't a regular cluster, so we don't inline the glyph and push + // it to `glyphs`. + if let Some(pending) = pending_inline_glyph.take() { + glyphs.push(pending); + total_glyphs += 1; + } + None + }; + + push_cluster( + clusters, + char_info, + cluster_start_char, + cluster_glyph_offset, + cluster_advance, + total_glyphs, + cluster_type, + inline_glyph_id, + ); + cluster_glyph_offset = total_glyphs; + + if num_components > 1 { + // Skip characters until we reach the current cluster + for i in 1..num_components { + cluster_start_char = char_indices_iter.next().unwrap(); + if to_whitespace(cluster_start_char.1) == Whitespace::Space { + break; + } + let char_info_ = match direction { + Direction::Ltr => char_infos[(cluster_id + i) as usize], + Direction::Rtl => char_infos[(cluster_id + num_components - i) as usize], + }; + push_cluster( + clusters, + char_info_, + cluster_start_char, + cluster_glyph_offset, + cluster_advance, + total_glyphs, + ClusterType::LigatureComponent, + None, + ); + } + } + cluster_start_char = char_indices_iter.next().unwrap(); + + cluster_advance = 0.0; + last_cluster_id = cluster_id; + cluster_id = glyph_info.cluster; + char_info = char_infos[cluster_id as usize]; + pending_inline_glyph = None; + } + + let glyph = Glyph { + id: glyph_info.glyph_id, + style_index: char_info.1, + x: (glyph_pos.x_offset as f32) * scale_factor, + // Convert from font space (Y-up) to layout space (Y-down) + y: -(glyph_pos.y_offset as f32) * scale_factor, + advance: (glyph_pos.x_advance as f32) * scale_factor, + }; + cluster_advance += glyph.advance; + // Push any pending glyph. If it was a zero-offset, single glyph cluster, it would + // have been pushed in the first `if` block. + if let Some(pending) = pending_inline_glyph.take() { + glyphs.push(pending); + total_glyphs += 1; + } + if total_glyphs == cluster_glyph_offset && glyph.x == 0.0 && glyph.y == 0.0 { + // Defer this potential zero-offset, single glyph cluster + pending_inline_glyph = Some(glyph); + } else { + glyphs.push(glyph); + total_glyphs += 1; + } + } + + // Push the last cluster + { + // See comment above `num_components` for why we use `char_infos.len()` for LTR and 0 for RTL. + let next_cluster_id = match direction { + Direction::Ltr => char_infos.len() as u32, + Direction::Rtl => 0, + }; + let num_components = num_components(next_cluster_id, cluster_id, last_cluster_id); + if num_components > 1 { + // This is a ligature - create ligature start + ligature components + + if let Some(pending) = pending_inline_glyph.take() { + glyphs.push(pending); + total_glyphs += 1; + } + let ligature_advance = cluster_advance / num_components as f32; + push_cluster( + clusters, + char_info, + cluster_start_char, + cluster_glyph_offset, + ligature_advance, + total_glyphs, + ClusterType::LigatureStart, + None, + ); + + cluster_glyph_offset = total_glyphs; + + // Create ligature component clusters for the remaining characters + for (i, char) in (1..).zip(char_indices_iter) { + if to_whitespace(char.1) == Whitespace::Space { + break; + } + let component_char_info = match direction { + Direction::Ltr => char_infos[(cluster_id + i) as usize], + Direction::Rtl => char_infos[(cluster_id + num_components - i) as usize], + }; + push_cluster( + clusters, + component_char_info, + char, + cluster_glyph_offset, + ligature_advance, + total_glyphs, + ClusterType::LigatureComponent, + None, + ); + } + } else { + let is_newline = to_whitespace(cluster_start_char.1) == Whitespace::Newline; + let cluster_type = if is_newline { + ClusterType::Newline + } else { + ClusterType::Regular + }; + let mut inline_glyph_id = None; + match cluster_type { + ClusterType::Regular => { + if total_glyphs == cluster_glyph_offset { + if let Some(pending) = pending_inline_glyph.take() { + inline_glyph_id = Some(pending.id); + } + } + } + _ => { + if let Some(pending) = pending_inline_glyph.take() { + glyphs.push(pending); + total_glyphs += 1; + } + } + } + push_cluster( + clusters, + char_info, + cluster_start_char, + cluster_glyph_offset, + cluster_advance, + total_glyphs, + cluster_type, + inline_glyph_id, + ); + } + } + + run_advance +} + +#[derive(PartialEq)] +enum Direction { + Ltr, + Rtl, +} + +enum ClusterType { + LigatureStart, + LigatureComponent, + Regular, + Newline, +} + +impl From<&ClusterType> for u16 { + fn from(cluster_type: &ClusterType) -> Self { + match cluster_type { + ClusterType::LigatureStart => ClusterData::LIGATURE_START, + ClusterType::LigatureComponent => ClusterData::LIGATURE_COMPONENT, + ClusterType::Regular | ClusterType::Newline => 0, // No special flags + } + } +} + +fn push_cluster( + clusters: &mut Vec, + char_info: (CharInfo, u16), + cluster_start_char: (usize, char), + glyph_offset: u32, + advance: f32, + total_glyphs: u32, + cluster_type: ClusterType, + inline_glyph_id: Option, +) { + let glyph_len = (total_glyphs - glyph_offset) as u8; + + let (final_glyph_len, final_glyph_offset, final_advance) = match cluster_type { + ClusterType::LigatureComponent => { + // Ligature components have no glyphs, only advance. + debug_assert_eq!(glyph_len, 0); + (0_u8, 0_u32, advance) + } + ClusterType::Newline => { + // Newline clusters are stripped of their glyph contribution. + debug_assert_eq!(glyph_len, 1); + (0_u8, 0_u32, 0.0) + } + _ if inline_glyph_id.is_some() => { + // Inline glyphs are stored inline within `ClusterData` + debug_assert_eq!(glyph_len, 0); + (0xFF_u8, inline_glyph_id.unwrap(), advance) + } + ClusterType::Regular | ClusterType::LigatureStart => { + // Regular and ligature start clusters maintain their glyphs and advance. + debug_assert_ne!(glyph_len, 0); + (glyph_len, glyph_offset, advance) + } + }; + + clusters.push(ClusterData { + info: ClusterInfo::new(char_info.0.boundary, cluster_start_char.1), + flags: (&cluster_type).into(), + style_index: char_info.1, + glyph_len: final_glyph_len, + text_len: cluster_start_char.1.len_utf8() as u8, + glyph_offset: final_glyph_offset, + text_offset: cluster_start_char.0 as u16, + advance: final_advance, + }); +} diff --git a/vendor/parley-0.11.1/src/layout/glyph.rs b/vendor/parley-0.11.1/src/layout/glyph.rs new file mode 100644 index 0000000000..5fcf9c857b --- /dev/null +++ b/vendor/parley-0.11.1/src/layout/glyph.rs @@ -0,0 +1,19 @@ +// Copyright 2025 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +/// Glyph with an offset and advance. +#[derive(Copy, Clone, Default, Debug, PartialEq)] +pub struct Glyph { + pub id: u32, + pub style_index: u16, + pub x: f32, + pub y: f32, + pub advance: f32, +} + +impl Glyph { + /// Returns the index into the layout style collection. + pub fn style_index(&self) -> usize { + self.style_index as usize + } +} diff --git a/vendor/parley-0.11.1/src/layout/layout.rs b/vendor/parley-0.11.1/src/layout/layout.rs new file mode 100644 index 0000000000..b9eac241d2 --- /dev/null +++ b/vendor/parley-0.11.1/src/layout/layout.rs @@ -0,0 +1,234 @@ +// Copyright 2025 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use crate::InlineBox; +use crate::layout::alignment::align; +use crate::layout::alignment::unjustify; +use crate::layout::data::LayoutData; +use crate::style::Brush; +use core::cmp::Ordering; +use core::fmt; + +use crate::IndentOptions; +use crate::layout::{ + ContentWidths, Style, alignment::Alignment, alignment::AlignmentOptions, line::Line, + line_break::BreakLines, +}; + +/// Text layout. +/// +/// The [`Debug`] implementation prints a compact summary by default. +/// The alternate form (`{:#?}`) formats the full underlying data. +/// +/// [`Debug`]: core::fmt::Debug +#[derive(Clone)] +pub struct Layout { + pub(crate) data: LayoutData, +} + +impl fmt::Debug for Layout { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if f.alternate() { + f.debug_struct("Layout").field("data", &self.data).finish() + } else { + f.debug_struct("Layout") + .field("text_len", &self.data.text_len) + .field("width", &self.data.width) + .field("height", &self.data.height) + .field("lines", &self.data.lines.len()) + .field("runs", &self.data.runs.len()) + .field("styles", &self.data.styles.len()) + .finish_non_exhaustive() + } + } +} + +impl Layout { + /// Creates an empty layout. + pub fn new() -> Self { + Self::default() + } + + /// Returns the scale factor provided when creating the layout. + pub fn scale(&self) -> f32 { + self.data.scale + } + + /// Returns the style collection for the layout. + pub fn styles(&self) -> &[Style] { + &self.data.styles + } + + /// The `max_advance` that was used to line break the `Layout` + pub fn layout_max_advance(&self) -> f32 { + self.data.layout_max_advance + } + + /// Returns the computed width of the layout excluding the width of trailing whitespace. + pub fn width(&self) -> f32 { + self.data.width + } + + /// Returns the computed width of the layout including the width of trailing whitespace. + pub fn full_width(&self) -> f32 { + self.data.full_width + } + + /// Calculates the lower and upper bounds on the width of the layout. These + /// are recalculated every time this method is called. + /// + /// This method currently may not return the correct results for + /// mixed-direction text. + pub fn calculate_content_widths(&self) -> ContentWidths { + self.data.calculate_content_widths() + } + + /// Returns the height of the layout. + pub fn height(&self) -> f32 { + self.data.height + } + + /// Returns the number of lines in the layout. + pub fn len(&self) -> usize { + self.data.lines.len() + } + + /// Returns `true` if the layout is empty. + pub fn is_empty(&self) -> bool { + self.data.lines.is_empty() + } + + /// Returns the line at the specified index. + /// + /// Returns `None` if the index is out of bounds, i.e. if it's + /// not less than [`self.len()`](Self::len). + pub fn get(&self, index: usize) -> Option> { + Some(Line { + index: index as u32, + layout: self, + data: self.data.lines.get(index)?, + }) + } + + /// Returns `true` if the dominant direction of the layout is right-to-left. + pub fn is_rtl(&self) -> bool { + self.data.base_level & 1 != 0 + } + + pub fn inline_boxes(&self) -> &[InlineBox] { + &self.data.inline_boxes + } + + pub fn inline_boxes_mut(&mut self) -> &mut [InlineBox] { + &mut self.data.inline_boxes + } + + /// Returns an iterator over the lines in the layout. + pub fn lines( + &self, + ) -> impl ExactSizeIterator> + DoubleEndedIterator + '_ + Clone { + self.data + .lines + .iter() + .enumerate() + .map(move |(index, data)| Line { + index: index as u32, + layout: self, + data, + }) + } + + /// Sets the text-indent for the layout. + /// + /// The indent is applied as a margin on the start edge of indented lines, reducing the + /// available width for line breaking and offsetting content during alignment. Negative + /// values cause the line to protrude beyond the start edge. + /// + /// This must be called before [`Layout::break_all_lines`] or [`Layout::break_lines`], + /// and before [`Layout::align`]. + pub fn set_text_indent(&mut self, amount: f32, options: IndentOptions) { + self.data.indent_amount = amount; + self.data.indent_options = options; + } + + /// Returns line breaker to compute lines for the layout. + pub fn break_lines(&mut self) -> BreakLines<'_, B> { + unjustify(&mut self.data); + BreakLines::new(self) + } + + /// Breaks all lines with the specified maximum advance. + pub fn break_all_lines(&mut self, max_advance: Option) { + self.break_lines() + .break_remaining(max_advance.unwrap_or(f32::MAX)); + } + + /// Apply alignment to the layout. + /// + /// You must perform line breaking prior to aligning, through [`Layout::break_lines`] or + /// [`Layout::break_all_lines`]. + /// + /// If a finite `max_advance` is supplied to `Layout::break_all_lines` then that width will be applied + /// relative to that width. Otherwise alignment will be applied relative to the width of the + /// longest line as computed by line breaking. + /// + /// If line-specific `offset` and `max_advance` are set using the advanced methods on the `BreakLines` + /// struct then each line will be aligned individually within its line box. + pub fn align(&mut self, alignment: Alignment, options: AlignmentOptions) { + unjustify(&mut self.data); + align(&mut self.data, alignment, options); + } + + /// Returns the index and `Line` object for the line containing the + /// given byte `index` in the source text. + pub(crate) fn line_for_byte_index(&self, index: usize) -> Option<(usize, Line<'_, B>)> { + let line_index = self + .data + .lines + .binary_search_by(|line| { + if index < line.text_range.start { + Ordering::Greater + } else if index >= line.text_range.end { + Ordering::Less + } else { + Ordering::Equal + } + }) + .ok()?; + Some((line_index, self.get(line_index)?)) + } + + /// Returns the index and `Line` object for the line containing the + /// given `offset`. + /// + /// The offset is specified in the direction orthogonal to line direction. + /// For horizontal text, this is a vertical or y offset. If the offset is + /// on a line boundary, it is considered to be contained by the later line. + pub(crate) fn line_for_offset(&self, offset: f32) -> Option<(usize, Line<'_, B>)> { + if offset < 0.0 { + return Some((0, self.get(0)?)); + } + let maybe_line_index = self.data.lines.binary_search_by(|line| { + if offset < line.metrics.block_min_coord { + Ordering::Greater + } else if offset >= line.metrics.block_max_coord { + Ordering::Less + } else { + Ordering::Equal + } + }); + let line_index = match maybe_line_index { + Ok(index) => index, + Err(index) => index.saturating_sub(1), + }; + Some((line_index, self.get(line_index)?)) + } +} + +impl Default for Layout { + fn default() -> Self { + Self { + data: LayoutData::default(), + } + } +} diff --git a/vendor/parley-0.11.1/src/layout/line.rs b/vendor/parley-0.11.1/src/layout/line.rs new file mode 100644 index 0000000000..f3174b1506 --- /dev/null +++ b/vendor/parley-0.11.1/src/layout/line.rs @@ -0,0 +1,318 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use crate::layout::Style; +use crate::layout::data::BreakReason; +use crate::layout::data::{LayoutItemKind, LineData}; +use crate::layout::glyph::Glyph; +use crate::layout::layout::Layout; +use crate::layout::run::Run; +use crate::style::Brush; +use crate::{InlineBox, InlineBoxKind}; +use core::ops::Range; + +/// Line in a text layout. +#[derive(Copy, Clone)] +pub struct Line<'a, B: Brush> { + pub(crate) layout: &'a Layout, + pub(crate) index: u32, + pub(crate) data: &'a LineData, +} + +impl<'a, B: Brush> Line<'a, B> { + /// Returns the metrics for the line. + pub fn metrics(&self) -> &LineMetrics { + &self.data.metrics + } + + pub fn break_reason(&self) -> BreakReason { + self.data.break_reason + } + + /// Returns the range of text for the line. + pub fn text_range(&self) -> Range { + self.data.text_range.clone() + } + + /// Returns the number of items in the line. + pub fn len(&self) -> usize { + self.data.item_range.len() + } + + /// Returns `true` if the line is empty. + pub fn is_empty(&self) -> bool { + self.data.item_range.is_empty() + } + + /// Returns the line item at the specified index. + pub(crate) fn item(&self, index: usize) -> Option> { + let original_index = index; + let index = self.data.item_range.start + index; + if index >= self.data.item_range.end { + return None; + } + let item = self.layout.data.line_items.get(index)?; + + Some(match item.kind { + LayoutItemKind::TextRun => LineItem::Run(Run { + layout: self.layout, + line_index: self.index, + index: original_index as u32, + data: self.layout.data.runs.get(item.index)?, + line_data: Some(item), + }), + LayoutItemKind::InlineBox => { + LineItem::InlineBox(self.layout.data.inline_boxes.get(item.index)?) + } + }) + } + + /// Returns an iterator over the runs for the line. + pub fn runs(&self) -> impl Iterator> + 'a + Clone { + self.items_nonpositioned().filter_map(|item| item.run()) + } + + /// Returns an iterator over the non-glyph runs and inline boxes for the line. + pub(crate) fn items_nonpositioned(&self) -> impl Iterator> + Clone { + let copy = self.clone(); + let line_items = ©.layout.data.line_items[self.data.item_range.clone()]; + line_items + .iter() + .enumerate() + .map(move |(item_index, line_data)| match line_data.kind { + LayoutItemKind::TextRun => LineItem::Run(Run { + layout: copy.layout, + line_index: copy.index, + index: item_index as u32, + data: ©.layout.data.runs[line_data.index], + line_data: Some(line_data), + }), + LayoutItemKind::InlineBox => { + LineItem::InlineBox(©.layout.data.inline_boxes[line_data.index]) + } + }) + } + + /// Returns an iterator over the glyph runs for the line. + pub fn items(&self) -> impl Iterator> + 'a + Clone { + GlyphRunIter { + line: self.clone(), + item_index: 0, + glyph_start: 0, + offset: 0., + } + } +} + +/// Metrics information for a line. +#[derive(Copy, Clone, Default, Debug, PartialEq)] +pub struct LineMetrics { + /// Typographic ascent. + pub ascent: f32, + /// Typographic descent. + pub descent: f32, + /// Typographic leading. + pub leading: f32, + /// The absolute line height (in layout units). + pub line_height: f32, + /// Offset to the baseline. + pub baseline: f32, + /// Offset for alignment. + pub offset: f32, + /// Full advance of the line, including trailing whitespace. + pub advance: f32, + /// Advance of trailing whitespace. + pub trailing_whitespace: f32, + /// Minimum coordinate in the line direction. + /// + /// For horizontal text, this would be the left of the line. + pub inline_min_coord: f32, + /// Maximum coordinate in the line direction. + /// + /// For horizontal text, this would be the right of the line. + pub inline_max_coord: f32, + /// Minimum coordinate in the direction orthogonal to line + /// direction. + /// + /// For horizontal text, this would be the top of the line. + pub block_min_coord: f32, + /// Maximum coordinate in the direction orthogonal to line + /// direction. + /// + /// For horizontal text, this would be the bottom of the line. + pub block_max_coord: f32, +} + +impl LineMetrics { + /// Returns the size of the line + pub fn size(&self) -> f32 { + self.line_height + } +} + +/// A line item and its corresponding data (a run or inline box). Unlike a +/// [`PositionedLayoutItem`], runs are not guaranteed to be split by style. +pub(crate) enum LineItem<'a, B: Brush> { + Run(Run<'a, B>), + InlineBox(&'a InlineBox), +} + +impl<'a, B: Brush> LineItem<'a, B> { + pub(crate) fn run(self) -> Option> { + match self { + LineItem::Run(run) => Some(run), + _ => None, + } + } +} + +/// The computed result of an item (glyph run or inline box) within a layout +#[derive(Clone)] +pub enum PositionedLayoutItem<'a, B: Brush> { + GlyphRun(GlyphRun<'a, B>), + InlineBox(PositionedInlineBox), +} + +/// The computed position of an inline box within a layout +#[derive(Debug, Clone)] +pub struct PositionedInlineBox { + pub x: f32, + pub y: f32, + pub width: f32, + pub height: f32, + pub id: u64, + pub kind: InlineBoxKind, +} + +/// Sequence of fully positioned glyphs with the same style. +#[derive(Clone)] +pub struct GlyphRun<'a, B: Brush> { + run: Run<'a, B>, + style: &'a Style, + glyph_start: usize, + glyph_count: usize, + offset: f32, + baseline: f32, + advance: f32, +} + +impl<'a, B: Brush> GlyphRun<'a, B> { + /// Returns the underlying run. + pub fn run(&self) -> &Run<'a, B> { + &self.run + } + + /// Returns the associated style. + pub fn style(&self) -> &Style { + self.style + } + + /// Returns the offset to the baseline. + pub fn baseline(&self) -> f32 { + self.baseline + } + + /// Returns the offset to the first glyph along the baseline. + pub fn offset(&self) -> f32 { + self.offset + } + + /// Returns the total advance of the run. + pub fn advance(&self) -> f32 { + self.advance + } + + /// Returns an iterator over the glyphs in the run. + pub fn glyphs(&'a self) -> impl Iterator + 'a + Clone { + self.run + .visual_clusters() + .flat_map(|cluster| cluster.glyphs()) + .skip(self.glyph_start) + .take(self.glyph_count) + } + + /// Returns an iterator over the fully positioned glyphs in the run. + pub fn positioned_glyphs(&'a self) -> impl Iterator + 'a + Clone { + let mut offset = self.offset; + let baseline = self.baseline; + self.glyphs().map(move |mut g| { + g.x += offset; + g.y += baseline; + offset += g.advance; + g + }) + } +} + +#[derive(Clone)] +struct GlyphRunIter<'a, B: Brush> { + line: Line<'a, B>, + item_index: usize, + glyph_start: usize, + offset: f32, +} + +impl<'a, B: Brush> Iterator for GlyphRunIter<'a, B> { + type Item = PositionedLayoutItem<'a, B>; + + fn next(&mut self) -> Option { + loop { + let item = self.line.item(self.item_index)?; + match item { + LineItem::InlineBox(inline_box) => { + let x = self.offset + + self.line.data.metrics.inline_min_coord + + self.line.data.metrics.offset; + + self.item_index += 1; + self.glyph_start = 0; + if inline_box.kind == InlineBoxKind::InFlow { + self.offset += inline_box.width; + } + return Some(PositionedLayoutItem::InlineBox(PositionedInlineBox { + x, + y: self.line.data.metrics.baseline - inline_box.height, + width: inline_box.width, + height: inline_box.height, + id: inline_box.id, + kind: inline_box.kind, + })); + } + LineItem::Run(run) => { + let mut iter = run + .visual_clusters() + .flat_map(|c| c.glyphs()) + .skip(self.glyph_start); + + if let Some(first) = iter.next() { + let mut advance = first.advance; + let style_index = first.style_index(); + let mut glyph_count = 1; + for glyph in iter.take_while(|g| g.style_index() == style_index) { + glyph_count += 1; + advance += glyph.advance; + } + let style = run.layout.data.styles.get(style_index)?; + let glyph_start = self.glyph_start; + self.glyph_start += glyph_count; + let offset = self.offset; + self.offset += advance; + return Some(PositionedLayoutItem::GlyphRun(GlyphRun { + run, + style, + glyph_start, + glyph_count, + offset: offset + + self.line.data.metrics.inline_min_coord + + self.line.data.metrics.offset, + baseline: self.line.data.metrics.baseline, + advance, + })); + } + self.item_index += 1; + self.glyph_start = 0; + } + } + } + } +} diff --git a/vendor/parley-0.11.1/src/layout/line_break.rs b/vendor/parley-0.11.1/src/layout/line_break.rs new file mode 100644 index 0000000000..039e81720c --- /dev/null +++ b/vendor/parley-0.11.1/src/layout/line_break.rs @@ -0,0 +1,1358 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Greedy line breaking. + +use alloc::vec::Vec; + +#[cfg(feature = "libm")] +#[allow(unused_imports)] +use core_maths::CoreFloat; + +use crate::analysis::Boundary; +use crate::analysis::cluster::Whitespace; +use crate::data::ClusterData; +use crate::layout::{ + BreakReason, Layout, LayoutData, LayoutItem, LayoutItemKind, LineData, LineItemData, + LineMetrics, Run, +}; +use crate::style::Brush; +use crate::{InlineBoxKind, OverflowWrap, TextWrapMode}; + +use core::ops::Range; + +#[derive(Default)] +struct LineLayout { + lines: Vec, + line_items: Vec, +} + +impl LineLayout { + fn swap(&mut self, layout: &mut LayoutData) { + core::mem::swap(&mut self.lines, &mut layout.lines); + core::mem::swap(&mut self.line_items, &mut layout.line_items); + } +} + +#[derive(Clone, Default)] +struct LineState { + x: f32, + items: Range, + clusters: Range, + num_spaces: usize, + /// Of the line currently being built, the maximum line height seen so far. + /// This represents a lower-bound on the eventual line height of the line. + running_line_height: f32, + /// This is set to true if we encounter something on the line (either a glyph or an inline box) + /// that is taller than the `line_max_height`. When in this state `break_next` should yield control + /// flow to the caller to handle the constraint violation. + /// + /// This never happens when calling `break_all_lines` as it never sets `line_max_height`, and it defaults to `f32::MAX`. + max_height_exceeded: bool, + + /// We lag the text-wrap-mode by one cluster due to line-breaking boundaries only + /// being triggered on the cluster after the linebreak. + text_wrap_mode: TextWrapMode, +} + +#[derive(Clone, Default)] +struct PrevBoundaryState { + item_idx: usize, + run_idx: usize, + cluster_idx: usize, + state: LineState, +} + +/// Reason that the line breaker has yielded control flow +#[derive(Clone, Debug)] +pub enum YieldData { + /// Control flow was yielded because a line break occurred. + /// The `reason` field of the [`LineBreakData`] contains a specific reason about what caused a + /// line break at this location. + LineBreak(LineBreakData), + /// Control flow was yielded because content on the line caused the line to exceed the max height + /// + /// The caller is responsible for finding a new location for the line with a greater available height + /// adjusting the line geometry to the new position and resuming iteration. + /// + /// Note: that by default no max height is set (and one is not required for laying out text into + /// rectangular regions), so you will only encounter this if you explicitly set a max height + /// using `BreakLine::set_line_max_height`. + MaxHeightExceeded(MaxHeightBreakData), + /// Control flow was yielded because an inline box with kind [`InlineBoxKind::CustomOutOfFlow`] + /// was encountered. + /// + /// Parley does not position these boxes itself. The caller is responsible for + /// placing the box (e.g. via a caller-owned algorithm), adjusting the line geometry through + /// [`BreakerState`], and then resuming iteration. + InlineBoxBreak(BoxBreakData), +} + +#[derive(Clone, Debug)] +/// Information about a line break +pub struct LineBreakData { + /// The reason for the line break (see [`BreakReason`] for details) + pub reason: BreakReason, + /// The computed advance (width) of the line + pub advance: f32, + /// The computed height of the line + pub line_height: f32, + /// The position of the top of the line + pub line_y_start: f64, + /// The position of the bottom of the line + pub line_y_end: f64, +} + +#[derive(Clone, Debug)] +/// Information about a "max height break" (where control flow has been yielded due to the +/// line's configured max height being exceeded by content by laid out into the line). +pub struct MaxHeightBreakData { + /// The current advance of the in-progress line + pub advance: f32, + /// The current line height of the in-progress line + pub line_height: f32, +} + +#[derive(Clone, Debug)] +/// Information about a "box break" (where control flow has been yielded due to an inline box +/// with kind [`InlineBoxKind::CustomOutOfFlow`] being encountered during layout. +pub struct BoxBreakData { + /// The user-supplied ID for the inline box + pub inline_box_id: u64, + /// The index of the inline box within `Layout::inline_boxes()` + pub inline_box_index: usize, + /// The current advance of the line (up to but *not* including the `CustomOutOfFlow` box) + pub advance: f32, +} + +#[derive(Clone)] +/// The mutable state of the line breaker. +/// +/// This is exposed so that callers using [`BreakLines`] directly can inspect and +/// adjust line geometry between calls to [`BreakLines::break_next`]. +/// +/// A `BreakerState` can be cloned and later passed to [`BreakLines::revert_to`] +/// to retry layout from a saved checkpoint. +pub struct BreakerState { + /// The number of items that have been processed (used to revert state) + items: usize, + /// The number of lines that have been processed (used to revert state) + lines: usize, + + /// Iteration state: the current item (within the layout) + item_idx: usize, + /// Iteration state: the current run (within the layout) + run_idx: usize, + /// Iteration state: the current cluster (within the layout) + cluster_idx: usize, + + /// The x coordinate of the left/start of the current line + line_x: f32, + /// The y coordinate of the top/start of the current line + /// Use of f64 here is important. f32 causes test failures due to accumulated error + line_y: f64, + + /// The max advance of the entire layout. + layout_max_advance: f32, + /// The max advance (max width) of the current line. This must be <= the `layout_max_advance`. + line_max_advance: f32, + /// The max height available to the current line. + line_max_height: f32, + + /// The state of the current line + line: LineState, + + // Saved breaker states for reverting to a previously encountered line-breaking opportunity + /// Saved breaker state for the last non-emergency line-breaking opportunity + prev_boundary: Option, + /// Saved breaker state for the last emergency line-breaking opportunity + emergency_boundary: Option, +} + +impl Default for BreakerState { + fn default() -> Self { + Self { + items: 0, + lines: 0, + item_idx: 0, + run_idx: 0, + cluster_idx: 0, + line_x: 0.0, + line_y: 0.0, + layout_max_advance: 0.0, + line_max_advance: 0.0, + line_max_height: f32::MAX, + line: LineState::default(), + prev_boundary: None, + emergency_boundary: None, + } + } +} + +impl BreakerState { + /// Add the cluster(s) currently being evaluated to the current line + pub fn append_cluster_to_line(&mut self, next_x: f32, clusters_height: f32) { + self.line.items.end = self.item_idx + 1; + self.line.clusters.end = self.cluster_idx + 1; + self.cluster_idx += 1; + self.line.x = next_x; + self.add_line_height(clusters_height); + } + + /// Add inline box to line + pub fn append_inline_box_to_line(&mut self, next_x: f32, box_height: f32) { + self.item_idx += 1; + self.line.items.end += 1; + self.line.x = next_x; + self.add_line_height(box_height); + } + + /// Store the current iteration state so that we can revert to it if we later want to take + /// the line breaking opportunity at this point. + fn mark_line_break_opportunity(&mut self) { + self.prev_boundary = Some(PrevBoundaryState { + item_idx: self.item_idx, + run_idx: self.run_idx, + cluster_idx: self.cluster_idx, + state: self.line.clone(), + }); + } + + /// Store the current iteration state so that we can revert to it if we later want to take + /// an *emergency* line breaking opportunity at this point. + fn mark_emergency_break_opportunity(&mut self) { + self.emergency_boundary = Some(PrevBoundaryState { + item_idx: self.item_idx, + run_idx: self.run_idx, + cluster_idx: self.cluster_idx, + state: self.line.clone(), + }); + } + + /// Revert boundary state to prev state + fn reset_to(&mut self, prev_state: PrevBoundaryState) { + self.item_idx = prev_state.item_idx; + self.run_idx = prev_state.run_idx; + self.cluster_idx = prev_state.cluster_idx; + self.line = prev_state.state; + } + + #[inline(always)] + fn add_line_height(&mut self, height: f32) { + self.line.running_line_height = self.line.running_line_height.max(height); + self.line.max_height_exceeded = self.line.running_line_height > self.line_max_height; + } + + /// Get the max-advance of the entire layout + #[inline(always)] + pub fn layout_max_advance(&self) -> f32 { + self.layout_max_advance + } + /// Set the max-advance of the entire layout + #[inline(always)] + pub fn set_layout_max_advance(&mut self, advance: f32) { + self.layout_max_advance = advance; + } + + /// Get the max-advance of the current line + #[inline(always)] + pub fn line_max_advance(&self) -> f32 { + self.line_max_advance + } + /// Set the max-advance of the current line + #[inline(always)] + pub fn set_line_max_advance(&mut self, advance: f32) { + self.line_max_advance = advance; + } + + /// Get the max-height of the current line + #[inline(always)] + pub fn line_max_height(&self) -> f32 { + self.line_max_height + } + /// Set the max-height of the current line. + #[inline(always)] + pub fn set_line_max_height(&mut self, height: f32) { + self.line_max_height = height; + } + + /// Get the x-offset of the current line + #[inline(always)] + pub fn line_x(&self) -> f32 { + self.line_x + } + /// Set the x-offset for the current line. + #[inline(always)] + pub fn set_line_x(&mut self, x: f32) { + self.line_x = x; + } + + /// Get the y-offset of the current line + #[inline(always)] + pub fn line_y(&self) -> f64 { + self.line_y + } + /// Set the y-offset for the current line. + #[inline(always)] + pub fn set_line_y(&mut self, y: f64) { + self.line_y = y; + } +} + +/// Line breaking support for a paragraph. +pub struct BreakLines<'a, B: Brush> { + layout: &'a mut Layout, + lines: LineLayout, + state: BreakerState, + prev_state: Option, + done: bool, +} + +impl<'a, B: Brush> BreakLines<'a, B> { + pub(crate) fn new(layout: &'a mut Layout) -> Self { + layout.data.width = 0.; + layout.data.height = 0.; + let mut lines = LineLayout::default(); + lines.swap(&mut layout.data); + lines.lines.clear(); + lines.line_items.clear(); + Self { + layout, + lines, + state: BreakerState::default(), + prev_state: None, + done: false, + } + } + + /// Reset state when a line has been committed + fn start_new_line( + &mut self, + reason: BreakReason, + max_advance: f32, + line_indent: f32, + ) -> Option { + commit_line( + self.layout, + &mut self.lines, + &mut self.state.line, + max_advance, + reason, + line_indent, + ); + + let line_height = self.state.line.running_line_height; + let line_y_start = self.state.line_y; + + self.state.items = self.lines.line_items.len(); + self.state.lines = self.lines.lines.len(); + self.state.line.x = 0.; + self.state.line.running_line_height = 0.; + self.state.prev_boundary = None; + self.state.emergency_boundary = None; + + self.finish_line(self.lines.lines.len() - 1, line_height); + + self.state.line_y += line_height as f64; + + Some(YieldData::LineBreak( + self.last_line_data(reason, line_y_start), + )) + } + + #[inline(always)] + fn last_line_data(&self, reason: BreakReason, line_y_start: f64) -> LineBreakData { + let line = self.lines.lines.last().unwrap(); + LineBreakData { + reason, + advance: line.metrics.advance, + line_height: line.size(), + line_y_start, + line_y_end: self.state.line_y, + } + } + + #[inline(always)] + fn max_height_break_data(&self, line_height: f32) -> Option { + Some(YieldData::MaxHeightExceeded(MaxHeightBreakData { + advance: self.state.line.x, + line_height, + })) + } + + #[inline(always)] + pub fn state(&self) -> &BreakerState { + &self.state + } + + #[inline(always)] + pub fn state_mut(&mut self) -> &mut BreakerState { + &mut self.state + } + + /// Returns the last committed line's metrics while line breaking is paused. + /// + /// Custom float layout can inspect the advance excluding hanging whitespace + /// before accepting a line, or use [`Self::revert_to`] to try a wider slot. + pub fn last_line_metrics(&self) -> Option<&LineMetrics> { + self.lines.lines.last().map(|line| &line.metrics) + } + + /// Set the max-advance of the previous line. + /// + /// This is an escape-hatch for allowing a custom width for + /// alignment on each line, which is different to the breaking width. + /// + /// Should be used in combination with [`AlignmentOptions`](crate::AlignmentOptions::align_when_overflowing). + /// + /// This method changes a line's reported [`inline_max_coord`](LineMetrics::inline_max_coord), so + /// if you use this method and read that value, you should be cautious. + // This escape hatch has not been carefully evaluated for unexpected consequences. + // It's current motivation is cases where Parley gives different line breaking results + // than blink, for reasons which haven't been fully understood. + #[doc(hidden)] + pub fn set_prior_line_width(&mut self, advance: f32) { + if let Some(line) = self.lines.lines.last_mut() { + line.metrics.inline_max_coord = line.metrics.inline_min_coord + advance; + } + } + + /// Reverts the to an externally saved state. + pub fn revert_to(&mut self, state: BreakerState) { + self.state = state; + self.lines.lines.truncate(self.state.lines); + self.lines.line_items.truncate(self.state.items); + self.done = false; + } + + /// Reverts the last computed line, returning to the previous state. + #[inline(always)] + pub fn revert(&mut self) -> bool { + if let Some(state) = self.prev_state.take() { + self.revert_to(state); + true + } else { + false + } + } + + /// Returns the y-coordinate of the top of the current line + #[inline(always)] + pub fn committed_y(&self) -> f64 { + self.state.line_y + } + + /// Returns true if all the text has been placed into lines. + #[inline(always)] + pub fn is_done(&self) -> bool { + self.done + } + + /// Computes the next line in the paragraph. Returns the advance and size + /// (width and height for horizontal layouts) of the line. + #[inline(always)] + pub fn break_next(&mut self) -> Option { + self.break_next_line_or_box() + } + + /// Computes the next line in the paragraph. Returns the advance and size + /// (width and height for horizontal layouts) of the line. + fn break_next_line_or_box(&mut self) -> Option { + assert!( + self.state.layout_max_advance == f32::INFINITY + || self.state.line_max_advance - self.state.layout_max_advance < 1.0 + ); + + // Maintain iterator state + if self.done { + return None; + } + self.prev_state = Some(self.state.clone()); + + // HACK: ignore max_advance for empty layouts + // Prevents crash when width is too small (https://github.com/linebender/parley/issues/186) + let max_advance = + if self.layout.data.text_len == 0 && self.layout.data.inline_boxes.is_empty() { + f32::MAX + } else { + self.state.line_max_advance + }; + + let line_indent = self.resolve_indent(); + + let max_advance = max_advance - line_indent; + + // dbg!(&self.layout.items); + + // println!("\nBREAK NEXT"); + // dbg!(&self.state.line.items); + + // Iterate over remaining runs in the Layout + let item_count = self.layout.data.items.len(); + while self.state.item_idx < item_count { + let item = &self.layout.data.items[self.state.item_idx]; + + // println!( + // "\nitem = {} {:?}. x: {}", + // self.state.item_idx, item.kind, self.state.line.x + // ); + // dbg!(&self.state.line.items); + + match item.kind { + LayoutItemKind::InlineBox => { + let inline_box = &self.layout.data.inline_boxes[item.index]; + + let (width_contribution, height_contribution) = match inline_box.kind { + InlineBoxKind::InFlow => (inline_box.width, inline_box.height), + InlineBoxKind::OutOfFlow => (0.0, 0.0), + // If the box is a `CustomOutOfFlow` box then we yield control flow back to the caller. + // It is then the caller's responsibility to handle placement of the box. + InlineBoxKind::CustomOutOfFlow => { + return Some(YieldData::InlineBoxBreak(BoxBreakData { + inline_box_id: inline_box.id, + inline_box_index: item.index, + advance: self.state.line.x, + })); + } + }; + + // Compute the x position of the content being currently processed + let next_x = self.state.line.x + width_contribution; + + // println!("BOX next_x: {}", next_x); + + let box_will_be_appended = next_x <= max_advance || self.state.line.x == 0.0; + if height_contribution > self.state.line_max_height && box_will_be_appended { + return self.max_height_break_data(height_contribution); + } + + // If the box fits on the current line (or we are at the start of the current line) + // then simply move on to the next item + if next_x <= max_advance || self.state.line.text_wrap_mode != TextWrapMode::Wrap + { + // println!("BOX FITS"); + + self.state + .append_inline_box_to_line(next_x, height_contribution); + + // We can always line break after an inline box + self.state.mark_line_break_opportunity(); + } else { + // If we're at the start of the line, this box will never fit, so consume it and accept the overflow. + let reason = if self.state.line.x == 0.0 { + // println!("BOX EMERGENCY BREAK"); + self.state + .append_inline_box_to_line(next_x, height_contribution); + BreakReason::Emergency + } else { + // println!("BOX BREAK"); + BreakReason::Regular + }; + return self.start_new_line(reason, max_advance, line_indent); + } + } + LayoutItemKind::TextRun => { + let run_idx = item.index; + let run_data = &self.layout.data.runs[run_idx]; + + let run = Run::new(self.layout, 0, 0, run_data, None); + let cluster_start = run_data.cluster_range.start; + let cluster_end = run_data.cluster_range.end; + + // println!("TextRun ({:?})", &run_data.text_range); + + // Iterate over remaining clusters in the Run + while self.state.cluster_idx < cluster_end { + let cluster = run.get(self.state.cluster_idx - cluster_start).unwrap(); + + // Retrieve metadata about the cluster + let is_ligature_continuation = cluster.is_ligature_continuation(); + let whitespace = cluster.info().whitespace(); + let is_newline = whitespace == Whitespace::Newline; + let is_space = whitespace.is_space_or_nbsp(); + let boundary = cluster.info().boundary(); + let line_height = run.metrics().line_height; + let max_height_exceeded = self.state.line.max_height_exceeded; + let style = &self.layout.data.styles[cluster.data.style_index as usize]; + + // Lag text_wrap_mode style by one cluster + let text_wrap_mode = self.state.line.text_wrap_mode; + self.state.line.text_wrap_mode = style.text_wrap_mode; + + if boundary == Boundary::Line && text_wrap_mode == TextWrapMode::Wrap { + // We do not currently handle breaking within a ligature, so we ignore boundaries in such a position. + // + // We also don't record boundaries when the advance is 0. As we do not want overflowing content to cause extra consecutive + // line breaks. We should accept the overflowing fragment in that scenario. + if !is_ligature_continuation && self.state.line.x != 0.0 { + self.state.mark_line_break_opportunity(); + // break_opportunity = true; + } + } else if is_newline { + if max_height_exceeded { + return self.max_height_break_data(line_height); + } + self.state + .append_cluster_to_line(self.state.line.x, line_height); + return self.start_new_line( + BreakReason::Explicit, + max_advance, + line_indent, + ); + } else if + // This text can contribute "emergency" line breaks. + style.overflow_wrap != OverflowWrap::Normal && !is_ligature_continuation + && text_wrap_mode == TextWrapMode::Wrap + // If we're at the start of the line, this particular cluster will never fit, so it's not a valid emergency break opportunity. + && self.state.line.x != 0.0 + { + self.state.mark_emergency_break_opportunity(); + } + + // If current cluster is the start of a ligature, then advance state to include + // the remaining clusters that make up the ligature + let mut advance = cluster.advance(); + if cluster.is_ligature_start() { + while let Some(cluster) = run.get(self.state.cluster_idx + 1) { + if !cluster.is_ligature_continuation() { + break; + } else { + advance += cluster.advance(); + self.state.cluster_idx += 1; + } + } + } + + // Compute the x position of the content being currently processed + let next_x = self.state.line.x + advance; + + // println!("Cluster {} next_x: {}", self.state.cluster_idx, next_x); + + // If the content fits (the x position does NOT exceed max_advance) + // + // We simply append the cluster(s) to the current line + if next_x <= max_advance { + if max_height_exceeded { + return self.max_height_break_data(line_height); + } + self.state.append_cluster_to_line(next_x, line_height); + if is_space { + self.state.line.num_spaces += 1; + } + } + // Else we attempt to line break: + // + // This will only succeed if there is an available line-break opportunity that has been marked earlier + // in the line. If there is no such line-breaking opportunity (such as if wrapping is disabled), then + // we fall back to appending the content to the line anyway. + else { + // Case: cluster is a space character (and wrapping is enabled) + // + // We hang any overflowing whitespace and then line-break. + if is_space && text_wrap_mode == TextWrapMode::Wrap { + if max_height_exceeded { + return self.max_height_break_data(line_height); + } + self.state.append_cluster_to_line(next_x, line_height); + return self.start_new_line( + BreakReason::Regular, + max_advance, + line_indent, + ); + } + // Case: we have previously encountered a REGULAR line-breaking opportunity in the current line + // + // We "take" the line-breaking opportunity by starting a new line and resetting our + // item/run/cluster iteration state back to how it was when the line-breaking opportunity was encountered + else if let Some(prev) = self.state.prev_boundary.take() { + self.state.reset_to(prev); + return self.start_new_line( + BreakReason::Regular, + max_advance, + line_indent, + ); + } + // Case: we have previously encountered an EMERGENCY line-breaking opportunity in the current line + // + // We "take" the line-breaking opportunity by starting a new line and resetting our + // item/run/cluster iteration state back to how it was when the line-breaking opportunity was encountered + else if let Some(prev_emergency) = + self.state.emergency_boundary.take() + { + self.state.reset_to(prev_emergency); + return self.start_new_line( + BreakReason::Emergency, + max_advance, + line_indent, + ); + } + // Case: no line-breaking opportunities available + // + // This can happen when wrapping is disabled (TextWrapMode::NoWrap) or when no wrapping opportunities + // (according to our `OverflowWrap` and `WordBreak` styles) have yet been encountered. + // + // We fall back to appending the content to the line. + else { + if max_height_exceeded { + return self.max_height_break_data(line_height); + } + self.state.append_cluster_to_line(next_x, line_height); + } + } + } + self.state.run_idx += 1; + self.state.item_idx += 1; + } + } + } + + if self.state.line.items.end == 0 { + self.state.line.items.end = 1; + } + self.done = true; + self.start_new_line(BreakReason::None, max_advance, line_indent) + } + + /// Computes the next line in the paragraph by character count. + /// + /// This method breaks lines based on the number of characters rather than advance width. + /// Each text cluster (including whitespace and newlines) counts as 1 character. + /// Each inline box also counts as 1 character. + /// Ligature components each count separately (matching character count). + /// + /// Unlike `break_next`, this method does not respect normal line break opportunities and + /// will break exactly when the character limit is reached. It does not break on newlines, for example. + /// + /// Inline boxes are supported and each contributes as 1 character. + pub fn break_next_with_length(&mut self, max_chars: u32) -> Option<()> { + if self.done { + return None; + } + + let line_indent = self.resolve_indent(); + + // Track cluster count for this line + let mut char_count: u32 = 0; + + let item_count = self.layout.data.items.len(); + while self.state.item_idx < item_count { + let item = &self.layout.data.items[self.state.item_idx]; + + match item.kind { + LayoutItemKind::InlineBox => { + let inline_box = &self.layout.data.inline_boxes[item.index]; + + if inline_box.kind != InlineBoxKind::InFlow { + self.state.append_inline_box_to_line(self.state.line.x, 0.0); + continue; + } + + // Check if adding this box would exceed the limit + if char_count >= max_chars && max_chars != 0 { + // Break before this box + self.start_new_line(BreakReason::Regular, f32::MAX, line_indent); + return Some(()); + } + + // Compute the x position for the line width tracking + let next_x = self.state.line.x + inline_box.width; + self.state + .append_inline_box_to_line(next_x, inline_box.height); + char_count += 1; + + // Check if we've reached the limit after adding this box + if char_count >= max_chars { + // Check if we've consumed all content (this is the last line). + let is_last_item = self.state.item_idx >= self.layout.data.items.len(); + let break_reason = if is_last_item { + BreakReason::None + } else { + BreakReason::Regular + }; + + if break_reason == BreakReason::None { + self.done = true; + } + self.start_new_line(break_reason, f32::MAX, line_indent); + return Some(()); + } + } + LayoutItemKind::TextRun => { + let run_idx = item.index; + let run_data = &self.layout.data.runs[run_idx]; + let run = Run::new(self.layout, 0, 0, run_data, None); + let cluster_start = run_data.cluster_range.start; + let cluster_end = run_data.cluster_range.end; + + while self.state.cluster_idx < cluster_end { + let cluster = run.get(self.state.cluster_idx - cluster_start).unwrap(); + + // Check if we should break before this cluster + if char_count >= max_chars && max_chars != 0 { + self.start_new_line(BreakReason::Regular, f32::MAX, line_indent); + return Some(()); + } + + let whitespace = cluster.info().whitespace(); + let is_newline = whitespace == Whitespace::Newline; + let is_space = whitespace.is_space_or_nbsp(); + let advance = cluster.advance(); + + // Compute the x position. + // Newlines don't contribute to line width (matching break_next behavior). + let next_x = if is_newline { + self.state.line.x + } else { + self.state.line.x + advance + }; + let line_height = run.metrics().line_height; + self.state.append_cluster_to_line(next_x, line_height); + char_count += 1; + + if is_space { + self.state.line.num_spaces += 1; + } + + // Check if we've reached the limit after adding this cluster + if char_count >= max_chars { + // Determine the break reason: + // - BreakReason::None for the last line (end of content) + // - BreakReason::Explicit if this line ends with a newline + // - BreakReason::Regular for soft wraps + let is_last_cluster_of_run = self.state.cluster_idx >= cluster_end; + let is_last_item = + self.state.item_idx + 1 >= self.layout.data.items.len(); + let break_reason = if is_last_cluster_of_run && is_last_item { + BreakReason::None + } else if is_newline { + BreakReason::Explicit + } else { + BreakReason::Regular + }; + + if break_reason == BreakReason::None { + self.done = true; + } + self.start_new_line(break_reason, f32::MAX, line_indent); + return Some(()); + } + } + self.state.run_idx += 1; + self.state.item_idx += 1; + } + } + } + + // Commit the final line (only reached if content remains after all break_next_with_length calls) + if self.state.line.items.end == 0 { + self.state.line.items.end = 1; + } + self.done = true; + self.start_new_line(BreakReason::None, f32::MAX, line_indent); + Some(()) + } + + /// Breaks all remaining lines with the specified maximum advance. This + /// consumes the line breaker. + pub fn break_remaining(mut self, max_advance: f32) { + // println!("\nDEBUG ITEMS"); + // for item in &self.layout.items { + // match item.kind { + // LayoutItemKind::InlineBox => println!("{:?}", item.kind), + // LayoutItemKind::TextRun => { + // let run_data = &self.layout.runs[item.index]; + // println!("{:?} ({:?})", item.kind, &run_data.text_range); + // } + // } + // } + + // println!("\nBREAK ALL"); + self.state.layout_max_advance = max_advance; + self.state.line_max_advance = max_advance; + while self.break_next().is_some() {} + self.finish(); + } + + /// Consumes the line breaker and finalizes all line computations. + pub fn finish(mut self) { + if self.layout.data.text_len == 0 { + if let Some(line) = self.lines.line_items.first_mut() { + line.text_range = 0..0; + line.cluster_range = 0..0; + } + } + } + + #[inline] + fn resolve_indent(&self) -> f32 { + let should_indent = { + let is_scope_line = if self.layout.data.indent_options.each_line { + self.lines.lines.is_empty() + || self.lines.lines.last().map(|l| l.break_reason) + == Some(BreakReason::Explicit) + } else { + self.lines.lines.is_empty() + }; + is_scope_line ^ self.layout.data.indent_options.hanging + }; + + if should_indent { + self.layout.data.indent_amount + } else { + 0.0 + } + } + + fn finish_line(&mut self, line_idx: usize, line_height: f32) { + let prev_line_metrics = match line_idx { + 0 => None, + idx => Some(self.lines.lines[idx - 1].metrics), + }; + let line = &mut self.lines.lines[line_idx]; + + // Reset metrics for line + line.metrics.ascent = 0.; + line.metrics.descent = 0.; + line.metrics.leading = 0.; + line.metrics.offset = 0.; + line.text_range.start = usize::MAX; + + line.metrics.line_height = line_height; + + if line.item_range.is_empty() { + line.text_range = self.layout.data.text_len..self.layout.data.text_len; + } + // Compute metrics for the line, but ignore trailing whitespace. + let mut have_metrics = false; + let mut needs_reorder = false; + for line_item in self.lines.line_items[line.item_range.clone()] + .iter_mut() + .rev() + { + match line_item.kind { + LayoutItemKind::InlineBox => { + let item = &self.layout.data.inline_boxes[line_item.index]; + + // Advance is already computed in "commit line" for items + if item.kind == InlineBoxKind::InFlow { + // Default vertical alignment is to align the bottom of boxes with the text baseline. + // This is equivalent to the entire height of the box being "ascent" + line.metrics.ascent = line.metrics.ascent.max(item.height); + + // Mark us as having seen non-whitespace content on this line + have_metrics = true; + } + } + LayoutItemKind::TextRun => { + line_item.compute_whitespace_properties(&self.layout.data); + + // Compute the text range for the line + // Q: Can we not simplify this computation by assuming that items are in order? + line.text_range.end = line.text_range.end.max(line_item.text_range.end); + line.text_range.start = line.text_range.start.min(line_item.text_range.start); + + // Mark line as needing bidi re-ordering if it contains any runs with non-zero bidi level + // (zero is the default level, so this is equivalent to marking lines that have multiple levels) + if line_item.bidi_level != 0 { + needs_reorder = true; + } + + // Compute the run's advance by summing the advances of its constituent clusters + line_item.advance = self.layout.data.clusters[line_item.cluster_range.clone()] + .iter() + .map(|c| c.advance) + .sum(); + + // Ignore trailing whitespace for metrics computation + // (we are iterating backwards so trailing whitespace comes first) + if !have_metrics && line_item.is_whitespace { + continue; + } + + // Compute the run's vertical metrics + let run = &self.layout.data.runs[line_item.index]; + line.metrics.ascent = line.metrics.ascent.max(run.metrics.ascent); + line.metrics.descent = line.metrics.descent.max(run.metrics.descent); + + // Mark us as having seen non-whitespace content on this line + have_metrics = true; + } + } + } + + // Reorder the items within the line (if required). Reordering is required if the line contains + // a mix of bidi levels (a mix of LTR and RTL text) + let item_count = line.item_range.end - line.item_range.start; + if needs_reorder && item_count > 1 { + reorder_line_items(&mut self.lines.line_items[line.item_range.clone()]); + } + + // Compute size of line's trailing whitespace. "Trailing" is considered the right edge + // for LTR text and the left edge for RTL text. + let run = if self.layout.is_rtl() { + self.lines.line_items[line.item_range.clone()].first() + } else { + self.lines.line_items[line.item_range.clone()].last() + }; + line.metrics.trailing_whitespace = run + .filter(|item| item.is_text_run() && item.has_trailing_whitespace) + .map(|run| { + fn whitespace_advance<'c, I: Iterator>(clusters: I) -> f32 { + clusters + .take_while(|cluster| cluster.info.whitespace() != Whitespace::None) + .map(|cluster| cluster.advance) + .sum() + } + + let clusters = &self.layout.data.clusters[run.cluster_range.clone()]; + if run.is_rtl() { + whitespace_advance(clusters.iter()) + } else { + whitespace_advance(clusters.iter().rev()) + } + }) + .unwrap_or(0.0); + + if !have_metrics { + // Line consisting entirely of whitespace? + if !line.item_range.is_empty() { + let line_item = &self.lines.line_items[line.item_range.start]; + if line_item.is_text_run() { + let run = &self.layout.data.runs[line_item.index]; + line.metrics.ascent = run.metrics.ascent; + line.metrics.descent = run.metrics.descent; + } + } else if let Some(metrics) = prev_line_metrics { + // HACK: copy metrics from previous line if we don't have + // any; this should only occur for an empty line following + // a newline at the end of a layout + line.metrics = metrics; + // If we have no items on this line, it must be the last (empty) + // line in a layout following a newline. Commit an empty run so + // that AccessKit has a node with which to identify the visual + // cursor position + if let Some((index, run)) = self + .layout + .data + .runs + .iter() + .enumerate() + .rfind(|(_, run)| !run.text_range.is_empty()) + { + let run_index = self.lines.line_items.len(); + let cluster = run.cluster_range.end; + let text = run.text_range.end; + self.lines.line_items.push(LineItemData { + kind: LayoutItemKind::TextRun, + index, + bidi_level: 0, + advance: 0., + is_whitespace: false, + has_trailing_whitespace: false, + cluster_range: cluster..cluster, + text_range: text..text, + }); + line.item_range = run_index..run_index + 1; + } + } + } + + line.metrics.leading = + line.metrics.line_height - (line.metrics.ascent + line.metrics.descent); + + // Whether metrics should be quantized to pixel boundaries + let quantize = self.layout.data.quantize; + + let (ascent, descent) = if quantize { + // We mimic Chrome in rounding ascent and descent separately, + // before calculating the rest. + // See lines_integral_line_height_ascent_descent_rounding() for more details. + (line.metrics.ascent.round(), line.metrics.descent.round()) + } else { + (line.metrics.ascent, line.metrics.descent) + }; + + let (leading_above, leading_below) = if quantize { + // Calculate leading using the rounded ascent and descent. + let leading = line.metrics.line_height - (ascent + descent); + // We mimic Chrome in giving 'below' the larger leading half. + // Although the comment in Chromium's NGLineHeightMetrics::AddLeading function + // in ng_line_height_metrics.cc claims it's for legacy test compatibility. + // So we might want to think about giving 'above' the larger half instead. + let above = (leading * 0.5).floor(); + let below = leading.round() - above; + (above, below) + } else { + (line.metrics.leading * 0.5, line.metrics.leading * 0.5) + }; + + let y = self.state.line_y; + line.metrics.baseline = + ascent + leading_above + if quantize { y.round() as f32 } else { y as f32 }; + + // Small line heights will cause leading to be negative. + // Negative leadings are correct for baseline calculation, but not for min/max coords. + // We clamp leading to zero for the purposes of min/max coords, + // which in turn clamps the selection box minimum height to ascent + descent. + line.metrics.block_min_coord = line.metrics.baseline - ascent - leading_above.max(0.); + line.metrics.block_max_coord = line.metrics.baseline + descent + leading_below.max(0.); + + // let max_advance = if self.state.line_max_advance < f32::MAX { + // self.state.line_max_advance + // } else { + // line.metrics.advance - line.metrics.trailing_whitespace + // }; + + line.metrics.inline_min_coord = self.state.line_x; + line.metrics.inline_max_coord = self.state.line_x + self.state.line_max_advance; + } +} + +impl Drop for BreakLines<'_, B> { + fn drop(&mut self) { + // Compute the overall width and height of the entire layout + // The "width" excludes trailing whitespace. The "full_width" includes it. + let mut layout_width = 0_f32; + let mut layout_full_width = 0_f32; + let mut height = 0_f64; // f32 causes test failures due to accumulated error + for line in &mut self.lines.lines { + let indent_extra = line.indent.max(0.0); + let line_max = line.metrics.inline_min_coord + line.metrics.advance + indent_extra; + layout_full_width = layout_full_width.max(line_max); + layout_width = layout_width.max(line_max - line.metrics.trailing_whitespace); + height += line.metrics.line_height as f64; + } + + // If laying out with infinite width constraint, then set all lines' "max_width" + // to the measured width of the longest line. + if self.state.layout_max_advance >= f32::MAX { + for line in &mut self.lines.lines { + if line.metrics.inline_max_coord >= f32::MAX { + line.metrics.inline_max_coord = layout_width; + } + } + } + + // Don't include the last line's line_height in the layout's height if the last line is empty + if let Some(last_line) = self.lines.lines.last() { + if last_line.item_range.is_empty() { + height -= last_line.metrics.line_height as f64; + } + } + + // Save the computed widths/height to the layout + self.layout.data.width = layout_width; + self.layout.data.full_width = layout_full_width; + self.layout.data.height = height as f32; + self.layout.data.layout_max_advance = self.state.layout_max_advance; + + // for (i, line) in self.lines.lines.iter().enumerate() { + // println!("LINE {i} (h:{})", line.metrics.line_height); + // for item_idx in line.item_range.clone() { + // let item = &self.lines.line_items[item_idx]; + // println!(" ITEM {:?} ({})", item.kind, item.advance); + // } + // } + + // Save the computed lines to the layout + self.lines.swap(&mut self.layout.data); + } +} + +fn commit_line( + layout: &Layout, + lines: &mut LineLayout, + state: &mut LineState, + max_advance: f32, + break_reason: BreakReason, + line_indent: f32, +) -> bool { + // Ensure that the cluster and item endpoints are within range + state.clusters.end = state.clusters.end.min(layout.data.clusters.len()); + state.items.end = state.items.end.min(layout.data.items.len()); + + let start_item_idx = lines.line_items.len(); + // let start_run_idx = lines.line_items.last().map(|item| item.index).unwrap_or(0); + + let items_to_commit = &layout.data.items[state.items.clone()]; + + // Compute first and last run index + let is_text_run = |item: &LayoutItem| item.kind == LayoutItemKind::TextRun; + let first_run_pos = items_to_commit.iter().position(is_text_run).unwrap_or(0); + let last_run_pos = items_to_commit.iter().rposition(is_text_run).unwrap_or(0); + + // Iterate over the items to commit + // println!("\nCOMMIT LINE"); + let mut last_item_kind = LayoutItemKind::TextRun; + let mut committed_text_run = false; + for (i, item) in items_to_commit.iter().enumerate() { + // println!("i = {} index = {} {:?}", i, item.index, item.kind); + + match item.kind { + LayoutItemKind::InlineBox => { + let inline_box = &layout.data.inline_boxes[item.index]; + + lines.line_items.push(LineItemData { + kind: LayoutItemKind::InlineBox, + index: item.index, + bidi_level: item.bidi_level, + advance: inline_box.width, + + // These properties are ignored for inline boxes. So we just put a dummy value. + is_whitespace: false, + has_trailing_whitespace: false, + cluster_range: 0..0, + text_range: 0..0, + }); + + last_item_kind = item.kind; + } + LayoutItemKind::TextRun => { + let run_data = &layout.data.runs[item.index]; + + // Compute cluster range + // The first and last ranges have overrides to account for line-breaks within runs + let mut cluster_range = run_data.cluster_range.clone(); + if i == first_run_pos { + cluster_range.start = state.clusters.start; + } + if i == last_run_pos { + cluster_range.end = state.clusters.end; + } + + if cluster_range.start >= run_data.cluster_range.end { + // println!("INVALID CLUSTER"); + // dbg!(&run_data.text_range); + // dbg!(cluster_range); + continue; + } + + last_item_kind = item.kind; + committed_text_run = true; + + // Push run to line + let run = Run::new(layout, 0, 0, run_data, None); + let text_range = if run_data.cluster_range.is_empty() { + 0..0 + } else { + let first_cluster = run + .get(cluster_range.start - run_data.cluster_range.start) + .unwrap(); + let last_cluster = run + .get((cluster_range.end - run_data.cluster_range.start).saturating_sub(1)) + .unwrap(); + first_cluster.text_range().start..last_cluster.text_range().end + }; + + lines.line_items.push(LineItemData { + kind: LayoutItemKind::TextRun, + index: item.index, + bidi_level: run_data.bidi_level, + advance: 0., + is_whitespace: false, + has_trailing_whitespace: false, + cluster_range, + text_range, + }); + } + } + } + // let end_run_idx = lines.line_items.last().map(|item| item.index).unwrap_or(0); + let end_item_idx = lines.line_items.len(); + + // Exclude the trailing space from justification space count. + // Only subtract if the line actually ends with a space — with + // WordBreak::BreakAll, regular breaks can land between non-space + // characters, in which case there is no trailing space to exclude. + let mut num_spaces = state.num_spaces; + if break_reason == BreakReason::Regular + && state.clusters.start < state.clusters.end + && layout.data.clusters[state.clusters.end - 1] + .info + .whitespace() + .is_space_or_nbsp() + { + num_spaces = num_spaces.saturating_sub(1); + } + + lines.lines.push(LineData { + item_range: start_item_idx..end_item_idx, + max_advance, + break_reason, + num_spaces, + indent: line_indent, + metrics: LineMetrics { + advance: state.x, + ..Default::default() + }, + ..Default::default() + }); + + // Reset state for the new line + state.num_spaces = 0; + if committed_text_run { + state.clusters.start = state.clusters.end; + } + + state.items.start = match last_item_kind { + // For text runs, the first item of line N+1 needs to be the SAME as + // the last item for line N. This is because the item (if it a text run + // may be split across the two lines with some clusters in line N and some + // in line N+1). The item is later filtered out (see `continue` in loop above) + // if there are not actually any clusters in line N+1. + LayoutItemKind::TextRun => state.items.end.saturating_sub(1), + // Inline boxes cannot be spread across multiple lines, so we should set + // the first item of line N+1 to be the item AFTER the last item in line N. + LayoutItemKind::InlineBox => state.items.end, + }; + + true +} + +/// Reorder items within line according to the bidi levels of the items +fn reorder_line_items(runs: &mut [LineItemData]) { + let run_count = runs.len(); + + // Find the max level and the min *odd* level + let mut max_level = 0; + let mut lowest_odd_level = 255; + for run in runs.iter() { + let level = run.bidi_level; + let is_odd = level & 1 != 0; + + // Update max level + if level > max_level { + max_level = level; + } + + // Update min odd level + if is_odd && level < lowest_odd_level { + lowest_odd_level = level; + } + } + + // Iterate over bidi levels + for level in (lowest_odd_level..=max_level).rev() { + // Iterate over text runs + let mut i = 0; + while i < run_count { + if runs[i].bidi_level >= level { + let mut end = i + 1; + while end < run_count && runs[end].bidi_level >= level { + end += 1; + } + + let mut j = i; + let mut k = end - 1; + while j < k { + runs.swap(j, k); + j += 1; + k -= 1; + } + + i = end; + } + i += 1; + } + } +} diff --git a/vendor/parley-0.11.1/src/layout/mod.rs b/vendor/parley-0.11.1/src/layout/mod.rs new file mode 100644 index 0000000000..439a1990da --- /dev/null +++ b/vendor/parley-0.11.1/src/layout/mod.rs @@ -0,0 +1,103 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Layout types. + +#[cfg(feature = "accesskit")] +mod accessibility; +mod alignment; +mod cluster; +mod glyph; +mod line; +mod line_break; +mod run; + +// TODO - Add to allowed lint set? +#[expect( + clippy::module_inception, + reason = "Private inner module for code organisation" +)] +mod layout; + +pub(crate) mod data; + +#[cfg(feature = "accesskit")] +pub use accessibility::LayoutAccessibility; +pub use alignment::{Alignment, AlignmentOptions}; +pub use cluster::{Affinity, Cluster, ClusterPath, ClusterSide}; +pub use data::BreakReason; +pub use glyph::Glyph; +pub use layout::Layout; +pub use line::{GlyphRun, Line, LineMetrics, PositionedInlineBox, PositionedLayoutItem}; +pub use line_break::{ + BoxBreakData, BreakLines, BreakerState, LineBreakData, MaxHeightBreakData, YieldData, +}; +pub use run::{Run, RunMetrics}; + +pub(crate) use data::{LayoutData, LayoutItem, LayoutItemKind, LineData, LineItemData}; +pub(crate) use line::LineItem; + +// TODO - Deprecation not yet active to ease internal code migration. +#[deprecated(since = "TBD", note = "Access from the `editing` module instead.")] +pub use crate::editing::{Cursor, Selection}; + +// TODO - Move the following to `style` module and submodules. + +use crate::style::Brush; +use crate::{LineHeight, OverflowWrap, TextWrapMode}; + +#[allow(clippy::partial_pub_fields)] +/// Style properties. +#[derive(Clone, Debug, PartialEq)] +pub struct Style { + /// Brush for drawing glyphs. + pub brush: B, + /// Underline decoration. + pub underline: Option>, + /// Strikethrough decoration. + pub strikethrough: Option>, + /// Partially resolved line height, either in in layout units or dependent on metrics + pub(crate) line_height: LineHeight, + /// Per-cluster overflow-wrap setting + pub(crate) overflow_wrap: OverflowWrap, + /// Per-cluster text-wrap-mode setting + pub(crate) text_wrap_mode: TextWrapMode, + #[cfg(feature = "accesskit")] + /// Locale if any, so we can set the corresponding AccessKit property + pub(crate) locale: Option, +} + +/// Underline or strikethrough decoration. +#[derive(Clone, Debug, PartialEq)] +pub struct Decoration { + /// Brush used to draw the decoration. + pub brush: B, + /// Offset of the decoration from the baseline. If `None`, use the metrics + /// of the containing run. + pub offset: Option, + /// Thickness of the decoration. If `None`, use the metrics of the + /// containing run. + pub size: Option, +} + +/// Lower and upper bounds on layout width based on its contents. +#[derive(Copy, Clone, Debug)] +pub struct ContentWidths { + /// The minimum content width. This is the width of the layout if _all_ soft line-breaking + /// opportunities are taken. + pub min: f32, + /// The maximum content width. This is the width of the layout if _no_ soft line-breaking + /// opportunities are taken. + pub max: f32, +} + +/// Options controlling text-indent behavior, corresponding to CSS `text-indent` keywords. +#[derive(Copy, Clone, Default, PartialEq, Debug)] +pub struct IndentOptions { + /// If `true`, indent also applies after every hard line break, not just to the first line. + /// Corresponds to the CSS `each-line` keyword. Defaults to `false`. + pub each_line: bool, + /// If `true`, inverts which lines are indented: continuation lines are indented + /// instead of the first line(s). Corresponds to the CSS `hanging` keyword. Defaults to `false`. + pub hanging: bool, +} diff --git a/vendor/parley-0.11.1/src/layout/run.rs b/vendor/parley-0.11.1/src/layout/run.rs new file mode 100644 index 0000000000..944b4cb103 --- /dev/null +++ b/vendor/parley-0.11.1/src/layout/run.rs @@ -0,0 +1,245 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use crate::FontData; +use crate::layout::cluster::{Cluster, ClusterPath}; +use crate::layout::data::{LineItemData, RunData}; +use crate::layout::layout::Layout; +use crate::style::Brush; +use core::ops::Range; +use fontique::Synthesis; + +/// Sequence of clusters with a single font and style. +#[derive(Copy, Clone)] +pub struct Run<'a, B: Brush> { + pub(crate) layout: &'a Layout, + pub(crate) line_index: u32, + pub(crate) index: u32, + pub(crate) data: &'a RunData, + pub(crate) line_data: Option<&'a LineItemData>, +} + +impl<'a, B: Brush> Run<'a, B> { + pub(crate) fn new( + layout: &'a Layout, + line_index: u32, + index: u32, + data: &'a RunData, + line_data: Option<&'a LineItemData>, + ) -> Self { + Self { + layout, + line_index, + index, + data, + line_data, + } + } + + /// Returns the index of the run within the line. + pub fn index(&self) -> usize { + self.index as usize + } + + /// Returns the font for the run. + pub fn font(&self) -> &FontData { + self.layout.data.fonts.get(self.data.font_index).unwrap() + } + + /// Returns the font size for the run. + pub fn font_size(&self) -> f32 { + self.data.font_size + } + + /// Returns the font attributes for the run. + pub fn font_attrs(&self) -> &fontique::Attributes { + &self.data.font_attrs + } + + /// Returns the synthesis suggestions for the font associated with the run. + pub fn synthesis(&self) -> Synthesis { + self.data.synthesis + } + + /// Returns the normalized variation coordinates for the font associated + /// with the run. + pub fn normalized_coords(&self) -> &[i16] { + self.layout + .data + .coords + .get(self.data.coords_range.clone()) + .unwrap_or(&[]) + } + + /// Returns metrics for the run. + pub fn metrics(&self) -> &RunMetrics { + &self.data.metrics + } + + /// Returns the advance for the run. + pub fn advance(&self) -> f32 { + self.line_data + .map(|d| d.advance) + .unwrap_or(self.data.advance) + } + + /// Returns the original text range for the run. + pub fn text_range(&self) -> Range { + self.line_data + .map(|d| &d.text_range) + .unwrap_or(&self.data.text_range) + .clone() + } + + /// Returns `true` if the run has right-to-left directionality. + pub fn is_rtl(&self) -> bool { + self.data.bidi_level & 1 != 0 + } + + /// Returns the cluster range for the run. + pub fn cluster_range(&self) -> Range { + self.line_data + .map(|d| &d.cluster_range) + .unwrap_or(&self.data.cluster_range) + .clone() + } + + /// Returns the number of clusters in the run. + pub fn len(&self) -> usize { + self.cluster_range().len() + } + + /// Returns `true` if the run is empty. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns the cluster at the specified index. + pub fn get(&self, index: usize) -> Option> { + let range = self + .line_data + .map(|d| &d.cluster_range) + .unwrap_or(&self.data.cluster_range); + let original_index = index; + let index = range.start + index; + Some(Cluster { + path: ClusterPath::new(self.line_index, self.index, original_index as u32), + run: self.clone(), + data: self.layout.data.clusters.get(index)?, + }) + } + + /// Returns an iterator over the clusters in logical order. + pub fn clusters(&'a self) -> impl Iterator> + 'a + Clone { + let range = self.cluster_range(); + Clusters { + run: self, + range, + rev: false, + } + } + + /// Returns the visual cluster index for the specified logical cluster index. + pub fn logical_to_visual(&self, logical_index: usize) -> Option { + let num_clusters = self.len(); + if logical_index >= num_clusters { + return None; + } + + let visual_index = if self.is_rtl() { + num_clusters - 1 - logical_index + } else { + logical_index + }; + + Some(visual_index) + } + + /// Returns the logical cluster index for the specified visual cluster index. + pub fn visual_to_logical(&self, visual_index: usize) -> Option { + let num_clusters = self.len(); + if visual_index >= num_clusters { + return None; + } + + let logical_index = if self.is_rtl() { + num_clusters - 1 - visual_index + } else { + visual_index + }; + + Some(logical_index) + } + + /// Returns an iterator over the clusters in visual order. + pub fn visual_clusters(&'a self) -> impl Iterator> + 'a + Clone { + let range = self.cluster_range(); + Clusters { + run: self, + range, + rev: self.is_rtl(), + } + } +} + +struct Clusters<'a, B: Brush> { + run: &'a Run<'a, B>, + range: Range, + rev: bool, +} + +impl Clone for Clusters<'_, B> { + fn clone(&self) -> Self { + Self { + run: self.run, + range: self.range.clone(), + rev: self.rev, + } + } +} + +impl<'a, B: Brush> Iterator for Clusters<'a, B> { + type Item = Cluster<'a, B>; + + fn next(&mut self) -> Option { + let index = if self.rev { + self.range.next_back()? + } else { + self.range.next()? + }; + Some(Cluster { + path: ClusterPath::new( + self.run.line_index, + self.run.index, + (index - self.run.cluster_range().start) as u32, + ), + run: self.run.clone(), + data: self.run.layout.data.clusters.get(index)?, + }) + } +} + +/// Metrics information for a run. +#[derive(Copy, Clone, Default, Debug, PartialEq)] +pub struct RunMetrics { + /// Typographic ascent. + pub ascent: f32, + /// Typographic descent. + pub descent: f32, + /// Typographic leading. + pub leading: f32, + /// Offset of the top of underline decoration from the baseline. + pub underline_offset: f32, + /// Thickness of the underline decoration. + pub underline_size: f32, + /// Offset of the top of strikethrough decoration from the baseline. + pub strikethrough_offset: f32, + /// Thickness of the strikethrough decoration. + pub strikethrough_size: f32, + /// The line height + pub line_height: f32, + /// Distance from the baseline to the top of short lowercase letters. + pub x_height: Option, + /// Distance from the baseline to the top of capital letters. + pub cap_height: Option, +} diff --git a/vendor/parley-0.11.1/src/lib.rs b/vendor/parley-0.11.1/src/lib.rs new file mode 100644 index 0000000000..f429647aa3 --- /dev/null +++ b/vendor/parley-0.11.1/src/lib.rs @@ -0,0 +1,154 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Parley is a library for rich text layout. +//! +//! Some key types are: +//! - [`FontContext`] and [`LayoutContext`] are resources which should be shared globally (or at coarse-grained boundaries). +//! - [`FontContext`] is database of fonts. +//! - [`LayoutContext`] is scratch space that allows for reuse of allocations between layouts. +//! - Builders for creating a [`Layout`]: +//! - [`RangedBuilder`]: styles specified as a flat `Vec` of property spans +//! - [`TreeBuilder`]: styles specified as a tree of spans +//! - [`StyleRunBuilder`]: styles specified as a shared style table plus non-overlapping indexed runs +//! +//! They are constructed using [`LayoutContext::ranged_builder`], [`LayoutContext::tree_builder`], +//! and [`LayoutContext::style_run_builder`]. +//! - [`Layout`] which represents styled paragraph(s) of text and can perform shaping, line-breaking, bidi-reordering, and alignment of that text. +//! +//! `Layout` supports re-linebreaking and re-aligning many times (in case the width at which wrapping should occur changes). But if the text content or +//! the styles applied to that content change then a new `Layout` must be created using a new +//! `RangedBuilder`, `TreeBuilder`, or `StyleRunBuilder`. +//! +//! ## Usage Example +//! +//! See the [examples](https://github.com/linebender/parley/tree/main/examples) directory for more complete usage examples that include rendering. +//! +//! ```rust +//! use parley::{ +//! Alignment, AlignmentOptions, FontContext, FontWeight, InlineBox, InlineBoxKind, Layout, +//! LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty, +//! }; +//! +//! // Create a FontContext (font database) and LayoutContext (scratch space). +//! // These are both intended to be constructed rarely (perhaps even once per app): +//! let mut font_cx = FontContext::new(); +//! let mut layout_cx = LayoutContext::new(); +//! +//! // Create a `RangedBuilder` or a `TreeBuilder`, which are used to construct a `Layout`. +//! const DISPLAY_SCALE : f32 = 1.0; +//! const TEXT : &str = "Lorem Ipsum..."; +//! let mut builder = layout_cx.ranged_builder(&mut font_cx, &TEXT, DISPLAY_SCALE, true); +//! +//! // Set default styles that apply to the entire layout +//! builder.push_default(StyleProperty::FontSize(16.0)); +//! +//! // Set a style that applies to the first 4 characters +//! builder.push(StyleProperty::FontWeight(FontWeight::new(600.0)), 0..4); +//! +//! // Add a box to be laid out inline with the text +//! builder.push_inline_box(InlineBox { id: 0, kind: InlineBoxKind::InFlow, index: 5, width: 50.0, height: 50.0 }); +//! +//! // Build the builder into a Layout +//! let mut layout: Layout<()> = builder.build(&TEXT); +//! +//! // Run line-breaking and alignment on the Layout +//! const MAX_WIDTH : Option = Some(100.0); +//! layout.break_all_lines(MAX_WIDTH); +//! layout.align(Alignment::Start, AlignmentOptions::default()); +//! +//! // Inspect computed layout (see examples for more details) +//! let width = layout.width(); +//! let height = layout.height(); +//! for line in layout.lines() { +//! for item in line.items() { +//! match item { +//! PositionedLayoutItem::GlyphRun(glyph_run) => { +//! // Render the glyph run +//! } +//! PositionedLayoutItem::InlineBox(inline_box) => { +//! // Render the inline box +//! } +//! }; +//! } +//! } +//! ``` + +// LINEBENDER LINT SET - lib.rs - v4 +// See https://linebender.org/wiki/canonical-lints/ +// These lints shouldn't apply to examples or tests. +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +// These lints shouldn't apply to examples. +#![warn(clippy::print_stdout, clippy::print_stderr)] +// Targeting e.g. 32-bit means structs containing usize can give false positives for 64-bit. +#![cfg_attr(target_pointer_width = "64", warn(clippy::trivially_copy_pass_by_ref))] +// END LINEBENDER LINT SET +#![cfg_attr(docsrs, feature(doc_cfg))] +#![no_std] +#![allow(missing_docs, reason = "We have many as-yet undocumented items.")] +#![expect( + missing_debug_implementations, + clippy::allow_attributes_without_reason, + clippy::cast_possible_truncation, + clippy::missing_assert_message, + reason = "Deferred" +)] +#![expect( + single_use_lifetimes, + reason = "False positive: https://github.com/rust-lang/rust/issues/129255" +)] + +#[cfg(not(any(feature = "std", feature = "libm")))] +compile_error!("parley requires either the `std` or `libm` feature to be enabled"); + +extern crate alloc; + +#[cfg(feature = "std")] +extern crate std; + +pub use fontique; + +mod analysis; +mod bidi; +mod break_overrides; +mod builder; +mod context; +mod convert; +mod font; +mod inline_box; +mod lru_cache; +mod resolve; +mod shape; +mod util; + +pub mod editing; +pub mod layout; +pub mod setting; +pub mod style; + +#[cfg(test)] +mod tests; + +pub use linebender_resource_handle::FontData; +pub use util::BoundingBox; + +pub use break_overrides::{ + AsciiLineBreakTable, AsciiLineBreakTableBuilder, CHROMIUM_LINE_BREAK_OVERRIDE, + LineBreakContext, LineBreakOverrideFn, +}; +pub use builder::{RangedBuilder, StyleRunBuilder, TreeBuilder}; +pub use context::LayoutContext; +pub use font::FontContext; +pub use inline_box::{InlineBox, InlineBoxKind}; +#[doc(inline)] +pub use layout::Layout; + +pub use editing::*; +pub use layout::*; +pub use style::*; + +#[deprecated( + note = "Old name for this type, use `parley::FontData` instead.", + since = "0.6.0" +)] +pub type Font = FontData; diff --git a/vendor/parley-0.11.1/src/lru_cache.rs b/vendor/parley-0.11.1/src/lru_cache.rs new file mode 100644 index 0000000000..592576b405 --- /dev/null +++ b/vendor/parley-0.11.1/src/lru_cache.rs @@ -0,0 +1,208 @@ +// Copyright 2025 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use alloc::vec::Vec; +use hashbrown::Equivalent; + +/// An entry in the cache. +pub(crate) struct Entry { + pub epoch: u64, + pub id: ID, + pub data: T, +} + +/// A least-recently-used cache. This cache uses a linear scan of its entries +/// to find a given entry - it is optimised for a low number of entries. Preferably +/// keep `max_entries` low - in the order of tens. +pub(crate) struct LruCache { + entries: Vec>, + epoch: u64, + max_entries: usize, +} + +impl LruCache { + pub(crate) fn new(max_entries: usize) -> Self { + Self { + entries: Vec::default(), + epoch: 0, + max_entries, + } + } + + /// Returns a reference to the entry with the given ID. If the entry is not + /// found, it is created and returned using `make_data`. + /// + /// The lookup key must be `Equivalent` to ID for lookups and convertible `Into` + /// for creating new entries. + pub(crate) fn entry(&mut self, id: K, make_data: impl FnOnce() -> T) -> &T + where + K: Equivalent + Into, + { + let index = self.find_entry(id, make_data); + self.epoch += 1; + let entry = &mut self.entries[index]; + entry.epoch = self.epoch; + &entry.data + } + + fn find_entry(&mut self, id: K, make_data: impl FnOnce() -> T) -> usize + where + K: Equivalent + Into, + { + let epoch = self.epoch; + let mut lowest_serial = epoch; + let mut lowest_index = 0; + for (i, entry) in self.entries.iter().enumerate() { + if id.equivalent(&entry.id) { + return i; + } + if entry.epoch < lowest_serial { + lowest_serial = entry.epoch; + lowest_index = i; + } + } + if self.entries.len() < self.max_entries { + lowest_index = self.entries.len(); + self.entries.push(Entry { + epoch, + id: id.into(), + data: make_data(), + }); + } else { + let entry = &mut self.entries[lowest_index]; + entry.epoch = epoch; + entry.id = id.into(); + entry.data = make_data(); + } + lowest_index + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::string::{String, ToString}; + + #[derive(Debug, Clone, PartialEq)] + struct TestId(String); + struct TestLookupKey<'a>(&'a str); + + impl<'a> Equivalent for TestLookupKey<'a> { + fn equivalent(&self, key: &TestId) -> bool { + self.0 == key.0.as_str() + } + } + + impl<'a> From> for TestId { + fn from(key: TestLookupKey<'a>) -> Self { + Self(key.0.to_string()) + } + } + + impl Equivalent for TestId { + fn equivalent(&self, key: &Self) -> bool { + self.0 == key.0 + } + } + + #[test] + fn test_retrieve_existing_entry() { + let mut cache = LruCache::new(3); + + // Insert an entry + let value1 = cache.entry(TestLookupKey("key1"), || 42); + assert_eq!(*value1, 42); + + // Retrieve the same entry - make_data should not be called + let value2 = cache.entry(TestLookupKey("key1"), || { + panic!("Should not create new data") + }); + assert_eq!(*value2, 42); + assert_eq!(cache.entries.len(), 1); + } + + #[test] + fn test_multiple_entries() { + let mut cache = LruCache::new(3); + + let value1 = cache.entry(TestLookupKey("key1"), || 1); + assert_eq!(*value1, 1); + + let value2 = cache.entry(TestLookupKey("key2"), || 2); + assert_eq!(*value2, 2); + + let value3 = cache.entry(TestLookupKey("key3"), || 3); + assert_eq!(*value3, 3); + + assert_eq!(cache.entries.len(), 3); + assert_eq!(cache.epoch, 3); + } + + #[test] + fn test_lru_eviction() { + let mut cache = LruCache::new(3); + + // Add three entries + cache.entry(TestLookupKey("key1"), || 1); + cache.entry(TestLookupKey("key2"), || 2); + cache.entry(TestLookupKey("key3"), || 3); + + // Access key1 to update its epoch + cache.entry(TestLookupKey("key1"), || panic!("Should not create")); + + // Add key4 - should evict key2 (oldest untouched) + cache.entry(TestLookupKey("key4"), || 4); + + // Verify key1 is still present + let value1 = cache.entry(TestLookupKey("key1"), || { + panic!("key1 should still be present") + }); + assert_eq!(*value1, 1); + + // Verify key2 was evicted + let mut was_created = false; + cache.entry(TestLookupKey("key2"), || { + was_created = true; + 20 + }); + assert!(was_created, "key2 should have been evicted"); + } + + #[test] + fn test_lru_eviction_after_multiple_hits() { + let mut cache = LruCache::new(3); + + cache.entry(TestLookupKey("key1"), || 1); + cache.entry(TestLookupKey("key2"), || 2); + cache.entry(TestLookupKey("key3"), || 3); + + // Hit all three in order: key3 first (making it LRU among hits), + // then key1, then key2 (most recently used). + cache.entry(TestLookupKey("key3"), || panic!("Should not create")); + cache.entry(TestLookupKey("key1"), || panic!("Should not create")); + cache.entry(TestLookupKey("key2"), || panic!("Should not create")); + + // Insert key4 — should evict key3 (least recently accessed) + cache.entry(TestLookupKey("key4"), || 4); + + // key1 and key2 should still be present with original values + // (check these first since verifying key3 eviction will trigger another eviction) + let v1 = cache.entry(TestLookupKey("key1"), || { + panic!("key1 should still be present") + }); + assert_eq!(*v1, 1); + + let v2 = cache.entry(TestLookupKey("key2"), || { + panic!("key2 should still be present") + }); + assert_eq!(*v2, 2); + + // key3 should have been evicted + let mut key3_recreated = false; + cache.entry(TestLookupKey("key3"), || { + key3_recreated = true; + 30 + }); + assert!(key3_recreated, "key3 should have been evicted as the LRU"); + } +} diff --git a/vendor/parley-0.11.1/src/resolve/mod.rs b/vendor/parley-0.11.1/src/resolve/mod.rs new file mode 100644 index 0000000000..f9cfc01d2f --- /dev/null +++ b/vendor/parley-0.11.1/src/resolve/mod.rs @@ -0,0 +1,531 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Resolution of dynamic properties within a context. + +pub(crate) mod range; +pub(crate) mod tree; + +pub(crate) use range::RangedStyleBuilder; + +use alloc::{vec, vec::Vec}; + +use super::style::{ + Brush, FontFamily, FontFamilyName, FontFeature, FontFeatures, FontStyle, FontVariation, + FontVariations, FontWeight, FontWidth, StyleProperty, +}; +use crate::font::FontContext; +use crate::style::TextStyle; +use crate::util::nearly_eq; +use crate::{LineHeight, OverflowWrap, layout}; +use crate::{TextWrapMode, WordBreak}; +use core::borrow::Borrow; +use core::ops::Range; +use fontique::FamilyId; +use fontique::Language; + +/// Style with an associated range. +#[derive(Debug, Clone)] +pub(crate) struct RangedStyle { + pub(crate) style: ResolvedStyle, + pub(crate) range: Range, +} + +/// Run that references a style in a shared style table. +#[derive(Debug, Clone)] +pub(crate) struct StyleRun { + pub(crate) style_index: u16, + pub(crate) range: Range, +} + +#[derive(Clone)] +struct RangedProperty { + property: ResolvedProperty, + range: Range, +} + +/// Handle for a managed property. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub(crate) struct Resolved { + index: usize, + _phantom: core::marker::PhantomData, +} + +impl Default for Resolved { + fn default() -> Self { + Self { + index: !0, + _phantom: core::marker::PhantomData, + } + } +} + +impl Resolved { + pub(crate) fn id(&self) -> usize { + self.index + } +} + +#[derive(Clone)] +struct Cache { + /// Items in the cache. May contain sequences. + items: Vec, + /// Each entry represents a range of items in `data`. + entries: Vec<(usize, usize)>, +} + +impl Default for Cache { + fn default() -> Self { + Self { + items: vec![], + entries: vec![], + } + } +} + +impl Cache { + pub(crate) fn clear(&mut self) { + self.items.clear(); + self.entries.clear(); + } + + pub(crate) fn insert(&mut self, items: &[T]) -> Resolved { + for (i, entry) in self.entries.iter().enumerate() { + let range = entry.0..entry.1; + if range.len() != items.len() { + continue; + } + if let Some(existing) = self.items.get(range) { + if existing == items { + return Resolved { + index: i, + _phantom: core::marker::PhantomData, + }; + } + } + } + let index = self.entries.len(); + let start = self.items.len(); + self.items.extend(items.iter().cloned()); + let end = self.items.len(); + self.entries.push((start, end)); + Resolved { + index, + _phantom: core::marker::PhantomData, + } + } + + pub(crate) fn get(&self, handle: Resolved) -> Option<&[T]> { + let (start, end) = *self.entries.get(handle.index)?; + self.items.get(start..end) + } +} + +/// Context for managing dynamic properties during layout. +#[derive(Clone, Default)] +pub(crate) struct ResolveContext { + families: Cache, + variations: Cache, + features: Cache, + tmp_families: Vec, + tmp_variations: Vec, + tmp_features: Vec, +} + +impl ResolveContext { + pub(crate) fn resolve_property( + &mut self, + fcx: &mut FontContext, + property: &StyleProperty<'_, B>, + scale: f32, + ) -> ResolvedProperty { + use ResolvedProperty::*; + match property { + StyleProperty::FontFamily(value) => FontFamily(self.resolve_font_family(fcx, value)), + StyleProperty::FontSize(value) => FontSize(*value * scale), + StyleProperty::FontWidth(value) => FontWidth(*value), + StyleProperty::FontStyle(value) => FontStyle(*value), + StyleProperty::FontWeight(value) => FontWeight(*value), + StyleProperty::FontVariations(value) => FontVariations(self.resolve_variations(value)), + StyleProperty::FontFeatures(value) => FontFeatures(self.resolve_features(value)), + StyleProperty::Locale(value) => Locale(*value), + StyleProperty::Brush(value) => Brush(value.clone()), + StyleProperty::Underline(value) => Underline(*value), + StyleProperty::UnderlineOffset(value) => UnderlineOffset(value.map(|x| x * scale)), + StyleProperty::UnderlineSize(value) => UnderlineSize(value.map(|x| x * scale)), + StyleProperty::UnderlineBrush(value) => UnderlineBrush(value.clone()), + StyleProperty::Strikethrough(value) => Strikethrough(*value), + StyleProperty::StrikethroughOffset(value) => { + StrikethroughOffset(value.map(|x| x * scale)) + } + StyleProperty::StrikethroughSize(value) => StrikethroughSize(value.map(|x| x * scale)), + StyleProperty::StrikethroughBrush(value) => StrikethroughBrush(value.clone()), + StyleProperty::LineHeight(value) => LineHeight(value.scale(scale)), + StyleProperty::WordSpacing(value) => WordSpacing(*value * scale), + StyleProperty::LetterSpacing(value) => LetterSpacing(*value * scale), + StyleProperty::WordBreak(value) => WordBreak(*value), + StyleProperty::OverflowWrap(value) => OverflowWrap(*value), + StyleProperty::TextWrapMode(value) => TextWrapMode(*value), + } + } + + pub(crate) fn resolve_entire_style_set( + &mut self, + fcx: &mut FontContext, + raw_style: &TextStyle<'_, '_, B>, + scale: f32, + ) -> ResolvedStyle { + ResolvedStyle { + font_family: self.resolve_font_family(fcx, &raw_style.font_family), + font_size: raw_style.font_size * scale, + font_width: raw_style.font_width, + font_style: raw_style.font_style, + font_weight: raw_style.font_weight, + font_variations: self.resolve_variations(&raw_style.font_variations), + font_features: self.resolve_features(&raw_style.font_features), + locale: raw_style.locale, + brush: raw_style.brush.clone(), + underline: ResolvedDecoration { + enabled: raw_style.has_underline, + offset: raw_style.underline_offset.map(|x| x * scale), + size: raw_style.underline_size.map(|x| x * scale), + brush: raw_style.underline_brush.clone(), + }, + strikethrough: ResolvedDecoration { + enabled: raw_style.has_strikethrough, + offset: raw_style.strikethrough_offset.map(|x| x * scale), + size: raw_style.strikethrough_size.map(|x| x * scale), + brush: raw_style.strikethrough_brush.clone(), + }, + line_height: raw_style.line_height.scale(scale), + word_spacing: raw_style.word_spacing * scale, + letter_spacing: raw_style.letter_spacing * scale, + word_break: raw_style.word_break, + overflow_wrap: raw_style.overflow_wrap, + text_wrap_mode: raw_style.text_wrap_mode, + } + } + + /// Resolves a `font-family` value. + pub(crate) fn resolve_font_family( + &mut self, + fcx: &mut FontContext, + value: &FontFamily<'_>, + ) -> Resolved { + self.tmp_families.clear(); + match value { + FontFamily::Source(source) => { + for family in FontFamilyName::parse_css_list(source).map_while(Result::ok) { + match family { + FontFamilyName::Named(name) => { + if let Some(family) = fcx.collection.family_by_name(&name) { + self.tmp_families.push(family.id()); + } + } + FontFamilyName::Generic(family) => { + self.tmp_families + .extend(fcx.collection.generic_families(family)); + } + } + } + } + FontFamily::Single(family) => match family { + FontFamilyName::Named(name) => { + if let Some(family) = fcx.collection.family_by_name(name) { + self.tmp_families.push(family.id()); + } + } + FontFamilyName::Generic(family) => { + self.tmp_families + .extend(fcx.collection.generic_families(*family)); + } + }, + FontFamily::List(families) => { + let families: &[FontFamilyName<'_>] = families.borrow(); + for family in families { + match family { + FontFamilyName::Named(name) => { + if let Some(family) = fcx.collection.family_by_name(name) { + self.tmp_families.push(family.id()); + } + } + FontFamilyName::Generic(family) => { + self.tmp_families + .extend(fcx.collection.generic_families(*family)); + } + } + } + } + } + let resolved = self.families.insert(&self.tmp_families); + self.tmp_families.clear(); + resolved + } + + /// Resolves font variation settings. + pub(crate) fn resolve_variations( + &mut self, + variations: &FontVariations<'_>, + ) -> Resolved { + match variations { + FontVariations::Source(source) => { + self.tmp_variations.clear(); + self.tmp_variations + .extend(FontVariation::parse_css_list(source).map_while(Result::ok)); + } + FontVariations::List(settings) => { + self.tmp_variations.clear(); + self.tmp_variations.extend_from_slice(settings); + } + } + if self.tmp_variations.is_empty() { + return Resolved::default(); + } + self.tmp_variations.sort_by_key(|a| a.tag); + let resolved = self.variations.insert(&self.tmp_variations); + self.tmp_variations.clear(); + resolved + } + + /// Resolves font feature settings. + pub(crate) fn resolve_features( + &mut self, + features: &FontFeatures<'_>, + ) -> Resolved { + match features { + FontFeatures::Source(source) => { + self.tmp_features.clear(); + self.tmp_features + .extend(FontFeature::parse_css_list(source).map_while(Result::ok)); + } + FontFeatures::List(settings) => { + self.tmp_features.clear(); + self.tmp_features.extend_from_slice(settings); + } + } + if self.tmp_features.is_empty() { + return Resolved::default(); + } + self.tmp_features.sort_by_key(|a| a.tag); + let resolved = self.features.insert(&self.tmp_features); + self.tmp_features.clear(); + resolved + } + + /// Returns the list of font families for the specified handle. + pub(crate) fn stack(&self, stack: Resolved) -> Option<&[FamilyId]> { + self.families.get(stack) + } + + /// Returns the list of font variations for the specified handle. + pub(crate) fn variations( + &self, + variations: Resolved, + ) -> Option<&[FontVariation]> { + self.variations.get(variations) + } + + /// Returns the list of font features for the specified handle. + pub(crate) fn features(&self, features: Resolved) -> Option<&[FontFeature]> { + self.features.get(features) + } + + /// Clears the resources in the context. + pub(crate) fn clear(&mut self) { + self.families.clear(); + self.variations.clear(); + self.features.clear(); + } +} + +/// Style property with resolved resources. +#[derive(Clone, PartialEq)] +pub(crate) enum ResolvedProperty { + /// `font-family`. + FontFamily(Resolved), + /// Font size. + FontSize(f32), + /// Font width. + FontWidth(FontWidth), + /// Font style. + FontStyle(FontStyle), + /// Font weight. + FontWeight(FontWeight), + /// Font variation settings. + FontVariations(Resolved), + /// Font feature settings. + FontFeatures(Resolved), + /// Locale. + Locale(Option), + /// Brush for rendering text. + Brush(B), + /// Underline decoration. + Underline(bool), + /// Offset of the underline decoration. + UnderlineOffset(Option), + /// Size of the underline decoration. + UnderlineSize(Option), + /// Brush for rendering the underline decoration. + UnderlineBrush(Option), + /// Strikethrough decoration. + Strikethrough(bool), + /// Offset of the strikethrough decoration. + StrikethroughOffset(Option), + /// Size of the strikethrough decoration. + StrikethroughSize(Option), + /// Brush for rendering the strikethrough decoration. + StrikethroughBrush(Option), + /// Line height. + LineHeight(LineHeight), + /// Extra spacing between words. + WordSpacing(f32), + /// Extra spacing between letters. + LetterSpacing(f32), + /// Control over where words can wrap. + WordBreak(WordBreak), + /// Control over "emergency" line-breaking. + OverflowWrap(OverflowWrap), + /// Control over non-"emergency" line-breaking. + TextWrapMode(TextWrapMode), +} + +/// Flattened group of style properties. +#[derive(Clone, PartialEq, Debug, Default)] +pub(crate) struct ResolvedStyle { + /// `font-family`. + pub(crate) font_family: Resolved, + /// Font size. + pub(crate) font_size: f32, + /// Font width. + pub(crate) font_width: FontWidth, + /// Font style. + pub(crate) font_style: FontStyle, + /// Font weight. + pub(crate) font_weight: FontWeight, + /// Font variation settings. + pub(crate) font_variations: Resolved, + /// Font feature settings. + pub(crate) font_features: Resolved, + /// Locale. + pub(crate) locale: Option, + /// Brush for rendering text. + pub(crate) brush: B, + /// Underline decoration. + pub(crate) underline: ResolvedDecoration, + /// Strikethrough decoration. + pub(crate) strikethrough: ResolvedDecoration, + /// Line height. + pub(crate) line_height: LineHeight, + /// Extra spacing between words. + pub(crate) word_spacing: f32, + /// Extra spacing between letters. + pub(crate) letter_spacing: f32, + /// Control over where words can wrap. + pub(crate) word_break: WordBreak, + /// Control over "emergency" line-breaking. + pub(crate) overflow_wrap: OverflowWrap, + /// Control over non-"emergency" line-breaking. + pub(crate) text_wrap_mode: TextWrapMode, +} + +impl ResolvedStyle { + /// Applies the specified property to this style. + pub(crate) fn apply(&mut self, property: ResolvedProperty) { + use ResolvedProperty::*; + match property { + FontFamily(value) => self.font_family = value, + FontSize(value) => self.font_size = value, + FontWidth(value) => self.font_width = value, + FontStyle(value) => self.font_style = value, + FontWeight(value) => self.font_weight = value, + FontVariations(value) => self.font_variations = value, + FontFeatures(value) => self.font_features = value, + Locale(value) => self.locale = value, + Brush(value) => self.brush = value, + Underline(value) => self.underline.enabled = value, + UnderlineOffset(value) => self.underline.offset = value, + UnderlineSize(value) => self.underline.size = value, + UnderlineBrush(value) => self.underline.brush = value, + Strikethrough(value) => self.strikethrough.enabled = value, + StrikethroughOffset(value) => self.strikethrough.offset = value, + StrikethroughSize(value) => self.strikethrough.size = value, + StrikethroughBrush(value) => self.strikethrough.brush = value, + LineHeight(value) => self.line_height = value, + WordSpacing(value) => self.word_spacing = value, + LetterSpacing(value) => self.letter_spacing = value, + WordBreak(value) => self.word_break = value, + OverflowWrap(value) => self.overflow_wrap = value, + TextWrapMode(value) => self.text_wrap_mode = value, + } + } + + pub(crate) fn check(&self, property: &ResolvedProperty) -> bool { + use ResolvedProperty::*; + match property { + FontFamily(value) => self.font_family == *value, + FontSize(value) => nearly_eq(self.font_size, *value), + FontWidth(value) => self.font_width == *value, + FontStyle(value) => self.font_style == *value, + FontWeight(value) => self.font_weight == *value, + FontVariations(value) => self.font_variations == *value, + FontFeatures(value) => self.font_features == *value, + Locale(value) => self.locale == *value, + Brush(value) => self.brush == *value, + Underline(value) => self.underline.enabled == *value, + UnderlineOffset(value) => self.underline.offset == *value, + UnderlineSize(value) => self.underline.size == *value, + UnderlineBrush(value) => self.underline.brush == *value, + Strikethrough(value) => self.strikethrough.enabled == *value, + StrikethroughOffset(value) => self.strikethrough.offset == *value, + StrikethroughSize(value) => self.strikethrough.size == *value, + StrikethroughBrush(value) => self.strikethrough.brush == *value, + LineHeight(value) => self.line_height.nearly_eq(*value), + WordSpacing(value) => nearly_eq(self.word_spacing, *value), + LetterSpacing(value) => nearly_eq(self.letter_spacing, *value), + WordBreak(value) => self.word_break == *value, + OverflowWrap(value) => self.overflow_wrap == *value, + TextWrapMode(value) => self.text_wrap_mode == *value, + } + } + + pub(crate) fn as_layout_style(&self) -> layout::Style { + layout::Style { + brush: self.brush.clone(), + underline: self.underline.as_layout_decoration(&self.brush), + strikethrough: self.strikethrough.as_layout_decoration(&self.brush), + line_height: self.line_height, + overflow_wrap: self.overflow_wrap, + text_wrap_mode: self.text_wrap_mode, + #[cfg(feature = "accesskit")] + locale: self.locale, + } + } +} + +/// Underline or strikethrough decoration. +#[derive(Clone, PartialEq, Default, Debug)] +pub(crate) struct ResolvedDecoration { + /// True if the decoration is enabled. + pub(crate) enabled: bool, + /// Offset of the decoration from the baseline. + pub(crate) offset: Option, + /// Thickness of the decoration stroke. + pub(crate) size: Option, + /// Brush for the decoration. + pub(crate) brush: Option, +} + +impl ResolvedDecoration { + /// Convert into a layout Decoration (filtering out disabled decorations) + pub(crate) fn as_layout_decoration(&self, default_brush: &B) -> Option> { + if self.enabled { + Some(layout::Decoration { + brush: self.brush.clone().unwrap_or_else(|| default_brush.clone()), + offset: self.offset, + size: self.size, + }) + } else { + None + } + } +} diff --git a/vendor/parley-0.11.1/src/resolve/range.rs b/vendor/parley-0.11.1/src/resolve/range.rs new file mode 100644 index 0000000000..08180d529e --- /dev/null +++ b/vendor/parley-0.11.1/src/resolve/range.rs @@ -0,0 +1,224 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Range based style application. + +use alloc::vec; + +use super::{Brush, RangedProperty, RangedStyle, ResolvedProperty, ResolvedStyle, StyleRun, Vec}; +use core::ops::{Bound, Range, RangeBounds}; + +/// Builder for constructing an ordered sequence of non-overlapping ranged +/// styles from a collection of ranged style properties. +#[derive(Clone)] +pub(crate) struct RangedStyleBuilder { + properties: Vec>, + scratch_styles: Vec>, + root_style: ResolvedStyle, + len: usize, +} + +impl Default for RangedStyleBuilder { + fn default() -> Self { + Self { + properties: vec![], + scratch_styles: vec![], + root_style: ResolvedStyle::default(), + // We use `usize::MAX` as a sentinel that `begin` hasn't been called. + // This is required (rather than requiring the root style in the constructor) + // as we want to support re-using this value. + len: usize::MAX, + } + } +} + +impl RangedStyleBuilder { + /// Prepares the builder for accepting ranged properties for text of the specified length. + /// + /// The provided `root_style` is the default style applied to all text unless overridden. + pub(crate) fn begin(&mut self, root_style: ResolvedStyle, len: usize) { + self.properties.clear(); + self.scratch_styles.clear(); + self.root_style = root_style; + self.len = len; + } + + /// Change a property of the root style, which covers the full range of text. + /// + /// # Panics + /// + /// If [`begin`](Self::begin) has not been called before using this method. + pub(crate) fn push_default(&mut self, property: ResolvedProperty) { + assert!( + self.len != usize::MAX, + "Internal error: Must call `begin` before setting properties on a `RangedStyleBuilder`." + ); + self.root_style.apply(property); + } + + /// Override a property for the specified range of text. + /// + /// # Panics + /// + /// If [`begin`](Self::begin) has not been called before using this method. + pub(crate) fn push(&mut self, property: ResolvedProperty, range: impl RangeBounds) { + assert!( + self.len != usize::MAX, + "Internal error: Must call `begin` before setting properties on a `RangedStyleBuilder`." + ); + let range = resolve_range(range, self.len); + self.properties.push(RangedProperty { property, range }); + } + + /// Computes style table + style runs for the ranged properties. + pub(crate) fn finish( + &mut self, + style_table: &mut Vec>, + style_runs: &mut Vec, + ) { + style_table.clear(); + style_runs.clear(); + if self.len == usize::MAX { + self.properties.clear(); + self.scratch_styles.clear(); + self.root_style = ResolvedStyle::default(); + return; + } + let styles = &mut self.scratch_styles; + styles.push(RangedStyle { + style: self.root_style.clone(), + range: 0..self.len, + }); + for prop in &self.properties { + if prop.range.start > prop.range.end { + continue; + } + let split_range = split_range(prop, styles); + let mut inserted = 0; + if let Some(first) = split_range.first { + let original_span = &mut styles[first]; + if !original_span.style.check(&prop.property) { + let mut new_span = original_span.clone(); + let original_end = original_span.range.end; + original_span.range.end = prop.range.start; + new_span.range.start = prop.range.start; + new_span.style.apply(prop.property.clone()); + if split_range.replace_len == 0 && split_range.last == Some(first) { + let mut new_end_span = original_span.clone(); + new_end_span.range.start = prop.range.end; + new_end_span.range.end = original_end; + new_span.range.end = prop.range.end; + styles.splice( + first + 1..first + 1, + [new_span, new_end_span].iter().cloned(), + ); + continue; + } else { + styles.insert(first + 1, new_span); + } + inserted += 1; + } + } + let replace_start = split_range.replace_start + inserted; + let replace_end = replace_start + split_range.replace_len; + for style in &mut styles[replace_start..replace_end] { + style.style.apply(prop.property.clone()); + } + if let Some(mut last) = split_range.last { + last += inserted; + let original_span = &mut styles[last]; + if !original_span.style.check(&prop.property) { + let mut new_span = original_span.clone(); + original_span.range.start = prop.range.end; + new_span.range.end = prop.range.end; + new_span.style.apply(prop.property.clone()); + styles.insert(last, new_span); + } + } + } + let mut prev_index = 0; + let mut merged_count = 0; + for i in 1..styles.len() { + if styles[prev_index].style == styles[i].style { + let end = styles[i].range.end; + styles[prev_index].range.end = end; + merged_count += 1; + } else { + prev_index += 1; + if prev_index != i { + let moved_span = styles[i].clone(); + styles[prev_index] = moved_span; + } + } + } + styles.truncate(styles.len() - merged_count); + + style_table.reserve(styles.len()); + style_runs.reserve(styles.len()); + for (style_index, style) in styles.drain(..).enumerate() { + style_table.push(style.style); + style_runs.push(StyleRun { + style_index: style_index as u16, + range: style.range, + }); + } + + self.properties.clear(); + self.root_style = ResolvedStyle::default(); + self.len = usize::MAX; + } +} + +#[derive(Default)] +struct SplitRange { + first: Option, + replace_start: usize, + replace_len: usize, + last: Option, +} + +fn split_range(prop: &RangedProperty, spans: &[RangedStyle]) -> SplitRange { + let mut range = SplitRange::default(); + let start_span_index = + match spans.binary_search_by(|span| span.range.start.cmp(&prop.range.start)) { + Ok(index) => index, + Err(index) => index.saturating_sub(1), + }; + let mut end_span_index = spans.len() - 1; + for (i, span) in spans[start_span_index..].iter().enumerate() { + if span.range.end >= prop.range.end { + end_span_index = i + start_span_index; + break; + } + } + let start_span = &spans[start_span_index]; + let end_span = &spans[end_span_index]; + if start_span.range.start < prop.range.start { + range.first = Some(start_span_index); + range.replace_start = start_span_index + 1; + } else { + range.replace_start = start_span_index; + } + if end_span.range.end > prop.range.end { + range.last = Some(end_span_index); + range.replace_len = end_span_index.saturating_sub(range.replace_start); + } else { + range.replace_len = (end_span_index + 1).saturating_sub(range.replace_start); + } + range +} + +/// Resolves a `RangeBounds` into a range in the range 0..len. +fn resolve_range(range: impl RangeBounds, len: usize) -> Range { + let start = match range.start_bound() { + Bound::Unbounded => 0, + Bound::Included(n) => *n, + Bound::Excluded(n) => *n + 1, + }; + let end = match range.end_bound() { + Bound::Unbounded => len, + Bound::Included(n) => *n + 1, + Bound::Excluded(n) => *n, + }; + start.min(len)..end.min(len) +} diff --git a/vendor/parley-0.11.1/src/resolve/tree.rs b/vendor/parley-0.11.1/src/resolve/tree.rs new file mode 100644 index 0000000000..9e280bd2b9 --- /dev/null +++ b/vendor/parley-0.11.1/src/resolve/tree.rs @@ -0,0 +1,310 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Hierarchical tree based style application. +use alloc::{string::String, vec::Vec}; + +use crate::style::WhiteSpaceCollapse; + +use super::{Brush, ResolvedProperty, ResolvedStyle, StyleRun}; + +#[derive(Debug, Clone)] +struct StyleTreeNode { + parent: Option, + style: ResolvedStyle, + style_id: Option, +} + +#[derive(Clone, Copy, PartialEq)] +pub(crate) enum ItemKind { + None, + InlineBox, + TextRun, +} + +/// Builder for constructing a tree of styles +#[derive(Clone)] +pub(crate) struct TreeStyleBuilder { + tree: Vec>, + style_table: Vec>, + style_runs: Vec, + white_space_collapse: WhiteSpaceCollapse, + text: String, + uncommitted_text: String, + current_span: usize, + is_span_first: bool, + last_item_kind: ItemKind, +} + +impl TreeStyleBuilder { + fn current_style(&self) -> ResolvedStyle { + self.tree[self.current_span].style.clone() + } +} + +impl Default for TreeStyleBuilder { + fn default() -> Self { + Self { + tree: Vec::new(), + style_table: Vec::new(), + style_runs: Vec::new(), + white_space_collapse: WhiteSpaceCollapse::Preserve, + text: String::new(), + uncommitted_text: String::new(), + current_span: usize::MAX, + is_span_first: false, + last_item_kind: ItemKind::None, + } + } +} + +impl TreeStyleBuilder { + /// Prepares the builder for accepting a tree of styles and text. + /// + /// The provided `root_style` is the default style applied to all text unless overridden. + pub(crate) fn begin(&mut self, root_style: ResolvedStyle) { + self.tree.clear(); + self.style_table.clear(); + self.style_runs.clear(); + self.white_space_collapse = WhiteSpaceCollapse::Preserve; + self.text.clear(); + self.uncommitted_text.clear(); + + self.tree.push(StyleTreeNode { + parent: None, + style: root_style, + style_id: None, + }); + self.current_span = 0; + self.is_span_first = true; + } + + pub(crate) fn set_white_space_mode(&mut self, white_space_collapse: WhiteSpaceCollapse) { + self.white_space_collapse = white_space_collapse; + } + + pub(crate) fn set_is_span_first(&mut self, is_span_first: bool) { + self.is_span_first = is_span_first; + } + + pub(crate) fn set_last_item_kind(&mut self, item_kind: ItemKind) { + self.last_item_kind = item_kind; + } + + pub(crate) fn push_uncommitted_text(&mut self, is_span_last: bool) { + let uncommitted_text = core::mem::take(&mut self.uncommitted_text); + let span_text = match self.white_space_collapse { + WhiteSpaceCollapse::Preserve => uncommitted_text, + WhiteSpaceCollapse::Collapse => { + let mut span_text = uncommitted_text.as_str(); + + if self.is_span_first + || (self.last_item_kind == ItemKind::TextRun + && self + .text + .chars() + .last() + .is_some_and(|c| c.is_ascii_whitespace())) + { + span_text = span_text.trim_start(); + } + if is_span_last { + span_text = span_text.trim_end(); + } + + // Collapse spaces + let mut last_char_whitespace = false; + span_text + .chars() + .filter_map(|c: char| { + let this_char_whitespace = c.is_ascii_whitespace(); + let prev_char_whitespace = last_char_whitespace; + last_char_whitespace = this_char_whitespace; + + if this_char_whitespace { + if prev_char_whitespace { + None + } else { + Some(' ') + } + } else { + Some(c) + } + }) + .collect() + } + }; + + // Nothing to do if there is no uncommitted text. + if span_text.is_empty() { + return; + } + + let range = self.text.len()..(self.text.len() + span_text.len()); + let style_index = self.resolve_current_style_id(); + self.style_runs.push(StyleRun { style_index, range }); + self.text.push_str(&span_text); + self.is_span_first = false; + self.last_item_kind = ItemKind::TextRun; + } + + fn resolve_current_style_id(&mut self) -> u16 { + if let Some(style_id) = self.tree[self.current_span].style_id { + return style_id; + } + let style_id = self.style_table.len() as u16; + self.style_table.push(self.current_style()); + self.tree[self.current_span].style_id = Some(style_id); + style_id + } + + pub(crate) fn current_text_len(&self) -> usize { + self.text.len() + } + + pub(crate) fn push_style_span(&mut self, style: ResolvedStyle) { + self.push_uncommitted_text(false); + + self.tree.push(StyleTreeNode { + parent: Some(self.current_span), + style, + style_id: None, + }); + self.current_span = self.tree.len() - 1; + self.is_span_first = true; + } + + pub(crate) fn push_style_modification_span( + &mut self, + properties: impl Iterator>, + ) { + let mut style = self.current_style(); + for prop in properties { + style.apply(prop.clone()); + } + self.push_style_span(style); + } + + pub(crate) fn pop_style_span(&mut self) { + self.push_uncommitted_text(true); + + self.current_span = self.tree[self.current_span] + .parent + .expect("Popped root style"); + } + + /// Pushes a property that covers the specified range of text. + pub(crate) fn push_text(&mut self, text: &str) { + if !text.is_empty() { + self.uncommitted_text.push_str(text); + } + } + + /// Computes style table + style runs and returns the final text buffer. + pub(crate) fn finish( + &mut self, + style_table: &mut Vec>, + style_runs: &mut Vec, + ) -> String { + while self.tree[self.current_span].parent.is_some() { + self.pop_style_span(); + } + + self.push_uncommitted_text(true); + + style_table.clear(); + style_runs.clear(); + style_table.extend_from_slice(&self.style_table); + style_runs.extend_from_slice(&self.style_runs); + + core::mem::take(&mut self.text) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec::Vec; + use core::ops::Range; + + #[test] + fn reuses_style_id_when_returning_to_parent_span() { + let mut builder = TreeStyleBuilder::::default(); + builder.begin(ResolvedStyle::default()); + builder.push_text("A"); + builder.push_style_modification_span([ResolvedProperty::FontSize(20.)].into_iter()); + builder.push_text("B"); + builder.pop_style_span(); + builder.push_text("C"); + + let mut style_table = Vec::new(); + let mut style_runs = Vec::new(); + let text = builder.finish(&mut style_table, &mut style_runs); + + assert_eq!(text, "ABC"); + assert_eq!(style_table.len(), 2); + assert_eq!(style_runs.len(), 3); + assert_eq!(style_runs[0].style_index, 0); + assert_eq!(style_runs[1].style_index, 1); + assert_eq!(style_runs[2].style_index, 0); + assert_eq!(style_runs[0].range, Range { start: 0, end: 1 }); + assert_eq!(style_runs[1].range, Range { start: 1, end: 2 }); + assert_eq!(style_runs[2].range, Range { start: 2, end: 3 }); + } + + #[test] + fn reuses_root_style_id_across_multiple_pop_return_cycles() { + let mut builder = TreeStyleBuilder::::default(); + builder.begin(ResolvedStyle::default()); + builder.push_text("A"); + builder.push_style_modification_span([ResolvedProperty::FontSize(20.)].into_iter()); + builder.push_text("B"); + builder.pop_style_span(); + builder.push_text("C"); + builder.push_style_modification_span([ResolvedProperty::LetterSpacing(1.)].into_iter()); + builder.push_text("D"); + builder.pop_style_span(); + builder.push_text("E"); + + let mut style_table = Vec::new(); + let mut style_runs = Vec::new(); + let text = builder.finish(&mut style_table, &mut style_runs); + + assert_eq!(text, "ABCDE"); + assert_eq!(style_table.len(), 3); + assert_eq!(style_runs.len(), 5); + assert_eq!(style_runs[0].style_index, 0); + assert_eq!(style_runs[1].style_index, 1); + assert_eq!(style_runs[2].style_index, 0); + assert_eq!(style_runs[3].style_index, 2); + assert_eq!(style_runs[4].style_index, 0); + } + + #[test] + fn reuses_parent_and_root_style_ids_after_nested_pop() { + let mut builder = TreeStyleBuilder::::default(); + builder.begin(ResolvedStyle::default()); + builder.push_text("R"); + builder.push_style_modification_span([ResolvedProperty::FontSize(20.)].into_iter()); + builder.push_text("A"); + builder.push_style_modification_span([ResolvedProperty::LetterSpacing(1.)].into_iter()); + builder.push_text("B"); + builder.pop_style_span(); + builder.push_text("C"); + builder.pop_style_span(); + builder.push_text("D"); + + let mut style_table = Vec::new(); + let mut style_runs = Vec::new(); + let text = builder.finish(&mut style_table, &mut style_runs); + + assert_eq!(text, "RABCD"); + assert_eq!(style_table.len(), 3); + assert_eq!(style_runs.len(), 5); + assert_eq!(style_runs[0].style_index, 0); + assert_eq!(style_runs[1].style_index, 1); + assert_eq!(style_runs[2].style_index, 2); + assert_eq!(style_runs[3].style_index, 1); + assert_eq!(style_runs[4].style_index, 0); + } +} diff --git a/vendor/parley-0.11.1/src/setting.rs b/vendor/parley-0.11.1/src/setting.rs new file mode 100644 index 0000000000..564b79d4eb --- /dev/null +++ b/vendor/parley-0.11.1/src/setting.rs @@ -0,0 +1,6 @@ +// Copyright 2025 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! OpenType settings (features and variations). + +pub use parlance::{FontFeature, FontVariation, Tag}; diff --git a/vendor/parley-0.11.1/src/shape/cache.rs b/vendor/parley-0.11.1/src/shape/cache.rs new file mode 100644 index 0000000000..6cfa2f788e --- /dev/null +++ b/vendor/parley-0.11.1/src/shape/cache.rs @@ -0,0 +1,178 @@ +// Copyright 2025 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use crate::FontVariation; +use alloc::boxed::Box; +use hashbrown::Equivalent; + +#[derive(PartialEq, Copy, Clone)] +pub(crate) struct ShapeDataKey { + /// The font collection's blob ID. + font_blob_id: u64, + /// The font's index in the font collection. + font_index: u32, +} + +impl ShapeDataKey { + pub(crate) const fn new(font_blob_id: u64, font_index: u32) -> Self { + Self { + font_blob_id, + font_index, + } + } +} + +impl Equivalent for ShapeDataKey { + #[inline(always)] + fn equivalent(&self, key: &Self) -> bool { + self == key + } +} + +impl From<&Self> for ShapeDataKey { + #[inline(always)] + fn from(key: &Self) -> Self { + *key + } +} + +pub(crate) struct ShapeInstanceId { + /// The font collection's blob ID. + font_blob_id: u64, + /// The font's index in the font collection. + font_index: u32, + synthesis: fontique::Synthesis, + variations: Option>, +} + +pub(crate) struct ShapeInstanceKey<'a> { + /// The font collection's blob ID. + font_blob_id: u64, + /// The font's index in the font collection. + font_index: u32, + synthesis: &'a fontique::Synthesis, + variations: Option<&'a [FontVariation]>, +} + +impl<'a> ShapeInstanceKey<'a> { + pub(crate) const fn new( + font_blob_id: u64, + font_index: u32, + synthesis: &'a fontique::Synthesis, + variations: Option<&'a [FontVariation]>, + ) -> Self { + Self { + font_blob_id, + font_index, + synthesis, + variations, + } + } +} + +impl<'a> Equivalent for ShapeInstanceKey<'a> { + #[inline(always)] + fn equivalent(&self, key: &ShapeInstanceId) -> bool { + self.font_blob_id == key.font_blob_id + && self.font_index == key.font_index + && *self.synthesis == key.synthesis + && self.variations == key.variations.as_deref() + } +} + +impl<'a> From> for ShapeInstanceId { + #[inline(always)] + fn from(key: ShapeInstanceKey<'a>) -> Self { + Self { + font_blob_id: key.font_blob_id, + font_index: key.font_index, + synthesis: *key.synthesis, + variations: key.variations.map(|v| v.to_vec().into()), + } + } +} + +pub(crate) struct ShapePlanId { + /// The font collection's blob ID. + font_blob_id: u64, + /// The font's index in the font collection. + font_index: u32, + synthesis: fontique::Synthesis, + direction: harfrust::Direction, + script: harfrust::Script, + language: Option, + features: Box<[harfrust::Feature]>, + variations: Option>, +} + +pub(crate) struct ShapePlanKey<'a> { + /// The font collection's blob ID. + font_blob_id: u64, + /// The font's index in the font collection. + font_index: u32, + synthesis: &'a fontique::Synthesis, + direction: harfrust::Direction, + script: harfrust::Script, + language: Option, + features: &'a [harfrust::Feature], + variations: Option<&'a [FontVariation]>, +} + +impl<'a> ShapePlanKey<'a> { + pub(crate) const fn new( + font_blob_id: u64, + font_index: u32, + synthesis: &'a fontique::Synthesis, + direction: harfrust::Direction, + script: harfrust::Script, + language: Option, + features: &'a [harfrust::Feature], + variations: Option<&'a [FontVariation]>, + ) -> Self { + Self { + font_blob_id, + font_index, + synthesis, + direction, + script, + language, + features, + variations, + } + } +} + +impl<'a> Equivalent for ShapePlanKey<'a> { + #[inline(always)] + fn equivalent(&self, key: &ShapePlanId) -> bool { + self.font_blob_id == key.font_blob_id + && self.font_index == key.font_index + && *self.synthesis == key.synthesis + && self.direction == key.direction + && self.script == key.script + && self.language == key.language + && self.features.len() == key.features.len() + && self.variations == key.variations.as_deref() + && self + .features + .iter() + .zip(key.features.iter()) + .all(|(a, b)| a == b) + } +} + +impl<'a> From> for ShapePlanId { + #[inline(always)] + fn from(key: ShapePlanKey<'a>) -> Self { + Self { + font_blob_id: key.font_blob_id, + font_index: key.font_index, + synthesis: *key.synthesis, + direction: key.direction, + script: key.script, + language: key.language, + features: key.features.to_vec().into(), + variations: key.variations.map(|v| v.to_vec().into()), + } + } +} diff --git a/vendor/parley-0.11.1/src/shape/mod.rs b/vendor/parley-0.11.1/src/shape/mod.rs new file mode 100644 index 0000000000..f2c1b339d1 --- /dev/null +++ b/vendor/parley-0.11.1/src/shape/mod.rs @@ -0,0 +1,672 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Text shaping implementation using `harfrust`for shaping +//! and `icu` for text analysis. + +use alloc::vec::Vec; +use core::mem; +use core::ops::RangeInclusive; +use harfrust::ShapeOptions; + +use super::layout::Layout; +use super::resolve::{ResolveContext, Resolved, ResolvedStyle}; +use super::style::{Brush, FontFeature, FontVariation}; +use crate::analysis::cluster::{Char, CharCluster, Status}; +use crate::analysis::{AnalysisDataSources, CharInfo}; +use crate::convert::script_to_harfrust; +use crate::inline_box::InlineBox; +use crate::lru_cache::LruCache; +use crate::util::nearly_eq; +use crate::{FontData, convert}; +use fontique::Language; +use icu_properties::props::Script; + +use fontique::{self, Query, QueryFamily, QueryFont}; + +mod cache; + +pub(crate) struct ShapeContext { + shape_data_cache: LruCache, + shape_instance_cache: LruCache, + shape_plan_cache: LruCache, + unicode_buffer: Option, + features: Vec, + char_cluster: CharCluster, +} + +impl Default for ShapeContext { + fn default() -> Self { + const MAX_ENTRIES: usize = 16; + Self { + shape_data_cache: LruCache::new(MAX_ENTRIES), + shape_instance_cache: LruCache::new(MAX_ENTRIES), + shape_plan_cache: LruCache::new(MAX_ENTRIES), + unicode_buffer: Some(harfrust::UnicodeBuffer::new()), + features: Vec::new(), + char_cluster: CharCluster::default(), + } + } +} + +struct Item { + style_index: u16, + size: f32, + script: Script, + level: u8, + locale: Option, + variations: Resolved, + features: Resolved, + word_spacing: f32, + letter_spacing: f32, +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn shape_text<'a, B: Brush>( + rcx: &'a ResolveContext, + mut fq: Query<'a>, + styles: &'a [ResolvedStyle], + inline_boxes: &[InlineBox], + infos: &[(CharInfo, u16)], + levels: &[u8], + scx: &mut ShapeContext, + mut text: &str, + layout: &mut Layout, + analysis_data_sources: &AnalysisDataSources, +) { + // If we have both empty text and no inline boxes, shape with a fake space + // to generate metrics that can be used to size a cursor. + if text.is_empty() && inline_boxes.is_empty() { + text = " "; + } + // Do nothing if there is no text or styles (there should always be a default style) + if text.is_empty() || styles.is_empty() { + // Process any remaining inline boxes whose index is greater than the length of the text + for box_idx in 0..inline_boxes.len() { + // Push the box to the list of items + layout.data.push_inline_box(box_idx); + } + return; + } + + // Setup mutable state for iteration + let initial_style_index = infos.first().map_or(0, |(_, style_index)| *style_index); + let mut style = &styles[initial_style_index as usize]; + let mut item = Item { + style_index: initial_style_index, + size: style.font_size, + level: levels.first().copied().unwrap_or(0), + script: infos + .iter() + .map(|x| x.0.script) + .find(|&script| real_script(script)) + .unwrap_or(Script::Latin), + locale: style.locale, + variations: style.font_variations, + features: style.font_features, + word_spacing: style.word_spacing, + letter_spacing: style.letter_spacing, + }; + + let mut char_range = 0..0; + let mut text_range = 0..0; + + let mut inline_box_iter = inline_boxes.iter().enumerate(); + let mut current_box = inline_box_iter.next(); + + // Iterate over characters in the text + for ((char_index, (byte_index, ch)), (info, style_index)) in + text.char_indices().enumerate().zip(infos) + { + let mut break_run = false; + let mut script = info.script; + if !real_script(script) { + script = item.script; + } + let level = levels.get(char_index).copied().unwrap_or(0); + if item.style_index != *style_index { + item.style_index = *style_index; + style = &styles[*style_index as usize]; + if !nearly_eq(style.font_size, item.size) + || style.locale != item.locale + || style.font_variations != item.variations + || style.font_features != item.features + || !nearly_eq(style.letter_spacing, item.letter_spacing) + || !nearly_eq(style.word_spacing, item.word_spacing) + { + break_run = true; + } + } + + if level != item.level || script != item.script { + break_run = true; + } + + // Check if there is an inline box at this index + // Note: + // - We loop because there may be multiple boxes at this index + // - We do this *before* processing the text run because we need to know whether we should + // break the run due to the presence of an inline box. + let mut deferred_boxes: Option> = None; + while let Some((box_idx, inline_box)) = current_box { + if inline_box.index == byte_index { + break_run = true; + if let Some(boxes) = &mut deferred_boxes { + deferred_boxes = Some((*boxes.start())..=box_idx); + } else { + deferred_boxes = Some(box_idx..=box_idx); + }; + // Update the current box to the next box + current_box = inline_box_iter.next(); + } else { + break; + } + } + + if break_run && !text_range.is_empty() { + shape_item( + &mut fq, + rcx, + styles, + &item, + scx, + text, + &text_range, + &char_range, + infos, + layout, + analysis_data_sources, + ); + item.size = style.font_size; + item.level = level; + item.script = script; + item.locale = style.locale; + item.variations = style.font_variations; + item.features = style.font_features; + item.word_spacing = style.word_spacing; + item.letter_spacing = style.letter_spacing; + text_range.start = text_range.end; + char_range.start = char_range.end; + } + + if let Some(deferred_boxes) = deferred_boxes { + for box_idx in deferred_boxes { + layout.data.push_inline_box(box_idx); + } + } + + text_range.end += ch.len_utf8(); + char_range.end += 1; + } + + if !text_range.is_empty() { + shape_item( + &mut fq, + rcx, + styles, + &item, + scx, + text, + &text_range, + &char_range, + infos, + layout, + analysis_data_sources, + ); + } + + // Process any remaining inline boxes whose index is greater than the length of the text + if let Some((box_idx, _inline_box)) = current_box { + layout.data.push_inline_box(box_idx); + } + for (box_idx, _inline_box) in inline_box_iter { + layout.data.push_inline_box(box_idx); + } +} + +// Rebuilds the provided `char_cluster` in-place using the existing allocation +// for the given grapheme `segment_text`, consuming items from `item_infos_iter`. +fn fill_cluster_in_place( + segment_text: &str, + item_infos_iter: &mut core::slice::Iter<'_, (CharInfo, u16)>, + code_unit_offset_in_string: &mut usize, + char_cluster: &mut CharCluster, +) { + // Reset cluster but keep allocation + char_cluster.clear(); + + let mut force_normalize = false; + let mut is_emoji_or_pictograph = false; + let mut map_len: u8 = 0; + let start = *code_unit_offset_in_string as u32; + + for ((_, ch), (info, style_index)) in segment_text.char_indices().zip(item_infos_iter.by_ref()) + { + force_normalize |= info.force_normalize(); + // TODO - make emoji detection more complete, as per (except using composite Trie tables as + // much as possible: + // https://github.com/conor-93/parley/blob/4637d826732a1a82bbb3c904c7f47a16a21cceec/parley/src/shape/mod.rs#L221-L269 + is_emoji_or_pictograph |= info.is_emoji_or_pictograph(); + *code_unit_offset_in_string += ch.len_utf8(); + + // TODO: Explore ignoring other modifiers in determining `contributes_to_shaping`: + // regional indicators, subdivision flag tag sequences, skin tone modifiers + // See also: https://github.com/google/emoji-segmenter + + // If the color emoji has a non-printing variation selector, ignore the variation selector. + // Its presentation depends on the platform and font. + // + // e.g. + // - `U+270C + U+FE0F`: `✌`, force basic presentation + // - `U+270C + U+FE0F`: `✌️`, force emoji presentation + // + // + let is_emoji_with_non_printing_variation_selector = + is_emoji_or_pictograph && info.is_variation_selector(); + + let contributes_to_shaping = + info.contributes_to_shaping() && !is_emoji_with_non_printing_variation_selector; + if contributes_to_shaping { + map_len += 1; + } + + char_cluster.chars.push(Char { + ch, + contributes_to_shaping, + glyph_id: 0, + style_index: *style_index, + is_control_character: info.is_control(), + }); + } + + // Finalize cluster metadata + let end = *code_unit_offset_in_string as u32; + char_cluster.is_emoji = is_emoji_or_pictograph; + char_cluster.map_len = map_len; + char_cluster.start = start; + char_cluster.end = end; + char_cluster.force_normalize = force_normalize; +} + +fn shape_item<'a, B: Brush>( + fq: &mut Query<'a>, + rcx: &'a ResolveContext, + styles: &'a [ResolvedStyle], + item: &Item, + scx: &mut ShapeContext, + text: &str, + text_range: &core::ops::Range, + char_range: &core::ops::Range, + infos: &[(CharInfo, u16)], + layout: &mut Layout, + analysis_data_sources: &AnalysisDataSources, +) { + let item_text = &text[text_range.clone()]; + let item_infos = &infos[char_range.start..char_range.end]; // Only process current item + let first_style_index = item_infos[0].1; + let fb_script = convert::script_to_fontique(item.script, analysis_data_sources); + let mut font_selector = + FontSelector::new(fq, rcx, styles, first_style_index, fb_script, item.locale); + + let grapheme_cluster_boundaries = analysis_data_sources + .grapheme_segmenter() + .segment_str(item_text); + let mut item_infos_iter = item_infos.iter(); + let mut code_unit_offset_in_string = text_range.start; + let char_cluster = &mut scx.char_cluster; + + // Build an iterator of boundaries and consume the first segment to seed the loop + let mut boundaries_iter = grapheme_cluster_boundaries.skip(1); + let mut last_boundary = 0_usize; + let Some(mut current_boundary) = boundaries_iter.next() else { + return; // No clusters + }; + + fill_cluster_in_place( + &item_text[last_boundary..current_boundary], + &mut item_infos_iter, + &mut code_unit_offset_in_string, + char_cluster, + ); + + let mut current_font = font_selector.select_font(char_cluster, analysis_data_sources); + + // Main segmentation loop (based on swash shape_clusters) - only within current item + while let Some(font) = current_font.take() { + // Collect all clusters for this font segment + let cluster_range = char_cluster.range(); + let segment_start_offset = cluster_range.start as usize - text_range.start; + let mut segment_end_offset = cluster_range.end as usize - text_range.start; + + for next_boundary in boundaries_iter.by_ref() { + // Build next cluster in-place + last_boundary = current_boundary; + current_boundary = next_boundary; + fill_cluster_in_place( + &item_text[last_boundary..current_boundary], + &mut item_infos_iter, + &mut code_unit_offset_in_string, + char_cluster, + ); + + if let Some(next_font) = font_selector.select_font(char_cluster, analysis_data_sources) + { + if next_font != font { + current_font = Some(next_font); + break; + } else { + // Same font - add to current segment + segment_end_offset = char_cluster.range().end as usize - text_range.start; + } + } else { + // No font determined, continue to next cluster + continue; + } + } + + // Shape this font segment with harfrust + let segment_text = &item_text[segment_start_offset..segment_end_offset]; + // Shape the entire segment text including newlines + // The line breaking algorithm will handle newlines automatically + + // TODO: How do we want to handle errors like this? + let font_ref = + harfrust::FontRef::from_index(font.font.blob.as_ref(), font.font.index).unwrap(); + + // Create harfrust shaper + let shaper_data = scx.shape_data_cache.entry( + cache::ShapeDataKey::new(font.font.blob.id(), font.font.index), + || harfrust::ShaperData::new(&font_ref), + ); + let instance = scx.shape_instance_cache.entry( + cache::ShapeInstanceKey::new( + font.font.blob.id(), + font.font.index, + &font.font.synthesis, + rcx.variations(item.variations), + ), + || { + harfrust::ShaperInstance::from_variations( + &font_ref, + variations_iter(&font.font.synthesis, rcx.variations(item.variations)), + ) + }, + ); + + let direction = if item.level & 1 != 0 { + harfrust::Direction::RightToLeft + } else { + harfrust::Direction::LeftToRight + }; + let hb_script = script_to_harfrust(fb_script); + let language = item + .locale + .as_ref() + .and_then(|lang| lang.language().parse::().ok()); + scx.features.clear(); + for feature in rcx.features(item.features).unwrap_or(&[]) { + scx.features.push(harfrust::Feature::new( + harfrust::Tag::new(&feature.tag.to_bytes()), + feature.value as u32, + .., + )); + } + let harf_shaper = shaper_data + .shaper(&font_ref) + .instance(Some(instance)) + .build(); + let shaper_plan = scx.shape_plan_cache.entry( + cache::ShapePlanKey::new( + font.font.blob.id(), + font.font.index, + &font.font.synthesis, + direction, + hb_script, + language.clone(), + &scx.features, + rcx.variations(item.variations), + ), + || { + harfrust::ShapePlan::new( + &harf_shaper, + direction, + Some(hb_script), + language.as_ref(), + &scx.features, + ) + }, + ); + + // Prepare harfrust buffer + let mut buffer = mem::take(&mut scx.unicode_buffer).unwrap(); + buffer.clear(); + + // Use the entire segment text including newlines + buffer.reserve(segment_text.len()); + for (i, ch) in segment_text.chars().enumerate() { + // Ensure that each cluster's index matches the index into `infos`. This is required + // for efficient cluster lookup within `data.rs`. + // + // In other words, instead of using `buffer.push_str`, which iterates `segment_text` + // with `char_indices`, push each char individually via `.chars` with a cluster index + // that matches its `infos` counterpart. This allows us to lookup `infos` via cluster + // index in `data.rs`. + buffer.add(ch, i as u32); + } + + buffer.set_direction(direction); + + buffer.set_script(hb_script); + + if let Some(lang) = language { + buffer.set_language(lang); + } + + let glyph_buffer = harf_shaper.shape( + buffer, + ShapeOptions::new() + .plan(Some(shaper_plan)) + .features(&scx.features) + .point_size(Some(item.size)), + ); + + // Extract relevant CharInfo slice for this segment + let char_start = char_range.start + item_text[..segment_start_offset].chars().count(); + let segment_char_start = char_start - char_range.start; + let segment_char_count = segment_text.chars().count(); + let segment_infos = + &item_infos[segment_char_start..(segment_char_start + segment_char_count)]; + + // Push harfrust-shaped run for the entire segment + layout.data.push_run( + FontData::new(font.font.blob.clone(), font.font.index), + item.size, + font.attrs, + font.font.synthesis, + &glyph_buffer, + item.level, + item.style_index, + item.word_spacing, + item.letter_spacing, + segment_text, + segment_infos, + (text_range.start + segment_start_offset)..(text_range.start + segment_end_offset), + harf_shaper.coords(), + ); + + // Replace buffer to reuse allocation in next iteration. + scx.unicode_buffer = Some(glyph_buffer.clear()); + } +} + +fn real_script(script: Script) -> bool { + script != Script::Common && script != Script::Unknown && script != Script::Inherited +} + +fn variations_iter<'a>( + synthesis: &'a fontique::Synthesis, + item: Option<&'a [FontVariation]>, +) -> impl Iterator + 'a { + synthesis + .variation_settings() + .iter() + .map(|(tag, value)| harfrust::Variation { + tag: *tag, + value: *value, + }) + .chain( + item.unwrap_or(&[]) + .iter() + .map(|variation| harfrust::Variation { + tag: harfrust::Tag::new(&variation.tag.to_bytes()), + value: variation.value, + }), + ) +} + +struct FontSelector<'a, 'b, B: Brush> { + query: &'b mut Query<'a>, + fonts_id: Option, + rcx: &'a ResolveContext, + styles: &'a [ResolvedStyle], + style_index: u16, + attrs: fontique::Attributes, + variations: &'a [FontVariation], + features: &'a [FontFeature], +} + +impl<'a, 'b, B: Brush> FontSelector<'a, 'b, B> { + fn new( + query: &'b mut Query<'a>, + rcx: &'a ResolveContext, + styles: &'a [ResolvedStyle], + style_index: u16, + fb_script: fontique::Script, + locale: Option, + ) -> Self { + let style = &styles[style_index as usize]; + let fonts_id = style.font_family.id(); + let fonts = rcx.stack(style.font_family).unwrap_or(&[]); + let attrs = fontique::Attributes { + width: style.font_width, + weight: style.font_weight, + style: style.font_style, + }; + let variations = rcx.variations(style.font_variations).unwrap_or(&[]); + let features = rcx.features(style.font_features).unwrap_or(&[]); + query.set_families(fonts.iter().copied()); + + query.set_fallbacks(fontique::FallbackKey::new(fb_script, locale.as_ref())); + query.set_attributes(attrs); + + Self { + query, + fonts_id: Some(fonts_id), + rcx, + styles, + style_index, + attrs, + variations, + features, + } + } + + fn select_font( + &mut self, + cluster: &mut CharCluster, + analysis_data_sources: &AnalysisDataSources, + ) -> Option { + let style_index = cluster.style_index(); + let is_emoji = cluster.is_emoji; + if style_index != self.style_index || is_emoji || self.fonts_id.is_none() { + self.style_index = style_index; + let style = &self.styles[style_index as usize]; + + let fonts_id = style.font_family.id(); + let fonts = self.rcx.stack(style.font_family).unwrap_or(&[]); + let fonts = fonts.iter().copied().map(QueryFamily::Id); + if is_emoji { + use core::iter::once; + let emoji_family = QueryFamily::Generic(fontique::GenericFamily::Emoji); + self.query.set_families(fonts.chain(once(emoji_family))); + self.fonts_id = None; + } else if self.fonts_id != Some(fonts_id) { + self.query.set_families(fonts); + self.fonts_id = Some(fonts_id); + } + + let attrs = fontique::Attributes { + width: style.font_width, + weight: style.font_weight, + style: style.font_style, + }; + if self.attrs != attrs { + self.query.set_attributes(attrs); + self.attrs = attrs; + } + self.variations = self.rcx.variations(style.font_variations).unwrap_or(&[]); + self.features = self.rcx.features(style.font_features).unwrap_or(&[]); + } + let mut selected_font = None; + self.query.matches_with(|font| { + let Some(charmap) = font.charmap() else { + return fontique::QueryStatus::Continue; + }; + + let map_status = cluster.map( + |ch| { + charmap + .map(ch) + .map(|g| { + // HACK: in reality, we're only computing coverage, so + // we only care about whether the font has a mapping + // for a particular glyph. Any non-zero value indicates + // the existence of a glyph so we can simplify this + // without a fallible conversion from u32 to u16. + (g != 0) as u16 + }) + .unwrap_or_default() + }, + analysis_data_sources, + ); + + match map_status { + Status::Complete => { + selected_font = Some(SelectedFont { + font: font.clone(), + attrs: self.attrs, + }); + fontique::QueryStatus::Stop + } + Status::Keep => { + selected_font = Some(SelectedFont { + font: font.clone(), + attrs: self.attrs, + }); + fontique::QueryStatus::Continue + } + Status::Discard => { + if selected_font.is_none() { + selected_font = Some(SelectedFont { + font: font.clone(), + attrs: self.attrs, + }); + } + fontique::QueryStatus::Continue + } + } + }); + selected_font + } +} + +struct SelectedFont { + font: QueryFont, + attrs: fontique::Attributes, +} + +impl PartialEq for SelectedFont { + fn eq(&self, other: &Self) -> bool { + self.font.family == other.font.family && self.font.synthesis == other.font.synthesis + } +} diff --git a/vendor/parley-0.11.1/src/style/brush.rs b/vendor/parley-0.11.1/src/style/brush.rs new file mode 100644 index 0000000000..ad671f068d --- /dev/null +++ b/vendor/parley-0.11.1/src/style/brush.rs @@ -0,0 +1,7 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +/// Trait for types that represent the color of glyphs or decorations. +pub trait Brush: Clone + PartialEq + Default + core::fmt::Debug {} + +impl Brush for T {} diff --git a/vendor/parley-0.11.1/src/style/font.rs b/vendor/parley-0.11.1/src/style/font.rs new file mode 100644 index 0000000000..29599a5251 --- /dev/null +++ b/vendor/parley-0.11.1/src/style/font.rs @@ -0,0 +1,76 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use alloc::borrow::Cow; + +pub use crate::setting::{FontFeature, FontVariation}; +pub use fontique::{FontStyle, FontWeight, FontWidth, GenericFamily}; +pub use parlance::{FontFamily, FontFamilyName}; + +/// Font variation settings that can be supplied as a raw source string or a parsed slice. +#[derive(Clone, PartialEq, Debug)] +pub enum FontVariations<'a> { + /// Setting source in CSS format. + Source(Cow<'a, str>), + /// List of settings. + List(Cow<'a, [FontVariation]>), +} + +impl<'a> FontVariations<'a> { + /// Creates an empty list of font variations. + pub const fn empty() -> Self { + Self::List(Cow::Borrowed(&[])) + } +} + +impl<'a> From<&'a str> for FontVariations<'a> { + fn from(value: &'a str) -> Self { + Self::Source(Cow::Borrowed(value)) + } +} + +impl<'a> From<&'a [FontVariation]> for FontVariations<'a> { + fn from(value: &'a [FontVariation]) -> Self { + Self::List(Cow::Borrowed(value)) + } +} + +impl<'a, const N: usize> From<&'a [FontVariation; N]> for FontVariations<'a> { + fn from(value: &'a [FontVariation; N]) -> Self { + Self::List(Cow::Borrowed(&value[..])) + } +} + +/// Font feature settings that can be supplied as a raw source string or a parsed slice. +#[derive(Clone, PartialEq, Debug)] +pub enum FontFeatures<'a> { + /// Setting source in CSS format. + Source(Cow<'a, str>), + /// List of settings. + List(Cow<'a, [FontFeature]>), +} + +impl<'a> FontFeatures<'a> { + /// Creates an empty list of font features. + pub const fn empty() -> Self { + Self::List(Cow::Borrowed(&[])) + } +} + +impl<'a> From<&'a str> for FontFeatures<'a> { + fn from(value: &'a str) -> Self { + Self::Source(Cow::Borrowed(value)) + } +} + +impl<'a> From<&'a [FontFeature]> for FontFeatures<'a> { + fn from(value: &'a [FontFeature]) -> Self { + Self::List(Cow::Borrowed(value)) + } +} + +impl<'a, const N: usize> From<&'a [FontFeature; N]> for FontFeatures<'a> { + fn from(value: &'a [FontFeature; N]) -> Self { + Self::List(Cow::Borrowed(&value[..])) + } +} diff --git a/vendor/parley-0.11.1/src/style/mod.rs b/vendor/parley-0.11.1/src/style/mod.rs new file mode 100644 index 0000000000..a8f720e449 --- /dev/null +++ b/vendor/parley-0.11.1/src/style/mod.rs @@ -0,0 +1,279 @@ +// Copyright 2021 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Rich styling support. + +mod brush; +mod font; +mod styleset; + +use alloc::borrow::Cow; + +pub use brush::*; +pub use font::{ + FontFamily, FontFamilyName, FontFeature, FontFeatures, FontStyle, FontVariation, + FontVariations, FontWeight, FontWidth, GenericFamily, +}; +pub use fontique::Language; +pub use parlance::{OverflowWrap, TextWrapMode, WordBreak}; +pub use styleset::StyleSet; + +use crate::util::nearly_eq; + +#[derive(Debug, Clone, Copy)] +pub enum WhiteSpaceCollapse { + Collapse, + Preserve, +} + +/// The height that this text takes up. The default is `MetricsRelative(1.0)`, which is the given +/// font's preferred line height. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum LineHeight { + /// The line's height is a multiple of the "line height" defined by the font's metrics--the sum + /// of the ascender height, descender height, and line gap / leading. + MetricsRelative(f32), + /// Line height specified as a multiple of the font size. This is how the CSS `line-height` + /// property behaves if given a unitless number. Useful if you're using system-defined generic + /// font families and want the line heights to be consistent across platforms. + FontSizeRelative(f32), + /// Line height specified in absolute units. This can be useful for ensuring all lines are + /// spaced a whole number of pixels apart, or fitting lines into a given layout container + /// height. + Absolute(f32), +} + +impl Default for LineHeight { + fn default() -> Self { + Self::MetricsRelative(1.0) + } +} + +impl LineHeight { + pub(crate) fn nearly_eq(self, other: Self) -> bool { + match (self, other) { + (Self::MetricsRelative(a), Self::MetricsRelative(b)) + | (Self::FontSizeRelative(a), Self::FontSizeRelative(b)) + | (Self::Absolute(a), Self::Absolute(b)) => nearly_eq(a, b), + _ => false, + } + } + + pub(crate) fn scale(self, scale: f32) -> Self { + match self { + Self::Absolute(value) => Self::Absolute(value * scale), + // The other variants are relative to the font size, so scaling here needn't do anything + value => value, + } + } +} + +/// Properties that define a style. +#[derive(Clone, PartialEq, Debug)] +pub enum StyleProperty<'a, B: Brush> { + /// CSS `font-family` property value. + FontFamily(FontFamily<'a>), + /// Font size. + FontSize(f32), + /// Font width. + FontWidth(FontWidth), + /// Font style. + FontStyle(FontStyle), + /// Font weight. + FontWeight(FontWeight), + /// Font variation settings. + FontVariations(FontVariations<'a>), + /// Font feature settings. + FontFeatures(FontFeatures<'a>), + /// Locale. + Locale(Option), + /// Brush for rendering text. + Brush(B), + /// Underline decoration. + Underline(bool), + /// Offset of the underline decoration. + UnderlineOffset(Option), + /// Size of the underline decoration. + UnderlineSize(Option), + /// Brush for rendering the underline decoration. + UnderlineBrush(Option), + /// Strikethrough decoration. + Strikethrough(bool), + /// Offset of the strikethrough decoration. + StrikethroughOffset(Option), + /// Size of the strikethrough decoration. + StrikethroughSize(Option), + /// Brush for rendering the strikethrough decoration. + StrikethroughBrush(Option), + /// Line height. + LineHeight(LineHeight), + /// Extra spacing between words. + WordSpacing(f32), + /// Extra spacing between letters. + LetterSpacing(f32), + /// Control over where words can wrap. + WordBreak(WordBreak), + /// Control over "emergency" line-breaking. + OverflowWrap(OverflowWrap), + /// Control over non-"emergency" line-breaking. + TextWrapMode(TextWrapMode), +} + +/// Unresolved styles. +#[derive(Clone, PartialEq, Debug)] +pub struct TextStyle<'family, 'settings, B: Brush> { + /// CSS `font-family` property value. + pub font_family: FontFamily<'family>, + /// Font size. + pub font_size: f32, + /// Font width. + pub font_width: FontWidth, + /// Font style. + pub font_style: FontStyle, + /// Font weight. + pub font_weight: FontWeight, + /// Font variation settings. + pub font_variations: FontVariations<'settings>, + /// Font feature settings. + pub font_features: FontFeatures<'settings>, + /// Locale. + pub locale: Option, + /// Brush for rendering text. + pub brush: B, + /// Underline decoration. + pub has_underline: bool, + /// Offset of the underline decoration. + pub underline_offset: Option, + /// Size of the underline decoration. + pub underline_size: Option, + /// Brush for rendering the underline decoration. + pub underline_brush: Option, + /// Strikethrough decoration. + pub has_strikethrough: bool, + /// Offset of the strikethrough decoration. + pub strikethrough_offset: Option, + /// Size of the strikethrough decoration. + pub strikethrough_size: Option, + /// Brush for rendering the strikethrough decoration. + pub strikethrough_brush: Option, + /// Line height. + pub line_height: LineHeight, + /// Extra spacing between words. + pub word_spacing: f32, + /// Extra spacing between letters. + pub letter_spacing: f32, + /// Control over where words can wrap. + pub word_break: WordBreak, + /// Control over "emergency" line-breaking. + pub overflow_wrap: OverflowWrap, + /// Control over non-"emergency" line-breaking. + pub text_wrap_mode: TextWrapMode, +} + +impl Default for TextStyle<'static, 'static, B> { + fn default() -> Self { + TextStyle { + font_family: FontFamily::Source(Cow::Borrowed("sans-serif")), + font_size: 16.0, + font_width: FontWidth::default(), + font_style: FontStyle::default(), + font_weight: FontWeight::default(), + font_variations: FontVariations::empty(), + font_features: FontFeatures::empty(), + locale: None, + brush: B::default(), + has_underline: false, + underline_offset: None, + underline_size: None, + underline_brush: None, + has_strikethrough: false, + strikethrough_offset: None, + strikethrough_size: None, + strikethrough_brush: None, + line_height: LineHeight::default(), + word_spacing: 0.0, + letter_spacing: 0.0, + word_break: WordBreak::default(), + overflow_wrap: OverflowWrap::default(), + text_wrap_mode: TextWrapMode::default(), + } + } +} + +impl<'a, B: Brush> From> for StyleProperty<'a, B> { + fn from(value: FontFamily<'a>) -> Self { + StyleProperty::FontFamily(value) + } +} + +impl<'a, B: Brush> From<&'a [FontFamilyName<'a>]> for StyleProperty<'a, B> { + fn from(value: &'a [FontFamilyName<'a>]) -> Self { + StyleProperty::FontFamily(value.into()) + } +} + +impl<'a, B: Brush> From> for StyleProperty<'a, B> { + fn from(value: FontFamilyName<'a>) -> Self { + StyleProperty::FontFamily(value.into()) + } +} + +impl<'a, B: Brush> From> for StyleProperty<'a, B> { + fn from(value: FontVariations<'a>) -> Self { + StyleProperty::FontVariations(value) + } +} + +impl<'a, B: Brush> From> for StyleProperty<'a, B> { + fn from(value: FontFeatures<'a>) -> Self { + StyleProperty::FontFeatures(value) + } +} + +impl From for StyleProperty<'_, B> { + fn from(f: GenericFamily) -> Self { + StyleProperty::FontFamily(f.into()) + } +} + +impl From for StyleProperty<'_, B> { + fn from(value: LineHeight) -> Self { + StyleProperty::LineHeight(value) + } +} + +impl From for StyleProperty<'_, B> { + fn from(value: FontWidth) -> Self { + StyleProperty::FontWidth(value) + } +} + +impl From for StyleProperty<'_, B> { + fn from(value: FontStyle) -> Self { + StyleProperty::FontStyle(value) + } +} + +impl From for StyleProperty<'_, B> { + fn from(value: FontWeight) -> Self { + StyleProperty::FontWeight(value) + } +} + +impl From for StyleProperty<'_, B> { + fn from(value: WordBreak) -> Self { + StyleProperty::WordBreak(value) + } +} + +impl From for StyleProperty<'_, B> { + fn from(value: OverflowWrap) -> Self { + StyleProperty::OverflowWrap(value) + } +} + +impl From for StyleProperty<'_, B> { + fn from(value: TextWrapMode) -> Self { + StyleProperty::TextWrapMode(value) + } +} diff --git a/vendor/parley-0.11.1/src/style/styleset.rs b/vendor/parley-0.11.1/src/style/styleset.rs new file mode 100644 index 0000000000..32aaf309ca --- /dev/null +++ b/vendor/parley-0.11.1/src/style/styleset.rs @@ -0,0 +1,83 @@ +// Copyright 2024 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use core::mem::Discriminant; +use hashbrown::HashMap; + +type StyleProperty = crate::StyleProperty<'static, Brush>; + +/// A long-lived collection of [`StyleProperties`](super::StyleProperty), containing at +/// most one of each property. +/// +/// This is used by [`PlainEditor`](crate::editing::PlainEditor) to provide a reasonably ergonomic +/// mutable API for styles applied to all text managed by it. +/// This can be accessed using [`PlainEditor::edit_styles`](crate::editing::PlainEditor::edit_styles). +/// +/// These styles do not have a corresponding range, and are generally unsuited for rich text. +#[derive(Clone, Debug)] +pub struct StyleSet( + HashMap>, StyleProperty>, +); + +impl StyleSet { + /// Create a new collection of styles. + /// + /// The font size will be `font_size`, and can be overwritten at runtime by + /// [inserting](Self::insert) a new [`FontSize`](crate::StyleProperty::FontSize). + pub fn new(font_size: f32) -> Self { + let mut this = Self(HashMap::default()); + this.insert(StyleProperty::FontSize(font_size)); + this + } + + /// Add `style` to this collection, returning any overwritten value. + /// + /// Note: Adding a [`font-family`](crate::StyleProperty::FontFamily) to this collection is not + /// additive, and instead overwrites any previously added font stack. + pub fn insert(&mut self, style: StyleProperty) -> Option> { + let discriminant = core::mem::discriminant(&style); + self.0.insert(discriminant, style) + } + + /// [Retain](alloc::vec::Vec::retain) only the styles for which `f` returns true. + /// + /// Styles which are removed return to their default values. + /// + /// Removing the [font size](crate::StyleProperty::FontSize) is not recommended, as an unspecified + /// fallback font size will be used. + pub fn retain(&mut self, mut f: impl FnMut(&StyleProperty) -> bool) { + self.0.retain(|_, v| f(v)); + } + + /// Remove the style with the discriminant `property`. + /// + /// Styles which are removed return to their default values. + /// + /// To get the discriminant requires constructing a valid `StyleProperty` for the + /// the desired property and passing it to [`core::mem::discriminant`]. + /// Getting this discriminant is usually possible in a `const` context. + /// + /// Removing the [font size](crate::StyleProperty::FontSize) is not recommended, as an unspecified + /// fallback font size will be used. + pub fn remove( + &mut self, + property: Discriminant>, + ) -> Option> { + self.0.remove(&property) + } + + /// Read the raw underlying storage of this. + /// + /// Write access is not provided due to the invariant that keys + /// are the discriminant of their corresponding value. + pub fn inner(&self) -> &HashMap>, StyleProperty> { + &self.0 + } + + /// Clear all styles. + /// + /// All styles return to their default values. + pub fn clear(&mut self) { + self.0.clear(); + } +} diff --git a/vendor/parley-0.11.1/src/tests/mod.rs b/vendor/parley-0.11.1/src/tests/mod.rs new file mode 100644 index 0000000000..52b77f872f --- /dev/null +++ b/vendor/parley-0.11.1/src/tests/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2024 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +mod test_analysis; +mod test_builders; +mod utils; diff --git a/vendor/parley-0.11.1/src/tests/test_analysis.rs b/vendor/parley-0.11.1/src/tests/test_analysis.rs new file mode 100644 index 0000000000..086390401a --- /dev/null +++ b/vendor/parley-0.11.1/src/tests/test_analysis.rs @@ -0,0 +1,1258 @@ +// Copyright 2025 the Parley Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use crate::analysis::Boundary; +use crate::{FontContext, LayoutContext, RangedBuilder, StyleProperty, WordBreak}; +use alloc::{vec, vec::Vec}; +use fontique::FontWeight; +use icu_properties::props::{GraphemeClusterBreak, Script}; + +#[derive(Default)] +struct TestContext { + pub layout_context: LayoutContext, + pub font_context: FontContext, +} + +impl TestContext { + fn expect_boundary_list(self, expected: Vec) -> Self { + let actual: Vec<_> = self + .layout_context + .info + .iter() + .map(|(info, _)| info.boundary) + .collect(); + assert_eq!(actual, expected, "Boundary list mismatch"); + self + } + + fn expect_bidi_embed_level_list(self, expected: Vec) -> Self { + let actual = self.layout_context.bidi.levels(); + assert_eq!(actual, expected, "Bidi embed level list mismatch"); + self + } + + fn expect_script_list(self, expected: Vec