mirror of
https://github.com/lexmount/moli.git
synced 2026-10-06 00:00:56 +00:00
fix(layout): place inline text in the first fitting float band
This commit is contained in:
@@ -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/
|
||||
|
||||
Generated
-2
@@ -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",
|
||||
|
||||
+2
-1
@@ -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" }
|
||||
|
||||
@@ -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![
|
||||
|
||||
Vendored
+173
@@ -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"
|
||||
Vendored
+201
@@ -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.
|
||||
Vendored
+25
@@ -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.
|
||||
Vendored
+14
@@ -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.
|
||||
Vendored
+55
@@ -0,0 +1,55 @@
|
||||
<div align="center">
|
||||
|
||||
# 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)
|
||||
|
||||
</div>
|
||||
|
||||
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.
|
||||
|
||||
<details>
|
||||
<summary>Click here if compiling fails.</summary>
|
||||
|
||||
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
|
||||
```
|
||||
</details>
|
||||
|
||||
## 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 <http://www.apache.org/licenses/LICENSE-2.0>)
|
||||
- MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
|
||||
|
||||
at your option.
|
||||
|
||||
[Rust code of conduct]: https://www.rust-lang.org/policies/code-of-conduct
|
||||
+382
@@ -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<Char>,
|
||||
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
|
||||
}
|
||||
}
|
||||
+565
@@ -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<B: Brush>(
|
||||
lcx: &mut LayoutContext<B>,
|
||||
mut text: &str,
|
||||
line_break_override: Option<&LineBreakOverrideFn>,
|
||||
) {
|
||||
struct WordBreakSegmentIter<'a, I: Iterator, B: Brush> {
|
||||
text: &'a str,
|
||||
style_runs: I,
|
||||
lcx: &'a LayoutContext<B>,
|
||||
char_indices: core::str::CharIndices<'a>,
|
||||
current_char: (usize, char),
|
||||
building_range_start: usize,
|
||||
previous_word_break_style: WordBreak,
|
||||
done: bool,
|
||||
_phantom: PhantomData<B>,
|
||||
}
|
||||
|
||||
impl<'a, I, B: Brush + 'a> WordBreakSegmentIter<'a, I, B>
|
||||
where
|
||||
I: Iterator<Item = &'a StyleRun>,
|
||||
{
|
||||
fn new(
|
||||
text: &'a str,
|
||||
style_runs: I,
|
||||
lcx: &'a LayoutContext<B>,
|
||||
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<Item = &'a StyleRun>,
|
||||
{
|
||||
type Item = (&'a str, WordBreak, bool);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
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<usize> = 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)
|
||||
}
|
||||
Vendored
+894
@@ -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<BidiLevel>,
|
||||
initial_types: Vec<BidiClass>,
|
||||
types: Vec<BidiClass>,
|
||||
brackets: Vec<(usize, char, BidiMirroringGlyph)>,
|
||||
bracket_pairs: Vec<(usize, usize)>,
|
||||
runs: Vec<Run>,
|
||||
indices: Vec<usize>,
|
||||
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<Item = (char, (BidiClass, BidiMirroringGlyph))>,
|
||||
base_level: Option<u8>,
|
||||
) {
|
||||
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<F>(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<usize>,
|
||||
}
|
||||
|
||||
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<usize> {
|
||||
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)
|
||||
}
|
||||
+430
@@ -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<char>,
|
||||
/// 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<bool> + Send + Sync;
|
||||
|
||||
/// A line break override function mirroring Chromium's preferred line breaking behavior.
|
||||
///
|
||||
/// See: <https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/text/character_property_data_generator.cc;l=449-495>
|
||||
///
|
||||
/// # 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<bool>);
|
||||
|
||||
fn chromium_override(cx: LineBreakContext) -> Option<bool> {
|
||||
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`
|
||||
// <https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/text/text_break_iterator.cc;l=282-303>
|
||||
//
|
||||
// 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 <https://github.com/chromium/chromium/blob/c6bee15e8f336c8feabf539d8bbb540c134ec20a/third_party/blink/renderer/platform/text/text_break_iterator.cc#L224-L240>
|
||||
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<const N: usize> {
|
||||
/// Maps each `before` character (`0..128`) to a row in `rows`.
|
||||
row_of: [u8; 128],
|
||||
/// The distinct rows.
|
||||
rows: [Row; N],
|
||||
}
|
||||
|
||||
impl<const N: usize> AsciiLineBreakTable<N> {
|
||||
/// 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<bool> {
|
||||
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<char>,
|
||||
after: RangeInclusive<char>,
|
||||
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<const N: usize>(&self) -> AsciiLineBreakTable<N> {
|
||||
// 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<char> = '!'..='\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<char>, 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})",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+366
@@ -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<B>,
|
||||
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<StyleProperty<'a, B>>) {
|
||||
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<StyleProperty<'a, B>>,
|
||||
range: impl RangeBounds<usize>,
|
||||
) {
|
||||
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<B>, text: impl AsRef<str>) {
|
||||
// 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<str>) -> Layout<B> {
|
||||
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<B>,
|
||||
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<usize>) {
|
||||
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<B>, text: impl AsRef<str>) {
|
||||
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<str>) -> Layout<B> {
|
||||
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<B>,
|
||||
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<Item = &'iter StyleProperty<'s, B>>,
|
||||
) 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<B>) -> 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<B>, String) {
|
||||
let mut layout = Layout::default();
|
||||
let text = self.build_into(&mut layout);
|
||||
(layout, text)
|
||||
}
|
||||
}
|
||||
|
||||
fn build_into_layout<B: Brush>(
|
||||
layout: &mut Layout<B>,
|
||||
scale: f32,
|
||||
quantize: bool,
|
||||
text: &str,
|
||||
lcx: &mut LayoutContext<B>,
|
||||
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<usize>, len: usize) -> Range<usize> {
|
||||
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)
|
||||
}
|
||||
Vendored
+204
@@ -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<B: Brush = [u8; 4]> {
|
||||
pub(crate) rcx: ResolveContext,
|
||||
pub(crate) style_table: Vec<ResolvedStyle<B>>,
|
||||
pub(crate) style_runs: Vec<StyleRun>,
|
||||
pub(crate) inline_boxes: Vec<InlineBox>,
|
||||
pub(crate) bidi: BidiResolver,
|
||||
|
||||
// Reusable style builders (to amortise allocations)
|
||||
pub(crate) ranged_style_builder: RangedStyleBuilder<B>,
|
||||
pub(crate) tree_style_builder: TreeStyleBuilder<B>,
|
||||
|
||||
// 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<B: Brush> LayoutContext<B> {
|
||||
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<B> {
|
||||
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<B: Brush> Default for LayoutContext<B> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: Brush> Clone for LayoutContext<B> {
|
||||
fn clone(&self) -> Self {
|
||||
// None of the internal state is visible so just return a new instance.
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
Vendored
+23
@@ -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)
|
||||
}
|
||||
+466
@@ -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<B: Brush>(layout: &Layout<B>, 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<B: Brush>(layout: &Layout<B>, 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<B: Brush>(
|
||||
pos: &TextPosition,
|
||||
layout: &Layout<B>,
|
||||
layout_access: &LayoutAccessibility,
|
||||
) -> Option<Self> {
|
||||
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<B: Brush>(
|
||||
layout: &Layout<B>,
|
||||
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<B: Brush>(&self, layout: &Layout<B>) -> 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<B: Brush>(&self, layout: &Layout<B>) -> 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<B: Brush>(&self, layout: &Layout<B>) -> 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<B: Brush>(&self, layout: &Layout<B>) -> 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<B: Brush>(&self, layout: &Layout<B>) -> 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<B: Brush>(&self, layout: &Layout<B>) -> 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<B: Brush>(&self, layout: &Layout<B>) -> 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<B: Brush>(&self, layout: &Layout<B>, 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<B>,
|
||||
) -> [Option<Cluster<'a, B>>; 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<B>,
|
||||
) -> [Option<Cluster<'a, B>>; 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<B: Brush>(self, layout: &Layout<B>) -> Option<(usize, Line<'_, B>)> {
|
||||
let geometry = self.geometry(layout, 0.0);
|
||||
layout.line_for_offset(geometry.y0 as f32)
|
||||
}
|
||||
|
||||
pub(crate) fn upstream_cluster<B: Brush>(self, layout: &Layout<B>) -> Option<Cluster<'_, B>> {
|
||||
self.index
|
||||
.checked_sub(1)
|
||||
.and_then(|index| Cluster::from_byte_index(layout, index))
|
||||
}
|
||||
|
||||
pub(crate) fn downstream_cluster<B: Brush>(self, layout: &Layout<B>) -> Option<Cluster<'_, B>> {
|
||||
Cluster::from_byte_index(layout, self.index)
|
||||
}
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub fn to_access_position<B: Brush>(
|
||||
&self,
|
||||
layout: &Layout<B>,
|
||||
layout_access: &LayoutAccessibility,
|
||||
) -> Option<TextPosition> {
|
||||
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<B: Brush>(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<B: Brush>(layout: &Layout<B>, 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()
|
||||
}
|
||||
}
|
||||
+1286
File diff suppressed because it is too large
Load Diff
+10
@@ -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::*;
|
||||
+672
@@ -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<f32>,
|
||||
}
|
||||
|
||||
#[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<B: Brush>(layout: &Layout<B>, 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<B: Brush>(layout: &Layout<B>, 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<B: Brush>(layout: &Layout<B>, 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<B: Brush>(layout: &Layout<B>, 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<B: Brush>(layout: &Layout<B>, 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<B: Brush>(
|
||||
selection: &accesskit::TextSelection,
|
||||
layout: &Layout<B>,
|
||||
layout_access: &LayoutAccessibility,
|
||||
) -> Option<Self> {
|
||||
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<B: Brush>(&self, layout: &Layout<B>) -> 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<usize> {
|
||||
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<B: Brush>(&self, layout: &Layout<B>, 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<B: Brush>(&self, layout: &Layout<B>, 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<B: Brush>(&self, layout: &Layout<B>, 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<B: Brush>(&self, layout: &Layout<B>, 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<B: Brush>(&self, layout: &Layout<B>, 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<B: Brush>(&self, layout: &Layout<B>, 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<B: Brush>(&self, layout: &Layout<B>, 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<B: Brush>(&self, layout: &Layout<B>, 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<B: Brush>(&self, layout: &Layout<B>, 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<B: Brush>(&self, layout: &Layout<B>, 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<B: Brush>(&self, layout: &Layout<B>, 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<B: Brush>(&self, layout: &Layout<B>, 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<B: Brush>(&self, layout: &Layout<B>, 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<B: Brush>(&self, layout: &Layout<B>, 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<B: Brush>(&self, layout: &Layout<B>) -> 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<B: Brush>(
|
||||
&self,
|
||||
layout: &Layout<B>,
|
||||
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<B: Brush>(
|
||||
&self,
|
||||
layout: &Layout<B>,
|
||||
layout_access: &LayoutAccessibility,
|
||||
) -> Option<accesskit::TextSelection> {
|
||||
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<Cursor> 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]
|
||||
}
|
||||
}
|
||||
Vendored
+22
@@ -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()
|
||||
}
|
||||
}
|
||||
+40
@@ -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,
|
||||
}
|
||||
+307
@@ -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<u8>,
|
||||
character_positions: Vec<f32>,
|
||||
character_widths: Vec<f32>,
|
||||
word_starts: Vec<u8>,
|
||||
) {
|
||||
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<ClusterPath, NodeId>,
|
||||
pub(crate) span_paths_by_access_id: HashMap<NodeId, ClusterPath>,
|
||||
// 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<ClusterPath, ClusterPath>,
|
||||
}
|
||||
|
||||
impl LayoutAccessibility {
|
||||
fn span_id_and_node<B: Brush>(
|
||||
&mut self,
|
||||
next_node_id: &mut impl FnMut() -> NodeId,
|
||||
ids: &mut HashSet<NodeId>,
|
||||
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<B: Brush>(
|
||||
&mut self,
|
||||
text: &str,
|
||||
layout: &Layout<B>,
|
||||
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<B>),
|
||||
) {
|
||||
self.span_paths_by_cluster_path.clear();
|
||||
// Build a set of node IDs for the runs encountered in this pass.
|
||||
let mut ids = HashSet::<NodeId>::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<u16> = 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
|
||||
});
|
||||
}
|
||||
}
|
||||
+193
@@ -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<B: Brush>(
|
||||
layout: &mut LayoutData<B>,
|
||||
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<B: Brush>(layout: &mut LayoutData<B>) {
|
||||
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<B: Brush, const UNDO_JUSTIFICATION: bool>(
|
||||
layout: &mut LayoutData<B>,
|
||||
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<Item = &LineItemData> = 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<Item = &mut ClusterData> =
|
||||
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;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+604
@@ -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<B>, byte_index: usize) -> Option<Self> {
|
||||
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<B>, 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<B>, 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<B>,
|
||||
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<usize> {
|
||||
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<B> {
|
||||
&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<Item = Glyph> + '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<BreakReason> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
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<f32> {
|
||||
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<B>) -> Option<Line<'a, B>> {
|
||||
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<B>) -> Option<Run<'a, B>> {
|
||||
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<B>) -> Option<Cluster<'a, B>> {
|
||||
self.run(layout)?.get(self.logical_index())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum GlyphIter<'a> {
|
||||
Single(Option<Glyph>),
|
||||
Slice(core::slice::Iter<'a, Glyph>),
|
||||
}
|
||||
|
||||
impl Iterator for GlyphIter<'_> {
|
||||
type Item = Glyph;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
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<Brush> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
+962
@@ -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<usize> {
|
||||
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<usize>,
|
||||
/// Range of the source text.
|
||||
pub(crate) text_range: Range<usize>,
|
||||
/// Bidi level for the run.
|
||||
pub(crate) bidi_level: u8,
|
||||
/// Range of clusters.
|
||||
pub(crate) cluster_range: Range<usize>,
|
||||
/// Base for glyph indices.
|
||||
pub(crate) glyph_start: usize,
|
||||
/// Metrics for the run.
|
||||
pub(crate) metrics: RunMetrics,
|
||||
/// Additional word spacing.
|
||||
pub(crate) word_spacing: f32,
|
||||
/// Additional letter spacing.
|
||||
pub(crate) letter_spacing: f32,
|
||||
/// Total advance of the run.
|
||||
pub(crate) advance: f32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, PartialEq, Debug)]
|
||||
pub enum BreakReason {
|
||||
#[default]
|
||||
None,
|
||||
Regular,
|
||||
Explicit,
|
||||
Emergency,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq)]
|
||||
pub(crate) struct LineData {
|
||||
/// Range of the source text.
|
||||
pub(crate) text_range: Range<usize>,
|
||||
/// Range of line items.
|
||||
pub(crate) item_range: Range<usize>,
|
||||
/// Metrics for the line.
|
||||
pub(crate) metrics: LineMetrics,
|
||||
/// The cause of the line break.
|
||||
pub(crate) break_reason: BreakReason,
|
||||
/// Maximum advance for the line.
|
||||
pub(crate) max_advance: f32,
|
||||
/// Number of justified clusters on the line.
|
||||
pub(crate) num_spaces: usize,
|
||||
/// Text indent applied to this line.
|
||||
pub(crate) indent: f32,
|
||||
}
|
||||
|
||||
impl LineData {
|
||||
pub(crate) fn size(&self) -> f32 {
|
||||
self.metrics.ascent + self.metrics.descent + self.metrics.leading
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct LineItemData {
|
||||
/// Whether the item is a run or an inline box
|
||||
pub(crate) kind: LayoutItemKind,
|
||||
/// The index of the run or inline box in the runs or `inline_boxes` vec
|
||||
pub(crate) index: usize,
|
||||
/// Bidi level for the item (used for reordering)
|
||||
pub(crate) bidi_level: u8,
|
||||
/// Advance (size in direction of text flow) for the run.
|
||||
pub(crate) advance: f32,
|
||||
|
||||
// Fields that only apply to text runs (Ignored for boxes)
|
||||
// TODO: factor this out?
|
||||
/// True if the run is composed entirely of whitespace.
|
||||
pub(crate) is_whitespace: bool,
|
||||
/// True if the run ends in whitespace.
|
||||
pub(crate) has_trailing_whitespace: bool,
|
||||
/// Range of the source text.
|
||||
pub(crate) text_range: Range<usize>,
|
||||
/// Range of clusters.
|
||||
pub(crate) cluster_range: Range<usize>,
|
||||
}
|
||||
|
||||
impl LineItemData {
|
||||
pub(crate) fn is_text_run(&self) -> bool {
|
||||
self.kind == LayoutItemKind::TextRun
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn is_rtl(&self) -> bool {
|
||||
self.bidi_level & 1 != 0
|
||||
}
|
||||
|
||||
/// If the item is a text run
|
||||
/// - Determine if it consists entirely of whitespace (`is_whitespace` property)
|
||||
/// - Determine if it has trailing whitespace (`has_trailing_whitespace` property)
|
||||
pub(crate) fn compute_whitespace_properties<B: Brush>(&mut self, layout_data: &LayoutData<B>) {
|
||||
// Skip items which are not text runs
|
||||
if self.kind != LayoutItemKind::TextRun {
|
||||
return;
|
||||
}
|
||||
|
||||
self.is_whitespace = true;
|
||||
if self.is_rtl() {
|
||||
// RTL runs check for "trailing" whitespace at the front.
|
||||
for cluster in layout_data.clusters[self.cluster_range.clone()].iter() {
|
||||
if cluster.info.is_whitespace() {
|
||||
self.has_trailing_whitespace = true;
|
||||
} else {
|
||||
self.is_whitespace = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for cluster in layout_data.clusters[self.cluster_range.clone()]
|
||||
.iter()
|
||||
.rev()
|
||||
{
|
||||
if cluster.info.is_whitespace() {
|
||||
self.has_trailing_whitespace = true;
|
||||
} else {
|
||||
self.is_whitespace = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum LayoutItemKind {
|
||||
TextRun,
|
||||
InlineBox,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct LayoutItem {
|
||||
/// Whether the item is a run or an inline box
|
||||
pub(crate) kind: LayoutItemKind,
|
||||
/// The index of the run or inline box in the runs or `inline_boxes` vec
|
||||
pub(crate) index: usize,
|
||||
/// Bidi level for the item (used for reordering)
|
||||
pub(crate) bidi_level: u8,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct LayoutData<B: Brush> {
|
||||
// General settings (directly from the "builder")
|
||||
/// The display scale factor
|
||||
pub(crate) scale: f32,
|
||||
/// Whether metrics should be quantized to pixel boundaries
|
||||
pub(crate) quantize: bool,
|
||||
/// The `BiDi` base level
|
||||
pub(crate) base_level: u8,
|
||||
/// The length of the text in the layout
|
||||
pub(crate) text_len: usize,
|
||||
|
||||
// Output of style resolution (input to line breaking)
|
||||
pub(crate) styles: Vec<Style<B>>,
|
||||
pub(crate) inline_boxes: Vec<InlineBox>,
|
||||
|
||||
// Output of shaping (input to line breaking)
|
||||
pub(crate) fonts: Vec<FontData>,
|
||||
pub(crate) coords: Vec<i16>,
|
||||
pub(crate) runs: Vec<RunData>,
|
||||
pub(crate) items: Vec<LayoutItem>,
|
||||
pub(crate) clusters: Vec<ClusterData>,
|
||||
pub(crate) glyphs: Vec<Glyph>,
|
||||
|
||||
// Output of line breaking
|
||||
/// The lines in the
|
||||
pub(crate) lines: Vec<LineData>,
|
||||
/// Items within each line
|
||||
pub(crate) line_items: Vec<LineItemData>,
|
||||
/// The width constraint that was used to line break the layout
|
||||
pub(crate) layout_max_advance: f32,
|
||||
/// The computed width of the layout excluding trailing whitespace
|
||||
pub(crate) width: f32,
|
||||
/// The computed width of the layout including trailing whitespace
|
||||
pub(crate) full_width: f32,
|
||||
/// The computed height of the layout
|
||||
pub(crate) height: f32,
|
||||
|
||||
// Output of alignment
|
||||
#[cfg(feature = "accesskit")]
|
||||
/// Directly store the alignment if accessibility is enabled so we can
|
||||
/// set the corresponding AccessKit property.
|
||||
pub(crate) alignment: Option<super::Alignment>,
|
||||
/// Whether the layout is aligned with [`crate::Alignment::Justify`].
|
||||
pub(crate) is_aligned_justified: bool,
|
||||
/// The text-indent amount in layout units.
|
||||
pub(crate) indent_amount: f32,
|
||||
/// Options controlling text-indent behavior (each-line, hanging).
|
||||
pub(crate) indent_options: IndentOptions,
|
||||
}
|
||||
|
||||
impl<B: Brush> Default for LayoutData<B> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scale: 1.,
|
||||
quantize: true,
|
||||
base_level: 0,
|
||||
text_len: 0,
|
||||
width: 0.,
|
||||
full_width: 0.,
|
||||
height: 0.,
|
||||
fonts: Vec::new(),
|
||||
coords: Vec::new(),
|
||||
styles: Vec::new(),
|
||||
inline_boxes: Vec::new(),
|
||||
runs: Vec::new(),
|
||||
items: Vec::new(),
|
||||
clusters: Vec::new(),
|
||||
glyphs: Vec::new(),
|
||||
lines: Vec::new(),
|
||||
line_items: Vec::new(),
|
||||
#[cfg(feature = "accesskit")]
|
||||
alignment: None,
|
||||
is_aligned_justified: false,
|
||||
layout_max_advance: 0.0,
|
||||
indent_amount: 0.0,
|
||||
indent_options: IndentOptions::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: Brush> LayoutData<B> {
|
||||
pub(crate) fn clear(&mut self) {
|
||||
self.scale = 1.;
|
||||
self.quantize = true;
|
||||
self.base_level = 0;
|
||||
self.text_len = 0;
|
||||
self.width = 0.;
|
||||
self.full_width = 0.;
|
||||
self.height = 0.;
|
||||
self.fonts.clear();
|
||||
self.coords.clear();
|
||||
self.styles.clear();
|
||||
self.inline_boxes.clear();
|
||||
self.runs.clear();
|
||||
self.items.clear();
|
||||
self.clusters.clear();
|
||||
self.glyphs.clear();
|
||||
self.lines.clear();
|
||||
self.line_items.clear();
|
||||
}
|
||||
|
||||
/// Push an inline box to the list of items
|
||||
pub(crate) fn push_inline_box(&mut self, index: usize) {
|
||||
// Give the box the same bidi level as the preceding text run
|
||||
// (or else default to 0 if there is not yet a text run)
|
||||
let bidi_level = self.runs.last().map(|r| r.bidi_level).unwrap_or(0);
|
||||
|
||||
self.items.push(LayoutItem {
|
||||
kind: LayoutItemKind::InlineBox,
|
||||
index,
|
||||
bidi_level,
|
||||
});
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn push_run(
|
||||
&mut self,
|
||||
font: FontData,
|
||||
font_size: f32,
|
||||
font_attrs: fontique::Attributes,
|
||||
synthesis: fontique::Synthesis,
|
||||
glyph_buffer: &harfrust::GlyphBuffer,
|
||||
bidi_level: u8,
|
||||
style_index: u16,
|
||||
word_spacing: f32,
|
||||
letter_spacing: f32,
|
||||
source_text: &str,
|
||||
char_infos: &[(CharInfo, u16)], // From text analysis
|
||||
text_range: Range<usize>, // The text range this run covers
|
||||
coords: &[harfrust::NormalizedCoord],
|
||||
) {
|
||||
let coords_start = self.coords.len();
|
||||
self.coords.extend(coords.iter().map(|c| c.to_bits()));
|
||||
let coords_end = self.coords.len();
|
||||
|
||||
let font_index = self
|
||||
.fonts
|
||||
.iter()
|
||||
.position(|f| *f == font)
|
||||
.unwrap_or_else(|| {
|
||||
let index = self.fonts.len();
|
||||
self.fonts.push(font);
|
||||
index
|
||||
});
|
||||
|
||||
let metrics = {
|
||||
let font = &self.fonts[font_index];
|
||||
let font_ref = skrifa::FontRef::from_index(font.data.as_ref(), font.index).unwrap();
|
||||
skrifa::metrics::Metrics::new(&font_ref, skrifa::prelude::Size::new(font_size), coords)
|
||||
};
|
||||
let units_per_em = metrics.units_per_em as f32;
|
||||
|
||||
let metrics = {
|
||||
let (underline_offset, underline_size) = if let Some(underline) = metrics.underline {
|
||||
(underline.offset, underline.thickness)
|
||||
} else {
|
||||
// Default values from Harfbuzz: https://github.com/harfbuzz/harfbuzz/blob/00492ec7df0038f41f78d43d477c183e4e4c506e/src/hb-ot-metrics.cc#L334
|
||||
let default = units_per_em / 18.0;
|
||||
(default, default)
|
||||
};
|
||||
let (strikethrough_offset, strikethrough_size) =
|
||||
if let Some(strikeout) = metrics.strikeout {
|
||||
(strikeout.offset, strikeout.thickness)
|
||||
} else {
|
||||
// Default values from HarfBuzz: https://github.com/harfbuzz/harfbuzz/blob/00492ec7df0038f41f78d43d477c183e4e4c506e/src/hb-ot-metrics.cc#L334-L347
|
||||
(metrics.ascent / 2.0, units_per_em / 18.0)
|
||||
};
|
||||
|
||||
// Compute line height
|
||||
let style = &self.styles[style_index as usize];
|
||||
let line_height = match style.line_height {
|
||||
LineHeight::Absolute(value) => value,
|
||||
LineHeight::FontSizeRelative(value) => value * font_size,
|
||||
LineHeight::MetricsRelative(value) => {
|
||||
(metrics.ascent - metrics.descent + metrics.leading) * value
|
||||
}
|
||||
};
|
||||
|
||||
RunMetrics {
|
||||
ascent: metrics.ascent,
|
||||
descent: -metrics.descent,
|
||||
leading: metrics.leading,
|
||||
underline_offset,
|
||||
underline_size,
|
||||
strikethrough_offset,
|
||||
strikethrough_size,
|
||||
line_height,
|
||||
x_height: metrics.x_height,
|
||||
cap_height: metrics.cap_height,
|
||||
}
|
||||
};
|
||||
|
||||
let cluster_range = self.clusters.len()..self.clusters.len();
|
||||
|
||||
let mut run = RunData {
|
||||
font_index,
|
||||
font_size,
|
||||
font_attrs,
|
||||
synthesis,
|
||||
coords_range: coords_start..coords_end,
|
||||
text_range,
|
||||
bidi_level,
|
||||
cluster_range,
|
||||
glyph_start: self.glyphs.len(),
|
||||
metrics,
|
||||
word_spacing,
|
||||
letter_spacing,
|
||||
advance: 0.,
|
||||
};
|
||||
|
||||
// `HarfRust` returns glyphs in visual order, so we need to process them as such while
|
||||
// maintaining logical ordering of clusters.
|
||||
|
||||
let glyph_infos = glyph_buffer.glyph_infos();
|
||||
if glyph_infos.is_empty() {
|
||||
return;
|
||||
}
|
||||
let glyph_positions = glyph_buffer.glyph_positions();
|
||||
let scale_factor = font_size / units_per_em;
|
||||
let cluster_range_start = self.clusters.len();
|
||||
let is_rtl = bidi_level & 1 == 1;
|
||||
if !is_rtl {
|
||||
run.advance = process_clusters(
|
||||
Direction::Ltr,
|
||||
&mut self.clusters,
|
||||
&mut self.glyphs,
|
||||
scale_factor,
|
||||
glyph_infos,
|
||||
glyph_positions,
|
||||
char_infos,
|
||||
source_text.char_indices(),
|
||||
);
|
||||
} else {
|
||||
run.advance = process_clusters(
|
||||
Direction::Rtl,
|
||||
&mut self.clusters,
|
||||
&mut self.glyphs,
|
||||
scale_factor,
|
||||
glyph_infos,
|
||||
glyph_positions,
|
||||
char_infos,
|
||||
source_text.char_indices().rev(),
|
||||
);
|
||||
// Reverse clusters into logical order for RTL
|
||||
let clusters_len = self.clusters.len();
|
||||
self.clusters[cluster_range_start..clusters_len].reverse();
|
||||
}
|
||||
|
||||
run.cluster_range = cluster_range_start..self.clusters.len();
|
||||
if !run.cluster_range.is_empty() {
|
||||
self.runs.push(run);
|
||||
self.items.push(LayoutItem {
|
||||
kind: LayoutItemKind::TextRun,
|
||||
index: self.runs.len() - 1,
|
||||
bidi_level,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn finish(&mut self) {
|
||||
for run in &self.runs {
|
||||
let word = run.word_spacing;
|
||||
let letter = run.letter_spacing;
|
||||
if nearly_zero(word) && nearly_zero(letter) {
|
||||
continue;
|
||||
}
|
||||
let clusters = &mut self.clusters[run.cluster_range.clone()];
|
||||
for cluster in clusters {
|
||||
let mut spacing = letter;
|
||||
if !nearly_zero(word) && cluster.info.whitespace().is_space_or_nbsp() {
|
||||
spacing += word;
|
||||
}
|
||||
if !nearly_zero(spacing) {
|
||||
cluster.advance += spacing;
|
||||
if cluster.glyph_len != 0xFF {
|
||||
let start = run.glyph_start + cluster.glyph_offset as usize;
|
||||
let end = start + cluster.glyph_len as usize;
|
||||
let glyphs = &mut self.glyphs[start..end];
|
||||
if let Some(last) = glyphs.last_mut() {
|
||||
last.advance += spacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this method does not handle mixed direction text at all.
|
||||
pub(crate) fn calculate_content_widths(&self) -> ContentWidths {
|
||||
fn whitespace_advance(cluster: Option<&ClusterData>) -> f32 {
|
||||
cluster
|
||||
.filter(|cluster| cluster.info.whitespace().is_space_or_nbsp())
|
||||
.map_or(0.0, |cluster| cluster.advance)
|
||||
}
|
||||
|
||||
let mut min_width = 0.0_f32;
|
||||
let mut max_width = 0.0_f32;
|
||||
|
||||
let mut running_min_width = 0.0;
|
||||
let mut running_max_width = 0.0;
|
||||
let mut text_wrap_mode = TextWrapMode::Wrap;
|
||||
let mut prev_cluster: Option<&ClusterData> = None;
|
||||
let is_rtl = self.base_level & 1 == 1;
|
||||
for item in &self.items {
|
||||
match item.kind {
|
||||
LayoutItemKind::TextRun => {
|
||||
let run = &self.runs[item.index];
|
||||
let clusters = &self.clusters[run.cluster_range.clone()];
|
||||
if is_rtl {
|
||||
prev_cluster = clusters.first();
|
||||
}
|
||||
for cluster in clusters {
|
||||
let boundary = cluster.info.boundary();
|
||||
let style = &self.styles[cluster.style_index as usize];
|
||||
let prev_text_wrap_mode = text_wrap_mode;
|
||||
text_wrap_mode = style.text_wrap_mode;
|
||||
if boundary == Boundary::Mandatory
|
||||
|| (prev_text_wrap_mode == TextWrapMode::Wrap
|
||||
&& (boundary == Boundary::Line
|
||||
|| style.overflow_wrap == OverflowWrap::Anywhere))
|
||||
{
|
||||
let trailing_whitespace = whitespace_advance(prev_cluster);
|
||||
min_width = min_width.max(running_min_width - trailing_whitespace);
|
||||
running_min_width = 0.0;
|
||||
if boundary == Boundary::Mandatory {
|
||||
max_width = max_width.max(running_max_width - trailing_whitespace);
|
||||
running_max_width = 0.0;
|
||||
}
|
||||
}
|
||||
running_min_width += cluster.advance;
|
||||
running_max_width += cluster.advance;
|
||||
if !is_rtl {
|
||||
prev_cluster = Some(cluster);
|
||||
}
|
||||
}
|
||||
let trailing_whitespace = whitespace_advance(prev_cluster);
|
||||
min_width = min_width.max(running_min_width - trailing_whitespace);
|
||||
}
|
||||
LayoutItemKind::InlineBox => {
|
||||
let ibox = &self.inline_boxes[item.index];
|
||||
if ibox.kind == InlineBoxKind::InFlow {
|
||||
running_max_width += ibox.width;
|
||||
if text_wrap_mode == TextWrapMode::Wrap {
|
||||
let trailing_whitespace = whitespace_advance(prev_cluster);
|
||||
min_width = min_width.max(running_min_width - trailing_whitespace);
|
||||
min_width = min_width.max(ibox.width);
|
||||
running_min_width = 0.0;
|
||||
} else {
|
||||
running_min_width += ibox.width;
|
||||
}
|
||||
}
|
||||
prev_cluster = None;
|
||||
}
|
||||
}
|
||||
let trailing_whitespace = whitespace_advance(prev_cluster);
|
||||
max_width = max_width.max(running_max_width - trailing_whitespace);
|
||||
}
|
||||
|
||||
let trailing_whitespace = whitespace_advance(prev_cluster);
|
||||
min_width = min_width.max(running_min_width - trailing_whitespace);
|
||||
|
||||
ContentWidths {
|
||||
min: min_width,
|
||||
max: max_width,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes shaped glyphs from `HarfRust` and converts them into `ClusterData` and `Glyph`.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// ## Output Parameters (mutated by this function):
|
||||
/// * `clusters` - Vector where new `ClusterData` entries will be pushed.
|
||||
/// * `glyphs` - Vector where new `Glyph` entries will be pushed. Note: single-glyph clusters
|
||||
/// with zero offsets may be inlined directly into `ClusterData`.
|
||||
///
|
||||
/// ## Input Parameters:
|
||||
/// * `direction` - Direction of the text.
|
||||
/// * `scale_factor` - Scaling factor used to convert font units to the target size.
|
||||
/// * `glyph_infos` - `HarfRust` glyph information in visual order.
|
||||
/// * `glyph_positions` - `HarfRust` glyph positioning data in visual order.
|
||||
/// * `char_infos` - Character information from text analysis, indexed by cluster ID.
|
||||
/// * `char_indices_iter` - Iterator over (`byte_offset`, `char`) pairs from the source text.
|
||||
/// Should be in logical order (forward for LTR, reverse for RTL).
|
||||
fn process_clusters<I: Iterator<Item = (usize, char)>>(
|
||||
direction: Direction,
|
||||
clusters: &mut Vec<ClusterData>,
|
||||
glyphs: &mut Vec<Glyph>,
|
||||
scale_factor: f32,
|
||||
glyph_infos: &[harfrust::GlyphInfo],
|
||||
glyph_positions: &[harfrust::GlyphPosition],
|
||||
char_infos: &[(CharInfo, u16)],
|
||||
char_indices_iter: I,
|
||||
) -> f32 {
|
||||
let mut char_indices_iter = char_indices_iter.peekable();
|
||||
let mut cluster_start_char = char_indices_iter.next().unwrap();
|
||||
let mut total_glyphs: u32 = 0;
|
||||
let mut cluster_glyph_offset: u32 = 0;
|
||||
let start_cluster_id = glyph_infos.first().unwrap().cluster;
|
||||
let mut cluster_id = start_cluster_id;
|
||||
let mut char_info = char_infos[cluster_id as usize];
|
||||
let mut run_advance = 0.0;
|
||||
let mut cluster_advance = 0.0;
|
||||
// If the current cluster might be a single-glyph, zero-offset cluster, we defer
|
||||
// pushing the first glyph to `glyphs` because it might be inlined into `ClusterData`.
|
||||
let mut pending_inline_glyph: Option<Glyph> = None;
|
||||
|
||||
// The mental model for understanding this function is best grasped by first reading
|
||||
// the HarfBuzz docs on [clusters](https://harfbuzz.github.io/working-with-harfbuzz-clusters.html).
|
||||
//
|
||||
// `num_components` is the number of characters in the current cluster. Since source text's characters
|
||||
// were inserted into `HarfRust`'s buffer using their logical indices as the cluster ID, `HarfRust` will
|
||||
// assign the first character's cluster ID (in logical order) to the merged cluster because the minimum
|
||||
// ID is selected for [merging](https://github.com/harfbuzz/harfrust/blob/a38025fb336230b492366740c86021bb406bcd0d/src/hb/buffer.rs#L920-L924).
|
||||
//
|
||||
// So, the number of components in a given cluster is dependent on `direction`.
|
||||
// - In LTR, `num_components` is the difference between the next cluster and the current cluster.
|
||||
// - In RTL, `num_components` is the difference between the last cluster and the current cluster.
|
||||
// This is because we must compare the current cluster to its next larger ID (in other words, the next
|
||||
// logical index, which is visually downstream in LTR and visually upstream in RTL).
|
||||
//
|
||||
// For example, consider the LTR text for "afi" where "fi" form a ligature.
|
||||
// Initial cluster values: 0, 1, 2 (logical + visual order)
|
||||
// `HarfRust` assignation: 0, 1, 1
|
||||
// Cluster count: 2
|
||||
// `num_components`: (1 - 0 =) 1, (3 - 1 =) 2
|
||||
//
|
||||
// Now consider the RTL text for "حداً".
|
||||
// Initial cluster values: 0, 1, 2, 3 (logical, or in-memory, order)
|
||||
// Reversed cluster values: 3, 2, 1, 0 (visual order - the return order of `HarfRust` for RTL)
|
||||
// `HarfRust` assignation: 3, 2, 0, 0
|
||||
// Cluster count: 3
|
||||
// `num_components`: (4 - 3 =) 1, (3 - 2 =) 1, (2 - 0 =) 2
|
||||
let num_components =
|
||||
|next_cluster: u32, current_cluster: u32, last_cluster: u32| match direction {
|
||||
Direction::Ltr => next_cluster - current_cluster,
|
||||
Direction::Rtl => last_cluster - current_cluster,
|
||||
};
|
||||
let mut last_cluster_id: u32 = match direction {
|
||||
Direction::Ltr => 0,
|
||||
Direction::Rtl => char_infos.len() as u32,
|
||||
};
|
||||
|
||||
for (glyph_info, glyph_pos) in glyph_infos.iter().zip(glyph_positions.iter()) {
|
||||
// Flush previous cluster if we've reached a new cluster
|
||||
if cluster_id != glyph_info.cluster {
|
||||
run_advance += cluster_advance;
|
||||
let num_components = num_components(glyph_info.cluster, cluster_id, last_cluster_id);
|
||||
cluster_advance /= num_components as f32;
|
||||
let is_newline = to_whitespace(cluster_start_char.1) == Whitespace::Newline;
|
||||
let cluster_type = if num_components > 1 {
|
||||
debug_assert!(!is_newline);
|
||||
ClusterType::LigatureStart
|
||||
} else if is_newline {
|
||||
ClusterType::Newline
|
||||
} else {
|
||||
ClusterType::Regular
|
||||
};
|
||||
|
||||
let inline_glyph_id = if matches!(cluster_type, ClusterType::Regular) {
|
||||
pending_inline_glyph.take().map(|g| g.id)
|
||||
} else {
|
||||
// This isn't a regular cluster, so we don't inline the glyph and push
|
||||
// it to `glyphs`.
|
||||
if let Some(pending) = pending_inline_glyph.take() {
|
||||
glyphs.push(pending);
|
||||
total_glyphs += 1;
|
||||
}
|
||||
None
|
||||
};
|
||||
|
||||
push_cluster(
|
||||
clusters,
|
||||
char_info,
|
||||
cluster_start_char,
|
||||
cluster_glyph_offset,
|
||||
cluster_advance,
|
||||
total_glyphs,
|
||||
cluster_type,
|
||||
inline_glyph_id,
|
||||
);
|
||||
cluster_glyph_offset = total_glyphs;
|
||||
|
||||
if num_components > 1 {
|
||||
// Skip characters until we reach the current cluster
|
||||
for i in 1..num_components {
|
||||
cluster_start_char = char_indices_iter.next().unwrap();
|
||||
if to_whitespace(cluster_start_char.1) == Whitespace::Space {
|
||||
break;
|
||||
}
|
||||
let char_info_ = match direction {
|
||||
Direction::Ltr => char_infos[(cluster_id + i) as usize],
|
||||
Direction::Rtl => char_infos[(cluster_id + num_components - i) as usize],
|
||||
};
|
||||
push_cluster(
|
||||
clusters,
|
||||
char_info_,
|
||||
cluster_start_char,
|
||||
cluster_glyph_offset,
|
||||
cluster_advance,
|
||||
total_glyphs,
|
||||
ClusterType::LigatureComponent,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
cluster_start_char = char_indices_iter.next().unwrap();
|
||||
|
||||
cluster_advance = 0.0;
|
||||
last_cluster_id = cluster_id;
|
||||
cluster_id = glyph_info.cluster;
|
||||
char_info = char_infos[cluster_id as usize];
|
||||
pending_inline_glyph = None;
|
||||
}
|
||||
|
||||
let glyph = Glyph {
|
||||
id: glyph_info.glyph_id,
|
||||
style_index: char_info.1,
|
||||
x: (glyph_pos.x_offset as f32) * scale_factor,
|
||||
// Convert from font space (Y-up) to layout space (Y-down)
|
||||
y: -(glyph_pos.y_offset as f32) * scale_factor,
|
||||
advance: (glyph_pos.x_advance as f32) * scale_factor,
|
||||
};
|
||||
cluster_advance += glyph.advance;
|
||||
// Push any pending glyph. If it was a zero-offset, single glyph cluster, it would
|
||||
// have been pushed in the first `if` block.
|
||||
if let Some(pending) = pending_inline_glyph.take() {
|
||||
glyphs.push(pending);
|
||||
total_glyphs += 1;
|
||||
}
|
||||
if total_glyphs == cluster_glyph_offset && glyph.x == 0.0 && glyph.y == 0.0 {
|
||||
// Defer this potential zero-offset, single glyph cluster
|
||||
pending_inline_glyph = Some(glyph);
|
||||
} else {
|
||||
glyphs.push(glyph);
|
||||
total_glyphs += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Push the last cluster
|
||||
{
|
||||
// See comment above `num_components` for why we use `char_infos.len()` for LTR and 0 for RTL.
|
||||
let next_cluster_id = match direction {
|
||||
Direction::Ltr => char_infos.len() as u32,
|
||||
Direction::Rtl => 0,
|
||||
};
|
||||
let num_components = num_components(next_cluster_id, cluster_id, last_cluster_id);
|
||||
if num_components > 1 {
|
||||
// This is a ligature - create ligature start + ligature components
|
||||
|
||||
if let Some(pending) = pending_inline_glyph.take() {
|
||||
glyphs.push(pending);
|
||||
total_glyphs += 1;
|
||||
}
|
||||
let ligature_advance = cluster_advance / num_components as f32;
|
||||
push_cluster(
|
||||
clusters,
|
||||
char_info,
|
||||
cluster_start_char,
|
||||
cluster_glyph_offset,
|
||||
ligature_advance,
|
||||
total_glyphs,
|
||||
ClusterType::LigatureStart,
|
||||
None,
|
||||
);
|
||||
|
||||
cluster_glyph_offset = total_glyphs;
|
||||
|
||||
// Create ligature component clusters for the remaining characters
|
||||
for (i, char) in (1..).zip(char_indices_iter) {
|
||||
if to_whitespace(char.1) == Whitespace::Space {
|
||||
break;
|
||||
}
|
||||
let component_char_info = match direction {
|
||||
Direction::Ltr => char_infos[(cluster_id + i) as usize],
|
||||
Direction::Rtl => char_infos[(cluster_id + num_components - i) as usize],
|
||||
};
|
||||
push_cluster(
|
||||
clusters,
|
||||
component_char_info,
|
||||
char,
|
||||
cluster_glyph_offset,
|
||||
ligature_advance,
|
||||
total_glyphs,
|
||||
ClusterType::LigatureComponent,
|
||||
None,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let is_newline = to_whitespace(cluster_start_char.1) == Whitespace::Newline;
|
||||
let cluster_type = if is_newline {
|
||||
ClusterType::Newline
|
||||
} else {
|
||||
ClusterType::Regular
|
||||
};
|
||||
let mut inline_glyph_id = None;
|
||||
match cluster_type {
|
||||
ClusterType::Regular => {
|
||||
if total_glyphs == cluster_glyph_offset {
|
||||
if let Some(pending) = pending_inline_glyph.take() {
|
||||
inline_glyph_id = Some(pending.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if let Some(pending) = pending_inline_glyph.take() {
|
||||
glyphs.push(pending);
|
||||
total_glyphs += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
push_cluster(
|
||||
clusters,
|
||||
char_info,
|
||||
cluster_start_char,
|
||||
cluster_glyph_offset,
|
||||
cluster_advance,
|
||||
total_glyphs,
|
||||
cluster_type,
|
||||
inline_glyph_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
run_advance
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum Direction {
|
||||
Ltr,
|
||||
Rtl,
|
||||
}
|
||||
|
||||
enum ClusterType {
|
||||
LigatureStart,
|
||||
LigatureComponent,
|
||||
Regular,
|
||||
Newline,
|
||||
}
|
||||
|
||||
impl From<&ClusterType> for u16 {
|
||||
fn from(cluster_type: &ClusterType) -> Self {
|
||||
match cluster_type {
|
||||
ClusterType::LigatureStart => ClusterData::LIGATURE_START,
|
||||
ClusterType::LigatureComponent => ClusterData::LIGATURE_COMPONENT,
|
||||
ClusterType::Regular | ClusterType::Newline => 0, // No special flags
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn push_cluster(
|
||||
clusters: &mut Vec<ClusterData>,
|
||||
char_info: (CharInfo, u16),
|
||||
cluster_start_char: (usize, char),
|
||||
glyph_offset: u32,
|
||||
advance: f32,
|
||||
total_glyphs: u32,
|
||||
cluster_type: ClusterType,
|
||||
inline_glyph_id: Option<u32>,
|
||||
) {
|
||||
let glyph_len = (total_glyphs - glyph_offset) as u8;
|
||||
|
||||
let (final_glyph_len, final_glyph_offset, final_advance) = match cluster_type {
|
||||
ClusterType::LigatureComponent => {
|
||||
// Ligature components have no glyphs, only advance.
|
||||
debug_assert_eq!(glyph_len, 0);
|
||||
(0_u8, 0_u32, advance)
|
||||
}
|
||||
ClusterType::Newline => {
|
||||
// Newline clusters are stripped of their glyph contribution.
|
||||
debug_assert_eq!(glyph_len, 1);
|
||||
(0_u8, 0_u32, 0.0)
|
||||
}
|
||||
_ if inline_glyph_id.is_some() => {
|
||||
// Inline glyphs are stored inline within `ClusterData`
|
||||
debug_assert_eq!(glyph_len, 0);
|
||||
(0xFF_u8, inline_glyph_id.unwrap(), advance)
|
||||
}
|
||||
ClusterType::Regular | ClusterType::LigatureStart => {
|
||||
// Regular and ligature start clusters maintain their glyphs and advance.
|
||||
debug_assert_ne!(glyph_len, 0);
|
||||
(glyph_len, glyph_offset, advance)
|
||||
}
|
||||
};
|
||||
|
||||
clusters.push(ClusterData {
|
||||
info: ClusterInfo::new(char_info.0.boundary, cluster_start_char.1),
|
||||
flags: (&cluster_type).into(),
|
||||
style_index: char_info.1,
|
||||
glyph_len: final_glyph_len,
|
||||
text_len: cluster_start_char.1.len_utf8() as u8,
|
||||
glyph_offset: final_glyph_offset,
|
||||
text_offset: cluster_start_char.0 as u16,
|
||||
advance: final_advance,
|
||||
});
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// Copyright 2025 the Parley Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
/// Glyph with an offset and advance.
|
||||
#[derive(Copy, Clone, Default, Debug, PartialEq)]
|
||||
pub struct Glyph {
|
||||
pub id: u32,
|
||||
pub style_index: u16,
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub advance: f32,
|
||||
}
|
||||
|
||||
impl Glyph {
|
||||
/// Returns the index into the layout style collection.
|
||||
pub fn style_index(&self) -> usize {
|
||||
self.style_index as usize
|
||||
}
|
||||
}
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
// Copyright 2025 the Parley Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
use crate::InlineBox;
|
||||
use crate::layout::alignment::align;
|
||||
use crate::layout::alignment::unjustify;
|
||||
use crate::layout::data::LayoutData;
|
||||
use crate::style::Brush;
|
||||
use core::cmp::Ordering;
|
||||
use core::fmt;
|
||||
|
||||
use crate::IndentOptions;
|
||||
use crate::layout::{
|
||||
ContentWidths, Style, alignment::Alignment, alignment::AlignmentOptions, line::Line,
|
||||
line_break::BreakLines,
|
||||
};
|
||||
|
||||
/// Text layout.
|
||||
///
|
||||
/// The [`Debug`] implementation prints a compact summary by default.
|
||||
/// The alternate form (`{:#?}`) formats the full underlying data.
|
||||
///
|
||||
/// [`Debug`]: core::fmt::Debug
|
||||
#[derive(Clone)]
|
||||
pub struct Layout<B: Brush> {
|
||||
pub(crate) data: LayoutData<B>,
|
||||
}
|
||||
|
||||
impl<B: Brush> fmt::Debug for Layout<B> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if f.alternate() {
|
||||
f.debug_struct("Layout").field("data", &self.data).finish()
|
||||
} else {
|
||||
f.debug_struct("Layout")
|
||||
.field("text_len", &self.data.text_len)
|
||||
.field("width", &self.data.width)
|
||||
.field("height", &self.data.height)
|
||||
.field("lines", &self.data.lines.len())
|
||||
.field("runs", &self.data.runs.len())
|
||||
.field("styles", &self.data.styles.len())
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: Brush> Layout<B> {
|
||||
/// Creates an empty layout.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Returns the scale factor provided when creating the layout.
|
||||
pub fn scale(&self) -> f32 {
|
||||
self.data.scale
|
||||
}
|
||||
|
||||
/// Returns the style collection for the layout.
|
||||
pub fn styles(&self) -> &[Style<B>] {
|
||||
&self.data.styles
|
||||
}
|
||||
|
||||
/// The `max_advance` that was used to line break the `Layout`
|
||||
pub fn layout_max_advance(&self) -> f32 {
|
||||
self.data.layout_max_advance
|
||||
}
|
||||
|
||||
/// Returns the computed width of the layout excluding the width of trailing whitespace.
|
||||
pub fn width(&self) -> f32 {
|
||||
self.data.width
|
||||
}
|
||||
|
||||
/// Returns the computed width of the layout including the width of trailing whitespace.
|
||||
pub fn full_width(&self) -> f32 {
|
||||
self.data.full_width
|
||||
}
|
||||
|
||||
/// Calculates the lower and upper bounds on the width of the layout. These
|
||||
/// are recalculated every time this method is called.
|
||||
///
|
||||
/// This method currently may not return the correct results for
|
||||
/// mixed-direction text.
|
||||
pub fn calculate_content_widths(&self) -> ContentWidths {
|
||||
self.data.calculate_content_widths()
|
||||
}
|
||||
|
||||
/// Returns the height of the layout.
|
||||
pub fn height(&self) -> f32 {
|
||||
self.data.height
|
||||
}
|
||||
|
||||
/// Returns the number of lines in the layout.
|
||||
pub fn len(&self) -> usize {
|
||||
self.data.lines.len()
|
||||
}
|
||||
|
||||
/// Returns `true` if the layout is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.data.lines.is_empty()
|
||||
}
|
||||
|
||||
/// Returns the line at the specified index.
|
||||
///
|
||||
/// Returns `None` if the index is out of bounds, i.e. if it's
|
||||
/// not less than [`self.len()`](Self::len).
|
||||
pub fn get(&self, index: usize) -> Option<Line<'_, B>> {
|
||||
Some(Line {
|
||||
index: index as u32,
|
||||
layout: self,
|
||||
data: self.data.lines.get(index)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns `true` if the dominant direction of the layout is right-to-left.
|
||||
pub fn is_rtl(&self) -> bool {
|
||||
self.data.base_level & 1 != 0
|
||||
}
|
||||
|
||||
pub fn inline_boxes(&self) -> &[InlineBox] {
|
||||
&self.data.inline_boxes
|
||||
}
|
||||
|
||||
pub fn inline_boxes_mut(&mut self) -> &mut [InlineBox] {
|
||||
&mut self.data.inline_boxes
|
||||
}
|
||||
|
||||
/// Returns an iterator over the lines in the layout.
|
||||
pub fn lines(
|
||||
&self,
|
||||
) -> impl ExactSizeIterator<Item = Line<'_, B>> + DoubleEndedIterator + '_ + Clone {
|
||||
self.data
|
||||
.lines
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(move |(index, data)| Line {
|
||||
index: index as u32,
|
||||
layout: self,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets the text-indent for the layout.
|
||||
///
|
||||
/// The indent is applied as a margin on the start edge of indented lines, reducing the
|
||||
/// available width for line breaking and offsetting content during alignment. Negative
|
||||
/// values cause the line to protrude beyond the start edge.
|
||||
///
|
||||
/// This must be called before [`Layout::break_all_lines`] or [`Layout::break_lines`],
|
||||
/// and before [`Layout::align`].
|
||||
pub fn set_text_indent(&mut self, amount: f32, options: IndentOptions) {
|
||||
self.data.indent_amount = amount;
|
||||
self.data.indent_options = options;
|
||||
}
|
||||
|
||||
/// Returns line breaker to compute lines for the layout.
|
||||
pub fn break_lines(&mut self) -> BreakLines<'_, B> {
|
||||
unjustify(&mut self.data);
|
||||
BreakLines::new(self)
|
||||
}
|
||||
|
||||
/// Breaks all lines with the specified maximum advance.
|
||||
pub fn break_all_lines(&mut self, max_advance: Option<f32>) {
|
||||
self.break_lines()
|
||||
.break_remaining(max_advance.unwrap_or(f32::MAX));
|
||||
}
|
||||
|
||||
/// Apply alignment to the layout.
|
||||
///
|
||||
/// You must perform line breaking prior to aligning, through [`Layout::break_lines`] or
|
||||
/// [`Layout::break_all_lines`].
|
||||
///
|
||||
/// If a finite `max_advance` is supplied to `Layout::break_all_lines` then that width will be applied
|
||||
/// relative to that width. Otherwise alignment will be applied relative to the width of the
|
||||
/// longest line as computed by line breaking.
|
||||
///
|
||||
/// If line-specific `offset` and `max_advance` are set using the advanced methods on the `BreakLines`
|
||||
/// struct then each line will be aligned individually within its line box.
|
||||
pub fn align(&mut self, alignment: Alignment, options: AlignmentOptions) {
|
||||
unjustify(&mut self.data);
|
||||
align(&mut self.data, alignment, options);
|
||||
}
|
||||
|
||||
/// Returns the index and `Line` object for the line containing the
|
||||
/// given byte `index` in the source text.
|
||||
pub(crate) fn line_for_byte_index(&self, index: usize) -> Option<(usize, Line<'_, B>)> {
|
||||
let line_index = self
|
||||
.data
|
||||
.lines
|
||||
.binary_search_by(|line| {
|
||||
if index < line.text_range.start {
|
||||
Ordering::Greater
|
||||
} else if index >= line.text_range.end {
|
||||
Ordering::Less
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
})
|
||||
.ok()?;
|
||||
Some((line_index, self.get(line_index)?))
|
||||
}
|
||||
|
||||
/// Returns the index and `Line` object for the line containing the
|
||||
/// given `offset`.
|
||||
///
|
||||
/// The offset is specified in the direction orthogonal to line direction.
|
||||
/// For horizontal text, this is a vertical or y offset. If the offset is
|
||||
/// on a line boundary, it is considered to be contained by the later line.
|
||||
pub(crate) fn line_for_offset(&self, offset: f32) -> Option<(usize, Line<'_, B>)> {
|
||||
if offset < 0.0 {
|
||||
return Some((0, self.get(0)?));
|
||||
}
|
||||
let maybe_line_index = self.data.lines.binary_search_by(|line| {
|
||||
if offset < line.metrics.block_min_coord {
|
||||
Ordering::Greater
|
||||
} else if offset >= line.metrics.block_max_coord {
|
||||
Ordering::Less
|
||||
} else {
|
||||
Ordering::Equal
|
||||
}
|
||||
});
|
||||
let line_index = match maybe_line_index {
|
||||
Ok(index) => index,
|
||||
Err(index) => index.saturating_sub(1),
|
||||
};
|
||||
Some((line_index, self.get(line_index)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: Brush> Default for Layout<B> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
data: LayoutData::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
// Copyright 2021 the Parley Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
use crate::layout::Style;
|
||||
use crate::layout::data::BreakReason;
|
||||
use crate::layout::data::{LayoutItemKind, LineData};
|
||||
use crate::layout::glyph::Glyph;
|
||||
use crate::layout::layout::Layout;
|
||||
use crate::layout::run::Run;
|
||||
use crate::style::Brush;
|
||||
use crate::{InlineBox, InlineBoxKind};
|
||||
use core::ops::Range;
|
||||
|
||||
/// Line in a text layout.
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Line<'a, B: Brush> {
|
||||
pub(crate) layout: &'a Layout<B>,
|
||||
pub(crate) index: u32,
|
||||
pub(crate) data: &'a LineData,
|
||||
}
|
||||
|
||||
impl<'a, B: Brush> Line<'a, B> {
|
||||
/// Returns the metrics for the line.
|
||||
pub fn metrics(&self) -> &LineMetrics {
|
||||
&self.data.metrics
|
||||
}
|
||||
|
||||
pub fn break_reason(&self) -> BreakReason {
|
||||
self.data.break_reason
|
||||
}
|
||||
|
||||
/// Returns the range of text for the line.
|
||||
pub fn text_range(&self) -> Range<usize> {
|
||||
self.data.text_range.clone()
|
||||
}
|
||||
|
||||
/// Returns the number of items in the line.
|
||||
pub fn len(&self) -> usize {
|
||||
self.data.item_range.len()
|
||||
}
|
||||
|
||||
/// Returns `true` if the line is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.data.item_range.is_empty()
|
||||
}
|
||||
|
||||
/// Returns the line item at the specified index.
|
||||
pub(crate) fn item(&self, index: usize) -> Option<LineItem<'a, B>> {
|
||||
let original_index = index;
|
||||
let index = self.data.item_range.start + index;
|
||||
if index >= self.data.item_range.end {
|
||||
return None;
|
||||
}
|
||||
let item = self.layout.data.line_items.get(index)?;
|
||||
|
||||
Some(match item.kind {
|
||||
LayoutItemKind::TextRun => LineItem::Run(Run {
|
||||
layout: self.layout,
|
||||
line_index: self.index,
|
||||
index: original_index as u32,
|
||||
data: self.layout.data.runs.get(item.index)?,
|
||||
line_data: Some(item),
|
||||
}),
|
||||
LayoutItemKind::InlineBox => {
|
||||
LineItem::InlineBox(self.layout.data.inline_boxes.get(item.index)?)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns an iterator over the runs for the line.
|
||||
pub fn runs(&self) -> impl Iterator<Item = Run<'a, B>> + 'a + Clone {
|
||||
self.items_nonpositioned().filter_map(|item| item.run())
|
||||
}
|
||||
|
||||
/// Returns an iterator over the non-glyph runs and inline boxes for the line.
|
||||
pub(crate) fn items_nonpositioned(&self) -> impl Iterator<Item = LineItem<'a, B>> + Clone {
|
||||
let copy = self.clone();
|
||||
let line_items = ©.layout.data.line_items[self.data.item_range.clone()];
|
||||
line_items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(move |(item_index, line_data)| match line_data.kind {
|
||||
LayoutItemKind::TextRun => LineItem::Run(Run {
|
||||
layout: copy.layout,
|
||||
line_index: copy.index,
|
||||
index: item_index as u32,
|
||||
data: ©.layout.data.runs[line_data.index],
|
||||
line_data: Some(line_data),
|
||||
}),
|
||||
LayoutItemKind::InlineBox => {
|
||||
LineItem::InlineBox(©.layout.data.inline_boxes[line_data.index])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns an iterator over the glyph runs for the line.
|
||||
pub fn items(&self) -> impl Iterator<Item = PositionedLayoutItem<'a, B>> + 'a + Clone {
|
||||
GlyphRunIter {
|
||||
line: self.clone(),
|
||||
item_index: 0,
|
||||
glyph_start: 0,
|
||||
offset: 0.,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Metrics information for a line.
|
||||
#[derive(Copy, Clone, Default, Debug, PartialEq)]
|
||||
pub struct LineMetrics {
|
||||
/// Typographic ascent.
|
||||
pub ascent: f32,
|
||||
/// Typographic descent.
|
||||
pub descent: f32,
|
||||
/// Typographic leading.
|
||||
pub leading: f32,
|
||||
/// The absolute line height (in layout units).
|
||||
pub line_height: f32,
|
||||
/// Offset to the baseline.
|
||||
pub baseline: f32,
|
||||
/// Offset for alignment.
|
||||
pub offset: f32,
|
||||
/// Full advance of the line, including trailing whitespace.
|
||||
pub advance: f32,
|
||||
/// Advance of trailing whitespace.
|
||||
pub trailing_whitespace: f32,
|
||||
/// Minimum coordinate in the line direction.
|
||||
///
|
||||
/// For horizontal text, this would be the left of the line.
|
||||
pub inline_min_coord: f32,
|
||||
/// Maximum coordinate in the line direction.
|
||||
///
|
||||
/// For horizontal text, this would be the right of the line.
|
||||
pub inline_max_coord: f32,
|
||||
/// Minimum coordinate in the direction orthogonal to line
|
||||
/// direction.
|
||||
///
|
||||
/// For horizontal text, this would be the top of the line.
|
||||
pub block_min_coord: f32,
|
||||
/// Maximum coordinate in the direction orthogonal to line
|
||||
/// direction.
|
||||
///
|
||||
/// For horizontal text, this would be the bottom of the line.
|
||||
pub block_max_coord: f32,
|
||||
}
|
||||
|
||||
impl LineMetrics {
|
||||
/// Returns the size of the line
|
||||
pub fn size(&self) -> f32 {
|
||||
self.line_height
|
||||
}
|
||||
}
|
||||
|
||||
/// A line item and its corresponding data (a run or inline box). Unlike a
|
||||
/// [`PositionedLayoutItem`], runs are not guaranteed to be split by style.
|
||||
pub(crate) enum LineItem<'a, B: Brush> {
|
||||
Run(Run<'a, B>),
|
||||
InlineBox(&'a InlineBox),
|
||||
}
|
||||
|
||||
impl<'a, B: Brush> LineItem<'a, B> {
|
||||
pub(crate) fn run(self) -> Option<Run<'a, B>> {
|
||||
match self {
|
||||
LineItem::Run(run) => Some(run),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The computed result of an item (glyph run or inline box) within a layout
|
||||
#[derive(Clone)]
|
||||
pub enum PositionedLayoutItem<'a, B: Brush> {
|
||||
GlyphRun(GlyphRun<'a, B>),
|
||||
InlineBox(PositionedInlineBox),
|
||||
}
|
||||
|
||||
/// The computed position of an inline box within a layout
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PositionedInlineBox {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
pub id: u64,
|
||||
pub kind: InlineBoxKind,
|
||||
}
|
||||
|
||||
/// Sequence of fully positioned glyphs with the same style.
|
||||
#[derive(Clone)]
|
||||
pub struct GlyphRun<'a, B: Brush> {
|
||||
run: Run<'a, B>,
|
||||
style: &'a Style<B>,
|
||||
glyph_start: usize,
|
||||
glyph_count: usize,
|
||||
offset: f32,
|
||||
baseline: f32,
|
||||
advance: f32,
|
||||
}
|
||||
|
||||
impl<'a, B: Brush> GlyphRun<'a, B> {
|
||||
/// Returns the underlying run.
|
||||
pub fn run(&self) -> &Run<'a, B> {
|
||||
&self.run
|
||||
}
|
||||
|
||||
/// Returns the associated style.
|
||||
pub fn style(&self) -> &Style<B> {
|
||||
self.style
|
||||
}
|
||||
|
||||
/// Returns the offset to the baseline.
|
||||
pub fn baseline(&self) -> f32 {
|
||||
self.baseline
|
||||
}
|
||||
|
||||
/// Returns the offset to the first glyph along the baseline.
|
||||
pub fn offset(&self) -> f32 {
|
||||
self.offset
|
||||
}
|
||||
|
||||
/// Returns the total advance of the run.
|
||||
pub fn advance(&self) -> f32 {
|
||||
self.advance
|
||||
}
|
||||
|
||||
/// Returns an iterator over the glyphs in the run.
|
||||
pub fn glyphs(&'a self) -> impl Iterator<Item = Glyph> + 'a + Clone {
|
||||
self.run
|
||||
.visual_clusters()
|
||||
.flat_map(|cluster| cluster.glyphs())
|
||||
.skip(self.glyph_start)
|
||||
.take(self.glyph_count)
|
||||
}
|
||||
|
||||
/// Returns an iterator over the fully positioned glyphs in the run.
|
||||
pub fn positioned_glyphs(&'a self) -> impl Iterator<Item = Glyph> + 'a + Clone {
|
||||
let mut offset = self.offset;
|
||||
let baseline = self.baseline;
|
||||
self.glyphs().map(move |mut g| {
|
||||
g.x += offset;
|
||||
g.y += baseline;
|
||||
offset += g.advance;
|
||||
g
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct GlyphRunIter<'a, B: Brush> {
|
||||
line: Line<'a, B>,
|
||||
item_index: usize,
|
||||
glyph_start: usize,
|
||||
offset: f32,
|
||||
}
|
||||
|
||||
impl<'a, B: Brush> Iterator for GlyphRunIter<'a, B> {
|
||||
type Item = PositionedLayoutItem<'a, B>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
let item = self.line.item(self.item_index)?;
|
||||
match item {
|
||||
LineItem::InlineBox(inline_box) => {
|
||||
let x = self.offset
|
||||
+ self.line.data.metrics.inline_min_coord
|
||||
+ self.line.data.metrics.offset;
|
||||
|
||||
self.item_index += 1;
|
||||
self.glyph_start = 0;
|
||||
if inline_box.kind == InlineBoxKind::InFlow {
|
||||
self.offset += inline_box.width;
|
||||
}
|
||||
return Some(PositionedLayoutItem::InlineBox(PositionedInlineBox {
|
||||
x,
|
||||
y: self.line.data.metrics.baseline - inline_box.height,
|
||||
width: inline_box.width,
|
||||
height: inline_box.height,
|
||||
id: inline_box.id,
|
||||
kind: inline_box.kind,
|
||||
}));
|
||||
}
|
||||
LineItem::Run(run) => {
|
||||
let mut iter = run
|
||||
.visual_clusters()
|
||||
.flat_map(|c| c.glyphs())
|
||||
.skip(self.glyph_start);
|
||||
|
||||
if let Some(first) = iter.next() {
|
||||
let mut advance = first.advance;
|
||||
let style_index = first.style_index();
|
||||
let mut glyph_count = 1;
|
||||
for glyph in iter.take_while(|g| g.style_index() == style_index) {
|
||||
glyph_count += 1;
|
||||
advance += glyph.advance;
|
||||
}
|
||||
let style = run.layout.data.styles.get(style_index)?;
|
||||
let glyph_start = self.glyph_start;
|
||||
self.glyph_start += glyph_count;
|
||||
let offset = self.offset;
|
||||
self.offset += advance;
|
||||
return Some(PositionedLayoutItem::GlyphRun(GlyphRun {
|
||||
run,
|
||||
style,
|
||||
glyph_start,
|
||||
glyph_count,
|
||||
offset: offset
|
||||
+ self.line.data.metrics.inline_min_coord
|
||||
+ self.line.data.metrics.offset,
|
||||
baseline: self.line.data.metrics.baseline,
|
||||
advance,
|
||||
}));
|
||||
}
|
||||
self.item_index += 1;
|
||||
self.glyph_start = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1358
File diff suppressed because it is too large
Load Diff
+103
@@ -0,0 +1,103 @@
|
||||
// Copyright 2021 the Parley Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Layout types.
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
mod accessibility;
|
||||
mod alignment;
|
||||
mod cluster;
|
||||
mod glyph;
|
||||
mod line;
|
||||
mod line_break;
|
||||
mod run;
|
||||
|
||||
// TODO - Add to allowed lint set?
|
||||
#[expect(
|
||||
clippy::module_inception,
|
||||
reason = "Private inner module for code organisation"
|
||||
)]
|
||||
mod layout;
|
||||
|
||||
pub(crate) mod data;
|
||||
|
||||
#[cfg(feature = "accesskit")]
|
||||
pub use accessibility::LayoutAccessibility;
|
||||
pub use alignment::{Alignment, AlignmentOptions};
|
||||
pub use cluster::{Affinity, Cluster, ClusterPath, ClusterSide};
|
||||
pub use data::BreakReason;
|
||||
pub use glyph::Glyph;
|
||||
pub use layout::Layout;
|
||||
pub use line::{GlyphRun, Line, LineMetrics, PositionedInlineBox, PositionedLayoutItem};
|
||||
pub use line_break::{
|
||||
BoxBreakData, BreakLines, BreakerState, LineBreakData, MaxHeightBreakData, YieldData,
|
||||
};
|
||||
pub use run::{Run, RunMetrics};
|
||||
|
||||
pub(crate) use data::{LayoutData, LayoutItem, LayoutItemKind, LineData, LineItemData};
|
||||
pub(crate) use line::LineItem;
|
||||
|
||||
// TODO - Deprecation not yet active to ease internal code migration.
|
||||
#[deprecated(since = "TBD", note = "Access from the `editing` module instead.")]
|
||||
pub use crate::editing::{Cursor, Selection};
|
||||
|
||||
// TODO - Move the following to `style` module and submodules.
|
||||
|
||||
use crate::style::Brush;
|
||||
use crate::{LineHeight, OverflowWrap, TextWrapMode};
|
||||
|
||||
#[allow(clippy::partial_pub_fields)]
|
||||
/// Style properties.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Style<B: Brush> {
|
||||
/// Brush for drawing glyphs.
|
||||
pub brush: B,
|
||||
/// Underline decoration.
|
||||
pub underline: Option<Decoration<B>>,
|
||||
/// Strikethrough decoration.
|
||||
pub strikethrough: Option<Decoration<B>>,
|
||||
/// Partially resolved line height, either in in layout units or dependent on metrics
|
||||
pub(crate) line_height: LineHeight,
|
||||
/// Per-cluster overflow-wrap setting
|
||||
pub(crate) overflow_wrap: OverflowWrap,
|
||||
/// Per-cluster text-wrap-mode setting
|
||||
pub(crate) text_wrap_mode: TextWrapMode,
|
||||
#[cfg(feature = "accesskit")]
|
||||
/// Locale if any, so we can set the corresponding AccessKit property
|
||||
pub(crate) locale: Option<fontique::Language>,
|
||||
}
|
||||
|
||||
/// Underline or strikethrough decoration.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Decoration<B: Brush> {
|
||||
/// Brush used to draw the decoration.
|
||||
pub brush: B,
|
||||
/// Offset of the decoration from the baseline. If `None`, use the metrics
|
||||
/// of the containing run.
|
||||
pub offset: Option<f32>,
|
||||
/// Thickness of the decoration. If `None`, use the metrics of the
|
||||
/// containing run.
|
||||
pub size: Option<f32>,
|
||||
}
|
||||
|
||||
/// Lower and upper bounds on layout width based on its contents.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct ContentWidths {
|
||||
/// The minimum content width. This is the width of the layout if _all_ soft line-breaking
|
||||
/// opportunities are taken.
|
||||
pub min: f32,
|
||||
/// The maximum content width. This is the width of the layout if _no_ soft line-breaking
|
||||
/// opportunities are taken.
|
||||
pub max: f32,
|
||||
}
|
||||
|
||||
/// Options controlling text-indent behavior, corresponding to CSS `text-indent` keywords.
|
||||
#[derive(Copy, Clone, Default, PartialEq, Debug)]
|
||||
pub struct IndentOptions {
|
||||
/// If `true`, indent also applies after every hard line break, not just to the first line.
|
||||
/// Corresponds to the CSS `each-line` keyword. Defaults to `false`.
|
||||
pub each_line: bool,
|
||||
/// If `true`, inverts which lines are indented: continuation lines are indented
|
||||
/// instead of the first line(s). Corresponds to the CSS `hanging` keyword. Defaults to `false`.
|
||||
pub hanging: bool,
|
||||
}
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
// Copyright 2021 the Parley Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
use crate::FontData;
|
||||
use crate::layout::cluster::{Cluster, ClusterPath};
|
||||
use crate::layout::data::{LineItemData, RunData};
|
||||
use crate::layout::layout::Layout;
|
||||
use crate::style::Brush;
|
||||
use core::ops::Range;
|
||||
use fontique::Synthesis;
|
||||
|
||||
/// Sequence of clusters with a single font and style.
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Run<'a, B: Brush> {
|
||||
pub(crate) layout: &'a Layout<B>,
|
||||
pub(crate) line_index: u32,
|
||||
pub(crate) index: u32,
|
||||
pub(crate) data: &'a RunData,
|
||||
pub(crate) line_data: Option<&'a LineItemData>,
|
||||
}
|
||||
|
||||
impl<'a, B: Brush> Run<'a, B> {
|
||||
pub(crate) fn new(
|
||||
layout: &'a Layout<B>,
|
||||
line_index: u32,
|
||||
index: u32,
|
||||
data: &'a RunData,
|
||||
line_data: Option<&'a LineItemData>,
|
||||
) -> Self {
|
||||
Self {
|
||||
layout,
|
||||
line_index,
|
||||
index,
|
||||
data,
|
||||
line_data,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the index of the run within the line.
|
||||
pub fn index(&self) -> usize {
|
||||
self.index as usize
|
||||
}
|
||||
|
||||
/// Returns the font for the run.
|
||||
pub fn font(&self) -> &FontData {
|
||||
self.layout.data.fonts.get(self.data.font_index).unwrap()
|
||||
}
|
||||
|
||||
/// Returns the font size for the run.
|
||||
pub fn font_size(&self) -> f32 {
|
||||
self.data.font_size
|
||||
}
|
||||
|
||||
/// Returns the font attributes for the run.
|
||||
pub fn font_attrs(&self) -> &fontique::Attributes {
|
||||
&self.data.font_attrs
|
||||
}
|
||||
|
||||
/// Returns the synthesis suggestions for the font associated with the run.
|
||||
pub fn synthesis(&self) -> Synthesis {
|
||||
self.data.synthesis
|
||||
}
|
||||
|
||||
/// Returns the normalized variation coordinates for the font associated
|
||||
/// with the run.
|
||||
pub fn normalized_coords(&self) -> &[i16] {
|
||||
self.layout
|
||||
.data
|
||||
.coords
|
||||
.get(self.data.coords_range.clone())
|
||||
.unwrap_or(&[])
|
||||
}
|
||||
|
||||
/// Returns metrics for the run.
|
||||
pub fn metrics(&self) -> &RunMetrics {
|
||||
&self.data.metrics
|
||||
}
|
||||
|
||||
/// Returns the advance for the run.
|
||||
pub fn advance(&self) -> f32 {
|
||||
self.line_data
|
||||
.map(|d| d.advance)
|
||||
.unwrap_or(self.data.advance)
|
||||
}
|
||||
|
||||
/// Returns the original text range for the run.
|
||||
pub fn text_range(&self) -> Range<usize> {
|
||||
self.line_data
|
||||
.map(|d| &d.text_range)
|
||||
.unwrap_or(&self.data.text_range)
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Returns `true` if the run has right-to-left directionality.
|
||||
pub fn is_rtl(&self) -> bool {
|
||||
self.data.bidi_level & 1 != 0
|
||||
}
|
||||
|
||||
/// Returns the cluster range for the run.
|
||||
pub fn cluster_range(&self) -> Range<usize> {
|
||||
self.line_data
|
||||
.map(|d| &d.cluster_range)
|
||||
.unwrap_or(&self.data.cluster_range)
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Returns the number of clusters in the run.
|
||||
pub fn len(&self) -> usize {
|
||||
self.cluster_range().len()
|
||||
}
|
||||
|
||||
/// Returns `true` if the run is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Returns the cluster at the specified index.
|
||||
pub fn get(&self, index: usize) -> Option<Cluster<'a, B>> {
|
||||
let range = self
|
||||
.line_data
|
||||
.map(|d| &d.cluster_range)
|
||||
.unwrap_or(&self.data.cluster_range);
|
||||
let original_index = index;
|
||||
let index = range.start + index;
|
||||
Some(Cluster {
|
||||
path: ClusterPath::new(self.line_index, self.index, original_index as u32),
|
||||
run: self.clone(),
|
||||
data: self.layout.data.clusters.get(index)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns an iterator over the clusters in logical order.
|
||||
pub fn clusters(&'a self) -> impl Iterator<Item = Cluster<'a, B>> + 'a + Clone {
|
||||
let range = self.cluster_range();
|
||||
Clusters {
|
||||
run: self,
|
||||
range,
|
||||
rev: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the visual cluster index for the specified logical cluster index.
|
||||
pub fn logical_to_visual(&self, logical_index: usize) -> Option<usize> {
|
||||
let num_clusters = self.len();
|
||||
if logical_index >= num_clusters {
|
||||
return None;
|
||||
}
|
||||
|
||||
let visual_index = if self.is_rtl() {
|
||||
num_clusters - 1 - logical_index
|
||||
} else {
|
||||
logical_index
|
||||
};
|
||||
|
||||
Some(visual_index)
|
||||
}
|
||||
|
||||
/// Returns the logical cluster index for the specified visual cluster index.
|
||||
pub fn visual_to_logical(&self, visual_index: usize) -> Option<usize> {
|
||||
let num_clusters = self.len();
|
||||
if visual_index >= num_clusters {
|
||||
return None;
|
||||
}
|
||||
|
||||
let logical_index = if self.is_rtl() {
|
||||
num_clusters - 1 - visual_index
|
||||
} else {
|
||||
visual_index
|
||||
};
|
||||
|
||||
Some(logical_index)
|
||||
}
|
||||
|
||||
/// Returns an iterator over the clusters in visual order.
|
||||
pub fn visual_clusters(&'a self) -> impl Iterator<Item = Cluster<'a, B>> + 'a + Clone {
|
||||
let range = self.cluster_range();
|
||||
Clusters {
|
||||
run: self,
|
||||
range,
|
||||
rev: self.is_rtl(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Clusters<'a, B: Brush> {
|
||||
run: &'a Run<'a, B>,
|
||||
range: Range<usize>,
|
||||
rev: bool,
|
||||
}
|
||||
|
||||
impl<B: Brush> Clone for Clusters<'_, B> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
run: self.run,
|
||||
range: self.range.clone(),
|
||||
rev: self.rev,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, B: Brush> Iterator for Clusters<'a, B> {
|
||||
type Item = Cluster<'a, B>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let index = if self.rev {
|
||||
self.range.next_back()?
|
||||
} else {
|
||||
self.range.next()?
|
||||
};
|
||||
Some(Cluster {
|
||||
path: ClusterPath::new(
|
||||
self.run.line_index,
|
||||
self.run.index,
|
||||
(index - self.run.cluster_range().start) as u32,
|
||||
),
|
||||
run: self.run.clone(),
|
||||
data: self.run.layout.data.clusters.get(index)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Metrics information for a run.
|
||||
#[derive(Copy, Clone, Default, Debug, PartialEq)]
|
||||
pub struct RunMetrics {
|
||||
/// Typographic ascent.
|
||||
pub ascent: f32,
|
||||
/// Typographic descent.
|
||||
pub descent: f32,
|
||||
/// Typographic leading.
|
||||
pub leading: f32,
|
||||
/// Offset of the top of underline decoration from the baseline.
|
||||
pub underline_offset: f32,
|
||||
/// Thickness of the underline decoration.
|
||||
pub underline_size: f32,
|
||||
/// Offset of the top of strikethrough decoration from the baseline.
|
||||
pub strikethrough_offset: f32,
|
||||
/// Thickness of the strikethrough decoration.
|
||||
pub strikethrough_size: f32,
|
||||
/// The line height
|
||||
pub line_height: f32,
|
||||
/// Distance from the baseline to the top of short lowercase letters.
|
||||
pub x_height: Option<f32>,
|
||||
/// Distance from the baseline to the top of capital letters.
|
||||
pub cap_height: Option<f32>,
|
||||
}
|
||||
Vendored
+154
@@ -0,0 +1,154 @@
|
||||
// Copyright 2021 the Parley Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Parley is a library for rich text layout.
|
||||
//!
|
||||
//! Some key types are:
|
||||
//! - [`FontContext`] and [`LayoutContext`] are resources which should be shared globally (or at coarse-grained boundaries).
|
||||
//! - [`FontContext`] is database of fonts.
|
||||
//! - [`LayoutContext`] is scratch space that allows for reuse of allocations between layouts.
|
||||
//! - Builders for creating a [`Layout`]:
|
||||
//! - [`RangedBuilder`]: styles specified as a flat `Vec` of property spans
|
||||
//! - [`TreeBuilder`]: styles specified as a tree of spans
|
||||
//! - [`StyleRunBuilder`]: styles specified as a shared style table plus non-overlapping indexed runs
|
||||
//!
|
||||
//! They are constructed using [`LayoutContext::ranged_builder`], [`LayoutContext::tree_builder`],
|
||||
//! and [`LayoutContext::style_run_builder`].
|
||||
//! - [`Layout`] which represents styled paragraph(s) of text and can perform shaping, line-breaking, bidi-reordering, and alignment of that text.
|
||||
//!
|
||||
//! `Layout` supports re-linebreaking and re-aligning many times (in case the width at which wrapping should occur changes). But if the text content or
|
||||
//! the styles applied to that content change then a new `Layout` must be created using a new
|
||||
//! `RangedBuilder`, `TreeBuilder`, or `StyleRunBuilder`.
|
||||
//!
|
||||
//! ## Usage Example
|
||||
//!
|
||||
//! See the [examples](https://github.com/linebender/parley/tree/main/examples) directory for more complete usage examples that include rendering.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use parley::{
|
||||
//! Alignment, AlignmentOptions, FontContext, FontWeight, InlineBox, InlineBoxKind, Layout,
|
||||
//! LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty,
|
||||
//! };
|
||||
//!
|
||||
//! // Create a FontContext (font database) and LayoutContext (scratch space).
|
||||
//! // These are both intended to be constructed rarely (perhaps even once per app):
|
||||
//! let mut font_cx = FontContext::new();
|
||||
//! let mut layout_cx = LayoutContext::new();
|
||||
//!
|
||||
//! // Create a `RangedBuilder` or a `TreeBuilder`, which are used to construct a `Layout`.
|
||||
//! const DISPLAY_SCALE : f32 = 1.0;
|
||||
//! const TEXT : &str = "Lorem Ipsum...";
|
||||
//! let mut builder = layout_cx.ranged_builder(&mut font_cx, &TEXT, DISPLAY_SCALE, true);
|
||||
//!
|
||||
//! // Set default styles that apply to the entire layout
|
||||
//! builder.push_default(StyleProperty::FontSize(16.0));
|
||||
//!
|
||||
//! // Set a style that applies to the first 4 characters
|
||||
//! builder.push(StyleProperty::FontWeight(FontWeight::new(600.0)), 0..4);
|
||||
//!
|
||||
//! // Add a box to be laid out inline with the text
|
||||
//! builder.push_inline_box(InlineBox { id: 0, kind: InlineBoxKind::InFlow, index: 5, width: 50.0, height: 50.0 });
|
||||
//!
|
||||
//! // Build the builder into a Layout
|
||||
//! let mut layout: Layout<()> = builder.build(&TEXT);
|
||||
//!
|
||||
//! // Run line-breaking and alignment on the Layout
|
||||
//! const MAX_WIDTH : Option<f32> = Some(100.0);
|
||||
//! layout.break_all_lines(MAX_WIDTH);
|
||||
//! layout.align(Alignment::Start, AlignmentOptions::default());
|
||||
//!
|
||||
//! // Inspect computed layout (see examples for more details)
|
||||
//! let width = layout.width();
|
||||
//! let height = layout.height();
|
||||
//! for line in layout.lines() {
|
||||
//! for item in line.items() {
|
||||
//! match item {
|
||||
//! PositionedLayoutItem::GlyphRun(glyph_run) => {
|
||||
//! // Render the glyph run
|
||||
//! }
|
||||
//! PositionedLayoutItem::InlineBox(inline_box) => {
|
||||
//! // Render the inline box
|
||||
//! }
|
||||
//! };
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
// LINEBENDER LINT SET - lib.rs - v4
|
||||
// See https://linebender.org/wiki/canonical-lints/
|
||||
// These lints shouldn't apply to examples or tests.
|
||||
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
|
||||
// These lints shouldn't apply to examples.
|
||||
#![warn(clippy::print_stdout, clippy::print_stderr)]
|
||||
// Targeting e.g. 32-bit means structs containing usize can give false positives for 64-bit.
|
||||
#![cfg_attr(target_pointer_width = "64", warn(clippy::trivially_copy_pass_by_ref))]
|
||||
// END LINEBENDER LINT SET
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![no_std]
|
||||
#![allow(missing_docs, reason = "We have many as-yet undocumented items.")]
|
||||
#![expect(
|
||||
missing_debug_implementations,
|
||||
clippy::allow_attributes_without_reason,
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::missing_assert_message,
|
||||
reason = "Deferred"
|
||||
)]
|
||||
#![expect(
|
||||
single_use_lifetimes,
|
||||
reason = "False positive: https://github.com/rust-lang/rust/issues/129255"
|
||||
)]
|
||||
|
||||
#[cfg(not(any(feature = "std", feature = "libm")))]
|
||||
compile_error!("parley requires either the `std` or `libm` feature to be enabled");
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
extern crate std;
|
||||
|
||||
pub use fontique;
|
||||
|
||||
mod analysis;
|
||||
mod bidi;
|
||||
mod break_overrides;
|
||||
mod builder;
|
||||
mod context;
|
||||
mod convert;
|
||||
mod font;
|
||||
mod inline_box;
|
||||
mod lru_cache;
|
||||
mod resolve;
|
||||
mod shape;
|
||||
mod util;
|
||||
|
||||
pub mod editing;
|
||||
pub mod layout;
|
||||
pub mod setting;
|
||||
pub mod style;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use linebender_resource_handle::FontData;
|
||||
pub use util::BoundingBox;
|
||||
|
||||
pub use break_overrides::{
|
||||
AsciiLineBreakTable, AsciiLineBreakTableBuilder, CHROMIUM_LINE_BREAK_OVERRIDE,
|
||||
LineBreakContext, LineBreakOverrideFn,
|
||||
};
|
||||
pub use builder::{RangedBuilder, StyleRunBuilder, TreeBuilder};
|
||||
pub use context::LayoutContext;
|
||||
pub use font::FontContext;
|
||||
pub use inline_box::{InlineBox, InlineBoxKind};
|
||||
#[doc(inline)]
|
||||
pub use layout::Layout;
|
||||
|
||||
pub use editing::*;
|
||||
pub use layout::*;
|
||||
pub use style::*;
|
||||
|
||||
#[deprecated(
|
||||
note = "Old name for this type, use `parley::FontData` instead.",
|
||||
since = "0.6.0"
|
||||
)]
|
||||
pub type Font = FontData;
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
// Copyright 2025 the Parley Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use hashbrown::Equivalent;
|
||||
|
||||
/// An entry in the cache.
|
||||
pub(crate) struct Entry<ID, T> {
|
||||
pub epoch: u64,
|
||||
pub id: ID,
|
||||
pub data: T,
|
||||
}
|
||||
|
||||
/// A least-recently-used cache. This cache uses a linear scan of its entries
|
||||
/// to find a given entry - it is optimised for a low number of entries. Preferably
|
||||
/// keep `max_entries` low - in the order of tens.
|
||||
pub(crate) struct LruCache<ID, T> {
|
||||
entries: Vec<Entry<ID, T>>,
|
||||
epoch: u64,
|
||||
max_entries: usize,
|
||||
}
|
||||
|
||||
impl<ID, T> LruCache<ID, T> {
|
||||
pub(crate) fn new(max_entries: usize) -> Self {
|
||||
Self {
|
||||
entries: Vec::default(),
|
||||
epoch: 0,
|
||||
max_entries,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the entry with the given ID. If the entry is not
|
||||
/// found, it is created and returned using `make_data`.
|
||||
///
|
||||
/// The lookup key must be `Equivalent` to ID for lookups and convertible `Into<ID>`
|
||||
/// for creating new entries.
|
||||
pub(crate) fn entry<K>(&mut self, id: K, make_data: impl FnOnce() -> T) -> &T
|
||||
where
|
||||
K: Equivalent<ID> + Into<ID>,
|
||||
{
|
||||
let index = self.find_entry(id, make_data);
|
||||
self.epoch += 1;
|
||||
let entry = &mut self.entries[index];
|
||||
entry.epoch = self.epoch;
|
||||
&entry.data
|
||||
}
|
||||
|
||||
fn find_entry<K>(&mut self, id: K, make_data: impl FnOnce() -> T) -> usize
|
||||
where
|
||||
K: Equivalent<ID> + Into<ID>,
|
||||
{
|
||||
let epoch = self.epoch;
|
||||
let mut lowest_serial = epoch;
|
||||
let mut lowest_index = 0;
|
||||
for (i, entry) in self.entries.iter().enumerate() {
|
||||
if id.equivalent(&entry.id) {
|
||||
return i;
|
||||
}
|
||||
if entry.epoch < lowest_serial {
|
||||
lowest_serial = entry.epoch;
|
||||
lowest_index = i;
|
||||
}
|
||||
}
|
||||
if self.entries.len() < self.max_entries {
|
||||
lowest_index = self.entries.len();
|
||||
self.entries.push(Entry {
|
||||
epoch,
|
||||
id: id.into(),
|
||||
data: make_data(),
|
||||
});
|
||||
} else {
|
||||
let entry = &mut self.entries[lowest_index];
|
||||
entry.epoch = epoch;
|
||||
entry.id = id.into();
|
||||
entry.data = make_data();
|
||||
}
|
||||
lowest_index
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use alloc::string::{String, ToString};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct TestId(String);
|
||||
struct TestLookupKey<'a>(&'a str);
|
||||
|
||||
impl<'a> Equivalent<TestId> for TestLookupKey<'a> {
|
||||
fn equivalent(&self, key: &TestId) -> bool {
|
||||
self.0 == key.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<TestLookupKey<'a>> for TestId {
|
||||
fn from(key: TestLookupKey<'a>) -> Self {
|
||||
Self(key.0.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Equivalent<Self> for TestId {
|
||||
fn equivalent(&self, key: &Self) -> bool {
|
||||
self.0 == key.0
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_retrieve_existing_entry() {
|
||||
let mut cache = LruCache::new(3);
|
||||
|
||||
// Insert an entry
|
||||
let value1 = cache.entry(TestLookupKey("key1"), || 42);
|
||||
assert_eq!(*value1, 42);
|
||||
|
||||
// Retrieve the same entry - make_data should not be called
|
||||
let value2 = cache.entry(TestLookupKey("key1"), || {
|
||||
panic!("Should not create new data")
|
||||
});
|
||||
assert_eq!(*value2, 42);
|
||||
assert_eq!(cache.entries.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_entries() {
|
||||
let mut cache = LruCache::new(3);
|
||||
|
||||
let value1 = cache.entry(TestLookupKey("key1"), || 1);
|
||||
assert_eq!(*value1, 1);
|
||||
|
||||
let value2 = cache.entry(TestLookupKey("key2"), || 2);
|
||||
assert_eq!(*value2, 2);
|
||||
|
||||
let value3 = cache.entry(TestLookupKey("key3"), || 3);
|
||||
assert_eq!(*value3, 3);
|
||||
|
||||
assert_eq!(cache.entries.len(), 3);
|
||||
assert_eq!(cache.epoch, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lru_eviction() {
|
||||
let mut cache = LruCache::new(3);
|
||||
|
||||
// Add three entries
|
||||
cache.entry(TestLookupKey("key1"), || 1);
|
||||
cache.entry(TestLookupKey("key2"), || 2);
|
||||
cache.entry(TestLookupKey("key3"), || 3);
|
||||
|
||||
// Access key1 to update its epoch
|
||||
cache.entry(TestLookupKey("key1"), || panic!("Should not create"));
|
||||
|
||||
// Add key4 - should evict key2 (oldest untouched)
|
||||
cache.entry(TestLookupKey("key4"), || 4);
|
||||
|
||||
// Verify key1 is still present
|
||||
let value1 = cache.entry(TestLookupKey("key1"), || {
|
||||
panic!("key1 should still be present")
|
||||
});
|
||||
assert_eq!(*value1, 1);
|
||||
|
||||
// Verify key2 was evicted
|
||||
let mut was_created = false;
|
||||
cache.entry(TestLookupKey("key2"), || {
|
||||
was_created = true;
|
||||
20
|
||||
});
|
||||
assert!(was_created, "key2 should have been evicted");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lru_eviction_after_multiple_hits() {
|
||||
let mut cache = LruCache::new(3);
|
||||
|
||||
cache.entry(TestLookupKey("key1"), || 1);
|
||||
cache.entry(TestLookupKey("key2"), || 2);
|
||||
cache.entry(TestLookupKey("key3"), || 3);
|
||||
|
||||
// Hit all three in order: key3 first (making it LRU among hits),
|
||||
// then key1, then key2 (most recently used).
|
||||
cache.entry(TestLookupKey("key3"), || panic!("Should not create"));
|
||||
cache.entry(TestLookupKey("key1"), || panic!("Should not create"));
|
||||
cache.entry(TestLookupKey("key2"), || panic!("Should not create"));
|
||||
|
||||
// Insert key4 — should evict key3 (least recently accessed)
|
||||
cache.entry(TestLookupKey("key4"), || 4);
|
||||
|
||||
// key1 and key2 should still be present with original values
|
||||
// (check these first since verifying key3 eviction will trigger another eviction)
|
||||
let v1 = cache.entry(TestLookupKey("key1"), || {
|
||||
panic!("key1 should still be present")
|
||||
});
|
||||
assert_eq!(*v1, 1);
|
||||
|
||||
let v2 = cache.entry(TestLookupKey("key2"), || {
|
||||
panic!("key2 should still be present")
|
||||
});
|
||||
assert_eq!(*v2, 2);
|
||||
|
||||
// key3 should have been evicted
|
||||
let mut key3_recreated = false;
|
||||
cache.entry(TestLookupKey("key3"), || {
|
||||
key3_recreated = true;
|
||||
30
|
||||
});
|
||||
assert!(key3_recreated, "key3 should have been evicted as the LRU");
|
||||
}
|
||||
}
|
||||
+531
@@ -0,0 +1,531 @@
|
||||
// Copyright 2021 the Parley Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Resolution of dynamic properties within a context.
|
||||
|
||||
pub(crate) mod range;
|
||||
pub(crate) mod tree;
|
||||
|
||||
pub(crate) use range::RangedStyleBuilder;
|
||||
|
||||
use alloc::{vec, vec::Vec};
|
||||
|
||||
use super::style::{
|
||||
Brush, FontFamily, FontFamilyName, FontFeature, FontFeatures, FontStyle, FontVariation,
|
||||
FontVariations, FontWeight, FontWidth, StyleProperty,
|
||||
};
|
||||
use crate::font::FontContext;
|
||||
use crate::style::TextStyle;
|
||||
use crate::util::nearly_eq;
|
||||
use crate::{LineHeight, OverflowWrap, layout};
|
||||
use crate::{TextWrapMode, WordBreak};
|
||||
use core::borrow::Borrow;
|
||||
use core::ops::Range;
|
||||
use fontique::FamilyId;
|
||||
use fontique::Language;
|
||||
|
||||
/// Style with an associated range.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct RangedStyle<B: Brush> {
|
||||
pub(crate) style: ResolvedStyle<B>,
|
||||
pub(crate) range: Range<usize>,
|
||||
}
|
||||
|
||||
/// Run that references a style in a shared style table.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct StyleRun {
|
||||
pub(crate) style_index: u16,
|
||||
pub(crate) range: Range<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RangedProperty<B: Brush> {
|
||||
property: ResolvedProperty<B>,
|
||||
range: Range<usize>,
|
||||
}
|
||||
|
||||
/// Handle for a managed property.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub(crate) struct Resolved<T> {
|
||||
index: usize,
|
||||
_phantom: core::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> Default for Resolved<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
index: !0,
|
||||
_phantom: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Resolved<T> {
|
||||
pub(crate) fn id(&self) -> usize {
|
||||
self.index
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Cache<T> {
|
||||
/// Items in the cache. May contain sequences.
|
||||
items: Vec<T>,
|
||||
/// Each entry represents a range of items in `data`.
|
||||
entries: Vec<(usize, usize)>,
|
||||
}
|
||||
|
||||
impl<T> Default for Cache<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
items: vec![],
|
||||
entries: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + PartialEq> Cache<T> {
|
||||
pub(crate) fn clear(&mut self) {
|
||||
self.items.clear();
|
||||
self.entries.clear();
|
||||
}
|
||||
|
||||
pub(crate) fn insert(&mut self, items: &[T]) -> Resolved<T> {
|
||||
for (i, entry) in self.entries.iter().enumerate() {
|
||||
let range = entry.0..entry.1;
|
||||
if range.len() != items.len() {
|
||||
continue;
|
||||
}
|
||||
if let Some(existing) = self.items.get(range) {
|
||||
if existing == items {
|
||||
return Resolved {
|
||||
index: i,
|
||||
_phantom: core::marker::PhantomData,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
let index = self.entries.len();
|
||||
let start = self.items.len();
|
||||
self.items.extend(items.iter().cloned());
|
||||
let end = self.items.len();
|
||||
self.entries.push((start, end));
|
||||
Resolved {
|
||||
index,
|
||||
_phantom: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get(&self, handle: Resolved<T>) -> Option<&[T]> {
|
||||
let (start, end) = *self.entries.get(handle.index)?;
|
||||
self.items.get(start..end)
|
||||
}
|
||||
}
|
||||
|
||||
/// Context for managing dynamic properties during layout.
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct ResolveContext {
|
||||
families: Cache<FamilyId>,
|
||||
variations: Cache<FontVariation>,
|
||||
features: Cache<FontFeature>,
|
||||
tmp_families: Vec<FamilyId>,
|
||||
tmp_variations: Vec<FontVariation>,
|
||||
tmp_features: Vec<FontFeature>,
|
||||
}
|
||||
|
||||
impl ResolveContext {
|
||||
pub(crate) fn resolve_property<B: Brush>(
|
||||
&mut self,
|
||||
fcx: &mut FontContext,
|
||||
property: &StyleProperty<'_, B>,
|
||||
scale: f32,
|
||||
) -> ResolvedProperty<B> {
|
||||
use ResolvedProperty::*;
|
||||
match property {
|
||||
StyleProperty::FontFamily(value) => FontFamily(self.resolve_font_family(fcx, value)),
|
||||
StyleProperty::FontSize(value) => FontSize(*value * scale),
|
||||
StyleProperty::FontWidth(value) => FontWidth(*value),
|
||||
StyleProperty::FontStyle(value) => FontStyle(*value),
|
||||
StyleProperty::FontWeight(value) => FontWeight(*value),
|
||||
StyleProperty::FontVariations(value) => FontVariations(self.resolve_variations(value)),
|
||||
StyleProperty::FontFeatures(value) => FontFeatures(self.resolve_features(value)),
|
||||
StyleProperty::Locale(value) => Locale(*value),
|
||||
StyleProperty::Brush(value) => Brush(value.clone()),
|
||||
StyleProperty::Underline(value) => Underline(*value),
|
||||
StyleProperty::UnderlineOffset(value) => UnderlineOffset(value.map(|x| x * scale)),
|
||||
StyleProperty::UnderlineSize(value) => UnderlineSize(value.map(|x| x * scale)),
|
||||
StyleProperty::UnderlineBrush(value) => UnderlineBrush(value.clone()),
|
||||
StyleProperty::Strikethrough(value) => Strikethrough(*value),
|
||||
StyleProperty::StrikethroughOffset(value) => {
|
||||
StrikethroughOffset(value.map(|x| x * scale))
|
||||
}
|
||||
StyleProperty::StrikethroughSize(value) => StrikethroughSize(value.map(|x| x * scale)),
|
||||
StyleProperty::StrikethroughBrush(value) => StrikethroughBrush(value.clone()),
|
||||
StyleProperty::LineHeight(value) => LineHeight(value.scale(scale)),
|
||||
StyleProperty::WordSpacing(value) => WordSpacing(*value * scale),
|
||||
StyleProperty::LetterSpacing(value) => LetterSpacing(*value * scale),
|
||||
StyleProperty::WordBreak(value) => WordBreak(*value),
|
||||
StyleProperty::OverflowWrap(value) => OverflowWrap(*value),
|
||||
StyleProperty::TextWrapMode(value) => TextWrapMode(*value),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_entire_style_set<B: Brush>(
|
||||
&mut self,
|
||||
fcx: &mut FontContext,
|
||||
raw_style: &TextStyle<'_, '_, B>,
|
||||
scale: f32,
|
||||
) -> ResolvedStyle<B> {
|
||||
ResolvedStyle {
|
||||
font_family: self.resolve_font_family(fcx, &raw_style.font_family),
|
||||
font_size: raw_style.font_size * scale,
|
||||
font_width: raw_style.font_width,
|
||||
font_style: raw_style.font_style,
|
||||
font_weight: raw_style.font_weight,
|
||||
font_variations: self.resolve_variations(&raw_style.font_variations),
|
||||
font_features: self.resolve_features(&raw_style.font_features),
|
||||
locale: raw_style.locale,
|
||||
brush: raw_style.brush.clone(),
|
||||
underline: ResolvedDecoration {
|
||||
enabled: raw_style.has_underline,
|
||||
offset: raw_style.underline_offset.map(|x| x * scale),
|
||||
size: raw_style.underline_size.map(|x| x * scale),
|
||||
brush: raw_style.underline_brush.clone(),
|
||||
},
|
||||
strikethrough: ResolvedDecoration {
|
||||
enabled: raw_style.has_strikethrough,
|
||||
offset: raw_style.strikethrough_offset.map(|x| x * scale),
|
||||
size: raw_style.strikethrough_size.map(|x| x * scale),
|
||||
brush: raw_style.strikethrough_brush.clone(),
|
||||
},
|
||||
line_height: raw_style.line_height.scale(scale),
|
||||
word_spacing: raw_style.word_spacing * scale,
|
||||
letter_spacing: raw_style.letter_spacing * scale,
|
||||
word_break: raw_style.word_break,
|
||||
overflow_wrap: raw_style.overflow_wrap,
|
||||
text_wrap_mode: raw_style.text_wrap_mode,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves a `font-family` value.
|
||||
pub(crate) fn resolve_font_family(
|
||||
&mut self,
|
||||
fcx: &mut FontContext,
|
||||
value: &FontFamily<'_>,
|
||||
) -> Resolved<FamilyId> {
|
||||
self.tmp_families.clear();
|
||||
match value {
|
||||
FontFamily::Source(source) => {
|
||||
for family in FontFamilyName::parse_css_list(source).map_while(Result::ok) {
|
||||
match family {
|
||||
FontFamilyName::Named(name) => {
|
||||
if let Some(family) = fcx.collection.family_by_name(&name) {
|
||||
self.tmp_families.push(family.id());
|
||||
}
|
||||
}
|
||||
FontFamilyName::Generic(family) => {
|
||||
self.tmp_families
|
||||
.extend(fcx.collection.generic_families(family));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
FontFamily::Single(family) => match family {
|
||||
FontFamilyName::Named(name) => {
|
||||
if let Some(family) = fcx.collection.family_by_name(name) {
|
||||
self.tmp_families.push(family.id());
|
||||
}
|
||||
}
|
||||
FontFamilyName::Generic(family) => {
|
||||
self.tmp_families
|
||||
.extend(fcx.collection.generic_families(*family));
|
||||
}
|
||||
},
|
||||
FontFamily::List(families) => {
|
||||
let families: &[FontFamilyName<'_>] = families.borrow();
|
||||
for family in families {
|
||||
match family {
|
||||
FontFamilyName::Named(name) => {
|
||||
if let Some(family) = fcx.collection.family_by_name(name) {
|
||||
self.tmp_families.push(family.id());
|
||||
}
|
||||
}
|
||||
FontFamilyName::Generic(family) => {
|
||||
self.tmp_families
|
||||
.extend(fcx.collection.generic_families(*family));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let resolved = self.families.insert(&self.tmp_families);
|
||||
self.tmp_families.clear();
|
||||
resolved
|
||||
}
|
||||
|
||||
/// Resolves font variation settings.
|
||||
pub(crate) fn resolve_variations(
|
||||
&mut self,
|
||||
variations: &FontVariations<'_>,
|
||||
) -> Resolved<FontVariation> {
|
||||
match variations {
|
||||
FontVariations::Source(source) => {
|
||||
self.tmp_variations.clear();
|
||||
self.tmp_variations
|
||||
.extend(FontVariation::parse_css_list(source).map_while(Result::ok));
|
||||
}
|
||||
FontVariations::List(settings) => {
|
||||
self.tmp_variations.clear();
|
||||
self.tmp_variations.extend_from_slice(settings);
|
||||
}
|
||||
}
|
||||
if self.tmp_variations.is_empty() {
|
||||
return Resolved::default();
|
||||
}
|
||||
self.tmp_variations.sort_by_key(|a| a.tag);
|
||||
let resolved = self.variations.insert(&self.tmp_variations);
|
||||
self.tmp_variations.clear();
|
||||
resolved
|
||||
}
|
||||
|
||||
/// Resolves font feature settings.
|
||||
pub(crate) fn resolve_features(
|
||||
&mut self,
|
||||
features: &FontFeatures<'_>,
|
||||
) -> Resolved<FontFeature> {
|
||||
match features {
|
||||
FontFeatures::Source(source) => {
|
||||
self.tmp_features.clear();
|
||||
self.tmp_features
|
||||
.extend(FontFeature::parse_css_list(source).map_while(Result::ok));
|
||||
}
|
||||
FontFeatures::List(settings) => {
|
||||
self.tmp_features.clear();
|
||||
self.tmp_features.extend_from_slice(settings);
|
||||
}
|
||||
}
|
||||
if self.tmp_features.is_empty() {
|
||||
return Resolved::default();
|
||||
}
|
||||
self.tmp_features.sort_by_key(|a| a.tag);
|
||||
let resolved = self.features.insert(&self.tmp_features);
|
||||
self.tmp_features.clear();
|
||||
resolved
|
||||
}
|
||||
|
||||
/// Returns the list of font families for the specified handle.
|
||||
pub(crate) fn stack(&self, stack: Resolved<FamilyId>) -> Option<&[FamilyId]> {
|
||||
self.families.get(stack)
|
||||
}
|
||||
|
||||
/// Returns the list of font variations for the specified handle.
|
||||
pub(crate) fn variations(
|
||||
&self,
|
||||
variations: Resolved<FontVariation>,
|
||||
) -> Option<&[FontVariation]> {
|
||||
self.variations.get(variations)
|
||||
}
|
||||
|
||||
/// Returns the list of font features for the specified handle.
|
||||
pub(crate) fn features(&self, features: Resolved<FontFeature>) -> Option<&[FontFeature]> {
|
||||
self.features.get(features)
|
||||
}
|
||||
|
||||
/// Clears the resources in the context.
|
||||
pub(crate) fn clear(&mut self) {
|
||||
self.families.clear();
|
||||
self.variations.clear();
|
||||
self.features.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Style property with resolved resources.
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub(crate) enum ResolvedProperty<B: Brush> {
|
||||
/// `font-family`.
|
||||
FontFamily(Resolved<FamilyId>),
|
||||
/// Font size.
|
||||
FontSize(f32),
|
||||
/// Font width.
|
||||
FontWidth(FontWidth),
|
||||
/// Font style.
|
||||
FontStyle(FontStyle),
|
||||
/// Font weight.
|
||||
FontWeight(FontWeight),
|
||||
/// Font variation settings.
|
||||
FontVariations(Resolved<FontVariation>),
|
||||
/// Font feature settings.
|
||||
FontFeatures(Resolved<FontFeature>),
|
||||
/// Locale.
|
||||
Locale(Option<Language>),
|
||||
/// Brush for rendering text.
|
||||
Brush(B),
|
||||
/// Underline decoration.
|
||||
Underline(bool),
|
||||
/// Offset of the underline decoration.
|
||||
UnderlineOffset(Option<f32>),
|
||||
/// Size of the underline decoration.
|
||||
UnderlineSize(Option<f32>),
|
||||
/// Brush for rendering the underline decoration.
|
||||
UnderlineBrush(Option<B>),
|
||||
/// Strikethrough decoration.
|
||||
Strikethrough(bool),
|
||||
/// Offset of the strikethrough decoration.
|
||||
StrikethroughOffset(Option<f32>),
|
||||
/// Size of the strikethrough decoration.
|
||||
StrikethroughSize(Option<f32>),
|
||||
/// Brush for rendering the strikethrough decoration.
|
||||
StrikethroughBrush(Option<B>),
|
||||
/// Line height.
|
||||
LineHeight(LineHeight),
|
||||
/// Extra spacing between words.
|
||||
WordSpacing(f32),
|
||||
/// Extra spacing between letters.
|
||||
LetterSpacing(f32),
|
||||
/// Control over where words can wrap.
|
||||
WordBreak(WordBreak),
|
||||
/// Control over "emergency" line-breaking.
|
||||
OverflowWrap(OverflowWrap),
|
||||
/// Control over non-"emergency" line-breaking.
|
||||
TextWrapMode(TextWrapMode),
|
||||
}
|
||||
|
||||
/// Flattened group of style properties.
|
||||
#[derive(Clone, PartialEq, Debug, Default)]
|
||||
pub(crate) struct ResolvedStyle<B: Brush> {
|
||||
/// `font-family`.
|
||||
pub(crate) font_family: Resolved<FamilyId>,
|
||||
/// Font size.
|
||||
pub(crate) font_size: f32,
|
||||
/// Font width.
|
||||
pub(crate) font_width: FontWidth,
|
||||
/// Font style.
|
||||
pub(crate) font_style: FontStyle,
|
||||
/// Font weight.
|
||||
pub(crate) font_weight: FontWeight,
|
||||
/// Font variation settings.
|
||||
pub(crate) font_variations: Resolved<FontVariation>,
|
||||
/// Font feature settings.
|
||||
pub(crate) font_features: Resolved<FontFeature>,
|
||||
/// Locale.
|
||||
pub(crate) locale: Option<Language>,
|
||||
/// Brush for rendering text.
|
||||
pub(crate) brush: B,
|
||||
/// Underline decoration.
|
||||
pub(crate) underline: ResolvedDecoration<B>,
|
||||
/// Strikethrough decoration.
|
||||
pub(crate) strikethrough: ResolvedDecoration<B>,
|
||||
/// Line height.
|
||||
pub(crate) line_height: LineHeight,
|
||||
/// Extra spacing between words.
|
||||
pub(crate) word_spacing: f32,
|
||||
/// Extra spacing between letters.
|
||||
pub(crate) letter_spacing: f32,
|
||||
/// Control over where words can wrap.
|
||||
pub(crate) word_break: WordBreak,
|
||||
/// Control over "emergency" line-breaking.
|
||||
pub(crate) overflow_wrap: OverflowWrap,
|
||||
/// Control over non-"emergency" line-breaking.
|
||||
pub(crate) text_wrap_mode: TextWrapMode,
|
||||
}
|
||||
|
||||
impl<B: Brush> ResolvedStyle<B> {
|
||||
/// Applies the specified property to this style.
|
||||
pub(crate) fn apply(&mut self, property: ResolvedProperty<B>) {
|
||||
use ResolvedProperty::*;
|
||||
match property {
|
||||
FontFamily(value) => self.font_family = value,
|
||||
FontSize(value) => self.font_size = value,
|
||||
FontWidth(value) => self.font_width = value,
|
||||
FontStyle(value) => self.font_style = value,
|
||||
FontWeight(value) => self.font_weight = value,
|
||||
FontVariations(value) => self.font_variations = value,
|
||||
FontFeatures(value) => self.font_features = value,
|
||||
Locale(value) => self.locale = value,
|
||||
Brush(value) => self.brush = value,
|
||||
Underline(value) => self.underline.enabled = value,
|
||||
UnderlineOffset(value) => self.underline.offset = value,
|
||||
UnderlineSize(value) => self.underline.size = value,
|
||||
UnderlineBrush(value) => self.underline.brush = value,
|
||||
Strikethrough(value) => self.strikethrough.enabled = value,
|
||||
StrikethroughOffset(value) => self.strikethrough.offset = value,
|
||||
StrikethroughSize(value) => self.strikethrough.size = value,
|
||||
StrikethroughBrush(value) => self.strikethrough.brush = value,
|
||||
LineHeight(value) => self.line_height = value,
|
||||
WordSpacing(value) => self.word_spacing = value,
|
||||
LetterSpacing(value) => self.letter_spacing = value,
|
||||
WordBreak(value) => self.word_break = value,
|
||||
OverflowWrap(value) => self.overflow_wrap = value,
|
||||
TextWrapMode(value) => self.text_wrap_mode = value,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn check(&self, property: &ResolvedProperty<B>) -> bool {
|
||||
use ResolvedProperty::*;
|
||||
match property {
|
||||
FontFamily(value) => self.font_family == *value,
|
||||
FontSize(value) => nearly_eq(self.font_size, *value),
|
||||
FontWidth(value) => self.font_width == *value,
|
||||
FontStyle(value) => self.font_style == *value,
|
||||
FontWeight(value) => self.font_weight == *value,
|
||||
FontVariations(value) => self.font_variations == *value,
|
||||
FontFeatures(value) => self.font_features == *value,
|
||||
Locale(value) => self.locale == *value,
|
||||
Brush(value) => self.brush == *value,
|
||||
Underline(value) => self.underline.enabled == *value,
|
||||
UnderlineOffset(value) => self.underline.offset == *value,
|
||||
UnderlineSize(value) => self.underline.size == *value,
|
||||
UnderlineBrush(value) => self.underline.brush == *value,
|
||||
Strikethrough(value) => self.strikethrough.enabled == *value,
|
||||
StrikethroughOffset(value) => self.strikethrough.offset == *value,
|
||||
StrikethroughSize(value) => self.strikethrough.size == *value,
|
||||
StrikethroughBrush(value) => self.strikethrough.brush == *value,
|
||||
LineHeight(value) => self.line_height.nearly_eq(*value),
|
||||
WordSpacing(value) => nearly_eq(self.word_spacing, *value),
|
||||
LetterSpacing(value) => nearly_eq(self.letter_spacing, *value),
|
||||
WordBreak(value) => self.word_break == *value,
|
||||
OverflowWrap(value) => self.overflow_wrap == *value,
|
||||
TextWrapMode(value) => self.text_wrap_mode == *value,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_layout_style(&self) -> layout::Style<B> {
|
||||
layout::Style {
|
||||
brush: self.brush.clone(),
|
||||
underline: self.underline.as_layout_decoration(&self.brush),
|
||||
strikethrough: self.strikethrough.as_layout_decoration(&self.brush),
|
||||
line_height: self.line_height,
|
||||
overflow_wrap: self.overflow_wrap,
|
||||
text_wrap_mode: self.text_wrap_mode,
|
||||
#[cfg(feature = "accesskit")]
|
||||
locale: self.locale,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Underline or strikethrough decoration.
|
||||
#[derive(Clone, PartialEq, Default, Debug)]
|
||||
pub(crate) struct ResolvedDecoration<B: Brush> {
|
||||
/// True if the decoration is enabled.
|
||||
pub(crate) enabled: bool,
|
||||
/// Offset of the decoration from the baseline.
|
||||
pub(crate) offset: Option<f32>,
|
||||
/// Thickness of the decoration stroke.
|
||||
pub(crate) size: Option<f32>,
|
||||
/// Brush for the decoration.
|
||||
pub(crate) brush: Option<B>,
|
||||
}
|
||||
|
||||
impl<B: Brush> ResolvedDecoration<B> {
|
||||
/// Convert into a layout Decoration (filtering out disabled decorations)
|
||||
pub(crate) fn as_layout_decoration(&self, default_brush: &B) -> Option<layout::Decoration<B>> {
|
||||
if self.enabled {
|
||||
Some(layout::Decoration {
|
||||
brush: self.brush.clone().unwrap_or_else(|| default_brush.clone()),
|
||||
offset: self.offset,
|
||||
size: self.size,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
// Copyright 2021 the Parley Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Range based style application.
|
||||
|
||||
use alloc::vec;
|
||||
|
||||
use super::{Brush, RangedProperty, RangedStyle, ResolvedProperty, ResolvedStyle, StyleRun, Vec};
|
||||
use core::ops::{Bound, Range, RangeBounds};
|
||||
|
||||
/// Builder for constructing an ordered sequence of non-overlapping ranged
|
||||
/// styles from a collection of ranged style properties.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RangedStyleBuilder<B: Brush> {
|
||||
properties: Vec<RangedProperty<B>>,
|
||||
scratch_styles: Vec<RangedStyle<B>>,
|
||||
root_style: ResolvedStyle<B>,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl<B: Brush> Default for RangedStyleBuilder<B> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
properties: vec![],
|
||||
scratch_styles: vec![],
|
||||
root_style: ResolvedStyle::default(),
|
||||
// We use `usize::MAX` as a sentinel that `begin` hasn't been called.
|
||||
// This is required (rather than requiring the root style in the constructor)
|
||||
// as we want to support re-using this value.
|
||||
len: usize::MAX,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: Brush> RangedStyleBuilder<B> {
|
||||
/// Prepares the builder for accepting ranged properties for text of the specified length.
|
||||
///
|
||||
/// The provided `root_style` is the default style applied to all text unless overridden.
|
||||
pub(crate) fn begin(&mut self, root_style: ResolvedStyle<B>, len: usize) {
|
||||
self.properties.clear();
|
||||
self.scratch_styles.clear();
|
||||
self.root_style = root_style;
|
||||
self.len = len;
|
||||
}
|
||||
|
||||
/// Change a property of the root style, which covers the full range of text.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If [`begin`](Self::begin) has not been called before using this method.
|
||||
pub(crate) fn push_default(&mut self, property: ResolvedProperty<B>) {
|
||||
assert!(
|
||||
self.len != usize::MAX,
|
||||
"Internal error: Must call `begin` before setting properties on a `RangedStyleBuilder`."
|
||||
);
|
||||
self.root_style.apply(property);
|
||||
}
|
||||
|
||||
/// Override a property for the specified range of text.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If [`begin`](Self::begin) has not been called before using this method.
|
||||
pub(crate) fn push(&mut self, property: ResolvedProperty<B>, range: impl RangeBounds<usize>) {
|
||||
assert!(
|
||||
self.len != usize::MAX,
|
||||
"Internal error: Must call `begin` before setting properties on a `RangedStyleBuilder`."
|
||||
);
|
||||
let range = resolve_range(range, self.len);
|
||||
self.properties.push(RangedProperty { property, range });
|
||||
}
|
||||
|
||||
/// Computes style table + style runs for the ranged properties.
|
||||
pub(crate) fn finish(
|
||||
&mut self,
|
||||
style_table: &mut Vec<ResolvedStyle<B>>,
|
||||
style_runs: &mut Vec<StyleRun>,
|
||||
) {
|
||||
style_table.clear();
|
||||
style_runs.clear();
|
||||
if self.len == usize::MAX {
|
||||
self.properties.clear();
|
||||
self.scratch_styles.clear();
|
||||
self.root_style = ResolvedStyle::default();
|
||||
return;
|
||||
}
|
||||
let styles = &mut self.scratch_styles;
|
||||
styles.push(RangedStyle {
|
||||
style: self.root_style.clone(),
|
||||
range: 0..self.len,
|
||||
});
|
||||
for prop in &self.properties {
|
||||
if prop.range.start > prop.range.end {
|
||||
continue;
|
||||
}
|
||||
let split_range = split_range(prop, styles);
|
||||
let mut inserted = 0;
|
||||
if let Some(first) = split_range.first {
|
||||
let original_span = &mut styles[first];
|
||||
if !original_span.style.check(&prop.property) {
|
||||
let mut new_span = original_span.clone();
|
||||
let original_end = original_span.range.end;
|
||||
original_span.range.end = prop.range.start;
|
||||
new_span.range.start = prop.range.start;
|
||||
new_span.style.apply(prop.property.clone());
|
||||
if split_range.replace_len == 0 && split_range.last == Some(first) {
|
||||
let mut new_end_span = original_span.clone();
|
||||
new_end_span.range.start = prop.range.end;
|
||||
new_end_span.range.end = original_end;
|
||||
new_span.range.end = prop.range.end;
|
||||
styles.splice(
|
||||
first + 1..first + 1,
|
||||
[new_span, new_end_span].iter().cloned(),
|
||||
);
|
||||
continue;
|
||||
} else {
|
||||
styles.insert(first + 1, new_span);
|
||||
}
|
||||
inserted += 1;
|
||||
}
|
||||
}
|
||||
let replace_start = split_range.replace_start + inserted;
|
||||
let replace_end = replace_start + split_range.replace_len;
|
||||
for style in &mut styles[replace_start..replace_end] {
|
||||
style.style.apply(prop.property.clone());
|
||||
}
|
||||
if let Some(mut last) = split_range.last {
|
||||
last += inserted;
|
||||
let original_span = &mut styles[last];
|
||||
if !original_span.style.check(&prop.property) {
|
||||
let mut new_span = original_span.clone();
|
||||
original_span.range.start = prop.range.end;
|
||||
new_span.range.end = prop.range.end;
|
||||
new_span.style.apply(prop.property.clone());
|
||||
styles.insert(last, new_span);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut prev_index = 0;
|
||||
let mut merged_count = 0;
|
||||
for i in 1..styles.len() {
|
||||
if styles[prev_index].style == styles[i].style {
|
||||
let end = styles[i].range.end;
|
||||
styles[prev_index].range.end = end;
|
||||
merged_count += 1;
|
||||
} else {
|
||||
prev_index += 1;
|
||||
if prev_index != i {
|
||||
let moved_span = styles[i].clone();
|
||||
styles[prev_index] = moved_span;
|
||||
}
|
||||
}
|
||||
}
|
||||
styles.truncate(styles.len() - merged_count);
|
||||
|
||||
style_table.reserve(styles.len());
|
||||
style_runs.reserve(styles.len());
|
||||
for (style_index, style) in styles.drain(..).enumerate() {
|
||||
style_table.push(style.style);
|
||||
style_runs.push(StyleRun {
|
||||
style_index: style_index as u16,
|
||||
range: style.range,
|
||||
});
|
||||
}
|
||||
|
||||
self.properties.clear();
|
||||
self.root_style = ResolvedStyle::default();
|
||||
self.len = usize::MAX;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct SplitRange {
|
||||
first: Option<usize>,
|
||||
replace_start: usize,
|
||||
replace_len: usize,
|
||||
last: Option<usize>,
|
||||
}
|
||||
|
||||
fn split_range<B: Brush>(prop: &RangedProperty<B>, spans: &[RangedStyle<B>]) -> SplitRange {
|
||||
let mut range = SplitRange::default();
|
||||
let start_span_index =
|
||||
match spans.binary_search_by(|span| span.range.start.cmp(&prop.range.start)) {
|
||||
Ok(index) => index,
|
||||
Err(index) => index.saturating_sub(1),
|
||||
};
|
||||
let mut end_span_index = spans.len() - 1;
|
||||
for (i, span) in spans[start_span_index..].iter().enumerate() {
|
||||
if span.range.end >= prop.range.end {
|
||||
end_span_index = i + start_span_index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let start_span = &spans[start_span_index];
|
||||
let end_span = &spans[end_span_index];
|
||||
if start_span.range.start < prop.range.start {
|
||||
range.first = Some(start_span_index);
|
||||
range.replace_start = start_span_index + 1;
|
||||
} else {
|
||||
range.replace_start = start_span_index;
|
||||
}
|
||||
if end_span.range.end > prop.range.end {
|
||||
range.last = Some(end_span_index);
|
||||
range.replace_len = end_span_index.saturating_sub(range.replace_start);
|
||||
} else {
|
||||
range.replace_len = (end_span_index + 1).saturating_sub(range.replace_start);
|
||||
}
|
||||
range
|
||||
}
|
||||
|
||||
/// Resolves a `RangeBounds` into a range in the range 0..len.
|
||||
fn resolve_range(range: impl RangeBounds<usize>, len: usize) -> Range<usize> {
|
||||
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)
|
||||
}
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
// Copyright 2021 the Parley Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Hierarchical tree based style application.
|
||||
use alloc::{string::String, vec::Vec};
|
||||
|
||||
use crate::style::WhiteSpaceCollapse;
|
||||
|
||||
use super::{Brush, ResolvedProperty, ResolvedStyle, StyleRun};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct StyleTreeNode<B: Brush> {
|
||||
parent: Option<usize>,
|
||||
style: ResolvedStyle<B>,
|
||||
style_id: Option<u16>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub(crate) enum ItemKind {
|
||||
None,
|
||||
InlineBox,
|
||||
TextRun,
|
||||
}
|
||||
|
||||
/// Builder for constructing a tree of styles
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TreeStyleBuilder<B: Brush> {
|
||||
tree: Vec<StyleTreeNode<B>>,
|
||||
style_table: Vec<ResolvedStyle<B>>,
|
||||
style_runs: Vec<StyleRun>,
|
||||
white_space_collapse: WhiteSpaceCollapse,
|
||||
text: String,
|
||||
uncommitted_text: String,
|
||||
current_span: usize,
|
||||
is_span_first: bool,
|
||||
last_item_kind: ItemKind,
|
||||
}
|
||||
|
||||
impl<B: Brush> TreeStyleBuilder<B> {
|
||||
fn current_style(&self) -> ResolvedStyle<B> {
|
||||
self.tree[self.current_span].style.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: Brush> Default for TreeStyleBuilder<B> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
tree: Vec::new(),
|
||||
style_table: Vec::new(),
|
||||
style_runs: Vec::new(),
|
||||
white_space_collapse: WhiteSpaceCollapse::Preserve,
|
||||
text: String::new(),
|
||||
uncommitted_text: String::new(),
|
||||
current_span: usize::MAX,
|
||||
is_span_first: false,
|
||||
last_item_kind: ItemKind::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: Brush> TreeStyleBuilder<B> {
|
||||
/// Prepares the builder for accepting a tree of styles and text.
|
||||
///
|
||||
/// The provided `root_style` is the default style applied to all text unless overridden.
|
||||
pub(crate) fn begin(&mut self, root_style: ResolvedStyle<B>) {
|
||||
self.tree.clear();
|
||||
self.style_table.clear();
|
||||
self.style_runs.clear();
|
||||
self.white_space_collapse = WhiteSpaceCollapse::Preserve;
|
||||
self.text.clear();
|
||||
self.uncommitted_text.clear();
|
||||
|
||||
self.tree.push(StyleTreeNode {
|
||||
parent: None,
|
||||
style: root_style,
|
||||
style_id: None,
|
||||
});
|
||||
self.current_span = 0;
|
||||
self.is_span_first = true;
|
||||
}
|
||||
|
||||
pub(crate) fn set_white_space_mode(&mut self, white_space_collapse: WhiteSpaceCollapse) {
|
||||
self.white_space_collapse = white_space_collapse;
|
||||
}
|
||||
|
||||
pub(crate) fn set_is_span_first(&mut self, is_span_first: bool) {
|
||||
self.is_span_first = is_span_first;
|
||||
}
|
||||
|
||||
pub(crate) fn set_last_item_kind(&mut self, item_kind: ItemKind) {
|
||||
self.last_item_kind = item_kind;
|
||||
}
|
||||
|
||||
pub(crate) fn push_uncommitted_text(&mut self, is_span_last: bool) {
|
||||
let uncommitted_text = core::mem::take(&mut self.uncommitted_text);
|
||||
let span_text = match self.white_space_collapse {
|
||||
WhiteSpaceCollapse::Preserve => uncommitted_text,
|
||||
WhiteSpaceCollapse::Collapse => {
|
||||
let mut span_text = uncommitted_text.as_str();
|
||||
|
||||
if self.is_span_first
|
||||
|| (self.last_item_kind == ItemKind::TextRun
|
||||
&& self
|
||||
.text
|
||||
.chars()
|
||||
.last()
|
||||
.is_some_and(|c| c.is_ascii_whitespace()))
|
||||
{
|
||||
span_text = span_text.trim_start();
|
||||
}
|
||||
if is_span_last {
|
||||
span_text = span_text.trim_end();
|
||||
}
|
||||
|
||||
// Collapse spaces
|
||||
let mut last_char_whitespace = false;
|
||||
span_text
|
||||
.chars()
|
||||
.filter_map(|c: char| {
|
||||
let this_char_whitespace = c.is_ascii_whitespace();
|
||||
let prev_char_whitespace = last_char_whitespace;
|
||||
last_char_whitespace = this_char_whitespace;
|
||||
|
||||
if this_char_whitespace {
|
||||
if prev_char_whitespace {
|
||||
None
|
||||
} else {
|
||||
Some(' ')
|
||||
}
|
||||
} else {
|
||||
Some(c)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
};
|
||||
|
||||
// Nothing to do if there is no uncommitted text.
|
||||
if span_text.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let range = self.text.len()..(self.text.len() + span_text.len());
|
||||
let style_index = self.resolve_current_style_id();
|
||||
self.style_runs.push(StyleRun { style_index, range });
|
||||
self.text.push_str(&span_text);
|
||||
self.is_span_first = false;
|
||||
self.last_item_kind = ItemKind::TextRun;
|
||||
}
|
||||
|
||||
fn resolve_current_style_id(&mut self) -> u16 {
|
||||
if let Some(style_id) = self.tree[self.current_span].style_id {
|
||||
return style_id;
|
||||
}
|
||||
let style_id = self.style_table.len() as u16;
|
||||
self.style_table.push(self.current_style());
|
||||
self.tree[self.current_span].style_id = Some(style_id);
|
||||
style_id
|
||||
}
|
||||
|
||||
pub(crate) fn current_text_len(&self) -> usize {
|
||||
self.text.len()
|
||||
}
|
||||
|
||||
pub(crate) fn push_style_span(&mut self, style: ResolvedStyle<B>) {
|
||||
self.push_uncommitted_text(false);
|
||||
|
||||
self.tree.push(StyleTreeNode {
|
||||
parent: Some(self.current_span),
|
||||
style,
|
||||
style_id: None,
|
||||
});
|
||||
self.current_span = self.tree.len() - 1;
|
||||
self.is_span_first = true;
|
||||
}
|
||||
|
||||
pub(crate) fn push_style_modification_span(
|
||||
&mut self,
|
||||
properties: impl Iterator<Item = ResolvedProperty<B>>,
|
||||
) {
|
||||
let mut style = self.current_style();
|
||||
for prop in properties {
|
||||
style.apply(prop.clone());
|
||||
}
|
||||
self.push_style_span(style);
|
||||
}
|
||||
|
||||
pub(crate) fn pop_style_span(&mut self) {
|
||||
self.push_uncommitted_text(true);
|
||||
|
||||
self.current_span = self.tree[self.current_span]
|
||||
.parent
|
||||
.expect("Popped root style");
|
||||
}
|
||||
|
||||
/// Pushes a property that covers the specified range of text.
|
||||
pub(crate) fn push_text(&mut self, text: &str) {
|
||||
if !text.is_empty() {
|
||||
self.uncommitted_text.push_str(text);
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes style table + style runs and returns the final text buffer.
|
||||
pub(crate) fn finish(
|
||||
&mut self,
|
||||
style_table: &mut Vec<ResolvedStyle<B>>,
|
||||
style_runs: &mut Vec<StyleRun>,
|
||||
) -> String {
|
||||
while self.tree[self.current_span].parent.is_some() {
|
||||
self.pop_style_span();
|
||||
}
|
||||
|
||||
self.push_uncommitted_text(true);
|
||||
|
||||
style_table.clear();
|
||||
style_runs.clear();
|
||||
style_table.extend_from_slice(&self.style_table);
|
||||
style_runs.extend_from_slice(&self.style_runs);
|
||||
|
||||
core::mem::take(&mut self.text)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use alloc::vec::Vec;
|
||||
use core::ops::Range;
|
||||
|
||||
#[test]
|
||||
fn reuses_style_id_when_returning_to_parent_span() {
|
||||
let mut builder = TreeStyleBuilder::<u32>::default();
|
||||
builder.begin(ResolvedStyle::default());
|
||||
builder.push_text("A");
|
||||
builder.push_style_modification_span([ResolvedProperty::FontSize(20.)].into_iter());
|
||||
builder.push_text("B");
|
||||
builder.pop_style_span();
|
||||
builder.push_text("C");
|
||||
|
||||
let mut style_table = Vec::new();
|
||||
let mut style_runs = Vec::new();
|
||||
let text = builder.finish(&mut style_table, &mut style_runs);
|
||||
|
||||
assert_eq!(text, "ABC");
|
||||
assert_eq!(style_table.len(), 2);
|
||||
assert_eq!(style_runs.len(), 3);
|
||||
assert_eq!(style_runs[0].style_index, 0);
|
||||
assert_eq!(style_runs[1].style_index, 1);
|
||||
assert_eq!(style_runs[2].style_index, 0);
|
||||
assert_eq!(style_runs[0].range, Range { start: 0, end: 1 });
|
||||
assert_eq!(style_runs[1].range, Range { start: 1, end: 2 });
|
||||
assert_eq!(style_runs[2].range, Range { start: 2, end: 3 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reuses_root_style_id_across_multiple_pop_return_cycles() {
|
||||
let mut builder = TreeStyleBuilder::<u32>::default();
|
||||
builder.begin(ResolvedStyle::default());
|
||||
builder.push_text("A");
|
||||
builder.push_style_modification_span([ResolvedProperty::FontSize(20.)].into_iter());
|
||||
builder.push_text("B");
|
||||
builder.pop_style_span();
|
||||
builder.push_text("C");
|
||||
builder.push_style_modification_span([ResolvedProperty::LetterSpacing(1.)].into_iter());
|
||||
builder.push_text("D");
|
||||
builder.pop_style_span();
|
||||
builder.push_text("E");
|
||||
|
||||
let mut style_table = Vec::new();
|
||||
let mut style_runs = Vec::new();
|
||||
let text = builder.finish(&mut style_table, &mut style_runs);
|
||||
|
||||
assert_eq!(text, "ABCDE");
|
||||
assert_eq!(style_table.len(), 3);
|
||||
assert_eq!(style_runs.len(), 5);
|
||||
assert_eq!(style_runs[0].style_index, 0);
|
||||
assert_eq!(style_runs[1].style_index, 1);
|
||||
assert_eq!(style_runs[2].style_index, 0);
|
||||
assert_eq!(style_runs[3].style_index, 2);
|
||||
assert_eq!(style_runs[4].style_index, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reuses_parent_and_root_style_ids_after_nested_pop() {
|
||||
let mut builder = TreeStyleBuilder::<u32>::default();
|
||||
builder.begin(ResolvedStyle::default());
|
||||
builder.push_text("R");
|
||||
builder.push_style_modification_span([ResolvedProperty::FontSize(20.)].into_iter());
|
||||
builder.push_text("A");
|
||||
builder.push_style_modification_span([ResolvedProperty::LetterSpacing(1.)].into_iter());
|
||||
builder.push_text("B");
|
||||
builder.pop_style_span();
|
||||
builder.push_text("C");
|
||||
builder.pop_style_span();
|
||||
builder.push_text("D");
|
||||
|
||||
let mut style_table = Vec::new();
|
||||
let mut style_runs = Vec::new();
|
||||
let text = builder.finish(&mut style_table, &mut style_runs);
|
||||
|
||||
assert_eq!(text, "RABCD");
|
||||
assert_eq!(style_table.len(), 3);
|
||||
assert_eq!(style_runs.len(), 5);
|
||||
assert_eq!(style_runs[0].style_index, 0);
|
||||
assert_eq!(style_runs[1].style_index, 1);
|
||||
assert_eq!(style_runs[2].style_index, 2);
|
||||
assert_eq!(style_runs[3].style_index, 1);
|
||||
assert_eq!(style_runs[4].style_index, 0);
|
||||
}
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
// Copyright 2025 the Parley Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! OpenType settings (features and variations).
|
||||
|
||||
pub use parlance::{FontFeature, FontVariation, Tag};
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
// Copyright 2025 the Parley Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
use crate::FontVariation;
|
||||
use alloc::boxed::Box;
|
||||
use hashbrown::Equivalent;
|
||||
|
||||
#[derive(PartialEq, Copy, Clone)]
|
||||
pub(crate) struct ShapeDataKey {
|
||||
/// The font collection's blob ID.
|
||||
font_blob_id: u64,
|
||||
/// The font's index in the font collection.
|
||||
font_index: u32,
|
||||
}
|
||||
|
||||
impl ShapeDataKey {
|
||||
pub(crate) const fn new(font_blob_id: u64, font_index: u32) -> Self {
|
||||
Self {
|
||||
font_blob_id,
|
||||
font_index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Equivalent<Self> for ShapeDataKey {
|
||||
#[inline(always)]
|
||||
fn equivalent(&self, key: &Self) -> bool {
|
||||
self == key
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Self> for ShapeDataKey {
|
||||
#[inline(always)]
|
||||
fn from(key: &Self) -> Self {
|
||||
*key
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ShapeInstanceId {
|
||||
/// The font collection's blob ID.
|
||||
font_blob_id: u64,
|
||||
/// The font's index in the font collection.
|
||||
font_index: u32,
|
||||
synthesis: fontique::Synthesis,
|
||||
variations: Option<Box<[FontVariation]>>,
|
||||
}
|
||||
|
||||
pub(crate) struct ShapeInstanceKey<'a> {
|
||||
/// The font collection's blob ID.
|
||||
font_blob_id: u64,
|
||||
/// The font's index in the font collection.
|
||||
font_index: u32,
|
||||
synthesis: &'a fontique::Synthesis,
|
||||
variations: Option<&'a [FontVariation]>,
|
||||
}
|
||||
|
||||
impl<'a> ShapeInstanceKey<'a> {
|
||||
pub(crate) const fn new(
|
||||
font_blob_id: u64,
|
||||
font_index: u32,
|
||||
synthesis: &'a fontique::Synthesis,
|
||||
variations: Option<&'a [FontVariation]>,
|
||||
) -> Self {
|
||||
Self {
|
||||
font_blob_id,
|
||||
font_index,
|
||||
synthesis,
|
||||
variations,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Equivalent<ShapeInstanceId> for ShapeInstanceKey<'a> {
|
||||
#[inline(always)]
|
||||
fn equivalent(&self, key: &ShapeInstanceId) -> bool {
|
||||
self.font_blob_id == key.font_blob_id
|
||||
&& self.font_index == key.font_index
|
||||
&& *self.synthesis == key.synthesis
|
||||
&& self.variations == key.variations.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<ShapeInstanceKey<'a>> for ShapeInstanceId {
|
||||
#[inline(always)]
|
||||
fn from(key: ShapeInstanceKey<'a>) -> Self {
|
||||
Self {
|
||||
font_blob_id: key.font_blob_id,
|
||||
font_index: key.font_index,
|
||||
synthesis: *key.synthesis,
|
||||
variations: key.variations.map(|v| v.to_vec().into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ShapePlanId {
|
||||
/// The font collection's blob ID.
|
||||
font_blob_id: u64,
|
||||
/// The font's index in the font collection.
|
||||
font_index: u32,
|
||||
synthesis: fontique::Synthesis,
|
||||
direction: harfrust::Direction,
|
||||
script: harfrust::Script,
|
||||
language: Option<harfrust::Language>,
|
||||
features: Box<[harfrust::Feature]>,
|
||||
variations: Option<Box<[FontVariation]>>,
|
||||
}
|
||||
|
||||
pub(crate) struct ShapePlanKey<'a> {
|
||||
/// The font collection's blob ID.
|
||||
font_blob_id: u64,
|
||||
/// The font's index in the font collection.
|
||||
font_index: u32,
|
||||
synthesis: &'a fontique::Synthesis,
|
||||
direction: harfrust::Direction,
|
||||
script: harfrust::Script,
|
||||
language: Option<harfrust::Language>,
|
||||
features: &'a [harfrust::Feature],
|
||||
variations: Option<&'a [FontVariation]>,
|
||||
}
|
||||
|
||||
impl<'a> ShapePlanKey<'a> {
|
||||
pub(crate) const fn new(
|
||||
font_blob_id: u64,
|
||||
font_index: u32,
|
||||
synthesis: &'a fontique::Synthesis,
|
||||
direction: harfrust::Direction,
|
||||
script: harfrust::Script,
|
||||
language: Option<harfrust::Language>,
|
||||
features: &'a [harfrust::Feature],
|
||||
variations: Option<&'a [FontVariation]>,
|
||||
) -> Self {
|
||||
Self {
|
||||
font_blob_id,
|
||||
font_index,
|
||||
synthesis,
|
||||
direction,
|
||||
script,
|
||||
language,
|
||||
features,
|
||||
variations,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Equivalent<ShapePlanId> for ShapePlanKey<'a> {
|
||||
#[inline(always)]
|
||||
fn equivalent(&self, key: &ShapePlanId) -> bool {
|
||||
self.font_blob_id == key.font_blob_id
|
||||
&& self.font_index == key.font_index
|
||||
&& *self.synthesis == key.synthesis
|
||||
&& self.direction == key.direction
|
||||
&& self.script == key.script
|
||||
&& self.language == key.language
|
||||
&& self.features.len() == key.features.len()
|
||||
&& self.variations == key.variations.as_deref()
|
||||
&& self
|
||||
.features
|
||||
.iter()
|
||||
.zip(key.features.iter())
|
||||
.all(|(a, b)| a == b)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<ShapePlanKey<'a>> for ShapePlanId {
|
||||
#[inline(always)]
|
||||
fn from(key: ShapePlanKey<'a>) -> Self {
|
||||
Self {
|
||||
font_blob_id: key.font_blob_id,
|
||||
font_index: key.font_index,
|
||||
synthesis: *key.synthesis,
|
||||
direction: key.direction,
|
||||
script: key.script,
|
||||
language: key.language,
|
||||
features: key.features.to_vec().into(),
|
||||
variations: key.variations.map(|v| v.to_vec().into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
+672
@@ -0,0 +1,672 @@
|
||||
// Copyright 2021 the Parley Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Text shaping implementation using `harfrust`for shaping
|
||||
//! and `icu` for text analysis.
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use core::mem;
|
||||
use core::ops::RangeInclusive;
|
||||
use harfrust::ShapeOptions;
|
||||
|
||||
use super::layout::Layout;
|
||||
use super::resolve::{ResolveContext, Resolved, ResolvedStyle};
|
||||
use super::style::{Brush, FontFeature, FontVariation};
|
||||
use crate::analysis::cluster::{Char, CharCluster, Status};
|
||||
use crate::analysis::{AnalysisDataSources, CharInfo};
|
||||
use crate::convert::script_to_harfrust;
|
||||
use crate::inline_box::InlineBox;
|
||||
use crate::lru_cache::LruCache;
|
||||
use crate::util::nearly_eq;
|
||||
use crate::{FontData, convert};
|
||||
use fontique::Language;
|
||||
use icu_properties::props::Script;
|
||||
|
||||
use fontique::{self, Query, QueryFamily, QueryFont};
|
||||
|
||||
mod cache;
|
||||
|
||||
pub(crate) struct ShapeContext {
|
||||
shape_data_cache: LruCache<cache::ShapeDataKey, harfrust::ShaperData>,
|
||||
shape_instance_cache: LruCache<cache::ShapeInstanceId, harfrust::ShaperInstance>,
|
||||
shape_plan_cache: LruCache<cache::ShapePlanId, harfrust::ShapePlan>,
|
||||
unicode_buffer: Option<harfrust::UnicodeBuffer>,
|
||||
features: Vec<harfrust::Feature>,
|
||||
char_cluster: CharCluster,
|
||||
}
|
||||
|
||||
impl Default for ShapeContext {
|
||||
fn default() -> Self {
|
||||
const MAX_ENTRIES: usize = 16;
|
||||
Self {
|
||||
shape_data_cache: LruCache::new(MAX_ENTRIES),
|
||||
shape_instance_cache: LruCache::new(MAX_ENTRIES),
|
||||
shape_plan_cache: LruCache::new(MAX_ENTRIES),
|
||||
unicode_buffer: Some(harfrust::UnicodeBuffer::new()),
|
||||
features: Vec::new(),
|
||||
char_cluster: CharCluster::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Item {
|
||||
style_index: u16,
|
||||
size: f32,
|
||||
script: Script,
|
||||
level: u8,
|
||||
locale: Option<Language>,
|
||||
variations: Resolved<FontVariation>,
|
||||
features: Resolved<FontFeature>,
|
||||
word_spacing: f32,
|
||||
letter_spacing: f32,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn shape_text<'a, B: Brush>(
|
||||
rcx: &'a ResolveContext,
|
||||
mut fq: Query<'a>,
|
||||
styles: &'a [ResolvedStyle<B>],
|
||||
inline_boxes: &[InlineBox],
|
||||
infos: &[(CharInfo, u16)],
|
||||
levels: &[u8],
|
||||
scx: &mut ShapeContext,
|
||||
mut text: &str,
|
||||
layout: &mut Layout<B>,
|
||||
analysis_data_sources: &AnalysisDataSources,
|
||||
) {
|
||||
// If we have both empty text and no inline boxes, shape with a fake space
|
||||
// to generate metrics that can be used to size a cursor.
|
||||
if text.is_empty() && inline_boxes.is_empty() {
|
||||
text = " ";
|
||||
}
|
||||
// Do nothing if there is no text or styles (there should always be a default style)
|
||||
if text.is_empty() || styles.is_empty() {
|
||||
// Process any remaining inline boxes whose index is greater than the length of the text
|
||||
for box_idx in 0..inline_boxes.len() {
|
||||
// Push the box to the list of items
|
||||
layout.data.push_inline_box(box_idx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup mutable state for iteration
|
||||
let initial_style_index = infos.first().map_or(0, |(_, style_index)| *style_index);
|
||||
let mut style = &styles[initial_style_index as usize];
|
||||
let mut item = Item {
|
||||
style_index: initial_style_index,
|
||||
size: style.font_size,
|
||||
level: levels.first().copied().unwrap_or(0),
|
||||
script: infos
|
||||
.iter()
|
||||
.map(|x| x.0.script)
|
||||
.find(|&script| real_script(script))
|
||||
.unwrap_or(Script::Latin),
|
||||
locale: style.locale,
|
||||
variations: style.font_variations,
|
||||
features: style.font_features,
|
||||
word_spacing: style.word_spacing,
|
||||
letter_spacing: style.letter_spacing,
|
||||
};
|
||||
|
||||
let mut char_range = 0..0;
|
||||
let mut text_range = 0..0;
|
||||
|
||||
let mut inline_box_iter = inline_boxes.iter().enumerate();
|
||||
let mut current_box = inline_box_iter.next();
|
||||
|
||||
// Iterate over characters in the text
|
||||
for ((char_index, (byte_index, ch)), (info, style_index)) in
|
||||
text.char_indices().enumerate().zip(infos)
|
||||
{
|
||||
let mut break_run = false;
|
||||
let mut script = info.script;
|
||||
if !real_script(script) {
|
||||
script = item.script;
|
||||
}
|
||||
let level = levels.get(char_index).copied().unwrap_or(0);
|
||||
if item.style_index != *style_index {
|
||||
item.style_index = *style_index;
|
||||
style = &styles[*style_index as usize];
|
||||
if !nearly_eq(style.font_size, item.size)
|
||||
|| style.locale != item.locale
|
||||
|| style.font_variations != item.variations
|
||||
|| style.font_features != item.features
|
||||
|| !nearly_eq(style.letter_spacing, item.letter_spacing)
|
||||
|| !nearly_eq(style.word_spacing, item.word_spacing)
|
||||
{
|
||||
break_run = true;
|
||||
}
|
||||
}
|
||||
|
||||
if level != item.level || script != item.script {
|
||||
break_run = true;
|
||||
}
|
||||
|
||||
// Check if there is an inline box at this index
|
||||
// Note:
|
||||
// - We loop because there may be multiple boxes at this index
|
||||
// - We do this *before* processing the text run because we need to know whether we should
|
||||
// break the run due to the presence of an inline box.
|
||||
let mut deferred_boxes: Option<RangeInclusive<usize>> = None;
|
||||
while let Some((box_idx, inline_box)) = current_box {
|
||||
if inline_box.index == byte_index {
|
||||
break_run = true;
|
||||
if let Some(boxes) = &mut deferred_boxes {
|
||||
deferred_boxes = Some((*boxes.start())..=box_idx);
|
||||
} else {
|
||||
deferred_boxes = Some(box_idx..=box_idx);
|
||||
};
|
||||
// Update the current box to the next box
|
||||
current_box = inline_box_iter.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if break_run && !text_range.is_empty() {
|
||||
shape_item(
|
||||
&mut fq,
|
||||
rcx,
|
||||
styles,
|
||||
&item,
|
||||
scx,
|
||||
text,
|
||||
&text_range,
|
||||
&char_range,
|
||||
infos,
|
||||
layout,
|
||||
analysis_data_sources,
|
||||
);
|
||||
item.size = style.font_size;
|
||||
item.level = level;
|
||||
item.script = script;
|
||||
item.locale = style.locale;
|
||||
item.variations = style.font_variations;
|
||||
item.features = style.font_features;
|
||||
item.word_spacing = style.word_spacing;
|
||||
item.letter_spacing = style.letter_spacing;
|
||||
text_range.start = text_range.end;
|
||||
char_range.start = char_range.end;
|
||||
}
|
||||
|
||||
if let Some(deferred_boxes) = deferred_boxes {
|
||||
for box_idx in deferred_boxes {
|
||||
layout.data.push_inline_box(box_idx);
|
||||
}
|
||||
}
|
||||
|
||||
text_range.end += ch.len_utf8();
|
||||
char_range.end += 1;
|
||||
}
|
||||
|
||||
if !text_range.is_empty() {
|
||||
shape_item(
|
||||
&mut fq,
|
||||
rcx,
|
||||
styles,
|
||||
&item,
|
||||
scx,
|
||||
text,
|
||||
&text_range,
|
||||
&char_range,
|
||||
infos,
|
||||
layout,
|
||||
analysis_data_sources,
|
||||
);
|
||||
}
|
||||
|
||||
// Process any remaining inline boxes whose index is greater than the length of the text
|
||||
if let Some((box_idx, _inline_box)) = current_box {
|
||||
layout.data.push_inline_box(box_idx);
|
||||
}
|
||||
for (box_idx, _inline_box) in inline_box_iter {
|
||||
layout.data.push_inline_box(box_idx);
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuilds the provided `char_cluster` in-place using the existing allocation
|
||||
// for the given grapheme `segment_text`, consuming items from `item_infos_iter`.
|
||||
fn fill_cluster_in_place(
|
||||
segment_text: &str,
|
||||
item_infos_iter: &mut core::slice::Iter<'_, (CharInfo, u16)>,
|
||||
code_unit_offset_in_string: &mut usize,
|
||||
char_cluster: &mut CharCluster,
|
||||
) {
|
||||
// Reset cluster but keep allocation
|
||||
char_cluster.clear();
|
||||
|
||||
let mut force_normalize = false;
|
||||
let mut is_emoji_or_pictograph = false;
|
||||
let mut map_len: u8 = 0;
|
||||
let start = *code_unit_offset_in_string as u32;
|
||||
|
||||
for ((_, ch), (info, style_index)) in segment_text.char_indices().zip(item_infos_iter.by_ref())
|
||||
{
|
||||
force_normalize |= info.force_normalize();
|
||||
// TODO - make emoji detection more complete, as per (except using composite Trie tables as
|
||||
// much as possible:
|
||||
// https://github.com/conor-93/parley/blob/4637d826732a1a82bbb3c904c7f47a16a21cceec/parley/src/shape/mod.rs#L221-L269
|
||||
is_emoji_or_pictograph |= info.is_emoji_or_pictograph();
|
||||
*code_unit_offset_in_string += ch.len_utf8();
|
||||
|
||||
// TODO: Explore ignoring other modifiers in determining `contributes_to_shaping`:
|
||||
// regional indicators, subdivision flag tag sequences, skin tone modifiers
|
||||
// See also: https://github.com/google/emoji-segmenter
|
||||
|
||||
// If the color emoji has a non-printing variation selector, ignore the variation selector.
|
||||
// Its presentation depends on the platform and font.
|
||||
//
|
||||
// e.g.
|
||||
// - `U+270C + U+FE0F`: `✌`, force basic presentation
|
||||
// - `U+270C + U+FE0F`: `✌️`, force emoji presentation
|
||||
//
|
||||
// <https://www.unicode.org/reports/tr37/>
|
||||
let is_emoji_with_non_printing_variation_selector =
|
||||
is_emoji_or_pictograph && info.is_variation_selector();
|
||||
|
||||
let contributes_to_shaping =
|
||||
info.contributes_to_shaping() && !is_emoji_with_non_printing_variation_selector;
|
||||
if contributes_to_shaping {
|
||||
map_len += 1;
|
||||
}
|
||||
|
||||
char_cluster.chars.push(Char {
|
||||
ch,
|
||||
contributes_to_shaping,
|
||||
glyph_id: 0,
|
||||
style_index: *style_index,
|
||||
is_control_character: info.is_control(),
|
||||
});
|
||||
}
|
||||
|
||||
// Finalize cluster metadata
|
||||
let end = *code_unit_offset_in_string as u32;
|
||||
char_cluster.is_emoji = is_emoji_or_pictograph;
|
||||
char_cluster.map_len = map_len;
|
||||
char_cluster.start = start;
|
||||
char_cluster.end = end;
|
||||
char_cluster.force_normalize = force_normalize;
|
||||
}
|
||||
|
||||
fn shape_item<'a, B: Brush>(
|
||||
fq: &mut Query<'a>,
|
||||
rcx: &'a ResolveContext,
|
||||
styles: &'a [ResolvedStyle<B>],
|
||||
item: &Item,
|
||||
scx: &mut ShapeContext,
|
||||
text: &str,
|
||||
text_range: &core::ops::Range<usize>,
|
||||
char_range: &core::ops::Range<usize>,
|
||||
infos: &[(CharInfo, u16)],
|
||||
layout: &mut Layout<B>,
|
||||
analysis_data_sources: &AnalysisDataSources,
|
||||
) {
|
||||
let item_text = &text[text_range.clone()];
|
||||
let item_infos = &infos[char_range.start..char_range.end]; // Only process current item
|
||||
let first_style_index = item_infos[0].1;
|
||||
let fb_script = convert::script_to_fontique(item.script, analysis_data_sources);
|
||||
let mut font_selector =
|
||||
FontSelector::new(fq, rcx, styles, first_style_index, fb_script, item.locale);
|
||||
|
||||
let grapheme_cluster_boundaries = analysis_data_sources
|
||||
.grapheme_segmenter()
|
||||
.segment_str(item_text);
|
||||
let mut item_infos_iter = item_infos.iter();
|
||||
let mut code_unit_offset_in_string = text_range.start;
|
||||
let char_cluster = &mut scx.char_cluster;
|
||||
|
||||
// Build an iterator of boundaries and consume the first segment to seed the loop
|
||||
let mut boundaries_iter = grapheme_cluster_boundaries.skip(1);
|
||||
let mut last_boundary = 0_usize;
|
||||
let Some(mut current_boundary) = boundaries_iter.next() else {
|
||||
return; // No clusters
|
||||
};
|
||||
|
||||
fill_cluster_in_place(
|
||||
&item_text[last_boundary..current_boundary],
|
||||
&mut item_infos_iter,
|
||||
&mut code_unit_offset_in_string,
|
||||
char_cluster,
|
||||
);
|
||||
|
||||
let mut current_font = font_selector.select_font(char_cluster, analysis_data_sources);
|
||||
|
||||
// Main segmentation loop (based on swash shape_clusters) - only within current item
|
||||
while let Some(font) = current_font.take() {
|
||||
// Collect all clusters for this font segment
|
||||
let cluster_range = char_cluster.range();
|
||||
let segment_start_offset = cluster_range.start as usize - text_range.start;
|
||||
let mut segment_end_offset = cluster_range.end as usize - text_range.start;
|
||||
|
||||
for next_boundary in boundaries_iter.by_ref() {
|
||||
// Build next cluster in-place
|
||||
last_boundary = current_boundary;
|
||||
current_boundary = next_boundary;
|
||||
fill_cluster_in_place(
|
||||
&item_text[last_boundary..current_boundary],
|
||||
&mut item_infos_iter,
|
||||
&mut code_unit_offset_in_string,
|
||||
char_cluster,
|
||||
);
|
||||
|
||||
if let Some(next_font) = font_selector.select_font(char_cluster, analysis_data_sources)
|
||||
{
|
||||
if next_font != font {
|
||||
current_font = Some(next_font);
|
||||
break;
|
||||
} else {
|
||||
// Same font - add to current segment
|
||||
segment_end_offset = char_cluster.range().end as usize - text_range.start;
|
||||
}
|
||||
} else {
|
||||
// No font determined, continue to next cluster
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Shape this font segment with harfrust
|
||||
let segment_text = &item_text[segment_start_offset..segment_end_offset];
|
||||
// Shape the entire segment text including newlines
|
||||
// The line breaking algorithm will handle newlines automatically
|
||||
|
||||
// TODO: How do we want to handle errors like this?
|
||||
let font_ref =
|
||||
harfrust::FontRef::from_index(font.font.blob.as_ref(), font.font.index).unwrap();
|
||||
|
||||
// Create harfrust shaper
|
||||
let shaper_data = scx.shape_data_cache.entry(
|
||||
cache::ShapeDataKey::new(font.font.blob.id(), font.font.index),
|
||||
|| harfrust::ShaperData::new(&font_ref),
|
||||
);
|
||||
let instance = scx.shape_instance_cache.entry(
|
||||
cache::ShapeInstanceKey::new(
|
||||
font.font.blob.id(),
|
||||
font.font.index,
|
||||
&font.font.synthesis,
|
||||
rcx.variations(item.variations),
|
||||
),
|
||||
|| {
|
||||
harfrust::ShaperInstance::from_variations(
|
||||
&font_ref,
|
||||
variations_iter(&font.font.synthesis, rcx.variations(item.variations)),
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
let direction = if item.level & 1 != 0 {
|
||||
harfrust::Direction::RightToLeft
|
||||
} else {
|
||||
harfrust::Direction::LeftToRight
|
||||
};
|
||||
let hb_script = script_to_harfrust(fb_script);
|
||||
let language = item
|
||||
.locale
|
||||
.as_ref()
|
||||
.and_then(|lang| lang.language().parse::<harfrust::Language>().ok());
|
||||
scx.features.clear();
|
||||
for feature in rcx.features(item.features).unwrap_or(&[]) {
|
||||
scx.features.push(harfrust::Feature::new(
|
||||
harfrust::Tag::new(&feature.tag.to_bytes()),
|
||||
feature.value as u32,
|
||||
..,
|
||||
));
|
||||
}
|
||||
let harf_shaper = shaper_data
|
||||
.shaper(&font_ref)
|
||||
.instance(Some(instance))
|
||||
.build();
|
||||
let shaper_plan = scx.shape_plan_cache.entry(
|
||||
cache::ShapePlanKey::new(
|
||||
font.font.blob.id(),
|
||||
font.font.index,
|
||||
&font.font.synthesis,
|
||||
direction,
|
||||
hb_script,
|
||||
language.clone(),
|
||||
&scx.features,
|
||||
rcx.variations(item.variations),
|
||||
),
|
||||
|| {
|
||||
harfrust::ShapePlan::new(
|
||||
&harf_shaper,
|
||||
direction,
|
||||
Some(hb_script),
|
||||
language.as_ref(),
|
||||
&scx.features,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
// Prepare harfrust buffer
|
||||
let mut buffer = mem::take(&mut scx.unicode_buffer).unwrap();
|
||||
buffer.clear();
|
||||
|
||||
// Use the entire segment text including newlines
|
||||
buffer.reserve(segment_text.len());
|
||||
for (i, ch) in segment_text.chars().enumerate() {
|
||||
// Ensure that each cluster's index matches the index into `infos`. This is required
|
||||
// for efficient cluster lookup within `data.rs`.
|
||||
//
|
||||
// In other words, instead of using `buffer.push_str`, which iterates `segment_text`
|
||||
// with `char_indices`, push each char individually via `.chars` with a cluster index
|
||||
// that matches its `infos` counterpart. This allows us to lookup `infos` via cluster
|
||||
// index in `data.rs`.
|
||||
buffer.add(ch, i as u32);
|
||||
}
|
||||
|
||||
buffer.set_direction(direction);
|
||||
|
||||
buffer.set_script(hb_script);
|
||||
|
||||
if let Some(lang) = language {
|
||||
buffer.set_language(lang);
|
||||
}
|
||||
|
||||
let glyph_buffer = harf_shaper.shape(
|
||||
buffer,
|
||||
ShapeOptions::new()
|
||||
.plan(Some(shaper_plan))
|
||||
.features(&scx.features)
|
||||
.point_size(Some(item.size)),
|
||||
);
|
||||
|
||||
// Extract relevant CharInfo slice for this segment
|
||||
let char_start = char_range.start + item_text[..segment_start_offset].chars().count();
|
||||
let segment_char_start = char_start - char_range.start;
|
||||
let segment_char_count = segment_text.chars().count();
|
||||
let segment_infos =
|
||||
&item_infos[segment_char_start..(segment_char_start + segment_char_count)];
|
||||
|
||||
// Push harfrust-shaped run for the entire segment
|
||||
layout.data.push_run(
|
||||
FontData::new(font.font.blob.clone(), font.font.index),
|
||||
item.size,
|
||||
font.attrs,
|
||||
font.font.synthesis,
|
||||
&glyph_buffer,
|
||||
item.level,
|
||||
item.style_index,
|
||||
item.word_spacing,
|
||||
item.letter_spacing,
|
||||
segment_text,
|
||||
segment_infos,
|
||||
(text_range.start + segment_start_offset)..(text_range.start + segment_end_offset),
|
||||
harf_shaper.coords(),
|
||||
);
|
||||
|
||||
// Replace buffer to reuse allocation in next iteration.
|
||||
scx.unicode_buffer = Some(glyph_buffer.clear());
|
||||
}
|
||||
}
|
||||
|
||||
fn real_script(script: Script) -> bool {
|
||||
script != Script::Common && script != Script::Unknown && script != Script::Inherited
|
||||
}
|
||||
|
||||
fn variations_iter<'a>(
|
||||
synthesis: &'a fontique::Synthesis,
|
||||
item: Option<&'a [FontVariation]>,
|
||||
) -> impl Iterator<Item = harfrust::Variation> + 'a {
|
||||
synthesis
|
||||
.variation_settings()
|
||||
.iter()
|
||||
.map(|(tag, value)| harfrust::Variation {
|
||||
tag: *tag,
|
||||
value: *value,
|
||||
})
|
||||
.chain(
|
||||
item.unwrap_or(&[])
|
||||
.iter()
|
||||
.map(|variation| harfrust::Variation {
|
||||
tag: harfrust::Tag::new(&variation.tag.to_bytes()),
|
||||
value: variation.value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
struct FontSelector<'a, 'b, B: Brush> {
|
||||
query: &'b mut Query<'a>,
|
||||
fonts_id: Option<usize>,
|
||||
rcx: &'a ResolveContext,
|
||||
styles: &'a [ResolvedStyle<B>],
|
||||
style_index: u16,
|
||||
attrs: fontique::Attributes,
|
||||
variations: &'a [FontVariation],
|
||||
features: &'a [FontFeature],
|
||||
}
|
||||
|
||||
impl<'a, 'b, B: Brush> FontSelector<'a, 'b, B> {
|
||||
fn new(
|
||||
query: &'b mut Query<'a>,
|
||||
rcx: &'a ResolveContext,
|
||||
styles: &'a [ResolvedStyle<B>],
|
||||
style_index: u16,
|
||||
fb_script: fontique::Script,
|
||||
locale: Option<Language>,
|
||||
) -> Self {
|
||||
let style = &styles[style_index as usize];
|
||||
let fonts_id = style.font_family.id();
|
||||
let fonts = rcx.stack(style.font_family).unwrap_or(&[]);
|
||||
let attrs = fontique::Attributes {
|
||||
width: style.font_width,
|
||||
weight: style.font_weight,
|
||||
style: style.font_style,
|
||||
};
|
||||
let variations = rcx.variations(style.font_variations).unwrap_or(&[]);
|
||||
let features = rcx.features(style.font_features).unwrap_or(&[]);
|
||||
query.set_families(fonts.iter().copied());
|
||||
|
||||
query.set_fallbacks(fontique::FallbackKey::new(fb_script, locale.as_ref()));
|
||||
query.set_attributes(attrs);
|
||||
|
||||
Self {
|
||||
query,
|
||||
fonts_id: Some(fonts_id),
|
||||
rcx,
|
||||
styles,
|
||||
style_index,
|
||||
attrs,
|
||||
variations,
|
||||
features,
|
||||
}
|
||||
}
|
||||
|
||||
fn select_font(
|
||||
&mut self,
|
||||
cluster: &mut CharCluster,
|
||||
analysis_data_sources: &AnalysisDataSources,
|
||||
) -> Option<SelectedFont> {
|
||||
let style_index = cluster.style_index();
|
||||
let is_emoji = cluster.is_emoji;
|
||||
if style_index != self.style_index || is_emoji || self.fonts_id.is_none() {
|
||||
self.style_index = style_index;
|
||||
let style = &self.styles[style_index as usize];
|
||||
|
||||
let fonts_id = style.font_family.id();
|
||||
let fonts = self.rcx.stack(style.font_family).unwrap_or(&[]);
|
||||
let fonts = fonts.iter().copied().map(QueryFamily::Id);
|
||||
if is_emoji {
|
||||
use core::iter::once;
|
||||
let emoji_family = QueryFamily::Generic(fontique::GenericFamily::Emoji);
|
||||
self.query.set_families(fonts.chain(once(emoji_family)));
|
||||
self.fonts_id = None;
|
||||
} else if self.fonts_id != Some(fonts_id) {
|
||||
self.query.set_families(fonts);
|
||||
self.fonts_id = Some(fonts_id);
|
||||
}
|
||||
|
||||
let attrs = fontique::Attributes {
|
||||
width: style.font_width,
|
||||
weight: style.font_weight,
|
||||
style: style.font_style,
|
||||
};
|
||||
if self.attrs != attrs {
|
||||
self.query.set_attributes(attrs);
|
||||
self.attrs = attrs;
|
||||
}
|
||||
self.variations = self.rcx.variations(style.font_variations).unwrap_or(&[]);
|
||||
self.features = self.rcx.features(style.font_features).unwrap_or(&[]);
|
||||
}
|
||||
let mut selected_font = None;
|
||||
self.query.matches_with(|font| {
|
||||
let Some(charmap) = font.charmap() else {
|
||||
return fontique::QueryStatus::Continue;
|
||||
};
|
||||
|
||||
let map_status = cluster.map(
|
||||
|ch| {
|
||||
charmap
|
||||
.map(ch)
|
||||
.map(|g| {
|
||||
// HACK: in reality, we're only computing coverage, so
|
||||
// we only care about whether the font has a mapping
|
||||
// for a particular glyph. Any non-zero value indicates
|
||||
// the existence of a glyph so we can simplify this
|
||||
// without a fallible conversion from u32 to u16.
|
||||
(g != 0) as u16
|
||||
})
|
||||
.unwrap_or_default()
|
||||
},
|
||||
analysis_data_sources,
|
||||
);
|
||||
|
||||
match map_status {
|
||||
Status::Complete => {
|
||||
selected_font = Some(SelectedFont {
|
||||
font: font.clone(),
|
||||
attrs: self.attrs,
|
||||
});
|
||||
fontique::QueryStatus::Stop
|
||||
}
|
||||
Status::Keep => {
|
||||
selected_font = Some(SelectedFont {
|
||||
font: font.clone(),
|
||||
attrs: self.attrs,
|
||||
});
|
||||
fontique::QueryStatus::Continue
|
||||
}
|
||||
Status::Discard => {
|
||||
if selected_font.is_none() {
|
||||
selected_font = Some(SelectedFont {
|
||||
font: font.clone(),
|
||||
attrs: self.attrs,
|
||||
});
|
||||
}
|
||||
fontique::QueryStatus::Continue
|
||||
}
|
||||
}
|
||||
});
|
||||
selected_font
|
||||
}
|
||||
}
|
||||
|
||||
struct SelectedFont {
|
||||
font: QueryFont,
|
||||
attrs: fontique::Attributes,
|
||||
}
|
||||
|
||||
impl PartialEq for SelectedFont {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.font.family == other.font.family && self.font.synthesis == other.font.synthesis
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Copyright 2021 the Parley Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
/// Trait for types that represent the color of glyphs or decorations.
|
||||
pub trait Brush: Clone + PartialEq + Default + core::fmt::Debug {}
|
||||
|
||||
impl<T: Clone + PartialEq + Default + core::fmt::Debug> Brush for T {}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
// Copyright 2021 the Parley Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
use alloc::borrow::Cow;
|
||||
|
||||
pub use crate::setting::{FontFeature, FontVariation};
|
||||
pub use fontique::{FontStyle, FontWeight, FontWidth, GenericFamily};
|
||||
pub use parlance::{FontFamily, FontFamilyName};
|
||||
|
||||
/// Font variation settings that can be supplied as a raw source string or a parsed slice.
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum FontVariations<'a> {
|
||||
/// Setting source in CSS format.
|
||||
Source(Cow<'a, str>),
|
||||
/// List of settings.
|
||||
List(Cow<'a, [FontVariation]>),
|
||||
}
|
||||
|
||||
impl<'a> FontVariations<'a> {
|
||||
/// Creates an empty list of font variations.
|
||||
pub const fn empty() -> Self {
|
||||
Self::List(Cow::Borrowed(&[]))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for FontVariations<'a> {
|
||||
fn from(value: &'a str) -> Self {
|
||||
Self::Source(Cow::Borrowed(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [FontVariation]> for FontVariations<'a> {
|
||||
fn from(value: &'a [FontVariation]) -> Self {
|
||||
Self::List(Cow::Borrowed(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, const N: usize> From<&'a [FontVariation; N]> for FontVariations<'a> {
|
||||
fn from(value: &'a [FontVariation; N]) -> Self {
|
||||
Self::List(Cow::Borrowed(&value[..]))
|
||||
}
|
||||
}
|
||||
|
||||
/// Font feature settings that can be supplied as a raw source string or a parsed slice.
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum FontFeatures<'a> {
|
||||
/// Setting source in CSS format.
|
||||
Source(Cow<'a, str>),
|
||||
/// List of settings.
|
||||
List(Cow<'a, [FontFeature]>),
|
||||
}
|
||||
|
||||
impl<'a> FontFeatures<'a> {
|
||||
/// Creates an empty list of font features.
|
||||
pub const fn empty() -> Self {
|
||||
Self::List(Cow::Borrowed(&[]))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for FontFeatures<'a> {
|
||||
fn from(value: &'a str) -> Self {
|
||||
Self::Source(Cow::Borrowed(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [FontFeature]> for FontFeatures<'a> {
|
||||
fn from(value: &'a [FontFeature]) -> Self {
|
||||
Self::List(Cow::Borrowed(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, const N: usize> From<&'a [FontFeature; N]> for FontFeatures<'a> {
|
||||
fn from(value: &'a [FontFeature; N]) -> Self {
|
||||
Self::List(Cow::Borrowed(&value[..]))
|
||||
}
|
||||
}
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
// Copyright 2021 the Parley Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Rich styling support.
|
||||
|
||||
mod brush;
|
||||
mod font;
|
||||
mod styleset;
|
||||
|
||||
use alloc::borrow::Cow;
|
||||
|
||||
pub use brush::*;
|
||||
pub use font::{
|
||||
FontFamily, FontFamilyName, FontFeature, FontFeatures, FontStyle, FontVariation,
|
||||
FontVariations, FontWeight, FontWidth, GenericFamily,
|
||||
};
|
||||
pub use fontique::Language;
|
||||
pub use parlance::{OverflowWrap, TextWrapMode, WordBreak};
|
||||
pub use styleset::StyleSet;
|
||||
|
||||
use crate::util::nearly_eq;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum WhiteSpaceCollapse {
|
||||
Collapse,
|
||||
Preserve,
|
||||
}
|
||||
|
||||
/// The height that this text takes up. The default is `MetricsRelative(1.0)`, which is the given
|
||||
/// font's preferred line height.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum LineHeight {
|
||||
/// The line's height is a multiple of the "line height" defined by the font's metrics--the sum
|
||||
/// of the ascender height, descender height, and line gap / leading.
|
||||
MetricsRelative(f32),
|
||||
/// Line height specified as a multiple of the font size. This is how the CSS `line-height`
|
||||
/// property behaves if given a unitless number. Useful if you're using system-defined generic
|
||||
/// font families and want the line heights to be consistent across platforms.
|
||||
FontSizeRelative(f32),
|
||||
/// Line height specified in absolute units. This can be useful for ensuring all lines are
|
||||
/// spaced a whole number of pixels apart, or fitting lines into a given layout container
|
||||
/// height.
|
||||
Absolute(f32),
|
||||
}
|
||||
|
||||
impl Default for LineHeight {
|
||||
fn default() -> Self {
|
||||
Self::MetricsRelative(1.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl LineHeight {
|
||||
pub(crate) fn nearly_eq(self, other: Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::MetricsRelative(a), Self::MetricsRelative(b))
|
||||
| (Self::FontSizeRelative(a), Self::FontSizeRelative(b))
|
||||
| (Self::Absolute(a), Self::Absolute(b)) => nearly_eq(a, b),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn scale(self, scale: f32) -> Self {
|
||||
match self {
|
||||
Self::Absolute(value) => Self::Absolute(value * scale),
|
||||
// The other variants are relative to the font size, so scaling here needn't do anything
|
||||
value => value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Properties that define a style.
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum StyleProperty<'a, B: Brush> {
|
||||
/// CSS `font-family` property value.
|
||||
FontFamily(FontFamily<'a>),
|
||||
/// Font size.
|
||||
FontSize(f32),
|
||||
/// Font width.
|
||||
FontWidth(FontWidth),
|
||||
/// Font style.
|
||||
FontStyle(FontStyle),
|
||||
/// Font weight.
|
||||
FontWeight(FontWeight),
|
||||
/// Font variation settings.
|
||||
FontVariations(FontVariations<'a>),
|
||||
/// Font feature settings.
|
||||
FontFeatures(FontFeatures<'a>),
|
||||
/// Locale.
|
||||
Locale(Option<Language>),
|
||||
/// Brush for rendering text.
|
||||
Brush(B),
|
||||
/// Underline decoration.
|
||||
Underline(bool),
|
||||
/// Offset of the underline decoration.
|
||||
UnderlineOffset(Option<f32>),
|
||||
/// Size of the underline decoration.
|
||||
UnderlineSize(Option<f32>),
|
||||
/// Brush for rendering the underline decoration.
|
||||
UnderlineBrush(Option<B>),
|
||||
/// Strikethrough decoration.
|
||||
Strikethrough(bool),
|
||||
/// Offset of the strikethrough decoration.
|
||||
StrikethroughOffset(Option<f32>),
|
||||
/// Size of the strikethrough decoration.
|
||||
StrikethroughSize(Option<f32>),
|
||||
/// Brush for rendering the strikethrough decoration.
|
||||
StrikethroughBrush(Option<B>),
|
||||
/// Line height.
|
||||
LineHeight(LineHeight),
|
||||
/// Extra spacing between words.
|
||||
WordSpacing(f32),
|
||||
/// Extra spacing between letters.
|
||||
LetterSpacing(f32),
|
||||
/// Control over where words can wrap.
|
||||
WordBreak(WordBreak),
|
||||
/// Control over "emergency" line-breaking.
|
||||
OverflowWrap(OverflowWrap),
|
||||
/// Control over non-"emergency" line-breaking.
|
||||
TextWrapMode(TextWrapMode),
|
||||
}
|
||||
|
||||
/// Unresolved styles.
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct TextStyle<'family, 'settings, B: Brush> {
|
||||
/// CSS `font-family` property value.
|
||||
pub font_family: FontFamily<'family>,
|
||||
/// Font size.
|
||||
pub font_size: f32,
|
||||
/// Font width.
|
||||
pub font_width: FontWidth,
|
||||
/// Font style.
|
||||
pub font_style: FontStyle,
|
||||
/// Font weight.
|
||||
pub font_weight: FontWeight,
|
||||
/// Font variation settings.
|
||||
pub font_variations: FontVariations<'settings>,
|
||||
/// Font feature settings.
|
||||
pub font_features: FontFeatures<'settings>,
|
||||
/// Locale.
|
||||
pub locale: Option<Language>,
|
||||
/// Brush for rendering text.
|
||||
pub brush: B,
|
||||
/// Underline decoration.
|
||||
pub has_underline: bool,
|
||||
/// Offset of the underline decoration.
|
||||
pub underline_offset: Option<f32>,
|
||||
/// Size of the underline decoration.
|
||||
pub underline_size: Option<f32>,
|
||||
/// Brush for rendering the underline decoration.
|
||||
pub underline_brush: Option<B>,
|
||||
/// Strikethrough decoration.
|
||||
pub has_strikethrough: bool,
|
||||
/// Offset of the strikethrough decoration.
|
||||
pub strikethrough_offset: Option<f32>,
|
||||
/// Size of the strikethrough decoration.
|
||||
pub strikethrough_size: Option<f32>,
|
||||
/// Brush for rendering the strikethrough decoration.
|
||||
pub strikethrough_brush: Option<B>,
|
||||
/// Line height.
|
||||
pub line_height: LineHeight,
|
||||
/// Extra spacing between words.
|
||||
pub word_spacing: f32,
|
||||
/// Extra spacing between letters.
|
||||
pub letter_spacing: f32,
|
||||
/// Control over where words can wrap.
|
||||
pub word_break: WordBreak,
|
||||
/// Control over "emergency" line-breaking.
|
||||
pub overflow_wrap: OverflowWrap,
|
||||
/// Control over non-"emergency" line-breaking.
|
||||
pub text_wrap_mode: TextWrapMode,
|
||||
}
|
||||
|
||||
impl<B: Brush> Default for TextStyle<'static, 'static, B> {
|
||||
fn default() -> Self {
|
||||
TextStyle {
|
||||
font_family: FontFamily::Source(Cow::Borrowed("sans-serif")),
|
||||
font_size: 16.0,
|
||||
font_width: FontWidth::default(),
|
||||
font_style: FontStyle::default(),
|
||||
font_weight: FontWeight::default(),
|
||||
font_variations: FontVariations::empty(),
|
||||
font_features: FontFeatures::empty(),
|
||||
locale: None,
|
||||
brush: B::default(),
|
||||
has_underline: false,
|
||||
underline_offset: None,
|
||||
underline_size: None,
|
||||
underline_brush: None,
|
||||
has_strikethrough: false,
|
||||
strikethrough_offset: None,
|
||||
strikethrough_size: None,
|
||||
strikethrough_brush: None,
|
||||
line_height: LineHeight::default(),
|
||||
word_spacing: 0.0,
|
||||
letter_spacing: 0.0,
|
||||
word_break: WordBreak::default(),
|
||||
overflow_wrap: OverflowWrap::default(),
|
||||
text_wrap_mode: TextWrapMode::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, B: Brush> From<FontFamily<'a>> for StyleProperty<'a, B> {
|
||||
fn from(value: FontFamily<'a>) -> Self {
|
||||
StyleProperty::FontFamily(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, B: Brush> From<&'a [FontFamilyName<'a>]> for StyleProperty<'a, B> {
|
||||
fn from(value: &'a [FontFamilyName<'a>]) -> Self {
|
||||
StyleProperty::FontFamily(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, B: Brush> From<FontFamilyName<'a>> for StyleProperty<'a, B> {
|
||||
fn from(value: FontFamilyName<'a>) -> Self {
|
||||
StyleProperty::FontFamily(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, B: Brush> From<FontVariations<'a>> for StyleProperty<'a, B> {
|
||||
fn from(value: FontVariations<'a>) -> Self {
|
||||
StyleProperty::FontVariations(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, B: Brush> From<FontFeatures<'a>> for StyleProperty<'a, B> {
|
||||
fn from(value: FontFeatures<'a>) -> Self {
|
||||
StyleProperty::FontFeatures(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: Brush> From<GenericFamily> for StyleProperty<'_, B> {
|
||||
fn from(f: GenericFamily) -> Self {
|
||||
StyleProperty::FontFamily(f.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: Brush> From<LineHeight> for StyleProperty<'_, B> {
|
||||
fn from(value: LineHeight) -> Self {
|
||||
StyleProperty::LineHeight(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: Brush> From<FontWidth> for StyleProperty<'_, B> {
|
||||
fn from(value: FontWidth) -> Self {
|
||||
StyleProperty::FontWidth(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: Brush> From<FontStyle> for StyleProperty<'_, B> {
|
||||
fn from(value: FontStyle) -> Self {
|
||||
StyleProperty::FontStyle(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: Brush> From<FontWeight> for StyleProperty<'_, B> {
|
||||
fn from(value: FontWeight) -> Self {
|
||||
StyleProperty::FontWeight(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: Brush> From<WordBreak> for StyleProperty<'_, B> {
|
||||
fn from(value: WordBreak) -> Self {
|
||||
StyleProperty::WordBreak(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: Brush> From<OverflowWrap> for StyleProperty<'_, B> {
|
||||
fn from(value: OverflowWrap) -> Self {
|
||||
StyleProperty::OverflowWrap(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: Brush> From<TextWrapMode> for StyleProperty<'_, B> {
|
||||
fn from(value: TextWrapMode) -> Self {
|
||||
StyleProperty::TextWrapMode(value)
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
// Copyright 2024 the Parley Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
use core::mem::Discriminant;
|
||||
use hashbrown::HashMap;
|
||||
|
||||
type StyleProperty<Brush> = crate::StyleProperty<'static, Brush>;
|
||||
|
||||
/// A long-lived collection of [`StyleProperties`](super::StyleProperty), containing at
|
||||
/// most one of each property.
|
||||
///
|
||||
/// This is used by [`PlainEditor`](crate::editing::PlainEditor) to provide a reasonably ergonomic
|
||||
/// mutable API for styles applied to all text managed by it.
|
||||
/// This can be accessed using [`PlainEditor::edit_styles`](crate::editing::PlainEditor::edit_styles).
|
||||
///
|
||||
/// These styles do not have a corresponding range, and are generally unsuited for rich text.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct StyleSet<Brush: crate::Brush>(
|
||||
HashMap<Discriminant<StyleProperty<Brush>>, StyleProperty<Brush>>,
|
||||
);
|
||||
|
||||
impl<Brush: crate::Brush> StyleSet<Brush> {
|
||||
/// Create a new collection of styles.
|
||||
///
|
||||
/// The font size will be `font_size`, and can be overwritten at runtime by
|
||||
/// [inserting](Self::insert) a new [`FontSize`](crate::StyleProperty::FontSize).
|
||||
pub fn new(font_size: f32) -> Self {
|
||||
let mut this = Self(HashMap::default());
|
||||
this.insert(StyleProperty::FontSize(font_size));
|
||||
this
|
||||
}
|
||||
|
||||
/// Add `style` to this collection, returning any overwritten value.
|
||||
///
|
||||
/// Note: Adding a [`font-family`](crate::StyleProperty::FontFamily) to this collection is not
|
||||
/// additive, and instead overwrites any previously added font stack.
|
||||
pub fn insert(&mut self, style: StyleProperty<Brush>) -> Option<StyleProperty<Brush>> {
|
||||
let discriminant = core::mem::discriminant(&style);
|
||||
self.0.insert(discriminant, style)
|
||||
}
|
||||
|
||||
/// [Retain](alloc::vec::Vec::retain) only the styles for which `f` returns true.
|
||||
///
|
||||
/// Styles which are removed return to their default values.
|
||||
///
|
||||
/// Removing the [font size](crate::StyleProperty::FontSize) is not recommended, as an unspecified
|
||||
/// fallback font size will be used.
|
||||
pub fn retain(&mut self, mut f: impl FnMut(&StyleProperty<Brush>) -> bool) {
|
||||
self.0.retain(|_, v| f(v));
|
||||
}
|
||||
|
||||
/// Remove the style with the discriminant `property`.
|
||||
///
|
||||
/// Styles which are removed return to their default values.
|
||||
///
|
||||
/// To get the discriminant requires constructing a valid `StyleProperty` for the
|
||||
/// the desired property and passing it to [`core::mem::discriminant`].
|
||||
/// Getting this discriminant is usually possible in a `const` context.
|
||||
///
|
||||
/// Removing the [font size](crate::StyleProperty::FontSize) is not recommended, as an unspecified
|
||||
/// fallback font size will be used.
|
||||
pub fn remove(
|
||||
&mut self,
|
||||
property: Discriminant<StyleProperty<Brush>>,
|
||||
) -> Option<StyleProperty<Brush>> {
|
||||
self.0.remove(&property)
|
||||
}
|
||||
|
||||
/// Read the raw underlying storage of this.
|
||||
///
|
||||
/// Write access is not provided due to the invariant that keys
|
||||
/// are the discriminant of their corresponding value.
|
||||
pub fn inner(&self) -> &HashMap<Discriminant<StyleProperty<Brush>>, StyleProperty<Brush>> {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Clear all styles.
|
||||
///
|
||||
/// All styles return to their default values.
|
||||
pub fn clear(&mut self) {
|
||||
self.0.clear();
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
// Copyright 2024 the Parley Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
mod test_analysis;
|
||||
mod test_builders;
|
||||
mod utils;
|
||||
+1258
File diff suppressed because it is too large
Load Diff
+546
@@ -0,0 +1,546 @@
|
||||
// Copyright 2025 the Parley Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Test that the various builders produce the same results.
|
||||
|
||||
use std::{borrow::Cow, path::PathBuf, sync::Arc};
|
||||
|
||||
use fontique::{Collection, CollectionOptions, FontStyle, FontWeight, FontWidth, SourceCache};
|
||||
use parlance::FontFamilyName;
|
||||
use peniko::{Blob, color::palette};
|
||||
|
||||
use super::utils::{ColorBrush, asserts::assert_eq_layout_data};
|
||||
use crate::{
|
||||
FontContext, FontFamily, FontFeatures, FontVariations, Layout, LayoutContext, LineHeight,
|
||||
OverflowWrap, RangedBuilder, StyleProperty, StyleRunBuilder, TextStyle, TextWrapMode,
|
||||
TreeBuilder, WordBreak,
|
||||
};
|
||||
|
||||
// TODO: `FONT_FAMILY_LIST`, `load_fonts`, and `create_font_context` are
|
||||
// duplicated between this crate and `parley_test`. We can't move the builder
|
||||
// tests into `parley_test` because they use private APIs, but should eventually
|
||||
// figure out some way to reduce the duplication.
|
||||
const FONT_FAMILY_LIST: &[FontFamilyName<'_>] = &[
|
||||
FontFamilyName::Named(Cow::Borrowed("Roboto")),
|
||||
FontFamilyName::Named(Cow::Borrowed("Noto Kufi Arabic")),
|
||||
];
|
||||
|
||||
pub(crate) fn load_fonts(
|
||||
collection: &mut Collection,
|
||||
font_dirs: impl Iterator<Item = PathBuf>,
|
||||
) -> std::io::Result<()> {
|
||||
for dir in font_dirs {
|
||||
let paths = std::fs::read_dir(dir)?;
|
||||
for entry in paths {
|
||||
let entry = entry?;
|
||||
if !entry.metadata()?.is_file() {
|
||||
continue;
|
||||
}
|
||||
let path = entry.path();
|
||||
if path
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.is_none_or(|ext| !["ttf", "otf", "ttc", "otc"].contains(&ext))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let font_data = std::fs::read(&path)?;
|
||||
collection.register_fonts(Blob::new(Arc::new(font_data)), None);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_font_context() -> FontContext {
|
||||
let mut collection = Collection::new(CollectionOptions {
|
||||
shared: false,
|
||||
system_fonts: false,
|
||||
});
|
||||
load_fonts(&mut collection, parley_dev::font_dirs()).unwrap();
|
||||
for font in FONT_FAMILY_LIST {
|
||||
if let FontFamilyName::Named(font_name) = font {
|
||||
collection
|
||||
.family_id(font_name)
|
||||
.unwrap_or_else(|| panic!("{font_name} font not found"));
|
||||
}
|
||||
}
|
||||
FontContext {
|
||||
collection,
|
||||
source_cache: SourceCache::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set of options for [`build_layout_with_ranged`].
|
||||
struct RangedOptions<'a> {
|
||||
scale: f32,
|
||||
quantize: bool,
|
||||
max_advance: Option<f32>,
|
||||
text: &'a str,
|
||||
}
|
||||
|
||||
/// Set of options for [`build_layout_with_tree`].
|
||||
struct TreeOptions<'a, 'b> {
|
||||
scale: f32,
|
||||
quantize: bool,
|
||||
max_advance: Option<f32>,
|
||||
root_style: &'a TextStyle<'b, 'b, ColorBrush>,
|
||||
}
|
||||
|
||||
/// Generates a `Layout` with a ranged builder.
|
||||
fn build_layout_with_ranged(
|
||||
fcx: &mut FontContext,
|
||||
lcx: &mut LayoutContext<ColorBrush>,
|
||||
opts: &RangedOptions<'_>,
|
||||
with_builder: impl Fn(&mut RangedBuilder<'_, ColorBrush>),
|
||||
) -> Layout<ColorBrush> {
|
||||
let mut rb = lcx.ranged_builder(fcx, opts.text, opts.scale, opts.quantize);
|
||||
with_builder(&mut rb);
|
||||
let mut layout = rb.build(opts.text);
|
||||
layout.break_all_lines(opts.max_advance);
|
||||
layout
|
||||
}
|
||||
|
||||
/// Generates a `Layout` with a tree builder.
|
||||
fn build_layout_with_tree(
|
||||
fcx: &mut FontContext,
|
||||
lcx: &mut LayoutContext<ColorBrush>,
|
||||
opts: &TreeOptions<'_, '_>,
|
||||
with_builder: impl Fn(&mut TreeBuilder<'_, ColorBrush>),
|
||||
) -> Layout<ColorBrush> {
|
||||
let mut tb = lcx.tree_builder(fcx, opts.scale, opts.quantize, opts.root_style);
|
||||
with_builder(&mut tb);
|
||||
let (mut layout, _) = tb.build();
|
||||
layout.break_all_lines(opts.max_advance);
|
||||
layout
|
||||
}
|
||||
|
||||
/// Generates a `Layout` with a style run builder.
|
||||
fn build_layout_with_style_runs(
|
||||
fcx: &mut FontContext,
|
||||
lcx: &mut LayoutContext<ColorBrush>,
|
||||
opts: &RangedOptions<'_>,
|
||||
with_builder: impl Fn(&mut StyleRunBuilder<'_, ColorBrush>),
|
||||
) -> Layout<ColorBrush> {
|
||||
let mut rb = lcx.style_run_builder(fcx, opts.text, opts.scale, opts.quantize);
|
||||
with_builder(&mut rb);
|
||||
let mut layout = rb.build(opts.text);
|
||||
layout.break_all_lines(opts.max_advance);
|
||||
layout
|
||||
}
|
||||
|
||||
/// Computes layout in various ways to ensure they all produce the same result.
|
||||
///
|
||||
/// ```text
|
||||
/// LayoutContext A - Ranged
|
||||
/// LayoutContext A - Ranged for idempotency
|
||||
///
|
||||
/// LayoutContext B - Tree
|
||||
/// LayoutContext B - Tree for idempotency
|
||||
///
|
||||
/// LayoutContext C - Ranged for dirt
|
||||
/// LayoutContext C - Tree from dirty
|
||||
///
|
||||
/// LayoutContext D - Tree for dirt
|
||||
/// LayoutContext D - Ranged from dirty
|
||||
/// ```
|
||||
fn assert_builders_produce_same_result<'b>(
|
||||
text: &str,
|
||||
scale: f32,
|
||||
quantize: bool,
|
||||
max_advance: Option<f32>,
|
||||
root_style: &TextStyle<'b, 'b, ColorBrush>,
|
||||
with_ranged_builder: impl Fn(&mut RangedBuilder<'_, ColorBrush>),
|
||||
with_tree_builder: impl Fn(&mut TreeBuilder<'_, ColorBrush>),
|
||||
expect_empty: bool,
|
||||
) {
|
||||
let mut fcx = create_font_context();
|
||||
|
||||
let mut lcx_a: LayoutContext<ColorBrush> = LayoutContext::new();
|
||||
let mut lcx_b: LayoutContext<ColorBrush> = LayoutContext::new();
|
||||
let mut lcx_c: LayoutContext<ColorBrush> = LayoutContext::new();
|
||||
let mut lcx_d: LayoutContext<ColorBrush> = LayoutContext::new();
|
||||
|
||||
let ropts = RangedOptions {
|
||||
scale,
|
||||
quantize,
|
||||
max_advance,
|
||||
text,
|
||||
};
|
||||
let topts = TreeOptions {
|
||||
scale,
|
||||
quantize,
|
||||
max_advance,
|
||||
root_style,
|
||||
};
|
||||
|
||||
// Source of truth - ranged builder from a clean layout context
|
||||
let layout_truth = build_layout_with_ranged(&mut fcx, &mut lcx_a, &ropts, &with_ranged_builder);
|
||||
assert!(
|
||||
layout_truth.data.runs.is_empty() == expect_empty,
|
||||
"expected runs to exist for lcx_a_rb_one"
|
||||
);
|
||||
|
||||
// Testing idempotence of ranged builder creation
|
||||
let layout = build_layout_with_ranged(&mut fcx, &mut lcx_a, &ropts, &with_ranged_builder);
|
||||
assert!(
|
||||
layout.data.runs.is_empty() == expect_empty,
|
||||
"expected runs to exist for lcx_a_rb_two"
|
||||
);
|
||||
assert_eq_layout_data(&layout_truth.data, &layout.data, "lcx_a_rb_two");
|
||||
|
||||
// Basic builder compatibility - tree builder from a clean layout context
|
||||
let layout = build_layout_with_tree(&mut fcx, &mut lcx_b, &topts, &with_tree_builder);
|
||||
assert!(
|
||||
layout.data.runs.is_empty() == expect_empty,
|
||||
"expected runs to exist for lcx_b_tb_one"
|
||||
);
|
||||
assert_eq_layout_data(&layout_truth.data, &layout.data, "lcx_b_tb_one");
|
||||
|
||||
// Testing idempotence of tree builder creation
|
||||
let layout = build_layout_with_tree(&mut fcx, &mut lcx_b, &topts, &with_tree_builder);
|
||||
assert!(
|
||||
layout.data.runs.is_empty() == expect_empty,
|
||||
"expected runs to exist for lcx_b_tb_two"
|
||||
);
|
||||
assert_eq_layout_data(&layout_truth.data, &layout.data, "lcx_b_tb_two");
|
||||
|
||||
// Priming a fresh layout context with ranged builder creation
|
||||
let _ = build_layout_with_ranged(&mut fcx, &mut lcx_c, &ropts, &with_ranged_builder);
|
||||
|
||||
// Testing tree builder creation with a dirty layout context
|
||||
let layout = build_layout_with_tree(&mut fcx, &mut lcx_c, &topts, &with_tree_builder);
|
||||
assert!(
|
||||
layout.data.runs.is_empty() == expect_empty,
|
||||
"expected runs to exist for lcx_c_tb_one"
|
||||
);
|
||||
assert_eq_layout_data(&layout_truth.data, &layout.data, "lcx_c_tb_one");
|
||||
|
||||
// Priming a fresh layout context with tree builder creation
|
||||
let _ = build_layout_with_tree(&mut fcx, &mut lcx_d, &topts, &with_tree_builder);
|
||||
|
||||
// Testing ranged builder creation with a dirty layout context
|
||||
let layout = build_layout_with_ranged(&mut fcx, &mut lcx_d, &ropts, &with_ranged_builder);
|
||||
assert!(
|
||||
layout.data.runs.is_empty() == expect_empty,
|
||||
"expected runs to exist for lcx_d_rb_one"
|
||||
);
|
||||
assert_eq_layout_data(&layout_truth.data, &layout.data, "lcx_d_rb_one");
|
||||
}
|
||||
|
||||
/// Returns a root style that uses non-default values.
|
||||
///
|
||||
/// The [`TreeBuilder`] version of [`set_root_style`].
|
||||
fn create_root_style() -> TextStyle<'static, 'static, ColorBrush> {
|
||||
TextStyle {
|
||||
font_family: FontFamily::from(FONT_FAMILY_LIST),
|
||||
font_size: 20.,
|
||||
font_width: FontWidth::CONDENSED,
|
||||
font_style: FontStyle::Italic,
|
||||
font_weight: FontWeight::BOLD,
|
||||
font_variations: FontVariations::empty(), // TODO: Set a non-default value
|
||||
font_features: FontFeatures::empty(), // TODO: Set a non-default value
|
||||
locale: Some("en-US".parse().unwrap()),
|
||||
brush: ColorBrush::new(palette::css::GREEN),
|
||||
has_underline: true,
|
||||
underline_offset: Some(2.),
|
||||
underline_size: Some(3.5),
|
||||
underline_brush: Some(ColorBrush::new(palette::css::CYAN)),
|
||||
has_strikethrough: true,
|
||||
strikethrough_offset: Some(1.3),
|
||||
strikethrough_size: Some(1.7),
|
||||
strikethrough_brush: Some(ColorBrush::new(palette::css::BEIGE)),
|
||||
line_height: LineHeight::Absolute(30.),
|
||||
word_spacing: 2.,
|
||||
letter_spacing: 1.5,
|
||||
word_break: WordBreak::BreakAll,
|
||||
overflow_wrap: OverflowWrap::Anywhere,
|
||||
text_wrap_mode: TextWrapMode::Wrap,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a root style with non-default values.
|
||||
///
|
||||
/// The [`RangedBuilder`] version of [`create_root_style`].
|
||||
fn set_root_style(rb: &mut RangedBuilder<'_, ColorBrush>) {
|
||||
rb.push_default(FontFamily::from(FONT_FAMILY_LIST));
|
||||
rb.push_default(StyleProperty::FontSize(20.));
|
||||
rb.push_default(StyleProperty::FontWidth(FontWidth::CONDENSED));
|
||||
rb.push_default(StyleProperty::FontStyle(FontStyle::Italic));
|
||||
rb.push_default(StyleProperty::FontWeight(FontWeight::BOLD));
|
||||
rb.push_default(FontVariations::empty());
|
||||
rb.push_default(FontFeatures::empty());
|
||||
rb.push_default(StyleProperty::Locale(Some("en-US".parse().unwrap())));
|
||||
rb.push_default(StyleProperty::Brush(ColorBrush::new(palette::css::GREEN)));
|
||||
rb.push_default(StyleProperty::Underline(true));
|
||||
rb.push_default(StyleProperty::UnderlineOffset(Some(2.)));
|
||||
rb.push_default(StyleProperty::UnderlineSize(Some(3.5)));
|
||||
rb.push_default(StyleProperty::UnderlineBrush(Some(ColorBrush::new(
|
||||
palette::css::CYAN,
|
||||
))));
|
||||
rb.push_default(StyleProperty::Strikethrough(true));
|
||||
rb.push_default(StyleProperty::StrikethroughOffset(Some(1.3)));
|
||||
rb.push_default(StyleProperty::StrikethroughSize(Some(1.7)));
|
||||
rb.push_default(StyleProperty::StrikethroughBrush(Some(ColorBrush::new(
|
||||
palette::css::BEIGE,
|
||||
))));
|
||||
rb.push_default(LineHeight::Absolute(30.));
|
||||
rb.push_default(StyleProperty::WordSpacing(2.));
|
||||
rb.push_default(StyleProperty::LetterSpacing(1.5));
|
||||
rb.push_default(StyleProperty::WordBreak(WordBreak::BreakAll));
|
||||
rb.push_default(StyleProperty::OverflowWrap(OverflowWrap::Anywhere));
|
||||
}
|
||||
|
||||
/// Test that all the builders have the same default behavior.
|
||||
#[test]
|
||||
fn builders_default() {
|
||||
let text = "Builders often wear hard hats for safety while working on construction sites.";
|
||||
let scale = 2.;
|
||||
let quantize = false;
|
||||
let max_advance = Some(50.);
|
||||
let root_style = TextStyle {
|
||||
font_family: FontFamily::from(FONT_FAMILY_LIST),
|
||||
..TextStyle::default()
|
||||
};
|
||||
|
||||
let with_ranged_builder = |rb: &mut RangedBuilder<'_, ColorBrush>| {
|
||||
rb.push_default(FontFamily::from(FONT_FAMILY_LIST));
|
||||
};
|
||||
let with_tree_builder = |tb: &mut TreeBuilder<'_, ColorBrush>| {
|
||||
tb.push_text(text);
|
||||
};
|
||||
|
||||
assert_builders_produce_same_result(
|
||||
text,
|
||||
scale,
|
||||
quantize,
|
||||
max_advance,
|
||||
&root_style,
|
||||
with_ranged_builder,
|
||||
with_tree_builder,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that `StyleRunBuilder` produces the same result as `RangedBuilder` when given equivalent
|
||||
/// styles.
|
||||
#[test]
|
||||
fn builders_style_runs_match_ranged() {
|
||||
let text = "Builders often wear hard hats.";
|
||||
let scale = 2.;
|
||||
let quantize = false;
|
||||
let max_advance = Some(120.);
|
||||
|
||||
let root_style: TextStyle<'static, 'static, ColorBrush> = TextStyle {
|
||||
font_family: FontFamily::from(FONT_FAMILY_LIST),
|
||||
..TextStyle::default()
|
||||
};
|
||||
|
||||
let split = text.len() / 2;
|
||||
let mut modified_style = root_style.clone();
|
||||
modified_style.font_size = 40.;
|
||||
modified_style.letter_spacing = 1.25;
|
||||
|
||||
let mut fcx = create_font_context();
|
||||
let mut lcx_a: LayoutContext<ColorBrush> = LayoutContext::new();
|
||||
let mut lcx_b: LayoutContext<ColorBrush> = LayoutContext::new();
|
||||
|
||||
let ropts = RangedOptions {
|
||||
scale,
|
||||
quantize,
|
||||
max_advance,
|
||||
text,
|
||||
};
|
||||
|
||||
let ranged = build_layout_with_ranged(&mut fcx, &mut lcx_a, &ropts, |rb| {
|
||||
rb.push_default(FontFamily::from(FONT_FAMILY_LIST));
|
||||
rb.push(
|
||||
StyleProperty::FontSize(modified_style.font_size),
|
||||
split..text.len(),
|
||||
);
|
||||
rb.push(
|
||||
StyleProperty::LetterSpacing(modified_style.letter_spacing),
|
||||
split..text.len(),
|
||||
);
|
||||
});
|
||||
|
||||
let runs = build_layout_with_style_runs(&mut fcx, &mut lcx_b, &ropts, |rb| {
|
||||
let family: FontFamily<'static> = root_style.font_family.clone().into_owned();
|
||||
let root_run: TextStyle<'static, 'static, ColorBrush> = TextStyle {
|
||||
font_family: family.clone(),
|
||||
..root_style.clone()
|
||||
};
|
||||
|
||||
let modified_run: TextStyle<'static, 'static, ColorBrush> = TextStyle {
|
||||
font_family: family,
|
||||
..modified_style.clone()
|
||||
};
|
||||
|
||||
let root_index = rb.push_style(root_run);
|
||||
let modified_index = rb.push_style(modified_run);
|
||||
rb.push_style_run(root_index, 0..split);
|
||||
rb.push_style_run(modified_index, split..text.len());
|
||||
});
|
||||
|
||||
assert_eq_layout_data(&ranged.data, &runs.data, "style_runs_match_ranged");
|
||||
}
|
||||
|
||||
/// Test that `StyleRunBuilder` handles a first run whose style table index is not zero.
|
||||
#[test]
|
||||
fn style_runs_first_run_can_use_nonzero_style_index() {
|
||||
let text = "Builders often wear hard hats.";
|
||||
let scale = 2.;
|
||||
let quantize = false;
|
||||
let max_advance = Some(50.);
|
||||
let root_style = create_root_style();
|
||||
let mut modified_style = root_style.clone();
|
||||
modified_style.font_size = 40.;
|
||||
|
||||
let mut fcx = create_font_context();
|
||||
let mut lcx_a: LayoutContext<ColorBrush> = LayoutContext::new();
|
||||
let mut lcx_b: LayoutContext<ColorBrush> = LayoutContext::new();
|
||||
|
||||
let ropts = RangedOptions {
|
||||
scale,
|
||||
quantize,
|
||||
max_advance,
|
||||
text,
|
||||
};
|
||||
|
||||
let ranged = build_layout_with_ranged(&mut fcx, &mut lcx_a, &ropts, |rb| {
|
||||
set_root_style(rb);
|
||||
rb.push(
|
||||
StyleProperty::FontSize(modified_style.font_size),
|
||||
0..text.len(),
|
||||
);
|
||||
});
|
||||
|
||||
let runs = build_layout_with_style_runs(&mut fcx, &mut lcx_b, &ropts, |rb| {
|
||||
let family: FontFamily<'static> = root_style.font_family.clone().into_owned();
|
||||
let root_run: TextStyle<'static, 'static, ColorBrush> = TextStyle {
|
||||
font_family: family.clone(),
|
||||
..root_style.clone()
|
||||
};
|
||||
|
||||
let modified_run: TextStyle<'static, 'static, ColorBrush> = TextStyle {
|
||||
font_family: family,
|
||||
..modified_style.clone()
|
||||
};
|
||||
|
||||
let _root_index = rb.push_style(root_run);
|
||||
let modified_index = rb.push_style(modified_run);
|
||||
rb.push_style_run(modified_index, 0..text.len());
|
||||
});
|
||||
|
||||
assert_eq_layout_data(
|
||||
&ranged.data,
|
||||
&runs.data,
|
||||
"style_runs_first_run_can_use_nonzero_style_index",
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that all the builders behave the same when given the same root style.
|
||||
#[test]
|
||||
fn builders_root_only() {
|
||||
let text = "Builders often wear hard hats for safety while working on construction sites.";
|
||||
let scale = 2.;
|
||||
let quantize = false;
|
||||
let max_advance = Some(50.);
|
||||
let root_style = create_root_style();
|
||||
|
||||
let with_ranged_builder = |rb: &mut RangedBuilder<'_, ColorBrush>| {
|
||||
set_root_style(rb);
|
||||
};
|
||||
let with_tree_builder = |tb: &mut TreeBuilder<'_, ColorBrush>| {
|
||||
tb.push_text(text);
|
||||
};
|
||||
|
||||
assert_builders_produce_same_result(
|
||||
text,
|
||||
scale,
|
||||
quantize,
|
||||
max_advance,
|
||||
&root_style,
|
||||
with_ranged_builder,
|
||||
with_tree_builder,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that an empty layout doesn't crash
|
||||
#[test]
|
||||
fn builders_empty() {
|
||||
let text = "";
|
||||
let scale = 1.;
|
||||
let quantize = false;
|
||||
let max_advance = Some(50.);
|
||||
let root_style = create_root_style();
|
||||
|
||||
let with_ranged_builder = |_rb: &mut RangedBuilder<'_, ColorBrush>| {};
|
||||
let with_tree_builder = |_tb: &mut TreeBuilder<'_, ColorBrush>| {};
|
||||
|
||||
assert_builders_produce_same_result(
|
||||
text,
|
||||
scale,
|
||||
quantize,
|
||||
max_advance,
|
||||
&root_style,
|
||||
with_ranged_builder,
|
||||
with_tree_builder,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that all the builders behave the same with mixed styles.
|
||||
#[test]
|
||||
fn builders_mixed_styles() {
|
||||
let text = "Builders often wear hard hats for safety while working on construction sites.";
|
||||
let scale = 2.;
|
||||
let quantize = false;
|
||||
let max_advance = Some(50.);
|
||||
let root_style = create_root_style();
|
||||
|
||||
let with_ranged_builder = |rb: &mut RangedBuilder<'_, ColorBrush>| {
|
||||
set_root_style(rb);
|
||||
|
||||
// Make the first word bigger
|
||||
rb.push(StyleProperty::FontSize(68.), 0..8);
|
||||
// Push two modified styles for the same range
|
||||
rb.push(StyleProperty::LetterSpacing(4.), 12..17);
|
||||
rb.push(StyleProperty::WordSpacing(3.), 12..17);
|
||||
// Plus, change the line height for the last letter
|
||||
rb.push(StyleProperty::LineHeight(LineHeight::Absolute(40.)), 16..17);
|
||||
};
|
||||
let with_tree_builder = |tb: &mut TreeBuilder<'_, ColorBrush>| {
|
||||
// Make the first word bigger
|
||||
tb.push_style_modification_span(&[StyleProperty::FontSize(68.)]);
|
||||
tb.push_text(&text[..8]);
|
||||
tb.pop_style_span();
|
||||
|
||||
tb.push_text(&text[8..12]);
|
||||
|
||||
// Push two modified styles in batch
|
||||
tb.push_style_modification_span(&[
|
||||
StyleProperty::LetterSpacing(4.),
|
||||
StyleProperty::WordSpacing(3.),
|
||||
]);
|
||||
tb.push_text(&text[12..16]);
|
||||
// Plus, change the line height for the last letter
|
||||
tb.push_style_modification_span(&[StyleProperty::LineHeight(LineHeight::Absolute(40.))]);
|
||||
tb.push_text(&text[16..17]);
|
||||
tb.pop_style_span();
|
||||
tb.pop_style_span();
|
||||
|
||||
tb.push_text(&text[17..]);
|
||||
};
|
||||
|
||||
assert_builders_produce_same_result(
|
||||
text,
|
||||
scale,
|
||||
quantize,
|
||||
max_advance,
|
||||
&root_style,
|
||||
with_ranged_builder,
|
||||
with_tree_builder,
|
||||
false,
|
||||
);
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
// Copyright 2025 the Parley Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Various helper functions to assert truths during testing.
|
||||
|
||||
use std::vec::Vec;
|
||||
|
||||
use crate::{Brush, data::LayoutData};
|
||||
|
||||
fn canonicalize_layout_data<B: Brush>(layout_data: &LayoutData<B>) -> LayoutData<B> {
|
||||
let mut normalized = layout_data.clone();
|
||||
let mut canonical_styles = Vec::with_capacity(normalized.styles.len());
|
||||
let mut remap = Vec::with_capacity(normalized.styles.len());
|
||||
|
||||
for style in &normalized.styles {
|
||||
if let Some(index) = canonical_styles
|
||||
.iter()
|
||||
.position(|existing| existing == style)
|
||||
{
|
||||
remap.push(index as u16);
|
||||
} else {
|
||||
let index = canonical_styles.len() as u16;
|
||||
canonical_styles.push(style.clone());
|
||||
remap.push(index);
|
||||
}
|
||||
}
|
||||
|
||||
for cluster in &mut normalized.clusters {
|
||||
cluster.style_index = remap[cluster.style_index as usize];
|
||||
}
|
||||
for glyph in &mut normalized.glyphs {
|
||||
glyph.style_index = remap[glyph.style_index as usize];
|
||||
}
|
||||
normalized.styles = canonical_styles;
|
||||
normalized
|
||||
}
|
||||
|
||||
/// Assert that the two provided `LayoutData` are equal.
|
||||
pub(crate) fn assert_eq_layout_data<B: Brush>(a: &LayoutData<B>, b: &LayoutData<B>, case: &str) {
|
||||
let a = canonicalize_layout_data(a);
|
||||
let b = canonicalize_layout_data(b);
|
||||
|
||||
assert_eq!(a.scale, b.scale, "{case} scale mismatch");
|
||||
assert_eq!(a.quantize, b.quantize, "{case} quantize mismatch");
|
||||
assert_eq!(a.base_level, b.base_level, "{case} base_level mismatch");
|
||||
assert_eq!(a.text_len, b.text_len, "{case} text_len mismatch");
|
||||
assert_eq!(a.width, b.width, "{case} width mismatch");
|
||||
assert_eq!(a.full_width, b.full_width, "{case} full_width mismatch");
|
||||
assert_eq!(a.height, b.height, "{case} height mismatch");
|
||||
assert_eq!(a.fonts, b.fonts, "{case} fonts mismatch");
|
||||
assert_eq!(a.coords, b.coords, "{case} coords mismatch");
|
||||
|
||||
// Input (/ output of style resolution)
|
||||
assert_eq!(a.styles, b.styles, "{case} styles mismatch");
|
||||
assert_eq!(
|
||||
a.inline_boxes, b.inline_boxes,
|
||||
"{case} inline_boxes mismatch"
|
||||
);
|
||||
|
||||
// Output of shaping
|
||||
assert_eq!(a.runs, b.runs, "{case} runs mismatch");
|
||||
assert_eq!(a.items, b.items, "{case} items mismatch");
|
||||
assert_eq!(a.clusters, b.clusters, "{case} clusters mismatch");
|
||||
assert_eq!(a.glyphs, b.glyphs, "{case} glyphs mismatch");
|
||||
|
||||
// Output of line breaking
|
||||
assert_eq!(a.lines, b.lines, "{case} lines mismatch");
|
||||
assert_eq!(a.line_items, b.line_items, "{case} line_items mismatch");
|
||||
|
||||
// Output of alignment
|
||||
assert_eq!(
|
||||
a.is_aligned_justified, b.is_aligned_justified,
|
||||
"{case} is_aligned_justified mismatch"
|
||||
);
|
||||
assert_eq!(
|
||||
a.layout_max_advance, b.layout_max_advance,
|
||||
"{case} alignment_width mismatch"
|
||||
);
|
||||
|
||||
// Also compare the whole struct in case any fields have been added that aren't
|
||||
// part of this test yet. If this triggers, add the missing assert to the above set.
|
||||
assert_eq!(a, b, "{case} LayoutData mismatch");
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
// Copyright 2024 the Parley Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
use peniko::Color;
|
||||
|
||||
pub(crate) mod asserts;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) struct ColorBrush {
|
||||
pub(crate) color: Color,
|
||||
}
|
||||
|
||||
impl ColorBrush {
|
||||
pub(crate) fn new(color: Color) -> Self {
|
||||
let rgba8 = color.to_rgba8();
|
||||
Self {
|
||||
color: Color::from_rgba8(rgba8.r, rgba8.g, rgba8.b, rgba8.a),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ColorBrush {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
color: Color::BLACK,
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+62
@@ -0,0 +1,62 @@
|
||||
// Copyright 2021 the Parley Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Misc helpers.
|
||||
|
||||
pub(crate) fn nearly_eq(x: f32, y: f32) -> bool {
|
||||
(x - y).abs() < f32::EPSILON
|
||||
}
|
||||
|
||||
pub(crate) fn nearly_zero(x: f32) -> bool {
|
||||
nearly_eq(x, 0.)
|
||||
}
|
||||
|
||||
/// A bounding box.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||
pub struct BoundingBox {
|
||||
/// The left edge.
|
||||
pub x0: f64,
|
||||
/// The top edge.
|
||||
pub y0: f64,
|
||||
/// The right edge.
|
||||
pub x1: f64,
|
||||
/// The bottom edge.
|
||||
pub y1: f64,
|
||||
}
|
||||
|
||||
impl BoundingBox {
|
||||
/// A new bounding box from minimum and maximum coordinates.
|
||||
#[inline(always)]
|
||||
pub const fn new(x0: f64, y0: f64, x1: f64, y1: f64) -> Self {
|
||||
Self { x0, y0, x1, y1 }
|
||||
}
|
||||
|
||||
/// The width of the bounding box.
|
||||
///
|
||||
/// Note: nothing forbids negative width.
|
||||
#[inline]
|
||||
pub fn width(&self) -> f64 {
|
||||
self.x1 - self.x0
|
||||
}
|
||||
|
||||
/// The height of the bounding box.
|
||||
///
|
||||
/// Note: nothing forbids negative height.
|
||||
#[inline]
|
||||
pub fn height(&self) -> f64 {
|
||||
self.y1 - self.y0
|
||||
}
|
||||
|
||||
/// The smallest bounding box enclosing two bounding boxes.
|
||||
///
|
||||
/// Results are valid only if width and height are non-negative.
|
||||
#[inline]
|
||||
pub fn union(&self, other: Self) -> Self {
|
||||
Self::new(
|
||||
self.x0.min(other.x0),
|
||||
self.y0.min(other.y0),
|
||||
self.x1.max(other.x1),
|
||||
self.y1.max(other.y1),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user