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**
+
+[](https://crates.io/crates/parley)
+[](https://docs.rs/parley)
+[](https://deps.rs/crate/parley)
+[](https://xi.zulipchat.com/#narrow/channel/205635-parley)
+[](#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