mirror of
https://github.com/typst/typst
synced 2025-06-28 16:22:53 +08:00
Split out four new crates (#5302)
This commit is contained in:
parent
b8034a3438
commit
be7cfc85d0
207
Cargo.lock
generated
207
Cargo.lock
generated
@ -2668,68 +2668,16 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
|
|||||||
name = "typst"
|
name = "typst"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
|
||||||
"az",
|
|
||||||
"bitflags 2.6.0",
|
|
||||||
"bumpalo",
|
|
||||||
"chinese-number",
|
|
||||||
"ciborium",
|
|
||||||
"comemo",
|
"comemo",
|
||||||
"csv",
|
|
||||||
"ecow",
|
"ecow",
|
||||||
"flate2",
|
"typst-eval",
|
||||||
"fontdb",
|
"typst-layout",
|
||||||
"hayagriva",
|
"typst-library",
|
||||||
"hypher",
|
|
||||||
"icu_properties",
|
|
||||||
"icu_provider",
|
|
||||||
"icu_provider_adapters",
|
|
||||||
"icu_provider_blob",
|
|
||||||
"icu_segmenter",
|
|
||||||
"if_chain",
|
|
||||||
"image",
|
|
||||||
"indexmap 2.6.0",
|
|
||||||
"kamadak-exif",
|
|
||||||
"kurbo",
|
|
||||||
"lipsum",
|
|
||||||
"log",
|
|
||||||
"once_cell",
|
|
||||||
"palette",
|
|
||||||
"phf",
|
|
||||||
"png",
|
|
||||||
"portable-atomic",
|
|
||||||
"qcms",
|
|
||||||
"rayon",
|
|
||||||
"regex",
|
|
||||||
"roxmltree",
|
|
||||||
"rust_decimal",
|
|
||||||
"rustybuzz",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"serde_yaml 0.9.34+deprecated",
|
|
||||||
"siphasher 1.0.1",
|
|
||||||
"smallvec",
|
|
||||||
"stacker",
|
|
||||||
"syntect",
|
|
||||||
"time",
|
|
||||||
"toml",
|
|
||||||
"ttf-parser",
|
|
||||||
"two-face",
|
|
||||||
"typed-arena",
|
|
||||||
"typst-assets",
|
|
||||||
"typst-dev-assets",
|
|
||||||
"typst-macros",
|
"typst-macros",
|
||||||
|
"typst-realize",
|
||||||
"typst-syntax",
|
"typst-syntax",
|
||||||
"typst-timing",
|
"typst-timing",
|
||||||
"typst-utils",
|
"typst-utils",
|
||||||
"unicode-bidi",
|
|
||||||
"unicode-math-class",
|
|
||||||
"unicode-script",
|
|
||||||
"unicode-segmentation",
|
|
||||||
"unscanny",
|
|
||||||
"usvg",
|
|
||||||
"wasmi",
|
|
||||||
"xmlwriter",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2769,7 +2717,7 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
"toml",
|
"toml",
|
||||||
"typst",
|
"typst",
|
||||||
"typst-assets",
|
"typst-eval",
|
||||||
"typst-kit",
|
"typst-kit",
|
||||||
"typst-macros",
|
"typst-macros",
|
||||||
"typst-pdf",
|
"typst-pdf",
|
||||||
@ -2791,7 +2739,6 @@ name = "typst-docs"
|
|||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"comemo",
|
|
||||||
"ecow",
|
"ecow",
|
||||||
"heck",
|
"heck",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@ -2809,6 +2756,24 @@ dependencies = [
|
|||||||
"yaml-front-matter",
|
"yaml-front-matter",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typst-eval"
|
||||||
|
version = "0.12.0"
|
||||||
|
dependencies = [
|
||||||
|
"comemo",
|
||||||
|
"ecow",
|
||||||
|
"if_chain",
|
||||||
|
"indexmap 2.6.0",
|
||||||
|
"stacker",
|
||||||
|
"toml",
|
||||||
|
"typst-library",
|
||||||
|
"typst-macros",
|
||||||
|
"typst-syntax",
|
||||||
|
"typst-timing",
|
||||||
|
"typst-utils",
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-fuzz"
|
name = "typst-fuzz"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
@ -2834,6 +2799,7 @@ dependencies = [
|
|||||||
"typst",
|
"typst",
|
||||||
"typst-assets",
|
"typst-assets",
|
||||||
"typst-dev-assets",
|
"typst-dev-assets",
|
||||||
|
"typst-eval",
|
||||||
"unscanny",
|
"unscanny",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2850,13 +2816,103 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"openssl",
|
"openssl",
|
||||||
"tar",
|
"tar",
|
||||||
"typst",
|
|
||||||
"typst-assets",
|
"typst-assets",
|
||||||
|
"typst-library",
|
||||||
|
"typst-syntax",
|
||||||
"typst-timing",
|
"typst-timing",
|
||||||
"typst-utils",
|
"typst-utils",
|
||||||
"ureq",
|
"ureq",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typst-layout"
|
||||||
|
version = "0.12.0"
|
||||||
|
dependencies = [
|
||||||
|
"az",
|
||||||
|
"bumpalo",
|
||||||
|
"comemo",
|
||||||
|
"ecow",
|
||||||
|
"hypher",
|
||||||
|
"icu_properties",
|
||||||
|
"icu_provider",
|
||||||
|
"icu_provider_adapters",
|
||||||
|
"icu_provider_blob",
|
||||||
|
"icu_segmenter",
|
||||||
|
"kurbo",
|
||||||
|
"once_cell",
|
||||||
|
"rustybuzz",
|
||||||
|
"smallvec",
|
||||||
|
"ttf-parser",
|
||||||
|
"typst-assets",
|
||||||
|
"typst-library",
|
||||||
|
"typst-macros",
|
||||||
|
"typst-syntax",
|
||||||
|
"typst-timing",
|
||||||
|
"typst-utils",
|
||||||
|
"unicode-bidi",
|
||||||
|
"unicode-math-class",
|
||||||
|
"unicode-script",
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typst-library"
|
||||||
|
version = "0.12.0"
|
||||||
|
dependencies = [
|
||||||
|
"az",
|
||||||
|
"bitflags 2.6.0",
|
||||||
|
"bumpalo",
|
||||||
|
"chinese-number",
|
||||||
|
"ciborium",
|
||||||
|
"comemo",
|
||||||
|
"csv",
|
||||||
|
"ecow",
|
||||||
|
"flate2",
|
||||||
|
"fontdb",
|
||||||
|
"hayagriva",
|
||||||
|
"icu_properties",
|
||||||
|
"icu_provider",
|
||||||
|
"icu_provider_blob",
|
||||||
|
"image",
|
||||||
|
"indexmap 2.6.0",
|
||||||
|
"kamadak-exif",
|
||||||
|
"kurbo",
|
||||||
|
"lipsum",
|
||||||
|
"once_cell",
|
||||||
|
"palette",
|
||||||
|
"phf",
|
||||||
|
"png",
|
||||||
|
"qcms",
|
||||||
|
"rayon",
|
||||||
|
"regex",
|
||||||
|
"roxmltree",
|
||||||
|
"rust_decimal",
|
||||||
|
"rustybuzz",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_yaml 0.9.34+deprecated",
|
||||||
|
"siphasher 1.0.1",
|
||||||
|
"smallvec",
|
||||||
|
"syntect",
|
||||||
|
"time",
|
||||||
|
"toml",
|
||||||
|
"ttf-parser",
|
||||||
|
"two-face",
|
||||||
|
"typed-arena",
|
||||||
|
"typst-assets",
|
||||||
|
"typst-dev-assets",
|
||||||
|
"typst-macros",
|
||||||
|
"typst-syntax",
|
||||||
|
"typst-timing",
|
||||||
|
"typst-utils",
|
||||||
|
"unicode-math-class",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"unscanny",
|
||||||
|
"usvg",
|
||||||
|
"wasmi",
|
||||||
|
"xmlwriter",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-macros"
|
name = "typst-macros"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
@ -2885,14 +2941,32 @@ dependencies = [
|
|||||||
"subsetter",
|
"subsetter",
|
||||||
"svg2pdf",
|
"svg2pdf",
|
||||||
"ttf-parser",
|
"ttf-parser",
|
||||||
"typst",
|
|
||||||
"typst-assets",
|
"typst-assets",
|
||||||
|
"typst-library",
|
||||||
"typst-macros",
|
"typst-macros",
|
||||||
|
"typst-syntax",
|
||||||
"typst-timing",
|
"typst-timing",
|
||||||
"unscanny",
|
"typst-utils",
|
||||||
"xmp-writer",
|
"xmp-writer",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typst-realize"
|
||||||
|
version = "0.12.0"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
"bumpalo",
|
||||||
|
"comemo",
|
||||||
|
"ecow",
|
||||||
|
"once_cell",
|
||||||
|
"regex",
|
||||||
|
"typst-library",
|
||||||
|
"typst-macros",
|
||||||
|
"typst-syntax",
|
||||||
|
"typst-timing",
|
||||||
|
"typst-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-render"
|
name = "typst-render"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
@ -2902,13 +2976,11 @@ dependencies = [
|
|||||||
"image",
|
"image",
|
||||||
"pixglyph",
|
"pixglyph",
|
||||||
"resvg",
|
"resvg",
|
||||||
"roxmltree",
|
|
||||||
"tiny-skia",
|
"tiny-skia",
|
||||||
"ttf-parser",
|
"ttf-parser",
|
||||||
"typst",
|
"typst-library",
|
||||||
"typst-macros",
|
"typst-macros",
|
||||||
"typst-timing",
|
"typst-timing",
|
||||||
"usvg",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2920,9 +2992,10 @@ dependencies = [
|
|||||||
"ecow",
|
"ecow",
|
||||||
"flate2",
|
"flate2",
|
||||||
"ttf-parser",
|
"ttf-parser",
|
||||||
"typst",
|
"typst-library",
|
||||||
"typst-macros",
|
"typst-macros",
|
||||||
"typst-timing",
|
"typst-timing",
|
||||||
|
"typst-utils",
|
||||||
"xmlparser",
|
"xmlparser",
|
||||||
"xmlwriter",
|
"xmlwriter",
|
||||||
]
|
]
|
||||||
@ -2935,6 +3008,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"serde",
|
"serde",
|
||||||
"toml",
|
"toml",
|
||||||
|
"typst-timing",
|
||||||
"typst-utils",
|
"typst-utils",
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
"unicode-math-class",
|
"unicode-math-class",
|
||||||
@ -2956,10 +3030,10 @@ dependencies = [
|
|||||||
"rayon",
|
"rayon",
|
||||||
"regex",
|
"regex",
|
||||||
"tiny-skia",
|
"tiny-skia",
|
||||||
"ttf-parser",
|
|
||||||
"typst",
|
"typst",
|
||||||
"typst-assets",
|
"typst-assets",
|
||||||
"typst-dev-assets",
|
"typst-dev-assets",
|
||||||
|
"typst-library",
|
||||||
"typst-pdf",
|
"typst-pdf",
|
||||||
"typst-render",
|
"typst-render",
|
||||||
"typst-svg",
|
"typst-svg",
|
||||||
@ -2974,7 +3048,6 @@ dependencies = [
|
|||||||
"parking_lot",
|
"parking_lot",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"typst-syntax",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -18,10 +18,14 @@ readme = "README.md"
|
|||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
typst = { path = "crates/typst", version = "0.12.0" }
|
typst = { path = "crates/typst", version = "0.12.0" }
|
||||||
typst-cli = { path = "crates/typst-cli", version = "0.12.0" }
|
typst-cli = { path = "crates/typst-cli", version = "0.12.0" }
|
||||||
|
typst-eval = { path = "crates/typst-eval", version = "0.12.0" }
|
||||||
typst-ide = { path = "crates/typst-ide", version = "0.12.0" }
|
typst-ide = { path = "crates/typst-ide", version = "0.12.0" }
|
||||||
typst-kit = { path = "crates/typst-kit", version = "0.12.0" }
|
typst-kit = { path = "crates/typst-kit", version = "0.12.0" }
|
||||||
|
typst-layout = { path = "crates/typst-layout", version = "0.12.0" }
|
||||||
|
typst-library = { path = "crates/typst-library", version = "0.12.0" }
|
||||||
typst-macros = { path = "crates/typst-macros", version = "0.12.0" }
|
typst-macros = { path = "crates/typst-macros", version = "0.12.0" }
|
||||||
typst-pdf = { path = "crates/typst-pdf", version = "0.12.0" }
|
typst-pdf = { path = "crates/typst-pdf", version = "0.12.0" }
|
||||||
|
typst-realize = { path = "crates/typst-realize", version = "0.12.0" }
|
||||||
typst-render = { path = "crates/typst-render", version = "0.12.0" }
|
typst-render = { path = "crates/typst-render", version = "0.12.0" }
|
||||||
typst-svg = { path = "crates/typst-svg", version = "0.12.0" }
|
typst-svg = { path = "crates/typst-svg", version = "0.12.0" }
|
||||||
typst-syntax = { path = "crates/typst-syntax", version = "0.12.0" }
|
typst-syntax = { path = "crates/typst-syntax", version = "0.12.0" }
|
||||||
@ -145,5 +149,8 @@ strip = true
|
|||||||
|
|
||||||
[workspace.lints.clippy]
|
[workspace.lints.clippy]
|
||||||
blocks_in_conditions = "allow"
|
blocks_in_conditions = "allow"
|
||||||
|
comparison_chain = "allow"
|
||||||
|
manual_range_contains = "allow"
|
||||||
mutable_key_type = "allow"
|
mutable_key_type = "allow"
|
||||||
uninlined_format_args = "warn"
|
uninlined_format_args = "warn"
|
||||||
|
wildcard_in_or_patterns = "allow"
|
||||||
|
@ -19,7 +19,7 @@ doc = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
typst = { workspace = true }
|
typst = { workspace = true }
|
||||||
typst-assets = { workspace = true, features = ["fonts"] }
|
typst-eval = { workspace = true }
|
||||||
typst-kit = { workspace = true }
|
typst-kit = { workspace = true }
|
||||||
typst-macros = { workspace = true }
|
typst-macros = { workspace = true }
|
||||||
typst-pdf = { workspace = true }
|
typst-pdf = { workspace = true }
|
||||||
@ -28,8 +28,8 @@ typst-svg = { workspace = true }
|
|||||||
typst-timing = { workspace = true }
|
typst-timing = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
color-print = { workspace = true }
|
|
||||||
codespan-reporting = { workspace = true }
|
codespan-reporting = { workspace = true }
|
||||||
|
color-print = { workspace = true }
|
||||||
comemo = { workspace = true }
|
comemo = { workspace = true }
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
ecow = { workspace = true }
|
ecow = { workspace = true }
|
||||||
|
@ -94,9 +94,10 @@ fn print_error(msg: &str) -> io::Result<()> {
|
|||||||
|
|
||||||
#[cfg(not(feature = "self-update"))]
|
#[cfg(not(feature = "self-update"))]
|
||||||
mod update {
|
mod update {
|
||||||
use crate::args::UpdateCommand;
|
|
||||||
use typst::diag::{bail, StrResult};
|
use typst::diag::{bail, StrResult};
|
||||||
|
|
||||||
|
use crate::args::UpdateCommand;
|
||||||
|
|
||||||
pub fn update(_: &UpdateCommand) -> StrResult<()> {
|
pub fn update(_: &UpdateCommand) -> StrResult<()> {
|
||||||
bail!(
|
bail!(
|
||||||
"self-updating is not enabled for this executable, \
|
"self-updating is not enabled for this executable, \
|
||||||
|
@ -2,11 +2,11 @@ use comemo::Track;
|
|||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typst::diag::{bail, HintedStrResult, StrResult, Warned};
|
use typst::diag::{bail, HintedStrResult, StrResult, Warned};
|
||||||
use typst::eval::{eval_string, EvalMode};
|
|
||||||
use typst::foundations::{Content, IntoValue, LocatableSelector, Scope};
|
use typst::foundations::{Content, IntoValue, LocatableSelector, Scope};
|
||||||
use typst::model::Document;
|
use typst::model::Document;
|
||||||
use typst::syntax::Span;
|
use typst::syntax::Span;
|
||||||
use typst::World;
|
use typst::World;
|
||||||
|
use typst_eval::{eval_string, EvalMode};
|
||||||
|
|
||||||
use crate::args::{QueryCommand, SerializationFormat};
|
use crate::args::{QueryCommand, SerializationFormat};
|
||||||
use crate::compile::print_diagnostics;
|
use crate::compile::print_diagnostics;
|
||||||
@ -56,6 +56,7 @@ fn retrieve(
|
|||||||
document: &Document,
|
document: &Document,
|
||||||
) -> HintedStrResult<Vec<Content>> {
|
) -> HintedStrResult<Vec<Content>> {
|
||||||
let selector = eval_string(
|
let selector = eval_string(
|
||||||
|
&typst::ROUTINES,
|
||||||
world.track(),
|
world.track(),
|
||||||
&command.selector,
|
&command.selector,
|
||||||
Span::detached(),
|
Span::detached(),
|
||||||
|
@ -72,7 +72,8 @@ impl Timer {
|
|||||||
let writer = BufWriter::with_capacity(1 << 20, file);
|
let writer = BufWriter::with_capacity(1 << 20, file);
|
||||||
|
|
||||||
typst_timing::export_json(writer, |span| {
|
typst_timing::export_json(writer, |span| {
|
||||||
resolve_span(world, span).unwrap_or_else(|| ("unknown".to_string(), 0))
|
resolve_span(world, Span::from_raw(span))
|
||||||
|
.unwrap_or_else(|| ("unknown".to_string(), 0))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(output)
|
Ok(output)
|
||||||
|
@ -16,7 +16,7 @@ use typst::utils::LazyHash;
|
|||||||
use typst::{Library, World};
|
use typst::{Library, World};
|
||||||
use typst_kit::fonts::{FontSlot, Fonts};
|
use typst_kit::fonts::{FontSlot, Fonts};
|
||||||
use typst_kit::package::PackageStorage;
|
use typst_kit::package::PackageStorage;
|
||||||
use typst_timing::{timed, TimingScope};
|
use typst_timing::timed;
|
||||||
|
|
||||||
use crate::args::{Input, SharedArgs};
|
use crate::args::{Input, SharedArgs};
|
||||||
use crate::compile::ExportCache;
|
use crate::compile::ExportCache;
|
||||||
@ -285,8 +285,6 @@ impl FileSlot {
|
|||||||
self.source.get_or_init(
|
self.source.get_or_init(
|
||||||
|| read(self.id, project_root, package_storage),
|
|| read(self.id, project_root, package_storage),
|
||||||
|data, prev| {
|
|data, prev| {
|
||||||
let name = if prev.is_some() { "reparsing file" } else { "parsing file" };
|
|
||||||
let _scope = TimingScope::new(name, None);
|
|
||||||
let text = decode_utf8(&data)?;
|
let text = decode_utf8(&data)?;
|
||||||
if let Some(mut prev) = prev {
|
if let Some(mut prev) = prev {
|
||||||
prev.replace(text);
|
prev.replace(text);
|
||||||
|
32
crates/typst-eval/Cargo.toml
Normal file
32
crates/typst-eval/Cargo.toml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
[package]
|
||||||
|
name = "typst-eval"
|
||||||
|
description = "Typst's code interpreter."
|
||||||
|
version = { workspace = true }
|
||||||
|
rust-version = { workspace = true }
|
||||||
|
authors = { workspace = true }
|
||||||
|
edition = { workspace = true }
|
||||||
|
homepage = { workspace = true }
|
||||||
|
repository = { workspace = true }
|
||||||
|
license = { workspace = true }
|
||||||
|
categories = { workspace = true }
|
||||||
|
keywords = { workspace = true }
|
||||||
|
readme = { workspace = true }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
typst-library = { workspace = true }
|
||||||
|
typst-macros = { workspace = true }
|
||||||
|
typst-syntax = { workspace = true }
|
||||||
|
typst-timing = { workspace = true }
|
||||||
|
typst-utils = { workspace = true }
|
||||||
|
comemo = { workspace = true }
|
||||||
|
ecow = { workspace = true }
|
||||||
|
if_chain = { workspace = true }
|
||||||
|
indexmap = { workspace = true }
|
||||||
|
toml = { workspace = true }
|
||||||
|
unicode-segmentation = { workspace = true }
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
stacker = { workspace = true }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
@ -1,9 +1,9 @@
|
|||||||
use ecow::eco_format;
|
use ecow::eco_format;
|
||||||
|
use typst_library::diag::{bail, At, Hint, SourceResult, Trace, Tracepoint};
|
||||||
|
use typst_library::foundations::{Dict, Value};
|
||||||
|
use typst_syntax::ast::{self, AstNode};
|
||||||
|
|
||||||
use crate::diag::{bail, At, Hint, SourceResult, Trace, Tracepoint};
|
use crate::{call_method_access, is_accessor_method, Eval, Vm};
|
||||||
use crate::eval::{Eval, Vm};
|
|
||||||
use crate::foundations::{call_method_access, is_accessor_method, Dict, Value};
|
|
||||||
use crate::syntax::ast::{self, AstNode};
|
|
||||||
|
|
||||||
/// Access an expression mutably.
|
/// Access an expression mutably.
|
||||||
pub(crate) trait Access {
|
pub(crate) trait Access {
|
||||||
@ -85,7 +85,7 @@ pub(crate) fn access_dict<'a>(
|
|||||||
Value::Symbol(_) | Value::Content(_) | Value::Module(_) | Value::Func(_)
|
Value::Symbol(_) | Value::Content(_) | Value::Module(_) | Value::Func(_)
|
||||||
) {
|
) {
|
||||||
bail!(span, "cannot mutate fields on {ty}");
|
bail!(span, "cannot mutate fields on {ty}");
|
||||||
} else if crate::foundations::fields_on(ty).is_empty() {
|
} else if typst_library::foundations::fields_on(ty).is_empty() {
|
||||||
bail!(span, "{ty} does not have accessible fields");
|
bail!(span, "{ty} does not have accessible fields");
|
||||||
} else {
|
} else {
|
||||||
// type supports static fields, which don't yet have
|
// type supports static fields, which don't yet have
|
@ -1,11 +1,11 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use ecow::eco_format;
|
use ecow::eco_format;
|
||||||
|
use typst_library::diag::{bail, error, At, SourceDiagnostic, SourceResult};
|
||||||
|
use typst_library::foundations::{Array, Dict, Value};
|
||||||
|
use typst_syntax::ast::{self, AstNode};
|
||||||
|
|
||||||
use crate::diag::{bail, error, At, SourceDiagnostic, SourceResult};
|
use crate::{Access, Eval, Vm};
|
||||||
use crate::eval::{Access, Eval, Vm};
|
|
||||||
use crate::foundations::{Array, Dict, Value};
|
|
||||||
use crate::syntax::ast::{self, AstNode};
|
|
||||||
|
|
||||||
impl Eval for ast::LetBinding<'_> {
|
impl Eval for ast::LetBinding<'_> {
|
||||||
type Output = Value;
|
type Output = Value;
|
@ -1,23 +1,24 @@
|
|||||||
use comemo::{Tracked, TrackedMut};
|
use comemo::{Tracked, TrackedMut};
|
||||||
use ecow::{eco_format, EcoString, EcoVec};
|
use ecow::{eco_format, EcoString, EcoVec};
|
||||||
|
use typst_library::diag::{
|
||||||
use crate::diag::{
|
|
||||||
bail, error, At, HintedStrResult, HintedString, SourceDiagnostic, SourceResult,
|
bail, error, At, HintedStrResult, HintedString, SourceDiagnostic, SourceResult,
|
||||||
Trace, Tracepoint,
|
Trace, Tracepoint,
|
||||||
};
|
};
|
||||||
use crate::engine::{Engine, Sink, Traced};
|
use typst_library::engine::{Engine, Sink, Traced};
|
||||||
use crate::eval::{Access, Eval, FlowEvent, Route, Vm};
|
use typst_library::foundations::{
|
||||||
use crate::foundations::{
|
Arg, Args, Bytes, Capturer, Closure, Content, Context, Func, IntoValue,
|
||||||
call_method_mut, is_mutating_method, Arg, Args, Bytes, Capturer, Closure, Content,
|
NativeElement, Scope, Scopes, Value,
|
||||||
Context, Func, IntoValue, NativeElement, Scope, Scopes, Value,
|
|
||||||
};
|
};
|
||||||
use crate::introspection::Introspector;
|
use typst_library::introspection::Introspector;
|
||||||
use crate::math::LrElem;
|
use typst_library::math::LrElem;
|
||||||
use crate::syntax::ast::{self, AstNode, Ident};
|
use typst_library::routines::Routines;
|
||||||
use crate::syntax::{Span, Spanned, SyntaxNode};
|
use typst_library::text::TextElem;
|
||||||
use crate::text::TextElem;
|
use typst_library::World;
|
||||||
use crate::utils::LazyHash;
|
use typst_syntax::ast::{self, AstNode, Ident};
|
||||||
use crate::World;
|
use typst_syntax::{Span, Spanned, SyntaxNode};
|
||||||
|
use typst_utils::LazyHash;
|
||||||
|
|
||||||
|
use crate::{call_method_mut, is_mutating_method, Access, Eval, FlowEvent, Route, Vm};
|
||||||
|
|
||||||
impl Eval for ast::FuncCall<'_> {
|
impl Eval for ast::FuncCall<'_> {
|
||||||
type Output = Value;
|
type Output = Value;
|
||||||
@ -159,9 +160,10 @@ impl Eval for ast::Closure<'_> {
|
|||||||
/// Call the function in the context with the arguments.
|
/// Call the function in the context with the arguments.
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(crate) fn call_closure(
|
pub fn eval_closure(
|
||||||
func: &Func,
|
func: &Func,
|
||||||
closure: &LazyHash<Closure>,
|
closure: &LazyHash<Closure>,
|
||||||
|
routines: &Routines,
|
||||||
world: Tracked<dyn World + '_>,
|
world: Tracked<dyn World + '_>,
|
||||||
introspector: Tracked<Introspector>,
|
introspector: Tracked<Introspector>,
|
||||||
traced: Tracked<Traced>,
|
traced: Tracked<Traced>,
|
||||||
@ -182,6 +184,7 @@ pub(crate) fn call_closure(
|
|||||||
|
|
||||||
// Prepare the engine.
|
// Prepare the engine.
|
||||||
let engine = Engine {
|
let engine = Engine {
|
||||||
|
routines,
|
||||||
world,
|
world,
|
||||||
introspector,
|
introspector,
|
||||||
traced,
|
traced,
|
||||||
@ -210,7 +213,7 @@ pub(crate) fn call_closure(
|
|||||||
vm.define(ident, args.expect::<Value>(&ident)?)
|
vm.define(ident, args.expect::<Value>(&ident)?)
|
||||||
}
|
}
|
||||||
pattern => {
|
pattern => {
|
||||||
crate::eval::destructure(
|
crate::destructure(
|
||||||
&mut vm,
|
&mut vm,
|
||||||
pattern,
|
pattern,
|
||||||
args.expect::<Value>("pattern parameter")?,
|
args.expect::<Value>("pattern parameter")?,
|
||||||
@ -583,8 +586,9 @@ impl<'a> CapturesVisitor<'a> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use typst_syntax::parse;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::syntax::parse;
|
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn test(text: &str, result: &[&str]) {
|
fn test(text: &str, result: &[&str]) {
|
@ -1,11 +1,12 @@
|
|||||||
use ecow::{eco_vec, EcoVec};
|
use ecow::{eco_vec, EcoVec};
|
||||||
|
use typst_library::diag::{bail, error, At, SourceResult};
|
||||||
use crate::diag::{bail, error, At, SourceResult};
|
use typst_library::foundations::{
|
||||||
use crate::eval::{ops, CapturesVisitor, Eval, Vm};
|
ops, Array, Capturer, Closure, Content, ContextElem, Dict, Func, NativeElement, Str,
|
||||||
use crate::foundations::{
|
Value,
|
||||||
Array, Capturer, Closure, Content, ContextElem, Dict, Func, NativeElement, Str, Value,
|
|
||||||
};
|
};
|
||||||
use crate::syntax::ast::{self, AstNode};
|
use typst_syntax::ast::{self, AstNode};
|
||||||
|
|
||||||
|
use crate::{CapturesVisitor, Eval, Vm};
|
||||||
|
|
||||||
impl Eval for ast::Code<'_> {
|
impl Eval for ast::Code<'_> {
|
||||||
type Output = Value;
|
type Output = Value;
|
@ -1,10 +1,10 @@
|
|||||||
|
use typst_library::diag::{bail, error, At, SourceDiagnostic, SourceResult};
|
||||||
|
use typst_library::foundations::{ops, IntoValue, Value};
|
||||||
|
use typst_syntax::ast::{self, AstNode};
|
||||||
|
use typst_syntax::{Span, SyntaxKind, SyntaxNode};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
use crate::diag::{bail, error, At, SourceDiagnostic, SourceResult};
|
use crate::{destructure, Eval, Vm};
|
||||||
use crate::eval::{destructure, ops, Eval, Vm};
|
|
||||||
use crate::foundations::{IntoValue, Value};
|
|
||||||
use crate::syntax::ast::{self, AstNode};
|
|
||||||
use crate::syntax::{Span, SyntaxKind, SyntaxNode};
|
|
||||||
|
|
||||||
/// The maximum number of loop iterations.
|
/// The maximum number of loop iterations.
|
||||||
const MAX_ITERATIONS: usize = 10_000;
|
const MAX_ITERATIONS: usize = 10_000;
|
@ -1,13 +1,15 @@
|
|||||||
use comemo::TrackedMut;
|
use comemo::TrackedMut;
|
||||||
use ecow::{eco_format, eco_vec, EcoString};
|
use ecow::{eco_format, eco_vec, EcoString};
|
||||||
|
use typst_library::diag::{
|
||||||
|
bail, error, warning, At, FileError, SourceResult, Trace, Tracepoint,
|
||||||
|
};
|
||||||
|
use typst_library::foundations::{Content, Module, Value};
|
||||||
|
use typst_library::World;
|
||||||
|
use typst_syntax::ast::{self, AstNode};
|
||||||
|
use typst_syntax::package::{PackageManifest, PackageSpec};
|
||||||
|
use typst_syntax::{FileId, Span, VirtualPath};
|
||||||
|
|
||||||
use crate::diag::{bail, error, warning, At, FileError, SourceResult, Trace, Tracepoint};
|
use crate::{eval, Eval, Vm};
|
||||||
use crate::eval::{eval, Eval, Vm};
|
|
||||||
use crate::foundations::{Content, Module, Value};
|
|
||||||
use crate::syntax::ast::{self, AstNode};
|
|
||||||
use crate::syntax::package::{PackageManifest, PackageSpec};
|
|
||||||
use crate::syntax::{FileId, Span, VirtualPath};
|
|
||||||
use crate::World;
|
|
||||||
|
|
||||||
impl Eval for ast::ModuleImport<'_> {
|
impl Eval for ast::ModuleImport<'_> {
|
||||||
type Output = Value;
|
type Output = Value;
|
||||||
@ -171,8 +173,9 @@ pub fn import(
|
|||||||
/// Import an external package.
|
/// Import an external package.
|
||||||
fn import_package(vm: &mut Vm, spec: PackageSpec, span: Span) -> SourceResult<Module> {
|
fn import_package(vm: &mut Vm, spec: PackageSpec, span: Span) -> SourceResult<Module> {
|
||||||
// Evaluate the manifest.
|
// Evaluate the manifest.
|
||||||
|
let world = vm.world();
|
||||||
let manifest_id = FileId::new(Some(spec.clone()), VirtualPath::new("typst.toml"));
|
let manifest_id = FileId::new(Some(spec.clone()), VirtualPath::new("typst.toml"));
|
||||||
let bytes = vm.world().file(manifest_id).at(span)?;
|
let bytes = world.file(manifest_id).at(span)?;
|
||||||
let string = std::str::from_utf8(&bytes).map_err(FileError::from).at(span)?;
|
let string = std::str::from_utf8(&bytes).map_err(FileError::from).at(span)?;
|
||||||
let manifest: PackageManifest = toml::from_str(string)
|
let manifest: PackageManifest = toml::from_str(string)
|
||||||
.map_err(|err| eco_format!("package manifest is malformed ({})", err.message()))
|
.map_err(|err| eco_format!("package manifest is malformed ({})", err.message()))
|
||||||
@ -181,7 +184,7 @@ fn import_package(vm: &mut Vm, spec: PackageSpec, span: Span) -> SourceResult<Mo
|
|||||||
|
|
||||||
// Evaluate the entry point.
|
// Evaluate the entry point.
|
||||||
let entrypoint_id = manifest_id.join(&manifest.package.entrypoint);
|
let entrypoint_id = manifest_id.join(&manifest.package.entrypoint);
|
||||||
let source = vm.world().source(entrypoint_id).at(span)?;
|
let source = world.source(entrypoint_id).at(span)?;
|
||||||
|
|
||||||
// Prevent cyclic importing.
|
// Prevent cyclic importing.
|
||||||
if vm.engine.route.contains(source.id()) {
|
if vm.engine.route.contains(source.id()) {
|
||||||
@ -190,13 +193,14 @@ fn import_package(vm: &mut Vm, spec: PackageSpec, span: Span) -> SourceResult<Mo
|
|||||||
|
|
||||||
let point = || Tracepoint::Import;
|
let point = || Tracepoint::Import;
|
||||||
Ok(eval(
|
Ok(eval(
|
||||||
vm.world(),
|
vm.engine.routines,
|
||||||
|
vm.engine.world,
|
||||||
vm.engine.traced,
|
vm.engine.traced,
|
||||||
TrackedMut::reborrow_mut(&mut vm.engine.sink),
|
TrackedMut::reborrow_mut(&mut vm.engine.sink),
|
||||||
vm.engine.route.track(),
|
vm.engine.route.track(),
|
||||||
&source,
|
&source,
|
||||||
)
|
)
|
||||||
.trace(vm.world(), point, span)?
|
.trace(world, point, span)?
|
||||||
.with_name(manifest.package.name))
|
.with_name(manifest.package.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,7 +219,8 @@ fn import_file(vm: &mut Vm, path: &str, span: Span) -> SourceResult<Module> {
|
|||||||
// Evaluate the file.
|
// Evaluate the file.
|
||||||
let point = || Tracepoint::Import;
|
let point = || Tracepoint::Import;
|
||||||
eval(
|
eval(
|
||||||
world,
|
vm.engine.routines,
|
||||||
|
vm.engine.world,
|
||||||
vm.engine.traced,
|
vm.engine.traced,
|
||||||
TrackedMut::reborrow_mut(&mut vm.engine.sink),
|
TrackedMut::reborrow_mut(&mut vm.engine.sink),
|
||||||
vm.engine.route.track(),
|
vm.engine.route.track(),
|
@ -1,4 +1,4 @@
|
|||||||
//! Evaluation of markup and code.
|
//! Typst's code interpreter.
|
||||||
|
|
||||||
pub(crate) mod ops;
|
pub(crate) mod ops;
|
||||||
|
|
||||||
@ -10,31 +10,35 @@ mod flow;
|
|||||||
mod import;
|
mod import;
|
||||||
mod markup;
|
mod markup;
|
||||||
mod math;
|
mod math;
|
||||||
|
mod methods;
|
||||||
mod rules;
|
mod rules;
|
||||||
mod vm;
|
mod vm;
|
||||||
|
|
||||||
pub use self::call::*;
|
pub use self::call::*;
|
||||||
pub use self::import::*;
|
pub use self::import::*;
|
||||||
pub use self::vm::*;
|
pub use self::vm::*;
|
||||||
|
pub use typst_library::routines::EvalMode;
|
||||||
|
|
||||||
pub(crate) use self::access::*;
|
use self::access::*;
|
||||||
pub(crate) use self::binding::*;
|
use self::binding::*;
|
||||||
pub(crate) use self::flow::*;
|
use self::flow::*;
|
||||||
|
use self::methods::*;
|
||||||
|
|
||||||
use comemo::{Track, Tracked, TrackedMut};
|
use comemo::{Track, Tracked, TrackedMut};
|
||||||
|
use typst_library::diag::{bail, SourceResult};
|
||||||
use crate::diag::{bail, SourceResult};
|
use typst_library::engine::{Engine, Route, Sink, Traced};
|
||||||
use crate::engine::{Engine, Route, Sink, Traced};
|
use typst_library::foundations::{Context, Module, NativeElement, Scope, Scopes, Value};
|
||||||
use crate::foundations::{Cast, Context, Module, NativeElement, Scope, Scopes, Value};
|
use typst_library::introspection::Introspector;
|
||||||
use crate::introspection::Introspector;
|
use typst_library::math::EquationElem;
|
||||||
use crate::math::EquationElem;
|
use typst_library::routines::Routines;
|
||||||
use crate::syntax::{ast, parse, parse_code, parse_math, Source, Span};
|
use typst_library::World;
|
||||||
use crate::World;
|
use typst_syntax::{ast, parse, parse_code, parse_math, Source, Span};
|
||||||
|
|
||||||
/// Evaluate a source file and return the resulting module.
|
/// Evaluate a source file and return the resulting module.
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
#[typst_macros::time(name = "eval", span = source.root().span())]
|
#[typst_macros::time(name = "eval", span = source.root().span())]
|
||||||
pub fn eval(
|
pub fn eval(
|
||||||
|
routines: &Routines,
|
||||||
world: Tracked<dyn World + '_>,
|
world: Tracked<dyn World + '_>,
|
||||||
traced: Tracked<Traced>,
|
traced: Tracked<Traced>,
|
||||||
sink: TrackedMut<Sink>,
|
sink: TrackedMut<Sink>,
|
||||||
@ -50,6 +54,7 @@ pub fn eval(
|
|||||||
// Prepare the engine.
|
// Prepare the engine.
|
||||||
let introspector = Introspector::default();
|
let introspector = Introspector::default();
|
||||||
let engine = Engine {
|
let engine = Engine {
|
||||||
|
routines,
|
||||||
world,
|
world,
|
||||||
introspector: introspector.track(),
|
introspector: introspector.track(),
|
||||||
traced,
|
traced,
|
||||||
@ -94,6 +99,7 @@ pub fn eval(
|
|||||||
/// Everything in the output is associated with the given `span`.
|
/// Everything in the output is associated with the given `span`.
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
pub fn eval_string(
|
pub fn eval_string(
|
||||||
|
routines: &Routines,
|
||||||
world: Tracked<dyn World + '_>,
|
world: Tracked<dyn World + '_>,
|
||||||
string: &str,
|
string: &str,
|
||||||
span: Span,
|
span: Span,
|
||||||
@ -119,6 +125,7 @@ pub fn eval_string(
|
|||||||
let introspector = Introspector::default();
|
let introspector = Introspector::default();
|
||||||
let traced = Traced::default();
|
let traced = Traced::default();
|
||||||
let engine = Engine {
|
let engine = Engine {
|
||||||
|
routines,
|
||||||
world,
|
world,
|
||||||
introspector: introspector.track(),
|
introspector: introspector.track(),
|
||||||
traced: traced.track(),
|
traced: traced.track(),
|
||||||
@ -153,17 +160,6 @@ pub fn eval_string(
|
|||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// In which mode to evaluate a string.
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
|
|
||||||
pub enum EvalMode {
|
|
||||||
/// Evaluate as code, as after a hash.
|
|
||||||
Code,
|
|
||||||
/// Evaluate as markup, like in a Typst file.
|
|
||||||
Markup,
|
|
||||||
/// Evaluate as math, as in an equation.
|
|
||||||
Math,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Evaluate an expression.
|
/// Evaluate an expression.
|
||||||
pub trait Eval {
|
pub trait Eval {
|
||||||
/// The output of evaluating the expression.
|
/// The output of evaluating the expression.
|
@ -1,18 +1,18 @@
|
|||||||
use crate::diag::{warning, At, SourceResult};
|
use typst_library::diag::{warning, At, SourceResult};
|
||||||
use crate::eval::{Eval, Vm};
|
use typst_library::foundations::{
|
||||||
use crate::foundations::{
|
Content, Label, NativeElement, Repr, Smart, Symbol, Unlabellable, Value,
|
||||||
Content, Label, NativeElement, Repr, Smart, Unlabellable, Value,
|
|
||||||
};
|
};
|
||||||
use crate::math::EquationElem;
|
use typst_library::math::EquationElem;
|
||||||
use crate::model::{
|
use typst_library::model::{
|
||||||
EmphElem, EnumItem, HeadingElem, LinkElem, ListItem, ParbreakElem, RefElem,
|
EmphElem, EnumItem, HeadingElem, LinkElem, ListItem, ParbreakElem, RefElem,
|
||||||
StrongElem, Supplement, TermItem, Url,
|
StrongElem, Supplement, TermItem, Url,
|
||||||
};
|
};
|
||||||
use crate::symbols::Symbol;
|
use typst_library::text::{
|
||||||
use crate::syntax::ast::{self, AstNode};
|
|
||||||
use crate::text::{
|
|
||||||
LinebreakElem, RawContent, RawElem, SmartQuoteElem, SpaceElem, TextElem,
|
LinebreakElem, RawContent, RawElem, SmartQuoteElem, SpaceElem, TextElem,
|
||||||
};
|
};
|
||||||
|
use typst_syntax::ast::{self, AstNode};
|
||||||
|
|
||||||
|
use crate::{Eval, Vm};
|
||||||
|
|
||||||
impl Eval for ast::Markup<'_> {
|
impl Eval for ast::Markup<'_> {
|
||||||
type Output = Content;
|
type Output = Content;
|
@ -1,12 +1,13 @@
|
|||||||
use ecow::eco_format;
|
use ecow::eco_format;
|
||||||
|
use typst_library::diag::{At, SourceResult};
|
||||||
|
use typst_library::foundations::{Content, NativeElement, Symbol, Value};
|
||||||
|
use typst_library::math::{
|
||||||
|
AlignPointElem, AttachElem, FracElem, LrElem, PrimesElem, RootElem,
|
||||||
|
};
|
||||||
|
use typst_library::text::TextElem;
|
||||||
|
use typst_syntax::ast::{self, AstNode};
|
||||||
|
|
||||||
use crate::diag::{At, SourceResult};
|
use crate::{Eval, Vm};
|
||||||
use crate::eval::{Eval, Vm};
|
|
||||||
use crate::foundations::{Content, NativeElement, Value};
|
|
||||||
use crate::math::{AlignPointElem, AttachElem, FracElem, LrElem, PrimesElem, RootElem};
|
|
||||||
use crate::symbols::Symbol;
|
|
||||||
use crate::syntax::ast::{self, AstNode};
|
|
||||||
use crate::text::TextElem;
|
|
||||||
|
|
||||||
impl Eval for ast::Math<'_> {
|
impl Eval for ast::Math<'_> {
|
||||||
type Output = Content;
|
type Output = Content;
|
@ -1,8 +1,8 @@
|
|||||||
//! Handles special built-in methods on values.
|
//! Handles special built-in methods on values.
|
||||||
|
|
||||||
use crate::diag::{At, SourceResult};
|
use typst_library::diag::{At, SourceResult};
|
||||||
use crate::foundations::{Args, Str, Type, Value};
|
use typst_library::foundations::{Args, Str, Type, Value};
|
||||||
use crate::syntax::Span;
|
use typst_syntax::Span;
|
||||||
|
|
||||||
/// Whether a specific method is mutating.
|
/// Whether a specific method is mutating.
|
||||||
pub(crate) fn is_mutating_method(method: &str) -> bool {
|
pub(crate) fn is_mutating_method(method: &str) -> bool {
|
91
crates/typst-eval/src/ops.rs
Normal file
91
crates/typst-eval/src/ops.rs
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
use typst_library::diag::{At, HintedStrResult, SourceResult};
|
||||||
|
use typst_library::foundations::{ops, IntoValue, Value};
|
||||||
|
use typst_syntax::ast::{self, AstNode};
|
||||||
|
|
||||||
|
use crate::{access_dict, Access, Eval, Vm};
|
||||||
|
|
||||||
|
impl Eval for ast::Unary<'_> {
|
||||||
|
type Output = Value;
|
||||||
|
|
||||||
|
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
|
||||||
|
let value = self.expr().eval(vm)?;
|
||||||
|
let result = match self.op() {
|
||||||
|
ast::UnOp::Pos => ops::pos(value),
|
||||||
|
ast::UnOp::Neg => ops::neg(value),
|
||||||
|
ast::UnOp::Not => ops::not(value),
|
||||||
|
};
|
||||||
|
result.at(self.span())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eval for ast::Binary<'_> {
|
||||||
|
type Output = Value;
|
||||||
|
|
||||||
|
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
|
||||||
|
match self.op() {
|
||||||
|
ast::BinOp::Add => apply_binary(self, vm, ops::add),
|
||||||
|
ast::BinOp::Sub => apply_binary(self, vm, ops::sub),
|
||||||
|
ast::BinOp::Mul => apply_binary(self, vm, ops::mul),
|
||||||
|
ast::BinOp::Div => apply_binary(self, vm, ops::div),
|
||||||
|
ast::BinOp::And => apply_binary(self, vm, ops::and),
|
||||||
|
ast::BinOp::Or => apply_binary(self, vm, ops::or),
|
||||||
|
ast::BinOp::Eq => apply_binary(self, vm, ops::eq),
|
||||||
|
ast::BinOp::Neq => apply_binary(self, vm, ops::neq),
|
||||||
|
ast::BinOp::Lt => apply_binary(self, vm, ops::lt),
|
||||||
|
ast::BinOp::Leq => apply_binary(self, vm, ops::leq),
|
||||||
|
ast::BinOp::Gt => apply_binary(self, vm, ops::gt),
|
||||||
|
ast::BinOp::Geq => apply_binary(self, vm, ops::geq),
|
||||||
|
ast::BinOp::In => apply_binary(self, vm, ops::in_),
|
||||||
|
ast::BinOp::NotIn => apply_binary(self, vm, ops::not_in),
|
||||||
|
ast::BinOp::Assign => apply_assignment(self, vm, |_, b| Ok(b)),
|
||||||
|
ast::BinOp::AddAssign => apply_assignment(self, vm, ops::add),
|
||||||
|
ast::BinOp::SubAssign => apply_assignment(self, vm, ops::sub),
|
||||||
|
ast::BinOp::MulAssign => apply_assignment(self, vm, ops::mul),
|
||||||
|
ast::BinOp::DivAssign => apply_assignment(self, vm, ops::div),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply a basic binary operation.
|
||||||
|
fn apply_binary(
|
||||||
|
binary: ast::Binary,
|
||||||
|
vm: &mut Vm,
|
||||||
|
op: fn(Value, Value) -> HintedStrResult<Value>,
|
||||||
|
) -> SourceResult<Value> {
|
||||||
|
let lhs = binary.lhs().eval(vm)?;
|
||||||
|
|
||||||
|
// Short-circuit boolean operations.
|
||||||
|
if (binary.op() == ast::BinOp::And && lhs == false.into_value())
|
||||||
|
|| (binary.op() == ast::BinOp::Or && lhs == true.into_value())
|
||||||
|
{
|
||||||
|
return Ok(lhs);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rhs = binary.rhs().eval(vm)?;
|
||||||
|
op(lhs, rhs).at(binary.span())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply an assignment operation.
|
||||||
|
fn apply_assignment(
|
||||||
|
binary: ast::Binary,
|
||||||
|
vm: &mut Vm,
|
||||||
|
op: fn(Value, Value) -> HintedStrResult<Value>,
|
||||||
|
) -> SourceResult<Value> {
|
||||||
|
let rhs = binary.rhs().eval(vm)?;
|
||||||
|
let lhs = binary.lhs();
|
||||||
|
|
||||||
|
// An assignment to a dictionary field is different from a normal access
|
||||||
|
// since it can create the field instead of just modifying it.
|
||||||
|
if binary.op() == ast::BinOp::Assign {
|
||||||
|
if let ast::Expr::FieldAccess(access) = lhs {
|
||||||
|
let dict = access_dict(vm, access)?;
|
||||||
|
dict.insert(access.field().get().clone().into(), rhs);
|
||||||
|
return Ok(Value::None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let location = binary.lhs().access(vm)?;
|
||||||
|
let lhs = std::mem::take(&mut *location);
|
||||||
|
*location = op(lhs, rhs).at(binary.span())?;
|
||||||
|
Ok(Value::None)
|
||||||
|
}
|
@ -1,11 +1,12 @@
|
|||||||
use crate::diag::{warning, At, SourceResult};
|
use typst_library::diag::{warning, At, SourceResult};
|
||||||
use crate::eval::{Eval, Vm};
|
use typst_library::foundations::{
|
||||||
use crate::foundations::{
|
|
||||||
Element, Fields, Func, Recipe, Selector, ShowableSelector, Styles, Transformation,
|
Element, Fields, Func, Recipe, Selector, ShowableSelector, Styles, Transformation,
|
||||||
};
|
};
|
||||||
use crate::layout::BlockElem;
|
use typst_library::layout::BlockElem;
|
||||||
use crate::model::ParElem;
|
use typst_library::model::ParElem;
|
||||||
use crate::syntax::ast::{self, AstNode};
|
use typst_syntax::ast::{self, AstNode};
|
||||||
|
|
||||||
|
use crate::{Eval, Vm};
|
||||||
|
|
||||||
impl Eval for ast::SetRule<'_> {
|
impl Eval for ast::SetRule<'_> {
|
||||||
type Output = Styles;
|
type Output = Styles;
|
@ -1,15 +1,15 @@
|
|||||||
use comemo::Tracked;
|
use comemo::Tracked;
|
||||||
|
use typst_library::engine::Engine;
|
||||||
|
use typst_library::foundations::{Context, IntoValue, Scopes, Value};
|
||||||
|
use typst_library::World;
|
||||||
|
use typst_syntax::ast::{self, AstNode};
|
||||||
|
use typst_syntax::Span;
|
||||||
|
|
||||||
use crate::engine::Engine;
|
use crate::FlowEvent;
|
||||||
use crate::eval::FlowEvent;
|
|
||||||
use crate::foundations::{Context, IntoValue, Scopes, Value};
|
|
||||||
use crate::syntax::ast::{self, AstNode};
|
|
||||||
use crate::syntax::Span;
|
|
||||||
use crate::World;
|
|
||||||
|
|
||||||
/// A virtual machine.
|
/// A virtual machine.
|
||||||
///
|
///
|
||||||
/// Holds the state needed to [evaluate](crate::eval::eval()) Typst sources. A
|
/// Holds the state needed to [evaluate](crate::eval()) Typst sources. A
|
||||||
/// new virtual machine is created for each module evaluation and function call.
|
/// new virtual machine is created for each module evaluation and function call.
|
||||||
pub struct Vm<'a> {
|
pub struct Vm<'a> {
|
||||||
/// The underlying virtual typesetter.
|
/// The underlying virtual typesetter.
|
@ -14,6 +14,7 @@ readme = { workspace = true }
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
typst = { workspace = true }
|
typst = { workspace = true }
|
||||||
|
typst-eval = { workspace = true }
|
||||||
comemo = { workspace = true }
|
comemo = { workspace = true }
|
||||||
ecow = { workspace = true }
|
ecow = { workspace = true }
|
||||||
if_chain = { workspace = true }
|
if_chain = { workspace = true }
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
use comemo::Track;
|
use comemo::Track;
|
||||||
use ecow::{eco_vec, EcoString, EcoVec};
|
use ecow::{eco_vec, EcoString, EcoVec};
|
||||||
use typst::engine::{Engine, Route, Sink, Traced};
|
use typst::engine::{Engine, Route, Sink, Traced};
|
||||||
use typst::eval::Vm;
|
|
||||||
use typst::foundations::{Context, Label, Scopes, Styles, Value};
|
use typst::foundations::{Context, Label, Scopes, Styles, Value};
|
||||||
use typst::introspection::Introspector;
|
use typst::introspection::Introspector;
|
||||||
use typst::model::{BibliographyElem, Document};
|
use typst::model::{BibliographyElem, Document};
|
||||||
use typst::syntax::{ast, LinkedNode, Span, SyntaxKind};
|
use typst::syntax::{ast, LinkedNode, Span, SyntaxKind};
|
||||||
use typst::World;
|
use typst::World;
|
||||||
|
use typst_eval::Vm;
|
||||||
|
|
||||||
/// Try to determine a set of possible values for an expression.
|
/// Try to determine a set of possible values for an expression.
|
||||||
pub fn analyze_expr(
|
pub fn analyze_expr(
|
||||||
@ -58,6 +58,7 @@ pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option<Value> {
|
|||||||
let traced = Traced::default();
|
let traced = Traced::default();
|
||||||
let mut sink = Sink::new();
|
let mut sink = Sink::new();
|
||||||
let engine = Engine {
|
let engine = Engine {
|
||||||
|
routines: &typst::ROUTINES,
|
||||||
world: world.track(),
|
world: world.track(),
|
||||||
introspector: introspector.track(),
|
introspector: introspector.track(),
|
||||||
traced: traced.track(),
|
traced: traced.track(),
|
||||||
@ -73,7 +74,7 @@ pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option<Value> {
|
|||||||
Span::detached(),
|
Span::detached(),
|
||||||
);
|
);
|
||||||
|
|
||||||
typst::eval::import(&mut vm, source, source_span, true)
|
typst_eval::import(&mut vm, source, source_span, true)
|
||||||
.ok()
|
.ok()
|
||||||
.map(Value::Module)
|
.map(Value::Module)
|
||||||
}
|
}
|
||||||
|
@ -1353,7 +1353,6 @@ impl<'a> CompletionContext<'a> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::autocomplete;
|
use super::autocomplete;
|
||||||
use crate::tests::TestWorld;
|
use crate::tests::TestWorld;
|
||||||
|
|
||||||
|
@ -191,7 +191,7 @@ mod tests {
|
|||||||
// Set page width to 120pt with 10pt margins, so that the inner page is
|
// Set page width to 120pt with 10pt margins, so that the inner page is
|
||||||
// exactly 100pt wide. Page height is unbounded and font size is 10pt so
|
// exactly 100pt wide. Page height is unbounded and font size is 10pt so
|
||||||
// that it multiplies to nice round numbers.
|
// that it multiplies to nice round numbers.
|
||||||
let mut lib = Library::default();
|
let mut lib = typst::Library::default();
|
||||||
lib.styles
|
lib.styles
|
||||||
.set(PageElem::set_width(Smart::Custom(Abs::pt(120.0).into())));
|
.set(PageElem::set_width(Smart::Custom(Abs::pt(120.0).into())));
|
||||||
lib.styles.set(PageElem::set_height(Smart::Auto));
|
lib.styles.set(PageElem::set_height(Smart::Auto));
|
||||||
|
@ -3,13 +3,13 @@ use std::fmt::Write;
|
|||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
use if_chain::if_chain;
|
use if_chain::if_chain;
|
||||||
use typst::engine::Sink;
|
use typst::engine::Sink;
|
||||||
use typst::eval::CapturesVisitor;
|
|
||||||
use typst::foundations::{repr, Capturer, CastInfo, Repr, Value};
|
use typst::foundations::{repr, Capturer, CastInfo, Repr, Value};
|
||||||
use typst::layout::Length;
|
use typst::layout::Length;
|
||||||
use typst::model::Document;
|
use typst::model::Document;
|
||||||
use typst::syntax::{ast, LinkedNode, Side, Source, SyntaxKind};
|
use typst::syntax::{ast, LinkedNode, Side, Source, SyntaxKind};
|
||||||
use typst::utils::{round_with_precision, Numeric};
|
use typst::utils::{round_with_precision, Numeric};
|
||||||
use typst::World;
|
use typst::World;
|
||||||
|
use typst_eval::CapturesVisitor;
|
||||||
|
|
||||||
use crate::{analyze_expr, analyze_labels, plain_docs_sentence, summarize_font_family};
|
use crate::{analyze_expr, analyze_labels, plain_docs_sentence, summarize_font_family};
|
||||||
|
|
||||||
|
@ -11,13 +11,14 @@ license = { workspace = true }
|
|||||||
readme = { workspace = true }
|
readme = { workspace = true }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
typst = { workspace = true }
|
|
||||||
typst-assets = { workspace = true, optional = true }
|
typst-assets = { workspace = true, optional = true }
|
||||||
|
typst-library = { workspace = true }
|
||||||
|
typst-syntax = { workspace = true }
|
||||||
typst-timing = { workspace = true }
|
typst-timing = { workspace = true }
|
||||||
typst-utils = { workspace = true }
|
typst-utils = { workspace = true }
|
||||||
|
dirs = { workspace = true, optional = true }
|
||||||
ecow = { workspace = true }
|
ecow = { workspace = true }
|
||||||
env_proxy = { workspace = true, optional = true }
|
env_proxy = { workspace = true, optional = true }
|
||||||
dirs = { workspace = true, optional = true }
|
|
||||||
flate2 = { workspace = true, optional = true }
|
flate2 = { workspace = true, optional = true }
|
||||||
fontdb = { workspace = true, optional = true }
|
fontdb = { workspace = true, optional = true }
|
||||||
native-tls = { workspace = true, optional = true }
|
native-tls = { workspace = true, optional = true }
|
||||||
|
@ -8,12 +8,12 @@
|
|||||||
//! - For math: New Computer Modern Math
|
//! - For math: New Computer Modern Math
|
||||||
//! - For code: Deja Vu Sans Mono
|
//! - For code: Deja Vu Sans Mono
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use std::{fs, path::Path};
|
|
||||||
|
|
||||||
use fontdb::{Database, Source};
|
use fontdb::{Database, Source};
|
||||||
use typst::text::{Font, FontBook, FontInfo};
|
use typst_library::text::{Font, FontBook, FontInfo};
|
||||||
use typst_timing::TimingScope;
|
use typst_timing::TimingScope;
|
||||||
|
|
||||||
/// Holds details about the location of a font and lazily the font itself.
|
/// Holds details about the location of a font and lazily the font itself.
|
||||||
@ -46,7 +46,7 @@ impl FontSlot {
|
|||||||
pub fn get(&self) -> Option<Font> {
|
pub fn get(&self) -> Option<Font> {
|
||||||
self.font
|
self.font
|
||||||
.get_or_init(|| {
|
.get_or_init(|| {
|
||||||
let _scope = TimingScope::new("load font", None);
|
let _scope = TimingScope::new("load font");
|
||||||
let data = fs::read(
|
let data = fs::read(
|
||||||
self.path
|
self.path
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@ -196,7 +196,7 @@ impl FontSearcher {
|
|||||||
#[cfg(feature = "embed-fonts")]
|
#[cfg(feature = "embed-fonts")]
|
||||||
fn add_embedded(&mut self) {
|
fn add_embedded(&mut self) {
|
||||||
for data in typst_assets::fonts() {
|
for data in typst_assets::fonts() {
|
||||||
let buffer = typst::foundations::Bytes::from_static(data);
|
let buffer = typst_library::foundations::Bytes::from_static(data);
|
||||||
for (i, font) in Font::iter(buffer).enumerate() {
|
for (i, font) in Font::iter(buffer).enumerate() {
|
||||||
self.book.push(font.info().clone());
|
self.book.push(font.info().clone());
|
||||||
self.fonts.push(FontSlot {
|
self.fonts.push(FontSlot {
|
||||||
|
@ -5,8 +5,8 @@ use std::path::{Path, PathBuf};
|
|||||||
|
|
||||||
use ecow::eco_format;
|
use ecow::eco_format;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use typst::diag::{bail, PackageError, PackageResult, StrResult};
|
use typst_library::diag::{bail, PackageError, PackageResult, StrResult};
|
||||||
use typst::syntax::package::{
|
use typst_syntax::package::{
|
||||||
PackageInfo, PackageSpec, PackageVersion, VersionlessPackageSpec,
|
PackageInfo, PackageSpec, PackageVersion, VersionlessPackageSpec,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
43
crates/typst-layout/Cargo.toml
Normal file
43
crates/typst-layout/Cargo.toml
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
[package]
|
||||||
|
name = "typst-layout"
|
||||||
|
description = "Typst's layout engine."
|
||||||
|
version = { workspace = true }
|
||||||
|
rust-version = { workspace = true }
|
||||||
|
authors = { workspace = true }
|
||||||
|
edition = { workspace = true }
|
||||||
|
homepage = { workspace = true }
|
||||||
|
repository = { workspace = true }
|
||||||
|
license = { workspace = true }
|
||||||
|
categories = { workspace = true }
|
||||||
|
keywords = { workspace = true }
|
||||||
|
readme = { workspace = true }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
typst-assets = { workspace = true }
|
||||||
|
typst-library = { workspace = true }
|
||||||
|
typst-macros = { workspace = true }
|
||||||
|
typst-syntax = { workspace = true }
|
||||||
|
typst-timing = { workspace = true }
|
||||||
|
typst-utils = { workspace = true }
|
||||||
|
az = { workspace = true }
|
||||||
|
bumpalo = { workspace = true }
|
||||||
|
comemo = { workspace = true }
|
||||||
|
ecow = { workspace = true }
|
||||||
|
hypher = { workspace = true }
|
||||||
|
icu_properties = { workspace = true }
|
||||||
|
icu_provider = { workspace = true }
|
||||||
|
icu_provider_adapters = { workspace = true }
|
||||||
|
icu_provider_blob = { workspace = true }
|
||||||
|
icu_segmenter = { workspace = true }
|
||||||
|
kurbo = { workspace = true }
|
||||||
|
once_cell = { workspace = true }
|
||||||
|
rustybuzz = { workspace = true }
|
||||||
|
smallvec = { workspace = true }
|
||||||
|
ttf-parser = { workspace = true }
|
||||||
|
unicode-bidi = { workspace = true }
|
||||||
|
unicode-math-class = { workspace = true }
|
||||||
|
unicode-script = { workspace = true }
|
||||||
|
unicode-segmentation = { workspace = true }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
401
crates/typst-layout/src/flow/block.rs
Normal file
401
crates/typst-layout/src/flow/block.rs
Normal file
@ -0,0 +1,401 @@
|
|||||||
|
use once_cell::unsync::Lazy;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use typst_library::diag::SourceResult;
|
||||||
|
use typst_library::engine::Engine;
|
||||||
|
use typst_library::foundations::{Packed, Resolve, StyleChain};
|
||||||
|
use typst_library::introspection::Locator;
|
||||||
|
use typst_library::layout::{
|
||||||
|
Abs, Axes, BlockBody, BlockElem, Fragment, Frame, FrameKind, Region, Regions, Rel,
|
||||||
|
Sides, Size, Sizing,
|
||||||
|
};
|
||||||
|
use typst_library::visualize::Stroke;
|
||||||
|
use typst_utils::Numeric;
|
||||||
|
|
||||||
|
use crate::shapes::{clip_rect, fill_and_stroke};
|
||||||
|
|
||||||
|
/// Lay this out as an unbreakable block.
|
||||||
|
#[typst_macros::time(name = "block", span = elem.span())]
|
||||||
|
pub fn layout_single_block(
|
||||||
|
elem: &Packed<BlockElem>,
|
||||||
|
engine: &mut Engine,
|
||||||
|
locator: Locator,
|
||||||
|
styles: StyleChain,
|
||||||
|
region: Region,
|
||||||
|
) -> SourceResult<Frame> {
|
||||||
|
// Fetch sizing properties.
|
||||||
|
let width = elem.width(styles);
|
||||||
|
let height = elem.height(styles);
|
||||||
|
let inset = elem.inset(styles).unwrap_or_default();
|
||||||
|
|
||||||
|
// Build the pod regions.
|
||||||
|
let pod = unbreakable_pod(&width.into(), &height, &inset, styles, region.size);
|
||||||
|
|
||||||
|
// Layout the body.
|
||||||
|
let body = elem.body(styles);
|
||||||
|
let mut frame = match body {
|
||||||
|
// If we have no body, just create one frame. Its size will be
|
||||||
|
// adjusted below.
|
||||||
|
None => Frame::hard(Size::zero()),
|
||||||
|
|
||||||
|
// If we have content as our body, just layout it.
|
||||||
|
Some(BlockBody::Content(body)) => {
|
||||||
|
crate::layout_frame(engine, body, locator.relayout(), styles, pod)?
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a child that wants to layout with just access to the
|
||||||
|
// base region, give it that.
|
||||||
|
Some(BlockBody::SingleLayouter(callback)) => {
|
||||||
|
callback.call(engine, locator, styles, pod)?
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a child that wants to layout with full region access,
|
||||||
|
// we layout it.
|
||||||
|
Some(BlockBody::MultiLayouter(callback)) => {
|
||||||
|
let expand = (pod.expand | region.expand) & pod.size.map(Abs::is_finite);
|
||||||
|
let pod = Region { expand, ..pod };
|
||||||
|
callback.call(engine, locator, styles, pod.into())?.into_frame()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Explicit blocks are boundaries for gradient relativeness.
|
||||||
|
if matches!(body, None | Some(BlockBody::Content(_))) {
|
||||||
|
frame.set_kind(FrameKind::Hard);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce a correct frame size on the expanded axes. Do this before
|
||||||
|
// applying the inset, since the pod shrunk.
|
||||||
|
frame.set_size(pod.expand.select(pod.size, frame.size()));
|
||||||
|
|
||||||
|
// Apply the inset.
|
||||||
|
if !inset.is_zero() {
|
||||||
|
crate::pad::grow(&mut frame, &inset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare fill and stroke.
|
||||||
|
let fill = elem.fill(styles);
|
||||||
|
let stroke = elem
|
||||||
|
.stroke(styles)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.map(|s| s.map(Stroke::unwrap_or_default));
|
||||||
|
|
||||||
|
// Only fetch these if necessary (for clipping or filling/stroking).
|
||||||
|
let outset = Lazy::new(|| elem.outset(styles).unwrap_or_default());
|
||||||
|
let radius = Lazy::new(|| elem.radius(styles).unwrap_or_default());
|
||||||
|
|
||||||
|
// Clip the contents, if requested.
|
||||||
|
if elem.clip(styles) {
|
||||||
|
let size = frame.size() + outset.relative_to(frame.size()).sum_by_axis();
|
||||||
|
frame.clip(clip_rect(size, &radius, &stroke));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add fill and/or stroke.
|
||||||
|
if fill.is_some() || stroke.iter().any(Option::is_some) {
|
||||||
|
fill_and_stroke(&mut frame, fill, &stroke, &outset, &radius, elem.span());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign label to each frame in the fragment.
|
||||||
|
if let Some(label) = elem.label() {
|
||||||
|
frame.label(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lay this out as a breakable block.
|
||||||
|
#[typst_macros::time(name = "block", span = elem.span())]
|
||||||
|
pub fn layout_multi_block(
|
||||||
|
elem: &Packed<BlockElem>,
|
||||||
|
engine: &mut Engine,
|
||||||
|
locator: Locator,
|
||||||
|
styles: StyleChain,
|
||||||
|
regions: Regions,
|
||||||
|
) -> SourceResult<Fragment> {
|
||||||
|
// Fetch sizing properties.
|
||||||
|
let width = elem.width(styles);
|
||||||
|
let height = elem.height(styles);
|
||||||
|
let inset = elem.inset(styles).unwrap_or_default();
|
||||||
|
|
||||||
|
// Allocate a small vector for backlogs.
|
||||||
|
let mut buf = SmallVec::<[Abs; 2]>::new();
|
||||||
|
|
||||||
|
// Build the pod regions.
|
||||||
|
let pod = breakable_pod(&width.into(), &height, &inset, styles, regions, &mut buf);
|
||||||
|
|
||||||
|
// Layout the body.
|
||||||
|
let body = elem.body(styles);
|
||||||
|
let mut fragment = match body {
|
||||||
|
// If we have no body, just create one frame plus one per backlog
|
||||||
|
// region. We create them zero-sized; if necessary, their size will
|
||||||
|
// be adjusted below.
|
||||||
|
None => {
|
||||||
|
let mut frames = vec![];
|
||||||
|
frames.push(Frame::hard(Size::zero()));
|
||||||
|
if pod.expand.y {
|
||||||
|
let mut iter = pod;
|
||||||
|
while !iter.backlog.is_empty() {
|
||||||
|
frames.push(Frame::hard(Size::zero()));
|
||||||
|
iter.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Fragment::frames(frames)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have content as our body, just layout it.
|
||||||
|
Some(BlockBody::Content(body)) => {
|
||||||
|
let mut fragment =
|
||||||
|
crate::layout_fragment(engine, body, locator.relayout(), styles, pod)?;
|
||||||
|
|
||||||
|
// If the body is automatically sized and produced more than one
|
||||||
|
// fragment, ensure that the width was consistent across all
|
||||||
|
// regions. If it wasn't, we need to relayout with expansion.
|
||||||
|
if !pod.expand.x
|
||||||
|
&& fragment
|
||||||
|
.as_slice()
|
||||||
|
.windows(2)
|
||||||
|
.any(|w| !w[0].width().approx_eq(w[1].width()))
|
||||||
|
{
|
||||||
|
let max_width =
|
||||||
|
fragment.iter().map(|frame| frame.width()).max().unwrap_or_default();
|
||||||
|
let pod = Regions {
|
||||||
|
size: Size::new(max_width, pod.size.y),
|
||||||
|
expand: Axes::new(true, pod.expand.y),
|
||||||
|
..pod
|
||||||
|
};
|
||||||
|
fragment = crate::layout_fragment(engine, body, locator, styles, pod)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a child that wants to layout with just access to the
|
||||||
|
// base region, give it that.
|
||||||
|
Some(BlockBody::SingleLayouter(callback)) => {
|
||||||
|
let pod = Region::new(pod.base(), pod.expand);
|
||||||
|
callback.call(engine, locator, styles, pod).map(Fragment::frame)?
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a child that wants to layout with full region access,
|
||||||
|
// we layout it.
|
||||||
|
//
|
||||||
|
// For auto-sized multi-layouters, we propagate the outer expansion
|
||||||
|
// so that they can decide for themselves. We also ensure again to
|
||||||
|
// only expand if the size is finite.
|
||||||
|
Some(BlockBody::MultiLayouter(callback)) => {
|
||||||
|
let expand = (pod.expand | regions.expand) & pod.size.map(Abs::is_finite);
|
||||||
|
let pod = Regions { expand, ..pod };
|
||||||
|
callback.call(engine, locator, styles, pod)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prepare fill and stroke.
|
||||||
|
let fill = elem.fill(styles);
|
||||||
|
let stroke = elem
|
||||||
|
.stroke(styles)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.map(|s| s.map(Stroke::unwrap_or_default));
|
||||||
|
|
||||||
|
// Only fetch these if necessary (for clipping or filling/stroking).
|
||||||
|
let outset = Lazy::new(|| elem.outset(styles).unwrap_or_default());
|
||||||
|
let radius = Lazy::new(|| elem.radius(styles).unwrap_or_default());
|
||||||
|
|
||||||
|
// Fetch/compute these outside of the loop.
|
||||||
|
let clip = elem.clip(styles);
|
||||||
|
let has_fill_or_stroke = fill.is_some() || stroke.iter().any(Option::is_some);
|
||||||
|
let has_inset = !inset.is_zero();
|
||||||
|
let is_explicit = matches!(body, None | Some(BlockBody::Content(_)));
|
||||||
|
|
||||||
|
// Skip filling/stroking the first frame if it is empty and a non-empty
|
||||||
|
// one follows.
|
||||||
|
let mut skip_first = false;
|
||||||
|
if let [first, rest @ ..] = fragment.as_slice() {
|
||||||
|
skip_first = has_fill_or_stroke
|
||||||
|
&& first.is_empty()
|
||||||
|
&& rest.iter().any(|frame| !frame.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post-process to apply insets, clipping, fills, and strokes.
|
||||||
|
for (i, (frame, region)) in fragment.iter_mut().zip(pod.iter()).enumerate() {
|
||||||
|
// Explicit blocks are boundaries for gradient relativeness.
|
||||||
|
if is_explicit {
|
||||||
|
frame.set_kind(FrameKind::Hard);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce a correct frame size on the expanded axes. Do this before
|
||||||
|
// applying the inset, since the pod shrunk.
|
||||||
|
frame.set_size(pod.expand.select(region, frame.size()));
|
||||||
|
|
||||||
|
// Apply the inset.
|
||||||
|
if has_inset {
|
||||||
|
crate::pad::grow(frame, &inset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clip the contents, if requested.
|
||||||
|
if clip {
|
||||||
|
let size = frame.size() + outset.relative_to(frame.size()).sum_by_axis();
|
||||||
|
frame.clip(clip_rect(size, &radius, &stroke));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add fill and/or stroke.
|
||||||
|
if has_fill_or_stroke && (i > 0 || !skip_first) {
|
||||||
|
fill_and_stroke(frame, fill.clone(), &stroke, &outset, &radius, elem.span());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign label to each frame in the fragment.
|
||||||
|
if let Some(label) = elem.label() {
|
||||||
|
for frame in fragment.iter_mut() {
|
||||||
|
frame.label(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(fragment)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the pod region for an unbreakable sized container.
|
||||||
|
pub(crate) fn unbreakable_pod(
|
||||||
|
width: &Sizing,
|
||||||
|
height: &Sizing,
|
||||||
|
inset: &Sides<Rel<Abs>>,
|
||||||
|
styles: StyleChain,
|
||||||
|
base: Size,
|
||||||
|
) -> Region {
|
||||||
|
// Resolve the size.
|
||||||
|
let mut size = Size::new(
|
||||||
|
match width {
|
||||||
|
// - For auto, the whole region is available.
|
||||||
|
// - Fr is handled outside and already factored into the `region`,
|
||||||
|
// so we can treat it equivalently to 100%.
|
||||||
|
Sizing::Auto | Sizing::Fr(_) => base.x,
|
||||||
|
// Resolve the relative sizing.
|
||||||
|
Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.x),
|
||||||
|
},
|
||||||
|
match height {
|
||||||
|
Sizing::Auto | Sizing::Fr(_) => base.y,
|
||||||
|
Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.y),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Take the inset, if any, into account.
|
||||||
|
if !inset.is_zero() {
|
||||||
|
size = crate::pad::shrink(size, inset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the child is manually, the size is forced and we should enable
|
||||||
|
// expansion.
|
||||||
|
let expand = Axes::new(
|
||||||
|
*width != Sizing::Auto && size.x.is_finite(),
|
||||||
|
*height != Sizing::Auto && size.y.is_finite(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Region::new(size, expand)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the pod regions for a breakable sized container.
|
||||||
|
fn breakable_pod<'a>(
|
||||||
|
width: &Sizing,
|
||||||
|
height: &Sizing,
|
||||||
|
inset: &Sides<Rel<Abs>>,
|
||||||
|
styles: StyleChain,
|
||||||
|
regions: Regions,
|
||||||
|
buf: &'a mut SmallVec<[Abs; 2]>,
|
||||||
|
) -> Regions<'a> {
|
||||||
|
let base = regions.base();
|
||||||
|
|
||||||
|
// The vertical region sizes we're about to build.
|
||||||
|
let first;
|
||||||
|
let full;
|
||||||
|
let backlog: &mut [Abs];
|
||||||
|
let last;
|
||||||
|
|
||||||
|
// If the block has a fixed height, things are very different, so we
|
||||||
|
// handle that case completely separately.
|
||||||
|
match height {
|
||||||
|
Sizing::Auto | Sizing::Fr(_) => {
|
||||||
|
// If the block is automatically sized, we can just inherit the
|
||||||
|
// regions.
|
||||||
|
first = regions.size.y;
|
||||||
|
full = regions.full;
|
||||||
|
buf.extend_from_slice(regions.backlog);
|
||||||
|
backlog = buf;
|
||||||
|
last = regions.last;
|
||||||
|
}
|
||||||
|
|
||||||
|
Sizing::Rel(rel) => {
|
||||||
|
// Resolve the sizing to a concrete size.
|
||||||
|
let resolved = rel.resolve(styles).relative_to(base.y);
|
||||||
|
|
||||||
|
// Since we're manually sized, the resolved size is the base height.
|
||||||
|
full = resolved;
|
||||||
|
|
||||||
|
// Distribute the fixed height across a start region and a backlog.
|
||||||
|
(first, backlog) = distribute(resolved, regions, buf);
|
||||||
|
|
||||||
|
// If the height is manually sized, we don't want a final repeatable
|
||||||
|
// region.
|
||||||
|
last = None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resolve the horizontal sizing to a concrete width and combine
|
||||||
|
// `width` and `first` into `size`.
|
||||||
|
let mut size = Size::new(
|
||||||
|
match width {
|
||||||
|
Sizing::Auto | Sizing::Fr(_) => regions.size.x,
|
||||||
|
Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.x),
|
||||||
|
},
|
||||||
|
first,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Take the inset, if any, into account, applying it to the
|
||||||
|
// individual region components.
|
||||||
|
let (mut full, mut last) = (full, last);
|
||||||
|
if !inset.is_zero() {
|
||||||
|
crate::pad::shrink_multiple(&mut size, &mut full, backlog, &mut last, inset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the child is manually, the size is forced and we should enable
|
||||||
|
// expansion.
|
||||||
|
let expand = Axes::new(
|
||||||
|
*width != Sizing::Auto && size.x.is_finite(),
|
||||||
|
*height != Sizing::Auto && size.y.is_finite(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Regions { size, full, backlog, last, expand }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Distribute a fixed height spread over existing regions into a new first
|
||||||
|
/// height and a new backlog.
|
||||||
|
fn distribute<'a>(
|
||||||
|
height: Abs,
|
||||||
|
mut regions: Regions,
|
||||||
|
buf: &'a mut SmallVec<[Abs; 2]>,
|
||||||
|
) -> (Abs, &'a mut [Abs]) {
|
||||||
|
// Build new region heights from old regions.
|
||||||
|
let mut remaining = height;
|
||||||
|
loop {
|
||||||
|
let limited = regions.size.y.clamp(Abs::zero(), remaining);
|
||||||
|
buf.push(limited);
|
||||||
|
remaining -= limited;
|
||||||
|
if remaining.approx_empty()
|
||||||
|
|| !regions.may_break()
|
||||||
|
|| (!regions.may_progress() && limited.approx_empty())
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
regions.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is still something remaining, apply it to the
|
||||||
|
// last region (it will overflow, but there's nothing else
|
||||||
|
// we can do).
|
||||||
|
if !remaining.approx_empty() {
|
||||||
|
if let Some(last) = buf.last_mut() {
|
||||||
|
*last += remaining;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distribute the heights to the first region and the
|
||||||
|
// backlog. There is no last region, since the height is
|
||||||
|
// fixed.
|
||||||
|
(buf[0], &mut buf[1..])
|
||||||
|
}
|
@ -6,22 +6,23 @@ use bumpalo::boxed::Box as BumpBox;
|
|||||||
use bumpalo::Bump;
|
use bumpalo::Bump;
|
||||||
use comemo::{Track, Tracked, TrackedMut};
|
use comemo::{Track, Tracked, TrackedMut};
|
||||||
use once_cell::unsync::Lazy;
|
use once_cell::unsync::Lazy;
|
||||||
|
use typst_library::diag::{bail, SourceResult};
|
||||||
use crate::diag::{bail, SourceResult};
|
use typst_library::engine::{Engine, Route, Sink, Traced};
|
||||||
use crate::engine::{Engine, Route, Sink, Traced};
|
use typst_library::foundations::{Packed, Resolve, Smart, StyleChain};
|
||||||
use crate::foundations::{Packed, Resolve, Smart, StyleChain};
|
use typst_library::introspection::{
|
||||||
use crate::introspection::{
|
|
||||||
Introspector, Location, Locator, LocatorLink, SplitLocator, Tag, TagElem,
|
Introspector, Location, Locator, LocatorLink, SplitLocator, Tag, TagElem,
|
||||||
};
|
};
|
||||||
use crate::layout::{
|
use typst_library::layout::{
|
||||||
layout_frame, Abs, AlignElem, Alignment, Axes, BlockElem, ColbreakElem,
|
Abs, AlignElem, Alignment, Axes, BlockElem, ColbreakElem, FixedAlignment, FlushElem,
|
||||||
FixedAlignment, FlushElem, Fr, Fragment, Frame, PagebreakElem, PlaceElem,
|
Fr, Fragment, Frame, PagebreakElem, PlaceElem, PlacementScope, Ratio, Region,
|
||||||
PlacementScope, Ratio, Region, Regions, Rel, Size, Sizing, Spacing, VElem,
|
Regions, Rel, Size, Sizing, Spacing, VElem,
|
||||||
};
|
};
|
||||||
use crate::model::ParElem;
|
use typst_library::model::ParElem;
|
||||||
use crate::realize::Pair;
|
use typst_library::routines::{Pair, Routines};
|
||||||
use crate::text::TextElem;
|
use typst_library::text::TextElem;
|
||||||
use crate::World;
|
use typst_library::World;
|
||||||
|
|
||||||
|
use super::{layout_multi_block, layout_single_block};
|
||||||
|
|
||||||
/// Collects all elements of the flow into prepared children. These are much
|
/// Collects all elements of the flow into prepared children. These are much
|
||||||
/// simpler to handle than the raw elements.
|
/// simpler to handle than the raw elements.
|
||||||
@ -110,7 +111,7 @@ impl<'a> Collector<'a, '_, '_> {
|
|||||||
let spacing = ParElem::spacing_in(styles);
|
let spacing = ParElem::spacing_in(styles);
|
||||||
let costs = TextElem::costs_in(styles);
|
let costs = TextElem::costs_in(styles);
|
||||||
|
|
||||||
let lines = crate::layout::layout_inline(
|
let lines = crate::layout_inline(
|
||||||
self.engine,
|
self.engine,
|
||||||
&elem.children,
|
&elem.children,
|
||||||
self.locator.next(&elem.span()),
|
self.locator.next(&elem.span()),
|
||||||
@ -332,6 +333,7 @@ impl SingleChild<'_> {
|
|||||||
// Vertical expansion is only kept if this block is the only child.
|
// Vertical expansion is only kept if this block is the only child.
|
||||||
region.expand.y &= self.alone;
|
region.expand.y &= self.alone;
|
||||||
layout_single_impl(
|
layout_single_impl(
|
||||||
|
engine.routines,
|
||||||
engine.world,
|
engine.world,
|
||||||
engine.introspector,
|
engine.introspector,
|
||||||
engine.traced,
|
engine.traced,
|
||||||
@ -350,6 +352,7 @@ impl SingleChild<'_> {
|
|||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn layout_single_impl(
|
fn layout_single_impl(
|
||||||
|
routines: &Routines,
|
||||||
world: Tracked<dyn World + '_>,
|
world: Tracked<dyn World + '_>,
|
||||||
introspector: Tracked<Introspector>,
|
introspector: Tracked<Introspector>,
|
||||||
traced: Tracked<Traced>,
|
traced: Tracked<Traced>,
|
||||||
@ -363,6 +366,7 @@ fn layout_single_impl(
|
|||||||
let link = LocatorLink::new(locator);
|
let link = LocatorLink::new(locator);
|
||||||
let locator = Locator::link(&link);
|
let locator = Locator::link(&link);
|
||||||
let mut engine = Engine {
|
let mut engine = Engine {
|
||||||
|
routines,
|
||||||
world,
|
world,
|
||||||
introspector,
|
introspector,
|
||||||
traced,
|
traced,
|
||||||
@ -370,7 +374,7 @@ fn layout_single_impl(
|
|||||||
route: Route::extend(route),
|
route: Route::extend(route),
|
||||||
};
|
};
|
||||||
|
|
||||||
elem.layout_single(&mut engine, locator, styles, region)
|
layout_single_block(elem, &mut engine, locator, styles, region)
|
||||||
.map(|frame| frame.post_processed(styles))
|
.map(|frame| frame.post_processed(styles))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -425,6 +429,7 @@ impl<'a> MultiChild<'a> {
|
|||||||
// Vertical expansion is only kept if this block is the only child.
|
// Vertical expansion is only kept if this block is the only child.
|
||||||
regions.expand.y &= self.alone;
|
regions.expand.y &= self.alone;
|
||||||
layout_multi_impl(
|
layout_multi_impl(
|
||||||
|
engine.routines,
|
||||||
engine.world,
|
engine.world,
|
||||||
engine.introspector,
|
engine.introspector,
|
||||||
engine.traced,
|
engine.traced,
|
||||||
@ -443,6 +448,7 @@ impl<'a> MultiChild<'a> {
|
|||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn layout_multi_impl(
|
fn layout_multi_impl(
|
||||||
|
routines: &Routines,
|
||||||
world: Tracked<dyn World + '_>,
|
world: Tracked<dyn World + '_>,
|
||||||
introspector: Tracked<Introspector>,
|
introspector: Tracked<Introspector>,
|
||||||
traced: Tracked<Traced>,
|
traced: Tracked<Traced>,
|
||||||
@ -456,6 +462,7 @@ fn layout_multi_impl(
|
|||||||
let link = LocatorLink::new(locator);
|
let link = LocatorLink::new(locator);
|
||||||
let locator = Locator::link(&link);
|
let locator = Locator::link(&link);
|
||||||
let mut engine = Engine {
|
let mut engine = Engine {
|
||||||
|
routines,
|
||||||
world,
|
world,
|
||||||
introspector,
|
introspector,
|
||||||
traced,
|
traced,
|
||||||
@ -463,8 +470,7 @@ fn layout_multi_impl(
|
|||||||
route: Route::extend(route),
|
route: Route::extend(route),
|
||||||
};
|
};
|
||||||
|
|
||||||
elem.layout_multiple(&mut engine, locator, styles, regions)
|
layout_multi_block(elem, &mut engine, locator, styles, regions).map(|mut fragment| {
|
||||||
.map(|mut fragment| {
|
|
||||||
for frame in &mut fragment {
|
for frame in &mut fragment {
|
||||||
frame.post_process(styles);
|
frame.post_process(styles);
|
||||||
}
|
}
|
||||||
@ -571,7 +577,7 @@ impl PlacedChild<'_> {
|
|||||||
let align = self.alignment.unwrap_or_else(|| Alignment::CENTER);
|
let align = self.alignment.unwrap_or_else(|| Alignment::CENTER);
|
||||||
let aligned = AlignElem::set_alignment(align).wrap();
|
let aligned = AlignElem::set_alignment(align).wrap();
|
||||||
|
|
||||||
let mut frame = layout_frame(
|
let mut frame = crate::layout_frame(
|
||||||
engine,
|
engine,
|
||||||
&self.elem.body,
|
&self.elem.body,
|
||||||
self.locator.relayout(),
|
self.locator.relayout(),
|
||||||
@ -614,7 +620,7 @@ impl<T> CachedCell<T> {
|
|||||||
T: Clone,
|
T: Clone,
|
||||||
F: FnOnce(I) -> T,
|
F: FnOnce(I) -> T,
|
||||||
{
|
{
|
||||||
let input_hash = crate::utils::hash128(&input);
|
let input_hash = typst_utils::hash128(&input);
|
||||||
|
|
||||||
let mut slot = self.0.borrow_mut();
|
let mut slot = self.0.borrow_mut();
|
||||||
if let Some((hash, output)) = &*slot {
|
if let Some((hash, output)) = &*slot {
|
@ -1,22 +1,23 @@
|
|||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
use super::{distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Stop, Work};
|
use typst_library::diag::SourceResult;
|
||||||
use crate::diag::SourceResult;
|
use typst_library::engine::Engine;
|
||||||
use crate::engine::Engine;
|
use typst_library::foundations::{Content, NativeElement, Packed, Resolve, Smart};
|
||||||
use crate::foundations::{Content, NativeElement, Packed, Resolve, Smart};
|
use typst_library::introspection::{
|
||||||
use crate::introspection::{
|
|
||||||
Counter, CounterDisplayElem, CounterState, CounterUpdate, Location, Locator,
|
Counter, CounterDisplayElem, CounterState, CounterUpdate, Location, Locator,
|
||||||
SplitLocator, Tag,
|
SplitLocator, Tag,
|
||||||
};
|
};
|
||||||
use crate::layout::{
|
use typst_library::layout::{
|
||||||
layout_fragment, layout_frame, Abs, Axes, Dir, FixedAlignment, Fragment, Frame,
|
Abs, Axes, Dir, FixedAlignment, Fragment, Frame, FrameItem, OuterHAlignment,
|
||||||
FrameItem, OuterHAlignment, PlacementScope, Point, Region, Regions, Rel, Size,
|
PlacementScope, Point, Region, Regions, Rel, Size,
|
||||||
};
|
};
|
||||||
use crate::model::{
|
use typst_library::model::{
|
||||||
FootnoteElem, FootnoteEntry, LineNumberingScope, Numbering, ParLineMarker,
|
FootnoteElem, FootnoteEntry, LineNumberingScope, Numbering, ParLineMarker,
|
||||||
};
|
};
|
||||||
use crate::syntax::Span;
|
use typst_syntax::Span;
|
||||||
use crate::utils::NonZeroExt;
|
use typst_utils::NonZeroExt;
|
||||||
|
|
||||||
|
use super::{distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Stop, Work};
|
||||||
|
|
||||||
/// Composes the contents of a single page/region. A region can have multiple
|
/// Composes the contents of a single page/region. A region can have multiple
|
||||||
/// columns/subregions.
|
/// columns/subregions.
|
||||||
@ -517,7 +518,7 @@ fn layout_footnote_separator(
|
|||||||
config: &Config,
|
config: &Config,
|
||||||
base: Size,
|
base: Size,
|
||||||
) -> SourceResult<Frame> {
|
) -> SourceResult<Frame> {
|
||||||
layout_frame(
|
crate::layout_frame(
|
||||||
engine,
|
engine,
|
||||||
&config.footnote.separator,
|
&config.footnote.separator,
|
||||||
Locator::root(),
|
Locator::root(),
|
||||||
@ -534,7 +535,7 @@ fn layout_footnote(
|
|||||||
pod: Regions,
|
pod: Regions,
|
||||||
) -> SourceResult<Fragment> {
|
) -> SourceResult<Fragment> {
|
||||||
let loc = elem.location().unwrap();
|
let loc = elem.location().unwrap();
|
||||||
layout_fragment(
|
crate::layout_fragment(
|
||||||
engine,
|
engine,
|
||||||
&FootnoteEntry::new(elem.clone()).pack(),
|
&FootnoteEntry::new(elem.clone()).pack(),
|
||||||
Locator::synthesize(loc),
|
Locator::synthesize(loc),
|
||||||
@ -785,7 +786,7 @@ fn layout_line_number_reset(
|
|||||||
let counter = Counter::of(ParLineMarker::elem());
|
let counter = Counter::of(ParLineMarker::elem());
|
||||||
let update = CounterUpdate::Set(CounterState::init(false));
|
let update = CounterUpdate::Set(CounterState::init(false));
|
||||||
let content = counter.update(Span::detached(), update);
|
let content = counter.update(Span::detached(), update);
|
||||||
layout_frame(
|
crate::layout_frame(
|
||||||
engine,
|
engine,
|
||||||
&content,
|
&content,
|
||||||
locator.next(&()),
|
locator.next(&()),
|
||||||
@ -821,7 +822,7 @@ fn layout_line_number(
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Layout the number.
|
// Layout the number.
|
||||||
let mut frame = layout_frame(
|
let mut frame = crate::layout_frame(
|
||||||
engine,
|
engine,
|
||||||
&content,
|
&content,
|
||||||
locator.next(&()),
|
locator.next(&()),
|
@ -1,12 +1,13 @@
|
|||||||
|
use typst_library::introspection::Tag;
|
||||||
|
use typst_library::layout::{
|
||||||
|
Abs, Axes, FixedAlignment, Fr, Frame, FrameItem, Point, Region, Regions, Rel, Size,
|
||||||
|
};
|
||||||
|
use typst_utils::Numeric;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
Child, Composer, FlowResult, LineChild, MultiChild, MultiSpill, PlacedChild,
|
Child, Composer, FlowResult, LineChild, MultiChild, MultiSpill, PlacedChild,
|
||||||
SingleChild, Stop, Work,
|
SingleChild, Stop, Work,
|
||||||
};
|
};
|
||||||
use crate::introspection::Tag;
|
|
||||||
use crate::layout::{
|
|
||||||
Abs, Axes, FixedAlignment, Fr, Frame, FrameItem, Point, Region, Regions, Rel, Size,
|
|
||||||
};
|
|
||||||
use crate::utils::Numeric;
|
|
||||||
|
|
||||||
/// Distributes as many children as fit from `composer.work` into the first
|
/// Distributes as many children as fit from `composer.work` into the first
|
||||||
/// region and returns the resulting frame.
|
/// region and returns the resulting frame.
|
@ -1,9 +1,12 @@
|
|||||||
//! Layout of content into a [`Frame`] or [`Fragment`].
|
//! Layout of content into a [`Frame`] or [`Fragment`].
|
||||||
|
|
||||||
|
mod block;
|
||||||
mod collect;
|
mod collect;
|
||||||
mod compose;
|
mod compose;
|
||||||
mod distribute;
|
mod distribute;
|
||||||
|
|
||||||
|
pub(crate) use self::block::unbreakable_pod;
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
@ -11,26 +14,40 @@ use std::rc::Rc;
|
|||||||
use bumpalo::Bump;
|
use bumpalo::Bump;
|
||||||
use comemo::{Track, Tracked, TrackedMut};
|
use comemo::{Track, Tracked, TrackedMut};
|
||||||
use ecow::EcoVec;
|
use ecow::EcoVec;
|
||||||
|
use typst_library::diag::{bail, At, SourceDiagnostic, SourceResult};
|
||||||
|
use typst_library::engine::{Engine, Route, Sink, Traced};
|
||||||
|
use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
|
||||||
|
use typst_library::introspection::{
|
||||||
|
Introspector, Location, Locator, LocatorLink, SplitLocator, Tag,
|
||||||
|
};
|
||||||
|
use typst_library::layout::{
|
||||||
|
Abs, ColumnsElem, Dir, Em, Fragment, Frame, PageElem, PlacementScope, Region,
|
||||||
|
Regions, Rel, Size,
|
||||||
|
};
|
||||||
|
use typst_library::model::{FootnoteElem, FootnoteEntry, LineNumberingScope, ParLine};
|
||||||
|
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
|
||||||
|
use typst_library::text::TextElem;
|
||||||
|
use typst_library::World;
|
||||||
|
use typst_utils::{NonZeroExt, Numeric};
|
||||||
|
|
||||||
|
use self::block::{layout_multi_block, layout_single_block};
|
||||||
use self::collect::{
|
use self::collect::{
|
||||||
collect, Child, LineChild, MultiChild, MultiSpill, PlacedChild, SingleChild,
|
collect, Child, LineChild, MultiChild, MultiSpill, PlacedChild, SingleChild,
|
||||||
};
|
};
|
||||||
use self::compose::{compose, Composer};
|
use self::compose::{compose, Composer};
|
||||||
use self::distribute::distribute;
|
use self::distribute::distribute;
|
||||||
use crate::diag::{bail, At, SourceDiagnostic, SourceResult};
|
|
||||||
use crate::engine::{Engine, Route, Sink, Traced};
|
/// Lays out content into a single region, producing a single frame.
|
||||||
use crate::foundations::{Content, Packed, Resolve, StyleChain};
|
pub fn layout_frame(
|
||||||
use crate::introspection::{
|
engine: &mut Engine,
|
||||||
Introspector, Location, Locator, LocatorLink, SplitLocator, Tag,
|
content: &Content,
|
||||||
};
|
locator: Locator,
|
||||||
use crate::layout::{
|
styles: StyleChain,
|
||||||
Abs, Dir, Em, Fragment, Frame, PageElem, PlacementScope, Region, Regions, Rel, Size,
|
region: Region,
|
||||||
};
|
) -> SourceResult<Frame> {
|
||||||
use crate::model::{FootnoteElem, FootnoteEntry, LineNumberingScope, ParLine};
|
layout_fragment(engine, content, locator, styles, region.into())
|
||||||
use crate::realize::{realize, Arenas, Pair, RealizationKind};
|
.map(Fragment::into_frame)
|
||||||
use crate::text::TextElem;
|
}
|
||||||
use crate::utils::{NonZeroExt, Numeric};
|
|
||||||
use crate::World;
|
|
||||||
|
|
||||||
/// Lays out content into multiple regions.
|
/// Lays out content into multiple regions.
|
||||||
///
|
///
|
||||||
@ -43,6 +60,7 @@ pub fn layout_fragment(
|
|||||||
regions: Regions,
|
regions: Regions,
|
||||||
) -> SourceResult<Fragment> {
|
) -> SourceResult<Fragment> {
|
||||||
layout_fragment_impl(
|
layout_fragment_impl(
|
||||||
|
engine.routines,
|
||||||
engine.world,
|
engine.world,
|
||||||
engine.introspector,
|
engine.introspector,
|
||||||
engine.traced,
|
engine.traced,
|
||||||
@ -57,50 +75,39 @@ pub fn layout_fragment(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lays out content into regions with columns.
|
/// Layout the columns.
|
||||||
///
|
///
|
||||||
/// This is different from just laying out into column-sized regions as the
|
/// This is different from just laying out into column-sized regions as the
|
||||||
/// columns can interact due to parent-scoped placed elements.
|
/// columns can interact due to parent-scoped placed elements.
|
||||||
pub fn layout_fragment_with_columns(
|
#[typst_macros::time(span = elem.span())]
|
||||||
|
pub fn layout_columns(
|
||||||
|
elem: &Packed<ColumnsElem>,
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
content: &Content,
|
|
||||||
locator: Locator,
|
locator: Locator,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
regions: Regions,
|
regions: Regions,
|
||||||
count: NonZeroUsize,
|
|
||||||
gutter: Rel<Abs>,
|
|
||||||
) -> SourceResult<Fragment> {
|
) -> SourceResult<Fragment> {
|
||||||
layout_fragment_impl(
|
layout_fragment_impl(
|
||||||
|
engine.routines,
|
||||||
engine.world,
|
engine.world,
|
||||||
engine.introspector,
|
engine.introspector,
|
||||||
engine.traced,
|
engine.traced,
|
||||||
TrackedMut::reborrow_mut(&mut engine.sink),
|
TrackedMut::reborrow_mut(&mut engine.sink),
|
||||||
engine.route.track(),
|
engine.route.track(),
|
||||||
content,
|
&elem.body,
|
||||||
locator.track(),
|
locator.track(),
|
||||||
styles,
|
styles,
|
||||||
regions,
|
regions,
|
||||||
count,
|
elem.count(styles),
|
||||||
gutter,
|
elem.gutter(styles),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lays out content into a single region, producing a single frame.
|
|
||||||
pub fn layout_frame(
|
|
||||||
engine: &mut Engine,
|
|
||||||
content: &Content,
|
|
||||||
locator: Locator,
|
|
||||||
styles: StyleChain,
|
|
||||||
region: Region,
|
|
||||||
) -> SourceResult<Frame> {
|
|
||||||
layout_fragment(engine, content, locator, styles, region.into())
|
|
||||||
.map(Fragment::into_frame)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The cached, internal implementation of [`layout_fragment`].
|
/// The cached, internal implementation of [`layout_fragment`].
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn layout_fragment_impl(
|
fn layout_fragment_impl(
|
||||||
|
routines: &Routines,
|
||||||
world: Tracked<dyn World + '_>,
|
world: Tracked<dyn World + '_>,
|
||||||
introspector: Tracked<Introspector>,
|
introspector: Tracked<Introspector>,
|
||||||
traced: Tracked<Traced>,
|
traced: Tracked<Traced>,
|
||||||
@ -123,6 +130,7 @@ fn layout_fragment_impl(
|
|||||||
let link = LocatorLink::new(locator);
|
let link = LocatorLink::new(locator);
|
||||||
let mut locator = Locator::link(&link).split();
|
let mut locator = Locator::link(&link).split();
|
||||||
let mut engine = Engine {
|
let mut engine = Engine {
|
||||||
|
routines,
|
||||||
world,
|
world,
|
||||||
introspector,
|
introspector,
|
||||||
traced,
|
traced,
|
||||||
@ -133,7 +141,7 @@ fn layout_fragment_impl(
|
|||||||
engine.route.check_layout_depth().at(content.span())?;
|
engine.route.check_layout_depth().at(content.span())?;
|
||||||
|
|
||||||
let arenas = Arenas::default();
|
let arenas = Arenas::default();
|
||||||
let children = realize(
|
let children = (engine.routines.realize)(
|
||||||
RealizationKind::Container,
|
RealizationKind::Container,
|
||||||
&mut engine,
|
&mut engine,
|
||||||
&mut locator,
|
&mut locator,
|
@ -1,161 +1,89 @@
|
|||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use comemo::Track;
|
|
||||||
use ecow::eco_format;
|
use ecow::eco_format;
|
||||||
|
use typst_library::diag::{bail, At, Hint, HintedStrResult, HintedString, SourceResult};
|
||||||
use super::lines::Line;
|
use typst_library::engine::Engine;
|
||||||
use super::repeated::{Footer, Header, Repeatable};
|
use typst_library::foundations::{Content, Smart, StyleChain};
|
||||||
use crate::diag::{bail, At, Hint, HintedStrResult, HintedString, SourceResult};
|
use typst_library::introspection::Locator;
|
||||||
use crate::engine::Engine;
|
use typst_library::layout::{
|
||||||
use crate::foundations::{
|
Abs, Alignment, Axes, Celled, Fragment, Length, Regions, Rel, ResolvedCelled, Sides,
|
||||||
Array, CastInfo, Content, Context, Fold, FromValue, Func, IntoValue, Reflect,
|
Sizing,
|
||||||
Resolve, Smart, StyleChain, Value,
|
|
||||||
};
|
};
|
||||||
use crate::introspection::Locator;
|
use typst_library::visualize::{Paint, Stroke};
|
||||||
use crate::layout::{
|
use typst_syntax::Span;
|
||||||
layout_fragment, Abs, Alignment, Axes, Fragment, Length, LinePosition, Regions, Rel,
|
use typst_utils::NonZeroExt;
|
||||||
Sides, Sizing,
|
|
||||||
};
|
|
||||||
use crate::syntax::Span;
|
|
||||||
use crate::utils::NonZeroExt;
|
|
||||||
use crate::visualize::{Paint, Stroke};
|
|
||||||
|
|
||||||
/// A value that can be configured per cell.
|
use super::{Footer, Header, Line, Repeatable};
|
||||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
|
||||||
pub enum Celled<T> {
|
|
||||||
/// A bare value, the same for all cells.
|
|
||||||
Value(T),
|
|
||||||
/// A closure mapping from cell coordinates to a value.
|
|
||||||
Func(Func),
|
|
||||||
/// An array of alignment values corresponding to each column.
|
|
||||||
Array(Vec<T>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Default + Clone + FromValue> Celled<T> {
|
/// Used for cell-like elements which are aware of their final properties in
|
||||||
/// Resolve the value based on the cell position.
|
/// the table, and may have property overrides.
|
||||||
pub fn resolve(
|
pub trait ResolvableCell {
|
||||||
&self,
|
/// Resolves the cell's fields, given its coordinates and default grid-wide
|
||||||
engine: &mut Engine,
|
/// fill, align, inset and stroke properties, plus the expected value of
|
||||||
styles: StyleChain,
|
/// the `breakable` field.
|
||||||
|
/// Returns a final Cell.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn resolve_cell<'a>(
|
||||||
|
self,
|
||||||
x: usize,
|
x: usize,
|
||||||
y: usize,
|
y: usize,
|
||||||
) -> SourceResult<T> {
|
fill: &Option<Paint>,
|
||||||
Ok(match self {
|
align: Smart<Alignment>,
|
||||||
Self::Value(value) => value.clone(),
|
inset: Sides<Option<Rel<Length>>>,
|
||||||
Self::Func(func) => func
|
stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>,
|
||||||
.call(engine, Context::new(None, Some(styles)).track(), [x, y])?
|
breakable: bool,
|
||||||
.cast()
|
locator: Locator<'a>,
|
||||||
.at(func.span())?,
|
|
||||||
Self::Array(array) => x
|
|
||||||
.checked_rem(array.len())
|
|
||||||
.and_then(|i| array.get(i))
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Default> Default for Celled<T> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Value(T::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Reflect> Reflect for Celled<T> {
|
|
||||||
fn input() -> CastInfo {
|
|
||||||
T::input() + Array::input() + Func::input()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn output() -> CastInfo {
|
|
||||||
T::output() + Array::output() + Func::output()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn castable(value: &Value) -> bool {
|
|
||||||
Array::castable(value) || Func::castable(value) || T::castable(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: IntoValue> IntoValue for Celled<T> {
|
|
||||||
fn into_value(self) -> Value {
|
|
||||||
match self {
|
|
||||||
Self::Value(value) => value.into_value(),
|
|
||||||
Self::Func(func) => func.into_value(),
|
|
||||||
Self::Array(arr) => arr.into_value(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: FromValue> FromValue for Celled<T> {
|
|
||||||
fn from_value(value: Value) -> HintedStrResult<Self> {
|
|
||||||
match value {
|
|
||||||
Value::Func(v) => Ok(Self::Func(v)),
|
|
||||||
Value::Array(array) => Ok(Self::Array(
|
|
||||||
array.into_iter().map(T::from_value).collect::<HintedStrResult<_>>()?,
|
|
||||||
)),
|
|
||||||
v if T::castable(&v) => Ok(Self::Value(T::from_value(v)?)),
|
|
||||||
v => Err(Self::error(&v)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Fold> Fold for Celled<T> {
|
|
||||||
fn fold(self, outer: Self) -> Self {
|
|
||||||
match (self, outer) {
|
|
||||||
(Self::Value(inner), Self::Value(outer)) => Self::Value(inner.fold(outer)),
|
|
||||||
(self_, _) => self_,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Resolve> Resolve for Celled<T> {
|
|
||||||
type Output = ResolvedCelled<T>;
|
|
||||||
|
|
||||||
fn resolve(self, styles: StyleChain) -> Self::Output {
|
|
||||||
match self {
|
|
||||||
Self::Value(value) => ResolvedCelled(Celled::Value(value.resolve(styles))),
|
|
||||||
Self::Func(func) => ResolvedCelled(Celled::Func(func)),
|
|
||||||
Self::Array(values) => ResolvedCelled(Celled::Array(
|
|
||||||
values.into_iter().map(|value| value.resolve(styles)).collect(),
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The result of resolving a Celled's value according to styles.
|
|
||||||
/// Holds resolved values which depend on each grid cell's position.
|
|
||||||
/// When it is a closure, however, it is only resolved when the closure is
|
|
||||||
/// called.
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
pub struct ResolvedCelled<T: Resolve>(Celled<T::Output>);
|
|
||||||
|
|
||||||
impl<T> ResolvedCelled<T>
|
|
||||||
where
|
|
||||||
T: FromValue + Resolve,
|
|
||||||
<T as Resolve>::Output: Default + Clone,
|
|
||||||
{
|
|
||||||
/// Resolve the value based on the cell position.
|
|
||||||
pub fn resolve(
|
|
||||||
&self,
|
|
||||||
engine: &mut Engine,
|
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
x: usize,
|
) -> Cell<'a>;
|
||||||
y: usize,
|
|
||||||
) -> SourceResult<T::Output> {
|
/// Returns this cell's column override.
|
||||||
Ok(match &self.0 {
|
fn x(&self, styles: StyleChain) -> Smart<usize>;
|
||||||
Celled::Value(value) => value.clone(),
|
|
||||||
Celled::Func(func) => func
|
/// Returns this cell's row override.
|
||||||
.call(engine, Context::new(None, Some(styles)).track(), [x, y])?
|
fn y(&self, styles: StyleChain) -> Smart<usize>;
|
||||||
.cast::<T>()
|
|
||||||
.at(func.span())?
|
/// The amount of columns spanned by this cell.
|
||||||
.resolve(styles),
|
fn colspan(&self, styles: StyleChain) -> NonZeroUsize;
|
||||||
Celled::Array(array) => x
|
|
||||||
.checked_rem(array.len())
|
/// The amount of rows spanned by this cell.
|
||||||
.and_then(|i| array.get(i))
|
fn rowspan(&self, styles: StyleChain) -> NonZeroUsize;
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default(),
|
/// The cell's span, for errors.
|
||||||
})
|
fn span(&self) -> Span;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A grid item, possibly affected by automatic cell positioning. Can be either
|
||||||
|
/// a line or a cell.
|
||||||
|
pub enum ResolvableGridItem<T: ResolvableCell> {
|
||||||
|
/// A horizontal line in the grid.
|
||||||
|
HLine {
|
||||||
|
/// The row above which the horizontal line is drawn.
|
||||||
|
y: Smart<usize>,
|
||||||
|
start: usize,
|
||||||
|
end: Option<NonZeroUsize>,
|
||||||
|
stroke: Option<Arc<Stroke<Abs>>>,
|
||||||
|
/// The span of the corresponding line element.
|
||||||
|
span: Span,
|
||||||
|
/// The line's position. "before" here means on top of row `y`, while
|
||||||
|
/// "after" means below it.
|
||||||
|
position: LinePosition,
|
||||||
|
},
|
||||||
|
/// A vertical line in the grid.
|
||||||
|
VLine {
|
||||||
|
/// The column before which the vertical line is drawn.
|
||||||
|
x: Smart<usize>,
|
||||||
|
start: usize,
|
||||||
|
end: Option<NonZeroUsize>,
|
||||||
|
stroke: Option<Arc<Stroke<Abs>>>,
|
||||||
|
/// The span of the corresponding line element.
|
||||||
|
span: Span,
|
||||||
|
/// The line's position. "before" here means to the left of column `x`,
|
||||||
|
/// while "after" means to its right (both considering LTR).
|
||||||
|
position: LinePosition,
|
||||||
|
},
|
||||||
|
/// A cell in the grid.
|
||||||
|
Cell(T),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents a cell in CellGrid, to be laid out by GridLayouter.
|
/// Represents a cell in CellGrid, to be laid out by GridLayouter.
|
||||||
@ -221,12 +149,24 @@ impl<'a> Cell<'a> {
|
|||||||
if disambiguator > 0 {
|
if disambiguator > 0 {
|
||||||
locator = locator.split().next_inner(disambiguator as u128);
|
locator = locator.split().next_inner(disambiguator as u128);
|
||||||
}
|
}
|
||||||
layout_fragment(engine, &self.body, locator, styles, regions)
|
crate::layout_fragment(engine, &self.body, locator, styles, regions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Indicates whether the line should be drawn before or after the track with
|
||||||
|
/// its index. This is mostly only relevant when gutter is used, since, then,
|
||||||
|
/// the position after a track is not the same as before the next
|
||||||
|
/// non-gutter track.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||||
|
pub enum LinePosition {
|
||||||
|
/// The line should be drawn before its track (e.g. hline on top of a row).
|
||||||
|
Before,
|
||||||
|
/// The line should be drawn after its track (e.g. hline below a row).
|
||||||
|
After,
|
||||||
|
}
|
||||||
|
|
||||||
/// A grid entry.
|
/// A grid entry.
|
||||||
pub(super) enum Entry<'a> {
|
pub enum Entry<'a> {
|
||||||
/// An entry which holds a cell.
|
/// An entry which holds a cell.
|
||||||
Cell(Cell<'a>),
|
Cell(Cell<'a>),
|
||||||
/// An entry which is merged with another cell.
|
/// An entry which is merged with another cell.
|
||||||
@ -246,39 +186,6 @@ impl<'a> Entry<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A grid item, possibly affected by automatic cell positioning. Can be either
|
|
||||||
/// a line or a cell.
|
|
||||||
pub enum ResolvableGridItem<T: ResolvableCell> {
|
|
||||||
/// A horizontal line in the grid.
|
|
||||||
HLine {
|
|
||||||
/// The row above which the horizontal line is drawn.
|
|
||||||
y: Smart<usize>,
|
|
||||||
start: usize,
|
|
||||||
end: Option<NonZeroUsize>,
|
|
||||||
stroke: Option<Arc<Stroke<Abs>>>,
|
|
||||||
/// The span of the corresponding line element.
|
|
||||||
span: Span,
|
|
||||||
/// The line's position. "before" here means on top of row `y`, while
|
|
||||||
/// "after" means below it.
|
|
||||||
position: LinePosition,
|
|
||||||
},
|
|
||||||
/// A vertical line in the grid.
|
|
||||||
VLine {
|
|
||||||
/// The column before which the vertical line is drawn.
|
|
||||||
x: Smart<usize>,
|
|
||||||
start: usize,
|
|
||||||
end: Option<NonZeroUsize>,
|
|
||||||
stroke: Option<Arc<Stroke<Abs>>>,
|
|
||||||
/// The span of the corresponding line element.
|
|
||||||
span: Span,
|
|
||||||
/// The line's position. "before" here means to the left of column `x`,
|
|
||||||
/// while "after" means to its right (both considering LTR).
|
|
||||||
position: LinePosition,
|
|
||||||
},
|
|
||||||
/// A cell in the grid.
|
|
||||||
Cell(T),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Any grid child, which can be either a header or an item.
|
/// Any grid child, which can be either a header or an item.
|
||||||
pub enum ResolvableGridChild<T: ResolvableCell, I> {
|
pub enum ResolvableGridChild<T: ResolvableCell, I> {
|
||||||
Header { repeat: bool, span: Span, items: I },
|
Header { repeat: bool, span: Span, items: I },
|
||||||
@ -286,65 +193,28 @@ pub enum ResolvableGridChild<T: ResolvableCell, I> {
|
|||||||
Item(ResolvableGridItem<T>),
|
Item(ResolvableGridItem<T>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Used for cell-like elements which are aware of their final properties in
|
|
||||||
/// the table, and may have property overrides.
|
|
||||||
pub trait ResolvableCell {
|
|
||||||
/// Resolves the cell's fields, given its coordinates and default grid-wide
|
|
||||||
/// fill, align, inset and stroke properties, plus the expected value of
|
|
||||||
/// the `breakable` field.
|
|
||||||
/// Returns a final Cell.
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn resolve_cell<'a>(
|
|
||||||
self,
|
|
||||||
x: usize,
|
|
||||||
y: usize,
|
|
||||||
fill: &Option<Paint>,
|
|
||||||
align: Smart<Alignment>,
|
|
||||||
inset: Sides<Option<Rel<Length>>>,
|
|
||||||
stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>,
|
|
||||||
breakable: bool,
|
|
||||||
locator: Locator<'a>,
|
|
||||||
styles: StyleChain,
|
|
||||||
) -> Cell<'a>;
|
|
||||||
|
|
||||||
/// Returns this cell's column override.
|
|
||||||
fn x(&self, styles: StyleChain) -> Smart<usize>;
|
|
||||||
|
|
||||||
/// Returns this cell's row override.
|
|
||||||
fn y(&self, styles: StyleChain) -> Smart<usize>;
|
|
||||||
|
|
||||||
/// The amount of columns spanned by this cell.
|
|
||||||
fn colspan(&self, styles: StyleChain) -> NonZeroUsize;
|
|
||||||
|
|
||||||
/// The amount of rows spanned by this cell.
|
|
||||||
fn rowspan(&self, styles: StyleChain) -> NonZeroUsize;
|
|
||||||
|
|
||||||
/// The cell's span, for errors.
|
|
||||||
fn span(&self) -> Span;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A grid of cells, including the columns, rows, and cell data.
|
/// A grid of cells, including the columns, rows, and cell data.
|
||||||
pub struct CellGrid<'a> {
|
pub struct CellGrid<'a> {
|
||||||
/// The grid cells.
|
/// The grid cells.
|
||||||
pub(super) entries: Vec<Entry<'a>>,
|
pub entries: Vec<Entry<'a>>,
|
||||||
/// The column tracks including gutter tracks.
|
/// The column tracks including gutter tracks.
|
||||||
pub(super) cols: Vec<Sizing>,
|
pub cols: Vec<Sizing>,
|
||||||
/// The row tracks including gutter tracks.
|
/// The row tracks including gutter tracks.
|
||||||
pub(super) rows: Vec<Sizing>,
|
pub rows: Vec<Sizing>,
|
||||||
/// The vertical lines before each column, or on the end border.
|
/// The vertical lines before each column, or on the end border.
|
||||||
/// Gutter columns are not included.
|
/// Gutter columns are not included.
|
||||||
/// Contains up to 'cols_without_gutter.len() + 1' vectors of lines.
|
/// Contains up to 'cols_without_gutter.len() + 1' vectors of lines.
|
||||||
pub(super) vlines: Vec<Vec<Line>>,
|
pub vlines: Vec<Vec<Line>>,
|
||||||
/// The horizontal lines on top of each row, or on the bottom border.
|
/// The horizontal lines on top of each row, or on the bottom border.
|
||||||
/// Gutter rows are not included.
|
/// Gutter rows are not included.
|
||||||
/// Contains up to 'rows_without_gutter.len() + 1' vectors of lines.
|
/// Contains up to 'rows_without_gutter.len() + 1' vectors of lines.
|
||||||
pub(super) hlines: Vec<Vec<Line>>,
|
pub hlines: Vec<Vec<Line>>,
|
||||||
/// The repeatable header of this grid.
|
/// The repeatable header of this grid.
|
||||||
pub(super) header: Option<Repeatable<Header>>,
|
pub header: Option<Repeatable<Header>>,
|
||||||
/// The repeatable footer of this grid.
|
/// The repeatable footer of this grid.
|
||||||
pub(super) footer: Option<Repeatable<Footer>>,
|
pub footer: Option<Repeatable<Footer>>,
|
||||||
/// Whether this grid has gutters.
|
/// Whether this grid has gutters.
|
||||||
pub(super) has_gutter: bool,
|
pub has_gutter: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> CellGrid<'a> {
|
impl<'a> CellGrid<'a> {
|
||||||
@ -1125,7 +995,7 @@ impl<'a> CellGrid<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Generates the cell grid, given the tracks and resolved entries.
|
/// Generates the cell grid, given the tracks and resolved entries.
|
||||||
pub(super) fn new_internal(
|
pub fn new_internal(
|
||||||
tracks: Axes<&[Sizing]>,
|
tracks: Axes<&[Sizing]>,
|
||||||
gutter: Axes<&[Sizing]>,
|
gutter: Axes<&[Sizing]>,
|
||||||
vlines: Vec<Vec<Line>>,
|
vlines: Vec<Vec<Line>>,
|
||||||
@ -1194,7 +1064,7 @@ impl<'a> CellGrid<'a> {
|
|||||||
///
|
///
|
||||||
/// Returns `None` if it's a gutter cell.
|
/// Returns `None` if it's a gutter cell.
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub(super) fn entry(&self, x: usize, y: usize) -> Option<&Entry<'a>> {
|
pub fn entry(&self, x: usize, y: usize) -> Option<&Entry<'a>> {
|
||||||
assert!(x < self.cols.len());
|
assert!(x < self.cols.len());
|
||||||
assert!(y < self.rows.len());
|
assert!(y < self.rows.len());
|
||||||
|
|
||||||
@ -1216,7 +1086,7 @@ impl<'a> CellGrid<'a> {
|
|||||||
///
|
///
|
||||||
/// Returns `None` if it's a gutter cell or merged position.
|
/// Returns `None` if it's a gutter cell or merged position.
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub(super) fn cell(&self, x: usize, y: usize) -> Option<&Cell<'a>> {
|
pub fn cell(&self, x: usize, y: usize) -> Option<&Cell<'a>> {
|
||||||
self.entry(x, y).and_then(Entry::as_cell)
|
self.entry(x, y).and_then(Entry::as_cell)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1228,7 +1098,7 @@ impl<'a> CellGrid<'a> {
|
|||||||
/// - If it is a merged cell, returns the parent cell's position.
|
/// - If it is a merged cell, returns the parent cell's position.
|
||||||
/// - If it is a gutter cell, returns None.
|
/// - If it is a gutter cell, returns None.
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub(super) fn parent_cell_position(&self, x: usize, y: usize) -> Option<Axes<usize>> {
|
pub fn parent_cell_position(&self, x: usize, y: usize) -> Option<Axes<usize>> {
|
||||||
self.entry(x, y).map(|entry| match entry {
|
self.entry(x, y).map(|entry| match entry {
|
||||||
Entry::Cell(_) => Axes::new(x, y),
|
Entry::Cell(_) => Axes::new(x, y),
|
||||||
Entry::Merged { parent } => {
|
Entry::Merged { parent } => {
|
||||||
@ -1260,7 +1130,7 @@ impl<'a> CellGrid<'a> {
|
|||||||
/// position, if it is gutter), if it exists; otherwise returns None (it's
|
/// position, if it is gutter), if it exists; otherwise returns None (it's
|
||||||
/// gutter and no cell spans it).
|
/// gutter and no cell spans it).
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub(super) fn effective_parent_cell_position(
|
pub fn effective_parent_cell_position(
|
||||||
&self,
|
&self,
|
||||||
x: usize,
|
x: usize,
|
||||||
y: usize,
|
y: usize,
|
||||||
@ -1283,14 +1153,14 @@ impl<'a> CellGrid<'a> {
|
|||||||
/// Checks if the track with the given index is gutter.
|
/// Checks if the track with the given index is gutter.
|
||||||
/// Does not check if the index is a valid track.
|
/// Does not check if the index is a valid track.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn is_gutter_track(&self, index: usize) -> bool {
|
pub fn is_gutter_track(&self, index: usize) -> bool {
|
||||||
self.has_gutter && index % 2 == 1
|
self.has_gutter && index % 2 == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the effective colspan of a cell, considering the gutters it
|
/// Returns the effective colspan of a cell, considering the gutters it
|
||||||
/// might span if the grid has gutters.
|
/// might span if the grid has gutters.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn effective_colspan_of_cell(&self, cell: &Cell) -> usize {
|
pub fn effective_colspan_of_cell(&self, cell: &Cell) -> usize {
|
||||||
if self.has_gutter {
|
if self.has_gutter {
|
||||||
2 * cell.colspan.get() - 1
|
2 * cell.colspan.get() - 1
|
||||||
} else {
|
} else {
|
||||||
@ -1301,7 +1171,7 @@ impl<'a> CellGrid<'a> {
|
|||||||
/// Returns the effective rowspan of a cell, considering the gutters it
|
/// Returns the effective rowspan of a cell, considering the gutters it
|
||||||
/// might span if the grid has gutters.
|
/// might span if the grid has gutters.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn effective_rowspan_of_cell(&self, cell: &Cell) -> usize {
|
pub fn effective_rowspan_of_cell(&self, cell: &Cell) -> usize {
|
||||||
if self.has_gutter {
|
if self.has_gutter {
|
||||||
2 * cell.rowspan.get() - 1
|
2 * cell.rowspan.get() - 1
|
||||||
} else {
|
} else {
|
@ -1,22 +1,21 @@
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use super::lines::{
|
use typst_library::diag::{bail, SourceResult};
|
||||||
generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, LinePosition,
|
use typst_library::engine::Engine;
|
||||||
LineSegment,
|
use typst_library::foundations::{Resolve, StyleChain};
|
||||||
|
use typst_library::layout::{
|
||||||
|
Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel,
|
||||||
|
Size, Sizing,
|
||||||
};
|
};
|
||||||
use super::repeated::Repeatable;
|
use typst_library::text::TextElem;
|
||||||
use super::rowspans::{Rowspan, UnbreakableRowGroup};
|
use typst_library::visualize::Geometry;
|
||||||
use crate::diag::{bail, SourceResult};
|
use typst_syntax::Span;
|
||||||
use crate::engine::Engine;
|
use typst_utils::{MaybeReverseIter, Numeric};
|
||||||
use crate::foundations::{Resolve, StyleChain};
|
|
||||||
use crate::layout::{
|
use super::{
|
||||||
Abs, Axes, Cell, CellGrid, Dir, Fr, Fragment, Frame, FrameItem, Length, Point,
|
generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Cell, CellGrid,
|
||||||
Region, Regions, Rel, Size, Sizing,
|
LinePosition, LineSegment, Repeatable, Rowspan, UnbreakableRowGroup,
|
||||||
};
|
};
|
||||||
use crate::syntax::Span;
|
|
||||||
use crate::text::TextElem;
|
|
||||||
use crate::utils::{MaybeReverseIter, Numeric};
|
|
||||||
use crate::visualize::Geometry;
|
|
||||||
|
|
||||||
/// Performs grid layout.
|
/// Performs grid layout.
|
||||||
pub struct GridLayouter<'a> {
|
pub struct GridLayouter<'a> {
|
@ -1,12 +1,11 @@
|
|||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use super::cells::CellGrid;
|
use typst_library::foundations::{AlternativeFold, Fold};
|
||||||
use super::layout::RowPiece;
|
use typst_library::layout::Abs;
|
||||||
use super::repeated::Repeatable;
|
use typst_library::visualize::Stroke;
|
||||||
use crate::foundations::{AlternativeFold, Fold};
|
|
||||||
use crate::layout::Abs;
|
use super::{CellGrid, LinePosition, Repeatable, RowPiece};
|
||||||
use crate::visualize::Stroke;
|
|
||||||
|
|
||||||
/// Represents an explicit grid line (horizontal or vertical) specified by the
|
/// Represents an explicit grid line (horizontal or vertical) specified by the
|
||||||
/// user.
|
/// user.
|
||||||
@ -38,22 +37,10 @@ pub struct Line {
|
|||||||
pub position: LinePosition,
|
pub position: LinePosition,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Indicates whether the line should be drawn before or after the track with
|
|
||||||
/// its index. This is mostly only relevant when gutter is used, since, then,
|
|
||||||
/// the position after a track is not the same as before the next
|
|
||||||
/// non-gutter track.
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
|
||||||
pub enum LinePosition {
|
|
||||||
/// The line should be drawn before its track (e.g. hline on top of a row).
|
|
||||||
Before,
|
|
||||||
/// The line should be drawn after its track (e.g. hline below a row).
|
|
||||||
After,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Indicates which priority a particular grid line segment should have, based
|
/// Indicates which priority a particular grid line segment should have, based
|
||||||
/// on the highest priority configuration that defined the segment's stroke.
|
/// on the highest priority configuration that defined the segment's stroke.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub(super) enum StrokePriority {
|
pub enum StrokePriority {
|
||||||
/// The stroke of the segment was derived solely from the grid's global
|
/// The stroke of the segment was derived solely from the grid's global
|
||||||
/// stroke setting, so it should have the lowest priority.
|
/// stroke setting, so it should have the lowest priority.
|
||||||
GridStroke = 0,
|
GridStroke = 0,
|
||||||
@ -71,19 +58,19 @@ pub(super) enum StrokePriority {
|
|||||||
/// Data for a particular line segment in the grid as generated by
|
/// Data for a particular line segment in the grid as generated by
|
||||||
/// `generate_line_segments`.
|
/// `generate_line_segments`.
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub(super) struct LineSegment {
|
pub struct LineSegment {
|
||||||
/// The stroke with which to draw this segment.
|
/// The stroke with which to draw this segment.
|
||||||
pub(super) stroke: Arc<Stroke<Abs>>,
|
pub stroke: Arc<Stroke<Abs>>,
|
||||||
/// The offset of this segment since the beginning of its axis.
|
/// The offset of this segment since the beginning of its axis.
|
||||||
/// For a vertical line segment, this is the offset since the top of the
|
/// For a vertical line segment, this is the offset since the top of the
|
||||||
/// table in the current page; for a horizontal line segment, this is the
|
/// table in the current page; for a horizontal line segment, this is the
|
||||||
/// offset since the start border of the table.
|
/// offset since the start border of the table.
|
||||||
pub(super) offset: Abs,
|
pub offset: Abs,
|
||||||
/// The length of this segment.
|
/// The length of this segment.
|
||||||
pub(super) length: Abs,
|
pub length: Abs,
|
||||||
/// The segment's drawing priority, indicating on top of which other
|
/// The segment's drawing priority, indicating on top of which other
|
||||||
/// segments this one should be drawn.
|
/// segments this one should be drawn.
|
||||||
pub(super) priority: StrokePriority,
|
pub priority: StrokePriority,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates the segments of lines that should be drawn alongside a certain
|
/// Generates the segments of lines that should be drawn alongside a certain
|
||||||
@ -119,7 +106,7 @@ pub(super) struct LineSegment {
|
|||||||
/// number, and they must be iterable over pairs of (number, size). For
|
/// number, and they must be iterable over pairs of (number, size). For
|
||||||
/// vertical lines, for instance, `tracks` would describe the rows in the
|
/// vertical lines, for instance, `tracks` would describe the rows in the
|
||||||
/// current region, as pairs (row index, row height).
|
/// current region, as pairs (row index, row height).
|
||||||
pub(super) fn generate_line_segments<'grid, F, I, L>(
|
pub fn generate_line_segments<'grid, F, I, L>(
|
||||||
grid: &'grid CellGrid,
|
grid: &'grid CellGrid,
|
||||||
tracks: I,
|
tracks: I,
|
||||||
index: usize,
|
index: usize,
|
||||||
@ -269,7 +256,7 @@ where
|
|||||||
current_segment =
|
current_segment =
|
||||||
Some(LineSegment { stroke, offset, length: size, priority });
|
Some(LineSegment { stroke, offset, length: size, priority });
|
||||||
}
|
}
|
||||||
} else if let Some(old_segment) = current_segment.take() {
|
} else if let Some(old_segment) = Option::take(&mut current_segment) {
|
||||||
// We shouldn't draw here (stroke of None), so we yield the
|
// We shouldn't draw here (stroke of None), so we yield the
|
||||||
// current segment, as it was interrupted.
|
// current segment, as it was interrupted.
|
||||||
offset += size;
|
offset += size;
|
||||||
@ -289,7 +276,9 @@ where
|
|||||||
// closure, the current segment will necessarily be 'None',
|
// closure, the current segment will necessarily be 'None',
|
||||||
// so the iterator will necessarily end (that is, we will return None)
|
// so the iterator will necessarily end (that is, we will return None)
|
||||||
// after this.
|
// after this.
|
||||||
current_segment.take()
|
//
|
||||||
|
// Note: Fully-qualified notation because rust-analyzer is confused.
|
||||||
|
Option::take(&mut current_segment)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,7 +301,7 @@ where
|
|||||||
///
|
///
|
||||||
/// The priority associated with the returned stroke follows the rules
|
/// The priority associated with the returned stroke follows the rules
|
||||||
/// described in the docs for `generate_line_segment`.
|
/// described in the docs for `generate_line_segment`.
|
||||||
pub(super) fn vline_stroke_at_row(
|
pub fn vline_stroke_at_row(
|
||||||
grid: &CellGrid,
|
grid: &CellGrid,
|
||||||
x: usize,
|
x: usize,
|
||||||
y: usize,
|
y: usize,
|
||||||
@ -432,7 +421,7 @@ pub(super) fn vline_stroke_at_row(
|
|||||||
///
|
///
|
||||||
/// This function assumes columns are sorted by increasing `x`, and rows are
|
/// This function assumes columns are sorted by increasing `x`, and rows are
|
||||||
/// sorted by increasing `y`.
|
/// sorted by increasing `y`.
|
||||||
pub(super) fn hline_stroke_at_column(
|
pub fn hline_stroke_at_column(
|
||||||
grid: &CellGrid,
|
grid: &CellGrid,
|
||||||
rows: &[RowPiece],
|
rows: &[RowPiece],
|
||||||
local_top_y: Option<usize>,
|
local_top_y: Option<usize>,
|
||||||
@ -599,12 +588,14 @@ pub(super) fn hline_stroke_at_column(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use typst_library::foundations::Content;
|
||||||
|
use typst_library::introspection::Locator;
|
||||||
|
use typst_library::layout::{Axes, Sides, Sizing};
|
||||||
|
use typst_utils::NonZeroExt;
|
||||||
|
|
||||||
use super::super::cells::Entry;
|
use super::super::cells::Entry;
|
||||||
|
use super::super::Cell;
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::foundations::Content;
|
|
||||||
use crate::introspection::Locator;
|
|
||||||
use crate::layout::{Axes, Cell, Sides, Sizing};
|
|
||||||
use crate::utils::NonZeroExt;
|
|
||||||
|
|
||||||
fn sample_cell() -> Cell<'static> {
|
fn sample_cell() -> Cell<'static> {
|
||||||
Cell {
|
Cell {
|
416
crates/typst-layout/src/grid/mod.rs
Normal file
416
crates/typst-layout/src/grid/mod.rs
Normal file
@ -0,0 +1,416 @@
|
|||||||
|
mod cells;
|
||||||
|
mod layouter;
|
||||||
|
mod lines;
|
||||||
|
mod repeated;
|
||||||
|
mod rowspans;
|
||||||
|
|
||||||
|
pub use self::cells::{Cell, CellGrid};
|
||||||
|
pub use self::layouter::GridLayouter;
|
||||||
|
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use ecow::eco_format;
|
||||||
|
use typst_library::diag::{SourceResult, Trace, Tracepoint};
|
||||||
|
use typst_library::engine::Engine;
|
||||||
|
use typst_library::foundations::{Fold, Packed, Smart, StyleChain};
|
||||||
|
use typst_library::introspection::Locator;
|
||||||
|
use typst_library::layout::{
|
||||||
|
Abs, Alignment, Axes, Dir, Fragment, GridCell, GridChild, GridElem, GridItem, Length,
|
||||||
|
OuterHAlignment, OuterVAlignment, Regions, Rel, Sides,
|
||||||
|
};
|
||||||
|
use typst_library::model::{TableCell, TableChild, TableElem, TableItem};
|
||||||
|
use typst_library::text::TextElem;
|
||||||
|
use typst_library::visualize::{Paint, Stroke};
|
||||||
|
use typst_syntax::Span;
|
||||||
|
|
||||||
|
use self::cells::{
|
||||||
|
LinePosition, ResolvableCell, ResolvableGridChild, ResolvableGridItem,
|
||||||
|
};
|
||||||
|
use self::layouter::RowPiece;
|
||||||
|
use self::lines::{
|
||||||
|
generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Line,
|
||||||
|
LineSegment,
|
||||||
|
};
|
||||||
|
use self::repeated::{Footer, Header, Repeatable};
|
||||||
|
use self::rowspans::{Rowspan, UnbreakableRowGroup};
|
||||||
|
|
||||||
|
/// Layout the grid.
|
||||||
|
#[typst_macros::time(span = elem.span())]
|
||||||
|
pub fn layout_grid(
|
||||||
|
elem: &Packed<GridElem>,
|
||||||
|
engine: &mut Engine,
|
||||||
|
locator: Locator,
|
||||||
|
styles: StyleChain,
|
||||||
|
regions: Regions,
|
||||||
|
) -> SourceResult<Fragment> {
|
||||||
|
let inset = elem.inset(styles);
|
||||||
|
let align = elem.align(styles);
|
||||||
|
let columns = elem.columns(styles);
|
||||||
|
let rows = elem.rows(styles);
|
||||||
|
let column_gutter = elem.column_gutter(styles);
|
||||||
|
let row_gutter = elem.row_gutter(styles);
|
||||||
|
let fill = elem.fill(styles);
|
||||||
|
let stroke = elem.stroke(styles);
|
||||||
|
|
||||||
|
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
|
||||||
|
let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
|
||||||
|
// Use trace to link back to the grid when a specific cell errors
|
||||||
|
let tracepoint = || Tracepoint::Call(Some(eco_format!("grid")));
|
||||||
|
let resolve_item = |item: &GridItem| grid_item_to_resolvable(item, styles);
|
||||||
|
let children = elem.children().iter().map(|child| match child {
|
||||||
|
GridChild::Header(header) => ResolvableGridChild::Header {
|
||||||
|
repeat: header.repeat(styles),
|
||||||
|
span: header.span(),
|
||||||
|
items: header.children().iter().map(resolve_item),
|
||||||
|
},
|
||||||
|
GridChild::Footer(footer) => ResolvableGridChild::Footer {
|
||||||
|
repeat: footer.repeat(styles),
|
||||||
|
span: footer.span(),
|
||||||
|
items: footer.children().iter().map(resolve_item),
|
||||||
|
},
|
||||||
|
GridChild::Item(item) => {
|
||||||
|
ResolvableGridChild::Item(grid_item_to_resolvable(item, styles))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let grid = CellGrid::resolve(
|
||||||
|
tracks,
|
||||||
|
gutter,
|
||||||
|
locator,
|
||||||
|
children,
|
||||||
|
fill,
|
||||||
|
align,
|
||||||
|
&inset,
|
||||||
|
&stroke,
|
||||||
|
engine,
|
||||||
|
styles,
|
||||||
|
elem.span(),
|
||||||
|
)
|
||||||
|
.trace(engine.world, tracepoint, elem.span())?;
|
||||||
|
|
||||||
|
let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
|
||||||
|
|
||||||
|
// Measure the columns and layout the grid row-by-row.
|
||||||
|
layouter.layout(engine)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout the table.
|
||||||
|
#[typst_macros::time(span = elem.span())]
|
||||||
|
pub fn layout_table(
|
||||||
|
elem: &Packed<TableElem>,
|
||||||
|
engine: &mut Engine,
|
||||||
|
locator: Locator,
|
||||||
|
styles: StyleChain,
|
||||||
|
regions: Regions,
|
||||||
|
) -> SourceResult<Fragment> {
|
||||||
|
let inset = elem.inset(styles);
|
||||||
|
let align = elem.align(styles);
|
||||||
|
let columns = elem.columns(styles);
|
||||||
|
let rows = elem.rows(styles);
|
||||||
|
let column_gutter = elem.column_gutter(styles);
|
||||||
|
let row_gutter = elem.row_gutter(styles);
|
||||||
|
let fill = elem.fill(styles);
|
||||||
|
let stroke = elem.stroke(styles);
|
||||||
|
|
||||||
|
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
|
||||||
|
let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
|
||||||
|
// Use trace to link back to the table when a specific cell errors
|
||||||
|
let tracepoint = || Tracepoint::Call(Some(eco_format!("table")));
|
||||||
|
let resolve_item = |item: &TableItem| table_item_to_resolvable(item, styles);
|
||||||
|
let children = elem.children().iter().map(|child| match child {
|
||||||
|
TableChild::Header(header) => ResolvableGridChild::Header {
|
||||||
|
repeat: header.repeat(styles),
|
||||||
|
span: header.span(),
|
||||||
|
items: header.children().iter().map(resolve_item),
|
||||||
|
},
|
||||||
|
TableChild::Footer(footer) => ResolvableGridChild::Footer {
|
||||||
|
repeat: footer.repeat(styles),
|
||||||
|
span: footer.span(),
|
||||||
|
items: footer.children().iter().map(resolve_item),
|
||||||
|
},
|
||||||
|
TableChild::Item(item) => {
|
||||||
|
ResolvableGridChild::Item(table_item_to_resolvable(item, styles))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let grid = CellGrid::resolve(
|
||||||
|
tracks,
|
||||||
|
gutter,
|
||||||
|
locator,
|
||||||
|
children,
|
||||||
|
fill,
|
||||||
|
align,
|
||||||
|
&inset,
|
||||||
|
&stroke,
|
||||||
|
engine,
|
||||||
|
styles,
|
||||||
|
elem.span(),
|
||||||
|
)
|
||||||
|
.trace(engine.world, tracepoint, elem.span())?;
|
||||||
|
|
||||||
|
let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
|
||||||
|
layouter.layout(engine)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grid_item_to_resolvable(
|
||||||
|
item: &GridItem,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> ResolvableGridItem<Packed<GridCell>> {
|
||||||
|
match item {
|
||||||
|
GridItem::HLine(hline) => ResolvableGridItem::HLine {
|
||||||
|
y: hline.y(styles),
|
||||||
|
start: hline.start(styles),
|
||||||
|
end: hline.end(styles),
|
||||||
|
stroke: hline.stroke(styles),
|
||||||
|
span: hline.span(),
|
||||||
|
position: match hline.position(styles) {
|
||||||
|
OuterVAlignment::Top => LinePosition::Before,
|
||||||
|
OuterVAlignment::Bottom => LinePosition::After,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
GridItem::VLine(vline) => ResolvableGridItem::VLine {
|
||||||
|
x: vline.x(styles),
|
||||||
|
start: vline.start(styles),
|
||||||
|
end: vline.end(styles),
|
||||||
|
stroke: vline.stroke(styles),
|
||||||
|
span: vline.span(),
|
||||||
|
position: match vline.position(styles) {
|
||||||
|
OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => {
|
||||||
|
LinePosition::After
|
||||||
|
}
|
||||||
|
OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => {
|
||||||
|
LinePosition::Before
|
||||||
|
}
|
||||||
|
OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before,
|
||||||
|
OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
GridItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn table_item_to_resolvable(
|
||||||
|
item: &TableItem,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> ResolvableGridItem<Packed<TableCell>> {
|
||||||
|
match item {
|
||||||
|
TableItem::HLine(hline) => ResolvableGridItem::HLine {
|
||||||
|
y: hline.y(styles),
|
||||||
|
start: hline.start(styles),
|
||||||
|
end: hline.end(styles),
|
||||||
|
stroke: hline.stroke(styles),
|
||||||
|
span: hline.span(),
|
||||||
|
position: match hline.position(styles) {
|
||||||
|
OuterVAlignment::Top => LinePosition::Before,
|
||||||
|
OuterVAlignment::Bottom => LinePosition::After,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TableItem::VLine(vline) => ResolvableGridItem::VLine {
|
||||||
|
x: vline.x(styles),
|
||||||
|
start: vline.start(styles),
|
||||||
|
end: vline.end(styles),
|
||||||
|
stroke: vline.stroke(styles),
|
||||||
|
span: vline.span(),
|
||||||
|
position: match vline.position(styles) {
|
||||||
|
OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => {
|
||||||
|
LinePosition::After
|
||||||
|
}
|
||||||
|
OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => {
|
||||||
|
LinePosition::Before
|
||||||
|
}
|
||||||
|
OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before,
|
||||||
|
OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TableItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResolvableCell for Packed<TableCell> {
|
||||||
|
fn resolve_cell<'a>(
|
||||||
|
mut self,
|
||||||
|
x: usize,
|
||||||
|
y: usize,
|
||||||
|
fill: &Option<Paint>,
|
||||||
|
align: Smart<Alignment>,
|
||||||
|
inset: Sides<Option<Rel<Length>>>,
|
||||||
|
stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>,
|
||||||
|
breakable: bool,
|
||||||
|
locator: Locator<'a>,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> Cell<'a> {
|
||||||
|
let cell = &mut *self;
|
||||||
|
let colspan = cell.colspan(styles);
|
||||||
|
let rowspan = cell.rowspan(styles);
|
||||||
|
let breakable = cell.breakable(styles).unwrap_or(breakable);
|
||||||
|
let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
|
||||||
|
|
||||||
|
let cell_stroke = cell.stroke(styles);
|
||||||
|
let stroke_overridden =
|
||||||
|
cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_))));
|
||||||
|
|
||||||
|
// Using a typical 'Sides' fold, an unspecified side loses to a
|
||||||
|
// specified side. Additionally, when both are specified, an inner
|
||||||
|
// None wins over the outer Some, and vice-versa. When both are
|
||||||
|
// specified and Some, fold occurs, which, remarkably, leads to an Arc
|
||||||
|
// clone.
|
||||||
|
//
|
||||||
|
// In the end, we flatten because, for layout purposes, an unspecified
|
||||||
|
// cell stroke is the same as specifying 'none', so we equate the two
|
||||||
|
// concepts.
|
||||||
|
let stroke = cell_stroke.fold(stroke).map(Option::flatten);
|
||||||
|
cell.push_x(Smart::Custom(x));
|
||||||
|
cell.push_y(Smart::Custom(y));
|
||||||
|
cell.push_fill(Smart::Custom(fill.clone()));
|
||||||
|
cell.push_align(match align {
|
||||||
|
Smart::Custom(align) => {
|
||||||
|
Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align)))
|
||||||
|
}
|
||||||
|
// Don't fold if the table is using outer alignment. Use the
|
||||||
|
// cell's alignment instead (which, in the end, will fold with
|
||||||
|
// the outer alignment when it is effectively displayed).
|
||||||
|
Smart::Auto => cell.align(styles),
|
||||||
|
});
|
||||||
|
cell.push_inset(Smart::Custom(
|
||||||
|
cell.inset(styles).map_or(inset, |inner| inner.fold(inset)),
|
||||||
|
));
|
||||||
|
cell.push_stroke(
|
||||||
|
// Here we convert the resolved stroke to a regular stroke, however
|
||||||
|
// with resolved units (that is, 'em' converted to absolute units).
|
||||||
|
// We also convert any stroke unspecified by both the cell and the
|
||||||
|
// outer stroke ('None' in the folded stroke) to 'none', that is,
|
||||||
|
// all sides are present in the resulting Sides object accessible
|
||||||
|
// by show rules on table cells.
|
||||||
|
stroke.as_ref().map(|side| {
|
||||||
|
Some(side.as_ref().map(|cell_stroke| {
|
||||||
|
Arc::new((**cell_stroke).clone().map(Length::from))
|
||||||
|
}))
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
cell.push_breakable(Smart::Custom(breakable));
|
||||||
|
Cell {
|
||||||
|
body: self.pack(),
|
||||||
|
locator,
|
||||||
|
fill,
|
||||||
|
colspan,
|
||||||
|
rowspan,
|
||||||
|
stroke,
|
||||||
|
stroke_overridden,
|
||||||
|
breakable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn x(&self, styles: StyleChain) -> Smart<usize> {
|
||||||
|
(**self).x(styles)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn y(&self, styles: StyleChain) -> Smart<usize> {
|
||||||
|
(**self).y(styles)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn colspan(&self, styles: StyleChain) -> NonZeroUsize {
|
||||||
|
(**self).colspan(styles)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rowspan(&self, styles: StyleChain) -> NonZeroUsize {
|
||||||
|
(**self).rowspan(styles)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn span(&self) -> Span {
|
||||||
|
Packed::span(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResolvableCell for Packed<GridCell> {
|
||||||
|
fn resolve_cell<'a>(
|
||||||
|
mut self,
|
||||||
|
x: usize,
|
||||||
|
y: usize,
|
||||||
|
fill: &Option<Paint>,
|
||||||
|
align: Smart<Alignment>,
|
||||||
|
inset: Sides<Option<Rel<Length>>>,
|
||||||
|
stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>,
|
||||||
|
breakable: bool,
|
||||||
|
locator: Locator<'a>,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> Cell<'a> {
|
||||||
|
let cell = &mut *self;
|
||||||
|
let colspan = cell.colspan(styles);
|
||||||
|
let rowspan = cell.rowspan(styles);
|
||||||
|
let breakable = cell.breakable(styles).unwrap_or(breakable);
|
||||||
|
let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
|
||||||
|
|
||||||
|
let cell_stroke = cell.stroke(styles);
|
||||||
|
let stroke_overridden =
|
||||||
|
cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_))));
|
||||||
|
|
||||||
|
// Using a typical 'Sides' fold, an unspecified side loses to a
|
||||||
|
// specified side. Additionally, when both are specified, an inner
|
||||||
|
// None wins over the outer Some, and vice-versa. When both are
|
||||||
|
// specified and Some, fold occurs, which, remarkably, leads to an Arc
|
||||||
|
// clone.
|
||||||
|
//
|
||||||
|
// In the end, we flatten because, for layout purposes, an unspecified
|
||||||
|
// cell stroke is the same as specifying 'none', so we equate the two
|
||||||
|
// concepts.
|
||||||
|
let stroke = cell_stroke.fold(stroke).map(Option::flatten);
|
||||||
|
cell.push_x(Smart::Custom(x));
|
||||||
|
cell.push_y(Smart::Custom(y));
|
||||||
|
cell.push_fill(Smart::Custom(fill.clone()));
|
||||||
|
cell.push_align(match align {
|
||||||
|
Smart::Custom(align) => {
|
||||||
|
Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align)))
|
||||||
|
}
|
||||||
|
// Don't fold if the grid is using outer alignment. Use the
|
||||||
|
// cell's alignment instead (which, in the end, will fold with
|
||||||
|
// the outer alignment when it is effectively displayed).
|
||||||
|
Smart::Auto => cell.align(styles),
|
||||||
|
});
|
||||||
|
cell.push_inset(Smart::Custom(
|
||||||
|
cell.inset(styles).map_or(inset, |inner| inner.fold(inset)),
|
||||||
|
));
|
||||||
|
cell.push_stroke(
|
||||||
|
// Here we convert the resolved stroke to a regular stroke, however
|
||||||
|
// with resolved units (that is, 'em' converted to absolute units).
|
||||||
|
// We also convert any stroke unspecified by both the cell and the
|
||||||
|
// outer stroke ('None' in the folded stroke) to 'none', that is,
|
||||||
|
// all sides are present in the resulting Sides object accessible
|
||||||
|
// by show rules on grid cells.
|
||||||
|
stroke.as_ref().map(|side| {
|
||||||
|
Some(side.as_ref().map(|cell_stroke| {
|
||||||
|
Arc::new((**cell_stroke).clone().map(Length::from))
|
||||||
|
}))
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
cell.push_breakable(Smart::Custom(breakable));
|
||||||
|
Cell {
|
||||||
|
body: self.pack(),
|
||||||
|
locator,
|
||||||
|
fill,
|
||||||
|
colspan,
|
||||||
|
rowspan,
|
||||||
|
stroke,
|
||||||
|
stroke_overridden,
|
||||||
|
breakable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn x(&self, styles: StyleChain) -> Smart<usize> {
|
||||||
|
(**self).x(styles)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn y(&self, styles: StyleChain) -> Smart<usize> {
|
||||||
|
(**self).y(styles)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn colspan(&self, styles: StyleChain) -> NonZeroUsize {
|
||||||
|
(**self).colspan(styles)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rowspan(&self, styles: StyleChain) -> NonZeroUsize {
|
||||||
|
(**self).rowspan(styles)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn span(&self) -> Span {
|
||||||
|
Packed::span(self)
|
||||||
|
}
|
||||||
|
}
|
@ -1,25 +1,27 @@
|
|||||||
|
use typst_library::diag::SourceResult;
|
||||||
|
use typst_library::engine::Engine;
|
||||||
|
use typst_library::layout::{Abs, Axes, Frame, Regions};
|
||||||
|
|
||||||
|
use super::layouter::GridLayouter;
|
||||||
use super::rowspans::UnbreakableRowGroup;
|
use super::rowspans::UnbreakableRowGroup;
|
||||||
use crate::diag::SourceResult;
|
|
||||||
use crate::engine::Engine;
|
|
||||||
use crate::layout::{Abs, Axes, Frame, GridLayouter, Regions};
|
|
||||||
|
|
||||||
/// A repeatable grid header. Starts at the first row.
|
/// A repeatable grid header. Starts at the first row.
|
||||||
pub(super) struct Header {
|
pub struct Header {
|
||||||
/// The index after the last row included in this header.
|
/// The index after the last row included in this header.
|
||||||
pub(super) end: usize,
|
pub end: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A repeatable grid footer. Stops at the last row.
|
/// A repeatable grid footer. Stops at the last row.
|
||||||
pub(super) struct Footer {
|
pub struct Footer {
|
||||||
/// The first row included in this footer.
|
/// The first row included in this footer.
|
||||||
pub(super) start: usize,
|
pub start: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A possibly repeatable grid object.
|
/// A possibly repeatable grid object.
|
||||||
/// It still exists even when not repeatable, but must not have additional
|
/// It still exists even when not repeatable, but must not have additional
|
||||||
/// considerations by grid layout, other than for consistency (such as making
|
/// considerations by grid layout, other than for consistency (such as making
|
||||||
/// a certain group of rows unbreakable).
|
/// a certain group of rows unbreakable).
|
||||||
pub(super) enum Repeatable<T> {
|
pub enum Repeatable<T> {
|
||||||
Repeated(T),
|
Repeated(T),
|
||||||
NotRepeated(T),
|
NotRepeated(T),
|
||||||
}
|
}
|
||||||
@ -27,7 +29,7 @@ pub(super) enum Repeatable<T> {
|
|||||||
impl<T> Repeatable<T> {
|
impl<T> Repeatable<T> {
|
||||||
/// Gets the value inside this repeatable, regardless of whether
|
/// Gets the value inside this repeatable, regardless of whether
|
||||||
/// it repeats.
|
/// it repeats.
|
||||||
pub(super) fn unwrap(&self) -> &T {
|
pub fn unwrap(&self) -> &T {
|
||||||
match self {
|
match self {
|
||||||
Self::Repeated(repeated) => repeated,
|
Self::Repeated(repeated) => repeated,
|
||||||
Self::NotRepeated(not_repeated) => not_repeated,
|
Self::NotRepeated(not_repeated) => not_repeated,
|
||||||
@ -35,7 +37,7 @@ impl<T> Repeatable<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `Some` if the value is repeated, `None` otherwise.
|
/// Returns `Some` if the value is repeated, `None` otherwise.
|
||||||
pub(super) fn as_repeated(&self) -> Option<&T> {
|
pub fn as_repeated(&self) -> Option<&T> {
|
||||||
match self {
|
match self {
|
||||||
Self::Repeated(repeated) => Some(repeated),
|
Self::Repeated(repeated) => Some(repeated),
|
||||||
Self::NotRepeated(_) => None,
|
Self::NotRepeated(_) => None,
|
||||||
@ -46,7 +48,7 @@ impl<T> Repeatable<T> {
|
|||||||
impl<'a> GridLayouter<'a> {
|
impl<'a> GridLayouter<'a> {
|
||||||
/// Layouts the header's rows.
|
/// Layouts the header's rows.
|
||||||
/// Skips regions as necessary.
|
/// Skips regions as necessary.
|
||||||
pub(super) fn layout_header(
|
pub fn layout_header(
|
||||||
&mut self,
|
&mut self,
|
||||||
header: &Header,
|
header: &Header,
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
@ -90,7 +92,7 @@ impl<'a> GridLayouter<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Simulate the header's group of rows.
|
/// Simulate the header's group of rows.
|
||||||
pub(super) fn simulate_header(
|
pub fn simulate_header(
|
||||||
&self,
|
&self,
|
||||||
header: &Header,
|
header: &Header,
|
||||||
regions: &Regions<'_>,
|
regions: &Regions<'_>,
|
||||||
@ -112,7 +114,7 @@ impl<'a> GridLayouter<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Updates `self.footer_height` by simulating the footer, and skips to fitting region.
|
/// Updates `self.footer_height` by simulating the footer, and skips to fitting region.
|
||||||
pub(super) fn prepare_footer(
|
pub fn prepare_footer(
|
||||||
&mut self,
|
&mut self,
|
||||||
footer: &Footer,
|
footer: &Footer,
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
@ -146,7 +148,7 @@ impl<'a> GridLayouter<'a> {
|
|||||||
|
|
||||||
/// Lays out all rows in the footer.
|
/// Lays out all rows in the footer.
|
||||||
/// They are unbreakable.
|
/// They are unbreakable.
|
||||||
pub(super) fn layout_footer(
|
pub fn layout_footer(
|
||||||
&mut self,
|
&mut self,
|
||||||
footer: &Footer,
|
footer: &Footer,
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
@ -167,7 +169,7 @@ impl<'a> GridLayouter<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Simulate the footer's group of rows.
|
// Simulate the footer's group of rows.
|
||||||
pub(super) fn simulate_footer(
|
pub fn simulate_footer(
|
||||||
&self,
|
&self,
|
||||||
footer: &Footer,
|
footer: &Footer,
|
||||||
regions: &Regions<'_>,
|
regions: &Regions<'_>,
|
@ -1,88 +1,88 @@
|
|||||||
use super::layout::{in_last_with_offset, points, Row, RowPiece};
|
use typst_library::diag::SourceResult;
|
||||||
|
use typst_library::engine::Engine;
|
||||||
|
use typst_library::foundations::Resolve;
|
||||||
|
use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing};
|
||||||
|
use typst_utils::MaybeReverseIter;
|
||||||
|
|
||||||
|
use super::layouter::{in_last_with_offset, points, Row, RowPiece};
|
||||||
use super::repeated::Repeatable;
|
use super::repeated::Repeatable;
|
||||||
use crate::diag::SourceResult;
|
use super::{Cell, GridLayouter};
|
||||||
use crate::engine::Engine;
|
|
||||||
use crate::foundations::Resolve;
|
|
||||||
use crate::layout::{
|
|
||||||
Abs, Axes, Cell, Frame, GridLayouter, Point, Region, Regions, Size, Sizing,
|
|
||||||
};
|
|
||||||
use crate::utils::MaybeReverseIter;
|
|
||||||
|
|
||||||
/// All information needed to layout a single rowspan.
|
/// All information needed to layout a single rowspan.
|
||||||
pub(super) struct Rowspan {
|
pub struct Rowspan {
|
||||||
/// First column of this rowspan.
|
/// First column of this rowspan.
|
||||||
pub(super) x: usize,
|
pub x: usize,
|
||||||
/// First row of this rowspan.
|
/// First row of this rowspan.
|
||||||
pub(super) y: usize,
|
pub y: usize,
|
||||||
/// The disambiguator for laying out the cells.
|
/// The disambiguator for laying out the cells.
|
||||||
pub(super) disambiguator: usize,
|
pub disambiguator: usize,
|
||||||
/// Amount of rows spanned by the cell at (x, y).
|
/// Amount of rows spanned by the cell at (x, y).
|
||||||
pub(super) rowspan: usize,
|
pub rowspan: usize,
|
||||||
/// Whether all rows of the rowspan are part of an unbreakable row group.
|
/// Whether all rows of the rowspan are part of an unbreakable row group.
|
||||||
/// This is true e.g. in headers and footers, regardless of what the user
|
/// This is true e.g. in headers and footers, regardless of what the user
|
||||||
/// specified for the parent cell's `breakable` field.
|
/// specified for the parent cell's `breakable` field.
|
||||||
pub(super) is_effectively_unbreakable: bool,
|
pub is_effectively_unbreakable: bool,
|
||||||
/// The horizontal offset of this rowspan in all regions.
|
/// The horizontal offset of this rowspan in all regions.
|
||||||
pub(super) dx: Abs,
|
pub dx: Abs,
|
||||||
/// The vertical offset of this rowspan in the first region.
|
/// The vertical offset of this rowspan in the first region.
|
||||||
pub(super) dy: Abs,
|
pub dy: Abs,
|
||||||
/// The index of the first region this rowspan appears in.
|
/// The index of the first region this rowspan appears in.
|
||||||
pub(super) first_region: usize,
|
pub first_region: usize,
|
||||||
/// The full height in the first region this rowspan appears in, for
|
/// The full height in the first region this rowspan appears in, for
|
||||||
/// relative sizing.
|
/// relative sizing.
|
||||||
pub(super) region_full: Abs,
|
pub region_full: Abs,
|
||||||
/// The vertical space available for this rowspan in each region.
|
/// The vertical space available for this rowspan in each region.
|
||||||
pub(super) heights: Vec<Abs>,
|
pub heights: Vec<Abs>,
|
||||||
/// The index of the largest resolved spanned row so far.
|
/// The index of the largest resolved spanned row so far.
|
||||||
/// Once a spanned row is resolved and its height added to `heights`, this
|
/// Once a spanned row is resolved and its height added to `heights`, this
|
||||||
/// number is increased. Older rows, even if repeated through e.g. a
|
/// number is increased. Older rows, even if repeated through e.g. a
|
||||||
/// header, will no longer contribute height to this rowspan.
|
/// header, will no longer contribute height to this rowspan.
|
||||||
///
|
///
|
||||||
/// This is `None` if no spanned rows were resolved in `finish_region` yet.
|
/// This is `None` if no spanned rows were resolved in `finish_region` yet.
|
||||||
pub(super) max_resolved_row: Option<usize>,
|
pub max_resolved_row: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The output of the simulation of an unbreakable row group.
|
/// The output of the simulation of an unbreakable row group.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub(super) struct UnbreakableRowGroup {
|
pub struct UnbreakableRowGroup {
|
||||||
/// The rows in this group of unbreakable rows.
|
/// The rows in this group of unbreakable rows.
|
||||||
/// Includes their indices and their predicted heights.
|
/// Includes their indices and their predicted heights.
|
||||||
pub(super) rows: Vec<(usize, Abs)>,
|
pub rows: Vec<(usize, Abs)>,
|
||||||
/// The total height of this row group.
|
/// The total height of this row group.
|
||||||
pub(super) height: Abs,
|
pub height: Abs,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Data used to measure a cell in an auto row.
|
/// Data used to measure a cell in an auto row.
|
||||||
pub(super) struct CellMeasurementData<'layouter> {
|
pub struct CellMeasurementData<'layouter> {
|
||||||
/// The available width for the cell across all regions.
|
/// The available width for the cell across all regions.
|
||||||
pub(super) width: Abs,
|
pub width: Abs,
|
||||||
/// The available height for the cell in its first region.
|
/// The available height for the cell in its first region.
|
||||||
/// Infinite when the auto row is unbreakable.
|
/// Infinite when the auto row is unbreakable.
|
||||||
pub(super) height: Abs,
|
pub height: Abs,
|
||||||
/// The backlog of heights available for the cell in later regions.
|
/// The backlog of heights available for the cell in later regions.
|
||||||
///
|
///
|
||||||
/// When this is `None`, the `custom_backlog` field should be used instead.
|
/// When this is `None`, the `custom_backlog` field should be used instead.
|
||||||
/// That's because, otherwise, this field would have to contain a reference
|
/// That's because, otherwise, this field would have to contain a reference
|
||||||
/// to the `custom_backlog` field, which isn't possible in Rust without
|
/// to the `custom_backlog` field, which isn't possible in Rust without
|
||||||
/// resorting to unsafe hacks.
|
/// resorting to unsafe hacks.
|
||||||
pub(super) backlog: Option<&'layouter [Abs]>,
|
pub backlog: Option<&'layouter [Abs]>,
|
||||||
/// If the backlog needs to be built from scratch instead of reusing the
|
/// If the backlog needs to be built from scratch instead of reusing the
|
||||||
/// one at the current region, which is the case of a multi-region rowspan
|
/// one at the current region, which is the case of a multi-region rowspan
|
||||||
/// (needs to join its backlog of already laid out heights with the current
|
/// (needs to join its backlog of already laid out heights with the current
|
||||||
/// backlog), then this vector will store the new backlog.
|
/// backlog), then this vector will store the new backlog.
|
||||||
pub(super) custom_backlog: Vec<Abs>,
|
pub custom_backlog: Vec<Abs>,
|
||||||
/// The full height of the first region of the cell.
|
/// The full height of the first region of the cell.
|
||||||
/// Infinite when the auto row is unbreakable.
|
/// Infinite when the auto row is unbreakable.
|
||||||
pub(super) full: Abs,
|
pub full: Abs,
|
||||||
/// The height of the last repeated region to use in the measurement pod,
|
/// The height of the last repeated region to use in the measurement pod,
|
||||||
/// if any.
|
/// if any.
|
||||||
pub(super) last: Option<Abs>,
|
pub last: Option<Abs>,
|
||||||
/// The total height of previous rows spanned by the cell in the current
|
/// The total height of previous rows spanned by the cell in the current
|
||||||
/// region (so far).
|
/// region (so far).
|
||||||
pub(super) height_in_this_region: Abs,
|
pub height_in_this_region: Abs,
|
||||||
/// The amount of previous regions spanned by the cell.
|
/// The amount of previous regions spanned by the cell.
|
||||||
/// They are skipped for measurement purposes.
|
/// They are skipped for measurement purposes.
|
||||||
pub(super) frames_in_previous_regions: usize,
|
pub frames_in_previous_regions: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> GridLayouter<'a> {
|
impl<'a> GridLayouter<'a> {
|
||||||
@ -95,7 +95,7 @@ impl<'a> GridLayouter<'a> {
|
|||||||
/// We need to do this only once we already know the heights of all
|
/// We need to do this only once we already know the heights of all
|
||||||
/// spanned rows, which is only possible after laying out the last row
|
/// spanned rows, which is only possible after laying out the last row
|
||||||
/// spanned by the rowspan (or some row immediately after the last one).
|
/// spanned by the rowspan (or some row immediately after the last one).
|
||||||
pub(super) fn layout_rowspan(
|
pub fn layout_rowspan(
|
||||||
&mut self,
|
&mut self,
|
||||||
rowspan_data: Rowspan,
|
rowspan_data: Rowspan,
|
||||||
current_region_data: Option<(&mut Frame, &[RowPiece])>,
|
current_region_data: Option<(&mut Frame, &[RowPiece])>,
|
||||||
@ -184,7 +184,7 @@ impl<'a> GridLayouter<'a> {
|
|||||||
|
|
||||||
/// Checks if a row contains the beginning of one or more rowspan cells.
|
/// Checks if a row contains the beginning of one or more rowspan cells.
|
||||||
/// If so, adds them to the rowspans vector.
|
/// If so, adds them to the rowspans vector.
|
||||||
pub(super) fn check_for_rowspans(&mut self, disambiguator: usize, y: usize) {
|
pub fn check_for_rowspans(&mut self, disambiguator: usize, y: usize) {
|
||||||
// We will compute the horizontal offset of each rowspan in advance.
|
// We will compute the horizontal offset of each rowspan in advance.
|
||||||
// For that reason, we must reverse the column order when using RTL.
|
// For that reason, we must reverse the column order when using RTL.
|
||||||
let offsets = points(self.rcols.iter().copied().rev_if(self.is_rtl));
|
let offsets = points(self.rcols.iter().copied().rev_if(self.is_rtl));
|
||||||
@ -219,7 +219,7 @@ impl<'a> GridLayouter<'a> {
|
|||||||
/// unbreakable row group, and, if so, advances regions until there is
|
/// unbreakable row group, and, if so, advances regions until there is
|
||||||
/// enough space for them. This can be needed, for example, if there's an
|
/// enough space for them. This can be needed, for example, if there's an
|
||||||
/// unbreakable rowspan crossing those rows.
|
/// unbreakable rowspan crossing those rows.
|
||||||
pub(super) fn check_for_unbreakable_rows(
|
pub fn check_for_unbreakable_rows(
|
||||||
&mut self,
|
&mut self,
|
||||||
current_row: usize,
|
current_row: usize,
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
@ -292,7 +292,7 @@ impl<'a> GridLayouter<'a> {
|
|||||||
///
|
///
|
||||||
/// This is used to figure out how much height the next unbreakable row
|
/// This is used to figure out how much height the next unbreakable row
|
||||||
/// group (if any) needs.
|
/// group (if any) needs.
|
||||||
pub(super) fn simulate_unbreakable_row_group(
|
pub fn simulate_unbreakable_row_group(
|
||||||
&self,
|
&self,
|
||||||
first_row: usize,
|
first_row: usize,
|
||||||
amount_unbreakable_rows: Option<usize>,
|
amount_unbreakable_rows: Option<usize>,
|
||||||
@ -359,7 +359,7 @@ impl<'a> GridLayouter<'a> {
|
|||||||
/// Checks if one or more of the cells at the given row are unbreakable.
|
/// Checks if one or more of the cells at the given row are unbreakable.
|
||||||
/// If so, returns the largest rowspan among the unbreakable cells;
|
/// If so, returns the largest rowspan among the unbreakable cells;
|
||||||
/// the spanned rows must, as a result, be laid out in the same region.
|
/// the spanned rows must, as a result, be laid out in the same region.
|
||||||
pub(super) fn check_for_unbreakable_cells(&self, y: usize) -> usize {
|
pub fn check_for_unbreakable_cells(&self, y: usize) -> usize {
|
||||||
(0..self.grid.cols.len())
|
(0..self.grid.cols.len())
|
||||||
.filter_map(|x| self.grid.cell(x, y))
|
.filter_map(|x| self.grid.cell(x, y))
|
||||||
.filter(|cell| !cell.breakable)
|
.filter(|cell| !cell.breakable)
|
||||||
@ -369,7 +369,7 @@ impl<'a> GridLayouter<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Used by `measure_auto_row` to gather data needed to measure the cell.
|
/// Used by `measure_auto_row` to gather data needed to measure the cell.
|
||||||
pub(super) fn prepare_auto_row_cell_measurement(
|
pub fn prepare_auto_row_cell_measurement(
|
||||||
&self,
|
&self,
|
||||||
parent: Axes<usize>,
|
parent: Axes<usize>,
|
||||||
cell: &Cell,
|
cell: &Cell,
|
||||||
@ -577,7 +577,7 @@ impl<'a> GridLayouter<'a> {
|
|||||||
/// expand the auto row based on the rowspan's demanded size, or `false`
|
/// expand the auto row based on the rowspan's demanded size, or `false`
|
||||||
/// otherwise.
|
/// otherwise.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(super) fn prepare_rowspan_sizes(
|
pub fn prepare_rowspan_sizes(
|
||||||
&self,
|
&self,
|
||||||
auto_row_y: usize,
|
auto_row_y: usize,
|
||||||
sizes: &mut Vec<Abs>,
|
sizes: &mut Vec<Abs>,
|
||||||
@ -667,7 +667,7 @@ impl<'a> GridLayouter<'a> {
|
|||||||
/// in each region and the pending rowspans' data (parent Y, rowspan amount
|
/// in each region and the pending rowspans' data (parent Y, rowspan amount
|
||||||
/// and vector of requested sizes).
|
/// and vector of requested sizes).
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(super) fn simulate_and_measure_rowspans_in_auto_row(
|
pub fn simulate_and_measure_rowspans_in_auto_row(
|
||||||
&self,
|
&self,
|
||||||
y: usize,
|
y: usize,
|
||||||
resolved: &mut Vec<Abs>,
|
resolved: &mut Vec<Abs>,
|
142
crates/typst-layout/src/image.rs
Normal file
142
crates/typst-layout/src/image.rs
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
use std::ffi::OsStr;
|
||||||
|
|
||||||
|
use typst_library::diag::{bail, warning, At, SourceResult, StrResult};
|
||||||
|
use typst_library::engine::Engine;
|
||||||
|
use typst_library::foundations::{Packed, Smart, StyleChain};
|
||||||
|
use typst_library::introspection::Locator;
|
||||||
|
use typst_library::layout::{
|
||||||
|
Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size,
|
||||||
|
};
|
||||||
|
use typst_library::loading::Readable;
|
||||||
|
use typst_library::text::families;
|
||||||
|
use typst_library::visualize::{
|
||||||
|
Image, ImageElem, ImageFit, ImageFormat, Path, RasterFormat, VectorFormat,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Layout the image.
|
||||||
|
#[typst_macros::time(span = elem.span())]
|
||||||
|
pub fn layout_image(
|
||||||
|
elem: &Packed<ImageElem>,
|
||||||
|
engine: &mut Engine,
|
||||||
|
_: Locator,
|
||||||
|
styles: StyleChain,
|
||||||
|
region: Region,
|
||||||
|
) -> SourceResult<Frame> {
|
||||||
|
let span = elem.span();
|
||||||
|
|
||||||
|
// Take the format that was explicitly defined, or parse the extension,
|
||||||
|
// or try to detect the format.
|
||||||
|
let data = elem.data();
|
||||||
|
let format = match elem.format(styles) {
|
||||||
|
Smart::Custom(v) => v,
|
||||||
|
Smart::Auto => determine_format(elem.path().as_str(), data).at(span)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Warn the user if the image contains a foreign object. Not perfect
|
||||||
|
// because the svg could also be encoded, but that's an edge case.
|
||||||
|
if format == ImageFormat::Vector(VectorFormat::Svg) {
|
||||||
|
let has_foreign_object =
|
||||||
|
data.as_str().is_some_and(|s| s.contains("<foreignObject"));
|
||||||
|
|
||||||
|
if has_foreign_object {
|
||||||
|
engine.sink.warn(warning!(
|
||||||
|
span,
|
||||||
|
"image contains foreign object";
|
||||||
|
hint: "SVG images with foreign objects might render incorrectly in typst";
|
||||||
|
hint: "see https://github.com/typst/typst/issues/1421 for more information"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the image itself.
|
||||||
|
let image = Image::with_fonts(
|
||||||
|
data.clone().into(),
|
||||||
|
format,
|
||||||
|
elem.alt(styles),
|
||||||
|
engine.world,
|
||||||
|
&families(styles).collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.at(span)?;
|
||||||
|
|
||||||
|
// Determine the image's pixel aspect ratio.
|
||||||
|
let pxw = image.width();
|
||||||
|
let pxh = image.height();
|
||||||
|
let px_ratio = pxw / pxh;
|
||||||
|
|
||||||
|
// Determine the region's aspect ratio.
|
||||||
|
let region_ratio = region.size.x / region.size.y;
|
||||||
|
|
||||||
|
// Find out whether the image is wider or taller than the region.
|
||||||
|
let wide = px_ratio > region_ratio;
|
||||||
|
|
||||||
|
// The space into which the image will be placed according to its fit.
|
||||||
|
let target = if region.expand.x && region.expand.y {
|
||||||
|
// If both width and height are forced, take them.
|
||||||
|
region.size
|
||||||
|
} else if region.expand.x {
|
||||||
|
// If just width is forced, take it.
|
||||||
|
Size::new(region.size.x, region.size.y.min(region.size.x / px_ratio))
|
||||||
|
} else if region.expand.y {
|
||||||
|
// If just height is forced, take it.
|
||||||
|
Size::new(region.size.x.min(region.size.y * px_ratio), region.size.y)
|
||||||
|
} else {
|
||||||
|
// If neither is forced, take the natural image size at the image's
|
||||||
|
// DPI bounded by the available space.
|
||||||
|
let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI);
|
||||||
|
let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi));
|
||||||
|
Size::new(
|
||||||
|
natural.x.min(region.size.x).min(region.size.y * px_ratio),
|
||||||
|
natural.y.min(region.size.y).min(region.size.x / px_ratio),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Compute the actual size of the fitted image.
|
||||||
|
let fit = elem.fit(styles);
|
||||||
|
let fitted = match fit {
|
||||||
|
ImageFit::Cover | ImageFit::Contain => {
|
||||||
|
if wide == (fit == ImageFit::Contain) {
|
||||||
|
Size::new(target.x, target.x / px_ratio)
|
||||||
|
} else {
|
||||||
|
Size::new(target.y * px_ratio, target.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImageFit::Stretch => target,
|
||||||
|
};
|
||||||
|
|
||||||
|
// First, place the image in a frame of exactly its size and then resize
|
||||||
|
// the frame to the target size, center aligning the image in the
|
||||||
|
// process.
|
||||||
|
let mut frame = Frame::soft(fitted);
|
||||||
|
frame.push(Point::zero(), FrameItem::Image(image, fitted, span));
|
||||||
|
frame.resize(target, Axes::splat(FixedAlignment::Center));
|
||||||
|
|
||||||
|
// Create a clipping group if only part of the image should be visible.
|
||||||
|
if fit == ImageFit::Cover && !target.fits(fitted) {
|
||||||
|
frame.clip(Path::rect(frame.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine the image format based on path and data.
|
||||||
|
fn determine_format(path: &str, data: &Readable) -> StrResult<ImageFormat> {
|
||||||
|
let ext = std::path::Path::new(path)
|
||||||
|
.extension()
|
||||||
|
.and_then(OsStr::to_str)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_lowercase();
|
||||||
|
|
||||||
|
Ok(match ext.as_str() {
|
||||||
|
"png" => ImageFormat::Raster(RasterFormat::Png),
|
||||||
|
"jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg),
|
||||||
|
"gif" => ImageFormat::Raster(RasterFormat::Gif),
|
||||||
|
"svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg),
|
||||||
|
_ => match &data {
|
||||||
|
Readable::Str(_) => ImageFormat::Vector(VectorFormat::Svg),
|
||||||
|
Readable::Bytes(bytes) => match RasterFormat::detect(bytes) {
|
||||||
|
Some(f) => ImageFormat::Raster(f),
|
||||||
|
None => bail!("unknown image format"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
87
crates/typst-layout/src/inline/box.rs
Normal file
87
crates/typst-layout/src/inline/box.rs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
use once_cell::unsync::Lazy;
|
||||||
|
use typst_library::diag::SourceResult;
|
||||||
|
use typst_library::engine::Engine;
|
||||||
|
use typst_library::foundations::{Packed, StyleChain};
|
||||||
|
use typst_library::introspection::Locator;
|
||||||
|
use typst_library::layout::{BoxElem, Frame, FrameKind, Size};
|
||||||
|
use typst_library::visualize::Stroke;
|
||||||
|
use typst_utils::Numeric;
|
||||||
|
|
||||||
|
use crate::flow::unbreakable_pod;
|
||||||
|
use crate::shapes::{clip_rect, fill_and_stroke};
|
||||||
|
|
||||||
|
/// Lay out a box as part of a paragraph.
|
||||||
|
#[typst_macros::time(name = "box", span = elem.span())]
|
||||||
|
pub fn layout_box(
|
||||||
|
elem: &Packed<BoxElem>,
|
||||||
|
engine: &mut Engine,
|
||||||
|
locator: Locator,
|
||||||
|
styles: StyleChain,
|
||||||
|
region: Size,
|
||||||
|
) -> SourceResult<Frame> {
|
||||||
|
// Fetch sizing properties.
|
||||||
|
let width = elem.width(styles);
|
||||||
|
let height = elem.height(styles);
|
||||||
|
let inset = elem.inset(styles).unwrap_or_default();
|
||||||
|
|
||||||
|
// Build the pod region.
|
||||||
|
let pod = unbreakable_pod(&width, &height.into(), &inset, styles, region);
|
||||||
|
|
||||||
|
// Layout the body.
|
||||||
|
let mut frame = match elem.body(styles) {
|
||||||
|
// If we have no body, just create an empty frame. If necessary,
|
||||||
|
// its size will be adjusted below.
|
||||||
|
None => Frame::hard(Size::zero()),
|
||||||
|
|
||||||
|
// If we have a child, layout it into the body. Boxes are boundaries
|
||||||
|
// for gradient relativeness, so we set the `FrameKind` to `Hard`.
|
||||||
|
Some(body) => crate::layout_frame(engine, body, locator, styles, pod)?
|
||||||
|
.with_kind(FrameKind::Hard),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Enforce a correct frame size on the expanded axes. Do this before
|
||||||
|
// applying the inset, since the pod shrunk.
|
||||||
|
frame.set_size(pod.expand.select(pod.size, frame.size()));
|
||||||
|
|
||||||
|
// Apply the inset.
|
||||||
|
if !inset.is_zero() {
|
||||||
|
crate::pad::grow(&mut frame, &inset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare fill and stroke.
|
||||||
|
let fill = elem.fill(styles);
|
||||||
|
let stroke = elem
|
||||||
|
.stroke(styles)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.map(|s| s.map(Stroke::unwrap_or_default));
|
||||||
|
|
||||||
|
// Only fetch these if necessary (for clipping or filling/stroking).
|
||||||
|
let outset = Lazy::new(|| elem.outset(styles).unwrap_or_default());
|
||||||
|
let radius = Lazy::new(|| elem.radius(styles).unwrap_or_default());
|
||||||
|
|
||||||
|
// Clip the contents, if requested.
|
||||||
|
if elem.clip(styles) {
|
||||||
|
let size = frame.size() + outset.relative_to(frame.size()).sum_by_axis();
|
||||||
|
frame.clip(clip_rect(size, &radius, &stroke));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add fill and/or stroke.
|
||||||
|
if fill.is_some() || stroke.iter().any(Option::is_some) {
|
||||||
|
fill_and_stroke(&mut frame, fill, &stroke, &outset, &radius, elem.span());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign label to the frame.
|
||||||
|
if let Some(label) = elem.label() {
|
||||||
|
frame.label(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply baseline shift. Do this after setting the size and applying the
|
||||||
|
// inset, so that a relative shift is resolved relative to the final
|
||||||
|
// height.
|
||||||
|
let shift = elem.baseline(styles).relative_to(frame.height());
|
||||||
|
if !shift.is_zero() {
|
||||||
|
frame.set_baseline(frame.baseline() - shift);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(frame)
|
||||||
|
}
|
@ -1,17 +1,18 @@
|
|||||||
use super::*;
|
use typst_library::diag::bail;
|
||||||
use crate::diag::bail;
|
use typst_library::foundations::{Packed, Resolve};
|
||||||
use crate::foundations::{Packed, Resolve};
|
use typst_library::introspection::{SplitLocator, Tag, TagElem};
|
||||||
use crate::introspection::{SplitLocator, Tag, TagElem};
|
use typst_library::layout::{
|
||||||
use crate::layout::{
|
|
||||||
Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing,
|
Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing,
|
||||||
Spacing,
|
Spacing,
|
||||||
};
|
};
|
||||||
use crate::syntax::Span;
|
use typst_library::text::{
|
||||||
use crate::text::{
|
|
||||||
is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes,
|
is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes,
|
||||||
SpaceElem, TextElem,
|
SpaceElem, TextElem,
|
||||||
};
|
};
|
||||||
use crate::utils::Numeric;
|
use typst_syntax::Span;
|
||||||
|
use typst_utils::Numeric;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
// The characters by which spacing, inline content and pins are replaced in the
|
// The characters by which spacing, inline content and pins are replaced in the
|
||||||
// paragraph's full text.
|
// paragraph's full text.
|
||||||
@ -222,7 +223,7 @@ pub fn collect<'a>(
|
|||||||
if let Sizing::Fr(v) = elem.width(styles) {
|
if let Sizing::Fr(v) = elem.width(styles) {
|
||||||
collector.push_item(Item::Fractional(v, Some((elem, loc, styles))));
|
collector.push_item(Item::Fractional(v, Some((elem, loc, styles))));
|
||||||
} else {
|
} else {
|
||||||
let frame = elem.layout(engine, loc, styles, region)?;
|
let frame = layout_box(elem, engine, loc, styles, region)?;
|
||||||
collector.push_item(Item::Frame(frame, styles));
|
collector.push_item(Item::Frame(frame, styles));
|
||||||
}
|
}
|
||||||
} else if let Some(elem) = child.to_packed::<TagElem>() {
|
} else if let Some(elem) = child.to_packed::<TagElem>() {
|
213
crates/typst-layout/src/inline/deco.rs
Normal file
213
crates/typst-layout/src/inline/deco.rs
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
use kurbo::{BezPath, Line, ParamCurve};
|
||||||
|
use ttf_parser::{GlyphId, OutlineBuilder};
|
||||||
|
use typst_library::layout::{Abs, Em, Frame, FrameItem, Point, Size};
|
||||||
|
use typst_library::text::{
|
||||||
|
BottomEdge, DecoLine, Decoration, TextEdgeBounds, TextItem, TopEdge,
|
||||||
|
};
|
||||||
|
use typst_library::visualize::{FixedStroke, Geometry};
|
||||||
|
use typst_syntax::Span;
|
||||||
|
|
||||||
|
use crate::shapes::styled_rect;
|
||||||
|
|
||||||
|
/// Add line decorations to a single run of shaped text.
|
||||||
|
pub fn decorate(
|
||||||
|
frame: &mut Frame,
|
||||||
|
deco: &Decoration,
|
||||||
|
text: &TextItem,
|
||||||
|
width: Abs,
|
||||||
|
shift: Abs,
|
||||||
|
pos: Point,
|
||||||
|
) {
|
||||||
|
let font_metrics = text.font.metrics();
|
||||||
|
|
||||||
|
if let DecoLine::Highlight { fill, stroke, top_edge, bottom_edge, radius } =
|
||||||
|
&deco.line
|
||||||
|
{
|
||||||
|
let (top, bottom) = determine_edges(text, *top_edge, *bottom_edge);
|
||||||
|
let size = Size::new(width + 2.0 * deco.extent, top + bottom);
|
||||||
|
let rects = styled_rect(size, radius, fill.clone(), stroke);
|
||||||
|
let origin = Point::new(pos.x - deco.extent, pos.y - top - shift);
|
||||||
|
frame.prepend_multiple(
|
||||||
|
rects
|
||||||
|
.into_iter()
|
||||||
|
.map(|shape| (origin, FrameItem::Shape(shape, Span::detached()))),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (stroke, metrics, offset, evade, background) = match &deco.line {
|
||||||
|
DecoLine::Strikethrough { stroke, offset, background } => {
|
||||||
|
(stroke, font_metrics.strikethrough, offset, false, *background)
|
||||||
|
}
|
||||||
|
DecoLine::Overline { stroke, offset, evade, background } => {
|
||||||
|
(stroke, font_metrics.overline, offset, *evade, *background)
|
||||||
|
}
|
||||||
|
DecoLine::Underline { stroke, offset, evade, background } => {
|
||||||
|
(stroke, font_metrics.underline, offset, *evade, *background)
|
||||||
|
}
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let offset = offset.unwrap_or(-metrics.position.at(text.size)) - shift;
|
||||||
|
let stroke = stroke.clone().unwrap_or(FixedStroke::from_pair(
|
||||||
|
text.fill.as_decoration(),
|
||||||
|
metrics.thickness.at(text.size),
|
||||||
|
));
|
||||||
|
|
||||||
|
let gap_padding = 0.08 * text.size;
|
||||||
|
let min_width = 0.162 * text.size;
|
||||||
|
|
||||||
|
let start = pos.x - deco.extent;
|
||||||
|
let end = pos.x + width + deco.extent;
|
||||||
|
|
||||||
|
let mut push_segment = |from: Abs, to: Abs, prepend: bool| {
|
||||||
|
let origin = Point::new(from, pos.y + offset);
|
||||||
|
let target = Point::new(to - from, Abs::zero());
|
||||||
|
|
||||||
|
if target.x >= min_width || !evade {
|
||||||
|
let shape = Geometry::Line(target).stroked(stroke.clone());
|
||||||
|
|
||||||
|
if prepend {
|
||||||
|
frame.prepend(origin, FrameItem::Shape(shape, Span::detached()));
|
||||||
|
} else {
|
||||||
|
frame.push(origin, FrameItem::Shape(shape, Span::detached()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !evade {
|
||||||
|
push_segment(start, end, background);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let line = Line::new(
|
||||||
|
kurbo::Point::new(pos.x.to_raw(), offset.to_raw()),
|
||||||
|
kurbo::Point::new((pos.x + width).to_raw(), offset.to_raw()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut x = pos.x;
|
||||||
|
let mut intersections = vec![];
|
||||||
|
|
||||||
|
for glyph in text.glyphs.iter() {
|
||||||
|
let dx = glyph.x_offset.at(text.size) + x;
|
||||||
|
let mut builder =
|
||||||
|
BezPathBuilder::new(font_metrics.units_per_em, text.size, dx.to_raw());
|
||||||
|
|
||||||
|
let bbox = text.font.ttf().outline_glyph(GlyphId(glyph.id), &mut builder);
|
||||||
|
let path = builder.finish();
|
||||||
|
|
||||||
|
x += glyph.x_advance.at(text.size);
|
||||||
|
|
||||||
|
// Only do the costly segments intersection test if the line
|
||||||
|
// intersects the bounding box.
|
||||||
|
let intersect = bbox.is_some_and(|bbox| {
|
||||||
|
let y_min = -text.font.to_em(bbox.y_max).at(text.size);
|
||||||
|
let y_max = -text.font.to_em(bbox.y_min).at(text.size);
|
||||||
|
offset >= y_min && offset <= y_max
|
||||||
|
});
|
||||||
|
|
||||||
|
if intersect {
|
||||||
|
// Find all intersections of segments with the line.
|
||||||
|
intersections.extend(
|
||||||
|
path.segments()
|
||||||
|
.flat_map(|seg| seg.intersect_line(line))
|
||||||
|
.map(|is| Abs::raw(line.eval(is.line_t).x)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add start and end points, taking padding into account.
|
||||||
|
intersections.push(start - gap_padding);
|
||||||
|
intersections.push(end + gap_padding);
|
||||||
|
// When emitting the decorative line segments, we move from left to
|
||||||
|
// right. The intersections are not necessarily in this order, yet.
|
||||||
|
intersections.sort();
|
||||||
|
|
||||||
|
for edge in intersections.windows(2) {
|
||||||
|
let l = edge[0];
|
||||||
|
let r = edge[1];
|
||||||
|
|
||||||
|
// If we are too close, don't draw the segment
|
||||||
|
if r - l < gap_padding {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
push_segment(l + gap_padding, r - gap_padding, background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the top/bottom edge of the text given the metric of the font.
|
||||||
|
fn determine_edges(
|
||||||
|
text: &TextItem,
|
||||||
|
top_edge: TopEdge,
|
||||||
|
bottom_edge: BottomEdge,
|
||||||
|
) -> (Abs, Abs) {
|
||||||
|
let mut top = Abs::zero();
|
||||||
|
let mut bottom = Abs::zero();
|
||||||
|
|
||||||
|
for g in text.glyphs.iter() {
|
||||||
|
let (t, b) = text.font.edges(
|
||||||
|
top_edge,
|
||||||
|
bottom_edge,
|
||||||
|
text.size,
|
||||||
|
TextEdgeBounds::Glyph(g.id),
|
||||||
|
);
|
||||||
|
top.set_max(t);
|
||||||
|
bottom.set_max(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
(top, bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a kurbo [`BezPath`] for a glyph.
|
||||||
|
struct BezPathBuilder {
|
||||||
|
path: BezPath,
|
||||||
|
units_per_em: f64,
|
||||||
|
font_size: Abs,
|
||||||
|
x_offset: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BezPathBuilder {
|
||||||
|
fn new(units_per_em: f64, font_size: Abs, x_offset: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
path: BezPath::new(),
|
||||||
|
units_per_em,
|
||||||
|
font_size,
|
||||||
|
x_offset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(self) -> BezPath {
|
||||||
|
self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn p(&self, x: f32, y: f32) -> kurbo::Point {
|
||||||
|
kurbo::Point::new(self.s(x) + self.x_offset, -self.s(y))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn s(&self, v: f32) -> f64 {
|
||||||
|
Em::from_units(v, self.units_per_em).at(self.font_size).to_raw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutlineBuilder for BezPathBuilder {
|
||||||
|
fn move_to(&mut self, x: f32, y: f32) {
|
||||||
|
self.path.move_to(self.p(x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_to(&mut self, x: f32, y: f32) {
|
||||||
|
self.path.line_to(self.p(x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
|
||||||
|
self.path.quad_to(self.p(x1, y1), self.p(x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
|
||||||
|
self.path.curve_to(self.p(x1, y1), self.p(x2, y2), self.p(x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close(&mut self) {
|
||||||
|
self.path.close_path();
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
|
use typst_library::introspection::SplitLocator;
|
||||||
|
use typst_utils::Numeric;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::introspection::SplitLocator;
|
|
||||||
use crate::utils::Numeric;
|
|
||||||
|
|
||||||
/// Turns the selected lines into frames.
|
/// Turns the selected lines into frames.
|
||||||
#[typst_macros::time]
|
#[typst_macros::time]
|
@ -1,14 +1,15 @@
|
|||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Debug, Formatter};
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
|
use typst_library::engine::Engine;
|
||||||
|
use typst_library::foundations::NativeElement;
|
||||||
|
use typst_library::introspection::{SplitLocator, Tag};
|
||||||
|
use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point};
|
||||||
|
use typst_library::model::{ParLine, ParLineMarker};
|
||||||
|
use typst_library::text::{Lang, TextElem};
|
||||||
|
use typst_utils::Numeric;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::engine::Engine;
|
|
||||||
use crate::foundations::NativeElement;
|
|
||||||
use crate::introspection::{SplitLocator, Tag};
|
|
||||||
use crate::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point};
|
|
||||||
use crate::model::{ParLine, ParLineMarker};
|
|
||||||
use crate::text::{Lang, TextElem};
|
|
||||||
use crate::utils::Numeric;
|
|
||||||
|
|
||||||
const SHY: char = '\u{ad}';
|
const SHY: char = '\u{ad}';
|
||||||
const HYPHEN: char = '-';
|
const HYPHEN: char = '-';
|
||||||
@ -510,7 +511,7 @@ pub fn commit(
|
|||||||
if let Some((elem, loc, styles)) = elem {
|
if let Some((elem, loc, styles)) = elem {
|
||||||
let region = Size::new(amount, full);
|
let region = Size::new(amount, full);
|
||||||
let mut frame =
|
let mut frame =
|
||||||
elem.layout(engine, loc.relayout(), *styles, region)?;
|
layout_box(elem, engine, loc.relayout(), *styles, region)?;
|
||||||
frame.translate(Point::with_y(TextElem::baseline_in(*styles)));
|
frame.translate(Point::with_y(TextElem::baseline_in(*styles)));
|
||||||
push(&mut offset, frame.post_processed(*styles));
|
push(&mut offset, frame.post_processed(*styles));
|
||||||
} else {
|
} else {
|
||||||
@ -590,7 +591,7 @@ fn add_par_line_marker(
|
|||||||
// where line numbers can be displayed), so we just need it to be in a tag
|
// where line numbers can be displayed), so we just need it to be in a tag
|
||||||
// and to be valid (to have a location).
|
// and to be valid (to have a location).
|
||||||
let mut marker = ParLineMarker::new(numbering, align, margin, clearance).pack();
|
let mut marker = ParLineMarker::new(numbering, align, margin, clearance).pack();
|
||||||
let key = crate::utils::hash128(&marker);
|
let key = typst_utils::hash128(&marker);
|
||||||
let loc = locator.next_location(engine.introspector, key);
|
let loc = locator.next_location(engine.introspector, key);
|
||||||
marker.set_location(loc);
|
marker.set_location(loc);
|
||||||
|
|
@ -8,14 +8,14 @@ use icu_provider_adapters::fork::ForkByKeyProvider;
|
|||||||
use icu_provider_blob::BlobDataProvider;
|
use icu_provider_blob::BlobDataProvider;
|
||||||
use icu_segmenter::LineSegmenter;
|
use icu_segmenter::LineSegmenter;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
use typst_library::engine::Engine;
|
||||||
|
use typst_library::layout::{Abs, Em};
|
||||||
|
use typst_library::model::Linebreaks;
|
||||||
|
use typst_library::text::{is_default_ignorable, Lang, TextElem};
|
||||||
|
use typst_syntax::link_prefix;
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::engine::Engine;
|
|
||||||
use crate::layout::{Abs, Em};
|
|
||||||
use crate::model::Linebreaks;
|
|
||||||
use crate::syntax::link_prefix;
|
|
||||||
use crate::text::{is_default_ignorable, Lang, TextElem};
|
|
||||||
|
|
||||||
/// The cost of a line or paragraph layout.
|
/// The cost of a line or paragraph layout.
|
||||||
type Cost = f64;
|
type Cost = f64;
|
@ -1,13 +1,27 @@
|
|||||||
|
#[path = "box.rs"]
|
||||||
|
mod box_;
|
||||||
mod collect;
|
mod collect;
|
||||||
|
mod deco;
|
||||||
mod finalize;
|
mod finalize;
|
||||||
mod line;
|
mod line;
|
||||||
mod linebreak;
|
mod linebreak;
|
||||||
mod prepare;
|
mod prepare;
|
||||||
mod shaping;
|
mod shaping;
|
||||||
|
|
||||||
|
pub use self::box_::layout_box;
|
||||||
|
|
||||||
use comemo::{Track, Tracked, TrackedMut};
|
use comemo::{Track, Tracked, TrackedMut};
|
||||||
|
use typst_library::diag::SourceResult;
|
||||||
|
use typst_library::engine::{Engine, Route, Sink, Traced};
|
||||||
|
use typst_library::foundations::{StyleChain, StyleVec};
|
||||||
|
use typst_library::introspection::{Introspector, Locator, LocatorLink};
|
||||||
|
use typst_library::layout::{Fragment, Size};
|
||||||
|
use typst_library::model::ParElem;
|
||||||
|
use typst_library::routines::Routines;
|
||||||
|
use typst_library::World;
|
||||||
|
|
||||||
use self::collect::{collect, Item, Segment, SpanMapper};
|
use self::collect::{collect, Item, Segment, SpanMapper};
|
||||||
|
use self::deco::decorate;
|
||||||
use self::finalize::finalize;
|
use self::finalize::finalize;
|
||||||
use self::line::{commit, line, Line};
|
use self::line::{commit, line, Line};
|
||||||
use self::linebreak::{linebreak, Breakpoint};
|
use self::linebreak::{linebreak, Breakpoint};
|
||||||
@ -16,19 +30,12 @@ use self::shaping::{
|
|||||||
cjk_punct_style, is_of_cj_script, shape_range, ShapedGlyph, ShapedText,
|
cjk_punct_style, is_of_cj_script, shape_range, ShapedGlyph, ShapedText,
|
||||||
BEGIN_PUNCT_PAT, END_PUNCT_PAT,
|
BEGIN_PUNCT_PAT, END_PUNCT_PAT,
|
||||||
};
|
};
|
||||||
use crate::diag::SourceResult;
|
|
||||||
use crate::engine::{Engine, Route, Sink, Traced};
|
|
||||||
use crate::foundations::{StyleChain, StyleVec};
|
|
||||||
use crate::introspection::{Introspector, Locator, LocatorLink};
|
|
||||||
use crate::layout::{Fragment, Size};
|
|
||||||
use crate::model::ParElem;
|
|
||||||
use crate::World;
|
|
||||||
|
|
||||||
/// Range of a substring of text.
|
/// Range of a substring of text.
|
||||||
type Range = std::ops::Range<usize>;
|
type Range = std::ops::Range<usize>;
|
||||||
|
|
||||||
/// Layouts content inline.
|
/// Layouts content inline.
|
||||||
pub(crate) fn layout_inline(
|
pub fn layout_inline(
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
children: &StyleVec,
|
children: &StyleVec,
|
||||||
locator: Locator,
|
locator: Locator,
|
||||||
@ -39,6 +46,7 @@ pub(crate) fn layout_inline(
|
|||||||
) -> SourceResult<Fragment> {
|
) -> SourceResult<Fragment> {
|
||||||
layout_inline_impl(
|
layout_inline_impl(
|
||||||
children,
|
children,
|
||||||
|
engine.routines,
|
||||||
engine.world,
|
engine.world,
|
||||||
engine.introspector,
|
engine.introspector,
|
||||||
engine.traced,
|
engine.traced,
|
||||||
@ -57,6 +65,7 @@ pub(crate) fn layout_inline(
|
|||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn layout_inline_impl(
|
fn layout_inline_impl(
|
||||||
children: &StyleVec,
|
children: &StyleVec,
|
||||||
|
routines: &Routines,
|
||||||
world: Tracked<dyn World + '_>,
|
world: Tracked<dyn World + '_>,
|
||||||
introspector: Tracked<Introspector>,
|
introspector: Tracked<Introspector>,
|
||||||
traced: Tracked<Traced>,
|
traced: Tracked<Traced>,
|
||||||
@ -71,6 +80,7 @@ fn layout_inline_impl(
|
|||||||
let link = LocatorLink::new(locator);
|
let link = LocatorLink::new(locator);
|
||||||
let locator = Locator::link(&link);
|
let locator = Locator::link(&link);
|
||||||
let mut engine = Engine {
|
let mut engine = Engine {
|
||||||
|
routines,
|
||||||
world,
|
world,
|
||||||
introspector,
|
introspector,
|
||||||
traced,
|
traced,
|
@ -1,10 +1,10 @@
|
|||||||
|
use typst_library::foundations::{Resolve, Smart};
|
||||||
|
use typst_library::layout::{Abs, AlignElem, Dir, Em, FixedAlignment};
|
||||||
|
use typst_library::model::Linebreaks;
|
||||||
|
use typst_library::text::{Costs, Lang, TextElem};
|
||||||
use unicode_bidi::{BidiInfo, Level as BidiLevel};
|
use unicode_bidi::{BidiInfo, Level as BidiLevel};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::foundations::{Resolve, Smart};
|
|
||||||
use crate::layout::{Abs, AlignElem, Dir, Em, FixedAlignment};
|
|
||||||
use crate::model::Linebreaks;
|
|
||||||
use crate::text::{Costs, Lang, TextElem};
|
|
||||||
|
|
||||||
/// A paragraph representation in which children are already layouted and text
|
/// A paragraph representation in which children are already layouted and text
|
||||||
/// is already preshaped.
|
/// is already preshaped.
|
@ -7,19 +7,19 @@ use az::SaturatingAs;
|
|||||||
use ecow::EcoString;
|
use ecow::EcoString;
|
||||||
use rustybuzz::{BufferFlags, ShapePlan, UnicodeBuffer};
|
use rustybuzz::{BufferFlags, ShapePlan, UnicodeBuffer};
|
||||||
use ttf_parser::Tag;
|
use ttf_parser::Tag;
|
||||||
|
use typst_library::engine::Engine;
|
||||||
|
use typst_library::foundations::{Smart, StyleChain};
|
||||||
|
use typst_library::layout::{Abs, Dir, Em, Frame, FrameItem, Point, Size};
|
||||||
|
use typst_library::text::{
|
||||||
|
families, features, is_default_ignorable, variant, Font, FontVariant, Glyph, Lang,
|
||||||
|
Region, TextEdgeBounds, TextElem, TextItem,
|
||||||
|
};
|
||||||
|
use typst_library::World;
|
||||||
|
use typst_utils::SliceExt;
|
||||||
use unicode_bidi::{BidiInfo, Level as BidiLevel};
|
use unicode_bidi::{BidiInfo, Level as BidiLevel};
|
||||||
use unicode_script::{Script, UnicodeScript};
|
use unicode_script::{Script, UnicodeScript};
|
||||||
|
|
||||||
use super::{Item, Range, SpanMapper};
|
use super::{decorate, Item, Range, SpanMapper};
|
||||||
use crate::engine::Engine;
|
|
||||||
use crate::foundations::{Smart, StyleChain};
|
|
||||||
use crate::layout::{Abs, Dir, Em, Frame, FrameItem, Point, Size};
|
|
||||||
use crate::text::{
|
|
||||||
decorate, families, features, is_default_ignorable, variant, Font, FontVariant,
|
|
||||||
Glyph, Lang, Region, TextEdgeBounds, TextElem, TextItem,
|
|
||||||
};
|
|
||||||
use crate::utils::SliceExt;
|
|
||||||
use crate::World;
|
|
||||||
|
|
||||||
/// The result of shaping text.
|
/// The result of shaping text.
|
||||||
///
|
///
|
30
crates/typst-layout/src/lib.rs
Normal file
30
crates/typst-layout/src/lib.rs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
//! Typst's layout engine.
|
||||||
|
|
||||||
|
mod flow;
|
||||||
|
mod grid;
|
||||||
|
mod image;
|
||||||
|
mod inline;
|
||||||
|
mod lists;
|
||||||
|
mod math;
|
||||||
|
mod pad;
|
||||||
|
mod pages;
|
||||||
|
mod repeat;
|
||||||
|
mod shapes;
|
||||||
|
mod stack;
|
||||||
|
mod transforms;
|
||||||
|
|
||||||
|
pub use self::flow::{layout_columns, layout_fragment, layout_frame};
|
||||||
|
pub use self::grid::{layout_grid, layout_table};
|
||||||
|
pub use self::image::layout_image;
|
||||||
|
pub use self::inline::{layout_box, layout_inline};
|
||||||
|
pub use self::lists::{layout_enum, layout_list};
|
||||||
|
pub use self::math::{layout_equation_block, layout_equation_inline};
|
||||||
|
pub use self::pad::layout_pad;
|
||||||
|
pub use self::pages::layout_document;
|
||||||
|
pub use self::repeat::layout_repeat;
|
||||||
|
pub use self::shapes::{
|
||||||
|
layout_circle, layout_ellipse, layout_line, layout_path, layout_polygon, layout_rect,
|
||||||
|
layout_square,
|
||||||
|
};
|
||||||
|
pub use self::stack::layout_stack;
|
||||||
|
pub use self::transforms::{layout_move, layout_rotate, layout_scale, layout_skew};
|
146
crates/typst-layout/src/lists.rs
Normal file
146
crates/typst-layout/src/lists.rs
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
use comemo::Track;
|
||||||
|
use smallvec::smallvec;
|
||||||
|
use typst_library::diag::SourceResult;
|
||||||
|
use typst_library::engine::Engine;
|
||||||
|
use typst_library::foundations::{Content, Context, Depth, Packed, StyleChain};
|
||||||
|
use typst_library::introspection::Locator;
|
||||||
|
use typst_library::layout::{Axes, Fragment, HAlignment, Regions, Sizing, VAlignment};
|
||||||
|
use typst_library::model::{EnumElem, ListElem, Numbering, ParElem};
|
||||||
|
use typst_library::text::TextElem;
|
||||||
|
|
||||||
|
use crate::grid::{Cell, CellGrid, GridLayouter};
|
||||||
|
|
||||||
|
/// Layout the list.
|
||||||
|
#[typst_macros::time(span = elem.span())]
|
||||||
|
pub fn layout_list(
|
||||||
|
elem: &Packed<ListElem>,
|
||||||
|
engine: &mut Engine,
|
||||||
|
locator: Locator,
|
||||||
|
styles: StyleChain,
|
||||||
|
regions: Regions,
|
||||||
|
) -> SourceResult<Fragment> {
|
||||||
|
let indent = elem.indent(styles);
|
||||||
|
let body_indent = elem.body_indent(styles);
|
||||||
|
let gutter = elem.spacing(styles).unwrap_or_else(|| {
|
||||||
|
if elem.tight(styles) {
|
||||||
|
ParElem::leading_in(styles).into()
|
||||||
|
} else {
|
||||||
|
ParElem::spacing_in(styles).into()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let Depth(depth) = ListElem::depth_in(styles);
|
||||||
|
let marker = elem
|
||||||
|
.marker(styles)
|
||||||
|
.resolve(engine, styles, depth)?
|
||||||
|
// avoid '#set align' interference with the list
|
||||||
|
.aligned(HAlignment::Start + VAlignment::Top);
|
||||||
|
|
||||||
|
let mut cells = vec![];
|
||||||
|
let mut locator = locator.split();
|
||||||
|
|
||||||
|
for item in elem.children() {
|
||||||
|
cells.push(Cell::new(Content::empty(), locator.next(&())));
|
||||||
|
cells.push(Cell::new(marker.clone(), locator.next(&marker.span())));
|
||||||
|
cells.push(Cell::new(Content::empty(), locator.next(&())));
|
||||||
|
cells.push(Cell::new(
|
||||||
|
item.body.clone().styled(ListElem::set_depth(Depth(1))),
|
||||||
|
locator.next(&item.body.span()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let grid = CellGrid::new(
|
||||||
|
Axes::with_x(&[
|
||||||
|
Sizing::Rel(indent.into()),
|
||||||
|
Sizing::Auto,
|
||||||
|
Sizing::Rel(body_indent.into()),
|
||||||
|
Sizing::Auto,
|
||||||
|
]),
|
||||||
|
Axes::with_y(&[gutter.into()]),
|
||||||
|
cells,
|
||||||
|
);
|
||||||
|
let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
|
||||||
|
|
||||||
|
layouter.layout(engine)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout the enumeration.
|
||||||
|
#[typst_macros::time(span = elem.span())]
|
||||||
|
pub fn layout_enum(
|
||||||
|
elem: &Packed<EnumElem>,
|
||||||
|
engine: &mut Engine,
|
||||||
|
locator: Locator,
|
||||||
|
styles: StyleChain,
|
||||||
|
regions: Regions,
|
||||||
|
) -> SourceResult<Fragment> {
|
||||||
|
let numbering = elem.numbering(styles);
|
||||||
|
let indent = elem.indent(styles);
|
||||||
|
let body_indent = elem.body_indent(styles);
|
||||||
|
let gutter = elem.spacing(styles).unwrap_or_else(|| {
|
||||||
|
if elem.tight(styles) {
|
||||||
|
ParElem::leading_in(styles).into()
|
||||||
|
} else {
|
||||||
|
ParElem::spacing_in(styles).into()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut cells = vec![];
|
||||||
|
let mut locator = locator.split();
|
||||||
|
let mut number = elem.start(styles);
|
||||||
|
let mut parents = EnumElem::parents_in(styles);
|
||||||
|
|
||||||
|
let full = elem.full(styles);
|
||||||
|
|
||||||
|
// Horizontally align based on the given respective parameter.
|
||||||
|
// Vertically align to the top to avoid inheriting `horizon` or `bottom`
|
||||||
|
// alignment from the context and having the number be displaced in
|
||||||
|
// relation to the item it refers to.
|
||||||
|
let number_align = elem.number_align(styles);
|
||||||
|
|
||||||
|
for item in elem.children() {
|
||||||
|
number = item.number(styles).unwrap_or(number);
|
||||||
|
|
||||||
|
let context = Context::new(None, Some(styles));
|
||||||
|
let resolved = if full {
|
||||||
|
parents.push(number);
|
||||||
|
let content = numbering.apply(engine, context.track(), &parents)?.display();
|
||||||
|
parents.pop();
|
||||||
|
content
|
||||||
|
} else {
|
||||||
|
match numbering {
|
||||||
|
Numbering::Pattern(pattern) => {
|
||||||
|
TextElem::packed(pattern.apply_kth(parents.len(), number))
|
||||||
|
}
|
||||||
|
other => other.apply(engine, context.track(), &[number])?.display(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Disable overhang as a workaround to end-aligned dots glitching
|
||||||
|
// and decreasing spacing between numbers and items.
|
||||||
|
let resolved =
|
||||||
|
resolved.aligned(number_align).styled(TextElem::set_overhang(false));
|
||||||
|
|
||||||
|
cells.push(Cell::new(Content::empty(), locator.next(&())));
|
||||||
|
cells.push(Cell::new(resolved, locator.next(&())));
|
||||||
|
cells.push(Cell::new(Content::empty(), locator.next(&())));
|
||||||
|
cells.push(Cell::new(
|
||||||
|
item.body.clone().styled(EnumElem::set_parents(smallvec![number])),
|
||||||
|
locator.next(&item.body.span()),
|
||||||
|
));
|
||||||
|
number = number.saturating_add(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let grid = CellGrid::new(
|
||||||
|
Axes::with_x(&[
|
||||||
|
Sizing::Rel(indent.into()),
|
||||||
|
Sizing::Auto,
|
||||||
|
Sizing::Rel(body_indent.into()),
|
||||||
|
Sizing::Auto,
|
||||||
|
]),
|
||||||
|
Axes::with_y(&[gutter.into()]),
|
||||||
|
cells,
|
||||||
|
);
|
||||||
|
let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
|
||||||
|
|
||||||
|
layouter.layout(engine)
|
||||||
|
}
|
75
crates/typst-layout/src/math/accent.rs
Normal file
75
crates/typst-layout/src/math/accent.rs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
use typst_library::diag::SourceResult;
|
||||||
|
use typst_library::foundations::{Packed, StyleChain};
|
||||||
|
use typst_library::layout::{Em, Frame, Point, Rel, Size};
|
||||||
|
use typst_library::math::{Accent, AccentElem};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
scaled_font_size, style_cramped, FrameFragment, GlyphFragment, MathContext,
|
||||||
|
MathFragment,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// How much the accent can be shorter than the base.
|
||||||
|
const ACCENT_SHORT_FALL: Em = Em::new(0.5);
|
||||||
|
|
||||||
|
/// Lays out an [`AccentElem`].
|
||||||
|
#[typst_macros::time(name = "math.accent", span = elem.span())]
|
||||||
|
pub fn layout_accent(
|
||||||
|
elem: &Packed<AccentElem>,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
let cramped = style_cramped();
|
||||||
|
let base = ctx.layout_into_fragment(elem.base(), styles.chain(&cramped))?;
|
||||||
|
|
||||||
|
// Preserve class to preserve automatic spacing.
|
||||||
|
let base_class = base.class();
|
||||||
|
let base_attach = base.accent_attach();
|
||||||
|
|
||||||
|
let width = elem
|
||||||
|
.size(styles)
|
||||||
|
.unwrap_or(Rel::one())
|
||||||
|
.at(scaled_font_size(ctx, styles))
|
||||||
|
.relative_to(base.width());
|
||||||
|
|
||||||
|
// Forcing the accent to be at least as large as the base makes it too
|
||||||
|
// wide in many case.
|
||||||
|
let Accent(c) = elem.accent();
|
||||||
|
let glyph = GlyphFragment::new(ctx, styles, *c, elem.span());
|
||||||
|
let short_fall = ACCENT_SHORT_FALL.at(glyph.font_size);
|
||||||
|
let variant = glyph.stretch_horizontal(ctx, width, short_fall);
|
||||||
|
let accent = variant.frame;
|
||||||
|
let accent_attach = variant.accent_attach;
|
||||||
|
|
||||||
|
// Descent is negative because the accent's ink bottom is above the
|
||||||
|
// baseline. Therefore, the default gap is the accent's negated descent
|
||||||
|
// minus the accent base height. Only if the base is very small, we need
|
||||||
|
// a larger gap so that the accent doesn't move too low.
|
||||||
|
let accent_base_height = scaled!(ctx, styles, accent_base_height);
|
||||||
|
let gap = -accent.descent() - base.height().min(accent_base_height);
|
||||||
|
let size = Size::new(base.width(), accent.height() + gap + base.height());
|
||||||
|
let accent_pos = Point::with_x(base_attach - accent_attach);
|
||||||
|
let base_pos = Point::with_y(accent.height() + gap);
|
||||||
|
let baseline = base_pos.y + base.ascent();
|
||||||
|
let base_italics_correction = base.italics_correction();
|
||||||
|
let base_text_like = base.is_text_like();
|
||||||
|
|
||||||
|
let base_ascent = match &base {
|
||||||
|
MathFragment::Frame(frame) => frame.base_ascent,
|
||||||
|
_ => base.ascent(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut frame = Frame::soft(size);
|
||||||
|
frame.set_baseline(baseline);
|
||||||
|
frame.push_frame(accent_pos, accent);
|
||||||
|
frame.push_frame(base_pos, base.into_frame());
|
||||||
|
ctx.push(
|
||||||
|
FrameFragment::new(ctx, styles, frame)
|
||||||
|
.with_class(base_class)
|
||||||
|
.with_base_ascent(base_ascent)
|
||||||
|
.with_italics_correction(base_italics_correction)
|
||||||
|
.with_accent_attach(base_attach)
|
||||||
|
.with_text_like(base_text_like),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1,14 +1,16 @@
|
|||||||
use unicode_math_class::MathClass;
|
use typst_library::diag::SourceResult;
|
||||||
|
use typst_library::foundations::{Packed, Smart, StyleChain};
|
||||||
use crate::diag::SourceResult;
|
use typst_library::layout::{Abs, Axis, Corner, Frame, Length, Point, Rel, Size};
|
||||||
use crate::foundations::{elem, Content, Packed, Smart, StyleChain};
|
use typst_library::math::{
|
||||||
use crate::layout::{Abs, Axis, Corner, Frame, Length, Point, Rel, Size};
|
AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem,
|
||||||
use crate::math::{
|
};
|
||||||
stretch_fragment, style_for_subscript, style_for_superscript, EquationElem,
|
use typst_library::text::TextElem;
|
||||||
FrameFragment, LayoutMath, MathContext, MathFragment, MathSize, Scaled, StretchElem,
|
use typst_utils::OptionExt;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
stretch_fragment, style_for_subscript, style_for_superscript, FrameFragment, Limits,
|
||||||
|
MathContext, MathFragment,
|
||||||
};
|
};
|
||||||
use crate::text::TextElem;
|
|
||||||
use crate::utils::OptionExt;
|
|
||||||
|
|
||||||
macro_rules! measure {
|
macro_rules! measure {
|
||||||
($e: ident, $attr: ident) => {
|
($e: ident, $attr: ident) => {
|
||||||
@ -16,51 +18,15 @@ macro_rules! measure {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A base with optional attachments.
|
/// Lays out an [`AttachElem`].
|
||||||
///
|
#[typst_macros::time(name = "math.attach", span = elem.span())]
|
||||||
/// ```example
|
pub fn layout_attach(
|
||||||
/// $ attach(
|
elem: &Packed<AttachElem>,
|
||||||
/// Pi, t: alpha, b: beta,
|
ctx: &mut MathContext,
|
||||||
/// tl: 1, tr: 2+3, bl: 4+5, br: 6,
|
styles: StyleChain,
|
||||||
/// ) $
|
) -> SourceResult<()> {
|
||||||
/// ```
|
let merged = elem.merge_base();
|
||||||
#[elem(LayoutMath)]
|
let elem = merged.as_ref().unwrap_or(elem);
|
||||||
pub struct AttachElem {
|
|
||||||
/// The base to which things are attached.
|
|
||||||
#[required]
|
|
||||||
pub base: Content,
|
|
||||||
|
|
||||||
/// The top attachment, smartly positioned at top-right or above the base.
|
|
||||||
///
|
|
||||||
/// You can wrap the base in `{limits()}` or `{scripts()}` to override the
|
|
||||||
/// smart positioning.
|
|
||||||
pub t: Option<Content>,
|
|
||||||
|
|
||||||
/// The bottom attachment, smartly positioned at the bottom-right or below
|
|
||||||
/// the base.
|
|
||||||
///
|
|
||||||
/// You can wrap the base in `{limits()}` or `{scripts()}` to override the
|
|
||||||
/// smart positioning.
|
|
||||||
pub b: Option<Content>,
|
|
||||||
|
|
||||||
/// The top-left attachment (before the base).
|
|
||||||
pub tl: Option<Content>,
|
|
||||||
|
|
||||||
/// The bottom-left attachment (before base).
|
|
||||||
pub bl: Option<Content>,
|
|
||||||
|
|
||||||
/// The top-right attachment (after the base).
|
|
||||||
pub tr: Option<Content>,
|
|
||||||
|
|
||||||
/// The bottom-right attachment (after the base).
|
|
||||||
pub br: Option<Content>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LayoutMath for Packed<AttachElem> {
|
|
||||||
#[typst_macros::time(name = "math.attach", span = self.span())]
|
|
||||||
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
|
|
||||||
let new_elem = merge_base(self);
|
|
||||||
let elem = new_elem.as_ref().unwrap_or(self);
|
|
||||||
let stretch = stretch_size(styles, elem);
|
let stretch = stretch_size(styles, elem);
|
||||||
|
|
||||||
let mut base = ctx.layout_into_fragment(elem.base(), styles)?;
|
let mut base = ctx.layout_into_fragment(elem.base(), styles)?;
|
||||||
@ -120,30 +86,16 @@ impl LayoutMath for Packed<AttachElem> {
|
|||||||
];
|
];
|
||||||
|
|
||||||
layout_attachments(ctx, styles, base, fragments)
|
layout_attachments(ctx, styles, base, fragments)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Grouped primes.
|
/// Lays out a [`PrimeElem`].
|
||||||
///
|
#[typst_macros::time(name = "math.primes", span = elem.span())]
|
||||||
/// ```example
|
pub fn layout_primes(
|
||||||
/// $ a'''_b = a^'''_b $
|
elem: &Packed<PrimesElem>,
|
||||||
/// ```
|
ctx: &mut MathContext,
|
||||||
///
|
styles: StyleChain,
|
||||||
/// # Syntax
|
) -> SourceResult<()> {
|
||||||
/// This function has dedicated syntax: use apostrophes instead of primes. They
|
match *elem.count() {
|
||||||
/// will automatically attach to the previous element, moving superscripts to
|
|
||||||
/// the next level.
|
|
||||||
#[elem(LayoutMath)]
|
|
||||||
pub struct PrimesElem {
|
|
||||||
/// The number of grouped primes.
|
|
||||||
#[required]
|
|
||||||
pub count: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LayoutMath for Packed<PrimesElem> {
|
|
||||||
#[typst_macros::time(name = "math.primes", span = self.span())]
|
|
||||||
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
|
|
||||||
match *self.count() {
|
|
||||||
count @ 1..=4 => {
|
count @ 1..=4 => {
|
||||||
let c = match count {
|
let c = match count {
|
||||||
1 => '′',
|
1 => '′',
|
||||||
@ -157,9 +109,8 @@ impl LayoutMath for Packed<PrimesElem> {
|
|||||||
}
|
}
|
||||||
count => {
|
count => {
|
||||||
// Custom amount of primes
|
// Custom amount of primes
|
||||||
let prime = ctx
|
let prime =
|
||||||
.layout_into_fragment(&TextElem::packed('′'), styles)?
|
ctx.layout_into_fragment(&TextElem::packed('′'), styles)?.into_frame();
|
||||||
.into_frame();
|
|
||||||
let width = prime.width() * (count + 1) as f64 / 2.0;
|
let width = prime.width() * (count + 1) as f64 / 2.0;
|
||||||
let mut frame = Frame::soft(Size::new(width, prime.height()));
|
let mut frame = Frame::soft(Size::new(width, prime.height()));
|
||||||
frame.set_baseline(prime.ascent());
|
frame.set_baseline(prime.ascent());
|
||||||
@ -174,142 +125,33 @@ impl LayoutMath for Packed<PrimesElem> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Forces a base to display attachments as scripts.
|
/// Lays out a [`ScriptsElem`].
|
||||||
///
|
#[typst_macros::time(name = "math.scripts", span = elem.span())]
|
||||||
/// ```example
|
pub fn layout_scripts(
|
||||||
/// $ scripts(sum)_1^2 != sum_1^2 $
|
elem: &Packed<ScriptsElem>,
|
||||||
/// ```
|
ctx: &mut MathContext,
|
||||||
#[elem(LayoutMath)]
|
styles: StyleChain,
|
||||||
pub struct ScriptsElem {
|
) -> SourceResult<()> {
|
||||||
/// The base to attach the scripts to.
|
let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?;
|
||||||
#[required]
|
|
||||||
pub body: Content,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LayoutMath for Packed<ScriptsElem> {
|
|
||||||
#[typst_macros::time(name = "math.scripts", span = self.span())]
|
|
||||||
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
|
|
||||||
let mut fragment = ctx.layout_into_fragment(self.body(), styles)?;
|
|
||||||
fragment.set_limits(Limits::Never);
|
fragment.set_limits(Limits::Never);
|
||||||
ctx.push(fragment);
|
ctx.push(fragment);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Forces a base to display attachments as limits.
|
/// Lays out a [`LimitsElem`].
|
||||||
///
|
#[typst_macros::time(name = "math.limits", span = elem.span())]
|
||||||
/// ```example
|
pub fn layout_limits(
|
||||||
/// $ limits(A)_1^2 != A_1^2 $
|
elem: &Packed<LimitsElem>,
|
||||||
/// ```
|
ctx: &mut MathContext,
|
||||||
#[elem(LayoutMath)]
|
styles: StyleChain,
|
||||||
pub struct LimitsElem {
|
) -> SourceResult<()> {
|
||||||
/// The base to attach the limits to.
|
let limits = if elem.inline(styles) { Limits::Always } else { Limits::Display };
|
||||||
#[required]
|
let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?;
|
||||||
pub body: Content,
|
|
||||||
|
|
||||||
/// Whether to also force limits in inline equations.
|
|
||||||
///
|
|
||||||
/// When applying limits globally (e.g., through a show rule), it is
|
|
||||||
/// typically a good idea to disable this.
|
|
||||||
#[default(true)]
|
|
||||||
pub inline: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LayoutMath for Packed<LimitsElem> {
|
|
||||||
#[typst_macros::time(name = "math.limits", span = self.span())]
|
|
||||||
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
|
|
||||||
let limits = if self.inline(styles) { Limits::Always } else { Limits::Display };
|
|
||||||
let mut fragment = ctx.layout_into_fragment(self.body(), styles)?;
|
|
||||||
fragment.set_limits(limits);
|
fragment.set_limits(limits);
|
||||||
ctx.push(fragment);
|
ctx.push(fragment);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Describes in which situation a frame should use limits for attachments.
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub enum Limits {
|
|
||||||
/// Always scripts.
|
|
||||||
Never,
|
|
||||||
/// Display limits only in `display` math.
|
|
||||||
Display,
|
|
||||||
/// Always limits.
|
|
||||||
Always,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Limits {
|
|
||||||
/// The default limit configuration if the given character is the base.
|
|
||||||
pub fn for_char(c: char) -> Self {
|
|
||||||
match unicode_math_class::class(c) {
|
|
||||||
Some(MathClass::Large) => {
|
|
||||||
if is_integral_char(c) {
|
|
||||||
Limits::Never
|
|
||||||
} else {
|
|
||||||
Limits::Display
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(MathClass::Relation) => Limits::Always,
|
|
||||||
_ => Limits::Never,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The default limit configuration for a math class.
|
|
||||||
pub fn for_class(class: MathClass) -> Self {
|
|
||||||
match class {
|
|
||||||
MathClass::Large => Self::Display,
|
|
||||||
MathClass::Relation => Self::Always,
|
|
||||||
_ => Self::Never,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether limits should be displayed in this context.
|
|
||||||
pub fn active(&self, styles: StyleChain) -> bool {
|
|
||||||
match self {
|
|
||||||
Self::Always => true,
|
|
||||||
Self::Display => EquationElem::size_in(styles) == MathSize::Display,
|
|
||||||
Self::Never => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If an AttachElem's base is also an AttachElem, merge attachments into the
|
|
||||||
/// base AttachElem where possible.
|
|
||||||
fn merge_base(elem: &Packed<AttachElem>) -> Option<Packed<AttachElem>> {
|
|
||||||
// Extract from an EquationElem.
|
|
||||||
let mut base = elem.base();
|
|
||||||
if let Some(equation) = base.to_packed::<EquationElem>() {
|
|
||||||
base = equation.body();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move attachments from elem into base where possible.
|
|
||||||
if let Some(base) = base.to_packed::<AttachElem>() {
|
|
||||||
let mut elem = elem.clone();
|
|
||||||
let mut base = base.clone();
|
|
||||||
|
|
||||||
macro_rules! merge {
|
|
||||||
($content:ident) => {
|
|
||||||
if base.$content.is_none() && elem.$content.is_some() {
|
|
||||||
base.$content = elem.$content.clone();
|
|
||||||
elem.$content = None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
merge!(t);
|
|
||||||
merge!(b);
|
|
||||||
merge!(tl);
|
|
||||||
merge!(tr);
|
|
||||||
merge!(bl);
|
|
||||||
merge!(br);
|
|
||||||
|
|
||||||
elem.base = base.pack();
|
|
||||||
return Some(elem);
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the size to stretch the base to, if the attach argument is true.
|
/// Get the size to stretch the base to, if the attach argument is true.
|
||||||
@ -326,7 +168,7 @@ fn stretch_size(
|
|||||||
base.to_packed::<StretchElem>().map(|stretch| stretch.size(styles))
|
base.to_packed::<StretchElem>().map(|stretch| stretch.size(styles))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Layout the attachments.
|
/// Lay out the attachments.
|
||||||
fn layout_attachments(
|
fn layout_attachments(
|
||||||
ctx: &mut MathContext,
|
ctx: &mut MathContext,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
@ -671,8 +513,3 @@ fn math_kern(
|
|||||||
// result in glyphs colliding.
|
// result in glyphs colliding.
|
||||||
summed_kern(corr_height_top).max(summed_kern(corr_height_bot))
|
summed_kern(corr_height_top).max(summed_kern(corr_height_bot))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determines if the character is one of a variety of integral signs.
|
|
||||||
fn is_integral_char(c: char) -> bool {
|
|
||||||
('∫'..='∳').contains(&c) || ('⨋'..='⨜').contains(&c)
|
|
||||||
}
|
|
144
crates/typst-layout/src/math/cancel.rs
Normal file
144
crates/typst-layout/src/math/cancel.rs
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
use comemo::Track;
|
||||||
|
use typst_library::diag::{At, SourceResult};
|
||||||
|
use typst_library::foundations::{Context, Packed, Smart, StyleChain};
|
||||||
|
use typst_library::layout::{Abs, Angle, Frame, FrameItem, Point, Rel, Size, Transform};
|
||||||
|
use typst_library::math::{CancelAngle, CancelElem};
|
||||||
|
use typst_library::text::TextElem;
|
||||||
|
use typst_library::visualize::{FixedStroke, Geometry};
|
||||||
|
use typst_syntax::Span;
|
||||||
|
|
||||||
|
use super::{scaled_font_size, FrameFragment, MathContext};
|
||||||
|
|
||||||
|
/// Lays out a [`CancelElem`].
|
||||||
|
#[typst_macros::time(name = "math.cancel", span = elem.span())]
|
||||||
|
pub fn layout_cancel(
|
||||||
|
elem: &Packed<CancelElem>,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
let body = ctx.layout_into_fragment(elem.body(), styles)?;
|
||||||
|
|
||||||
|
// Preserve properties of body.
|
||||||
|
let body_class = body.class();
|
||||||
|
let body_italics = body.italics_correction();
|
||||||
|
let body_attach = body.accent_attach();
|
||||||
|
let body_text_like = body.is_text_like();
|
||||||
|
|
||||||
|
let mut body = body.into_frame();
|
||||||
|
let body_size = body.size();
|
||||||
|
let span = elem.span();
|
||||||
|
let length = elem.length(styles).at(scaled_font_size(ctx, styles));
|
||||||
|
|
||||||
|
let stroke = elem.stroke(styles).unwrap_or(FixedStroke {
|
||||||
|
paint: TextElem::fill_in(styles).as_decoration(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let invert = elem.inverted(styles);
|
||||||
|
let cross = elem.cross(styles);
|
||||||
|
let angle = elem.angle(styles);
|
||||||
|
|
||||||
|
let invert_first_line = !cross && invert;
|
||||||
|
let first_line = draw_cancel_line(
|
||||||
|
ctx,
|
||||||
|
length,
|
||||||
|
stroke.clone(),
|
||||||
|
invert_first_line,
|
||||||
|
&angle,
|
||||||
|
body_size,
|
||||||
|
styles,
|
||||||
|
span,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// The origin of our line is the very middle of the element.
|
||||||
|
let center = body_size.to_point() / 2.0;
|
||||||
|
body.push_frame(center, first_line);
|
||||||
|
|
||||||
|
if cross {
|
||||||
|
// Draw the second line.
|
||||||
|
let second_line =
|
||||||
|
draw_cancel_line(ctx, length, stroke, true, &angle, body_size, styles, span)?;
|
||||||
|
|
||||||
|
body.push_frame(center, second_line);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.push(
|
||||||
|
FrameFragment::new(ctx, styles, body)
|
||||||
|
.with_class(body_class)
|
||||||
|
.with_italics_correction(body_italics)
|
||||||
|
.with_accent_attach(body_attach)
|
||||||
|
.with_text_like(body_text_like),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a cancel line.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn draw_cancel_line(
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
length_scale: Rel<Abs>,
|
||||||
|
stroke: FixedStroke,
|
||||||
|
invert: bool,
|
||||||
|
angle: &Smart<CancelAngle>,
|
||||||
|
body_size: Size,
|
||||||
|
styles: StyleChain,
|
||||||
|
span: Span,
|
||||||
|
) -> SourceResult<Frame> {
|
||||||
|
let default = default_angle(body_size);
|
||||||
|
let mut angle = match angle {
|
||||||
|
// Non specified angle defaults to the diagonal
|
||||||
|
Smart::Auto => default,
|
||||||
|
Smart::Custom(angle) => match angle {
|
||||||
|
// This specifies the absolute angle w.r.t y-axis clockwise.
|
||||||
|
CancelAngle::Angle(v) => *v,
|
||||||
|
// This specifies a function that takes the default angle as input.
|
||||||
|
CancelAngle::Func(func) => func
|
||||||
|
.call(ctx.engine, Context::new(None, Some(styles)).track(), [default])?
|
||||||
|
.cast()
|
||||||
|
.at(span)?,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// invert means flipping along the y-axis
|
||||||
|
if invert {
|
||||||
|
angle *= -1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// same as above, the default length is the diagonal of the body box.
|
||||||
|
let default_length = body_size.to_point().hypot();
|
||||||
|
let length = length_scale.relative_to(default_length);
|
||||||
|
|
||||||
|
// Draw a vertical line of length and rotate it by angle
|
||||||
|
let start = Point::new(Abs::zero(), length / 2.0);
|
||||||
|
let delta = Point::new(Abs::zero(), -length);
|
||||||
|
|
||||||
|
let mut frame = Frame::soft(body_size);
|
||||||
|
frame.push(start, FrameItem::Shape(Geometry::Line(delta).stroked(stroke), span));
|
||||||
|
|
||||||
|
// Having the middle of the line at the origin is convenient here.
|
||||||
|
frame.transform(Transform::rotate(angle));
|
||||||
|
Ok(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default line angle for a body of the given size.
|
||||||
|
fn default_angle(body: Size) -> Angle {
|
||||||
|
// The default cancel line is the diagonal.
|
||||||
|
// We infer the default angle from
|
||||||
|
// the diagonal w.r.t to the body box.
|
||||||
|
//
|
||||||
|
// The returned angle is in the range of [0, Pi/2]
|
||||||
|
//
|
||||||
|
// Note that the angle is computed w.r.t to the y-axis
|
||||||
|
//
|
||||||
|
// B
|
||||||
|
// /|
|
||||||
|
// diagonal / | height
|
||||||
|
// / |
|
||||||
|
// / |
|
||||||
|
// O ----
|
||||||
|
// width
|
||||||
|
let (width, height) = (body.x, body.y);
|
||||||
|
let default_angle = (width / height).atan(); // arctangent (in the range [0, Pi/2])
|
||||||
|
Angle::rad(default_angle)
|
||||||
|
}
|
@ -1,90 +1,47 @@
|
|||||||
use crate::diag::{bail, SourceResult};
|
use typst_library::diag::SourceResult;
|
||||||
use crate::foundations::{elem, Content, Packed, StyleChain, Value};
|
use typst_library::foundations::{Content, Packed, StyleChain};
|
||||||
use crate::layout::{Em, Frame, FrameItem, Point, Size};
|
use typst_library::layout::{Em, Frame, FrameItem, Point, Size};
|
||||||
use crate::math::{
|
use typst_library::math::{BinomElem, FracElem};
|
||||||
|
use typst_library::text::TextElem;
|
||||||
|
use typst_library::visualize::{FixedStroke, Geometry};
|
||||||
|
use typst_syntax::Span;
|
||||||
|
|
||||||
|
use super::{
|
||||||
scaled_font_size, style_for_denominator, style_for_numerator, FrameFragment,
|
scaled_font_size, style_for_denominator, style_for_numerator, FrameFragment,
|
||||||
GlyphFragment, LayoutMath, MathContext, Scaled, DELIM_SHORT_FALL,
|
GlyphFragment, MathContext, DELIM_SHORT_FALL,
|
||||||
};
|
};
|
||||||
use crate::syntax::{Span, Spanned};
|
|
||||||
use crate::text::TextElem;
|
|
||||||
use crate::visualize::{FixedStroke, Geometry};
|
|
||||||
|
|
||||||
const FRAC_AROUND: Em = Em::new(0.1);
|
const FRAC_AROUND: Em = Em::new(0.1);
|
||||||
|
|
||||||
/// A mathematical fraction.
|
/// Lays out a [`FracElem`].
|
||||||
///
|
#[typst_macros::time(name = "math.frac", span = elem.span())]
|
||||||
/// # Example
|
pub fn layout_frac(
|
||||||
/// ```example
|
elem: &Packed<FracElem>,
|
||||||
/// $ 1/2 < (x+1)/2 $
|
ctx: &mut MathContext,
|
||||||
/// $ ((x+1)) / 2 = frac(a, b) $
|
styles: StyleChain,
|
||||||
/// ```
|
) -> SourceResult<()> {
|
||||||
///
|
layout_frac_like(
|
||||||
/// # Syntax
|
|
||||||
/// This function also has dedicated syntax: Use a slash to turn neighbouring
|
|
||||||
/// expressions into a fraction. Multiple atoms can be grouped into a single
|
|
||||||
/// expression using round grouping parenthesis. Such parentheses are removed
|
|
||||||
/// from the output, but you can nest multiple to force them.
|
|
||||||
#[elem(title = "Fraction", LayoutMath)]
|
|
||||||
pub struct FracElem {
|
|
||||||
/// The fraction's numerator.
|
|
||||||
#[required]
|
|
||||||
pub num: Content,
|
|
||||||
|
|
||||||
/// The fraction's denominator.
|
|
||||||
#[required]
|
|
||||||
pub denom: Content,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LayoutMath for Packed<FracElem> {
|
|
||||||
#[typst_macros::time(name = "math.frac", span = self.span())]
|
|
||||||
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
|
|
||||||
layout(
|
|
||||||
ctx,
|
ctx,
|
||||||
styles,
|
styles,
|
||||||
self.num(),
|
elem.num(),
|
||||||
std::slice::from_ref(self.denom()),
|
std::slice::from_ref(elem.denom()),
|
||||||
false,
|
false,
|
||||||
self.span(),
|
elem.span(),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A binomial expression.
|
/// Lays out a [`BinomElem`].
|
||||||
///
|
#[typst_macros::time(name = "math.binom", span = elem.span())]
|
||||||
/// # Example
|
pub fn layout_binom(
|
||||||
/// ```example
|
elem: &Packed<BinomElem>,
|
||||||
/// $ binom(n, k) $
|
ctx: &mut MathContext,
|
||||||
/// $ binom(n, k_1, k_2, k_3, ..., k_m) $
|
styles: StyleChain,
|
||||||
/// ```
|
) -> SourceResult<()> {
|
||||||
#[elem(title = "Binomial", LayoutMath)]
|
layout_frac_like(ctx, styles, elem.upper(), elem.lower(), true, elem.span())
|
||||||
pub struct BinomElem {
|
|
||||||
/// The binomial's upper index.
|
|
||||||
#[required]
|
|
||||||
pub upper: Content,
|
|
||||||
|
|
||||||
/// The binomial's lower index.
|
|
||||||
#[required]
|
|
||||||
#[variadic]
|
|
||||||
#[parse(
|
|
||||||
let values = args.all::<Spanned<Value>>()?;
|
|
||||||
if values.is_empty() {
|
|
||||||
// Prevents one element binomials
|
|
||||||
bail!(args.span, "missing argument: lower");
|
|
||||||
}
|
|
||||||
values.into_iter().map(|spanned| spanned.v.display()).collect()
|
|
||||||
)]
|
|
||||||
pub lower: Vec<Content>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LayoutMath for Packed<BinomElem> {
|
|
||||||
#[typst_macros::time(name = "math.binom", span = self.span())]
|
|
||||||
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
|
|
||||||
layout(ctx, styles, self.upper(), self.lower(), true, self.span())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Layout a fraction or binomial.
|
/// Layout a fraction or binomial.
|
||||||
fn layout(
|
fn layout_frac_like(
|
||||||
ctx: &mut MathContext,
|
ctx: &mut MathContext,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
num: &Content,
|
num: &Content,
|
@ -1,22 +1,25 @@
|
|||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Debug, Formatter};
|
||||||
|
|
||||||
|
use rustybuzz::Feature;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use ttf_parser::gsub::AlternateSet;
|
use ttf_parser::gsub::{
|
||||||
|
AlternateSet, AlternateSubstitution, SingleSubstitution, SubstitutionSubtable,
|
||||||
|
};
|
||||||
|
use ttf_parser::opentype_layout::LayoutTable;
|
||||||
use ttf_parser::{GlyphId, Rect};
|
use ttf_parser::{GlyphId, Rect};
|
||||||
|
use typst_library::foundations::StyleChain;
|
||||||
|
use typst_library::introspection::Tag;
|
||||||
|
use typst_library::layout::{
|
||||||
|
Abs, Axis, Corner, Em, Frame, FrameItem, HideElem, Point, Size, VAlignment,
|
||||||
|
};
|
||||||
|
use typst_library::math::{EquationElem, MathSize};
|
||||||
|
use typst_library::model::{Destination, LinkElem};
|
||||||
|
use typst_library::text::{Font, Glyph, Lang, Region, TextElem, TextItem};
|
||||||
|
use typst_library::visualize::Paint;
|
||||||
|
use typst_syntax::Span;
|
||||||
use unicode_math_class::MathClass;
|
use unicode_math_class::MathClass;
|
||||||
|
|
||||||
use crate::foundations::StyleChain;
|
use super::{scaled_font_size, stretch_glyph, MathContext, Scaled};
|
||||||
use crate::introspection::Tag;
|
|
||||||
use crate::layout::{
|
|
||||||
Abs, Corner, Em, Frame, FrameItem, HideElem, Point, Size, VAlignment,
|
|
||||||
};
|
|
||||||
use crate::math::{
|
|
||||||
scaled_font_size, EquationElem, Limits, MathContext, MathSize, Scaled,
|
|
||||||
};
|
|
||||||
use crate::model::{Destination, LinkElem};
|
|
||||||
use crate::syntax::Span;
|
|
||||||
use crate::text::{Font, Glyph, Lang, Region, TextElem, TextItem};
|
|
||||||
use crate::visualize::Paint;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum MathFragment {
|
pub enum MathFragment {
|
||||||
@ -350,7 +353,6 @@ impl GlyphFragment {
|
|||||||
pub fn into_variant(self) -> VariantFragment {
|
pub fn into_variant(self) -> VariantFragment {
|
||||||
VariantFragment {
|
VariantFragment {
|
||||||
c: self.c,
|
c: self.c,
|
||||||
id: Some(self.id),
|
|
||||||
font_size: self.font_size,
|
font_size: self.font_size,
|
||||||
italics_correction: self.italics_correction,
|
italics_correction: self.italics_correction,
|
||||||
accent_attach: self.accent_attach,
|
accent_attach: self.accent_attach,
|
||||||
@ -406,6 +408,26 @@ impl GlyphFragment {
|
|||||||
self.set_id(ctx, alt_id);
|
self.set_id(ctx, alt_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Try to stretch a glyph to a desired height.
|
||||||
|
pub fn stretch_vertical(
|
||||||
|
self,
|
||||||
|
ctx: &MathContext,
|
||||||
|
height: Abs,
|
||||||
|
short_fall: Abs,
|
||||||
|
) -> VariantFragment {
|
||||||
|
stretch_glyph(ctx, self, height, short_fall, Axis::Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to stretch a glyph to a desired width.
|
||||||
|
pub fn stretch_horizontal(
|
||||||
|
self,
|
||||||
|
ctx: &MathContext,
|
||||||
|
width: Abs,
|
||||||
|
short_fall: Abs,
|
||||||
|
) -> VariantFragment {
|
||||||
|
stretch_glyph(ctx, self, width, short_fall, Axis::X)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for GlyphFragment {
|
impl Debug for GlyphFragment {
|
||||||
@ -417,7 +439,6 @@ impl Debug for GlyphFragment {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct VariantFragment {
|
pub struct VariantFragment {
|
||||||
pub c: char,
|
pub c: char,
|
||||||
pub id: Option<GlyphId>,
|
|
||||||
pub italics_correction: Abs,
|
pub italics_correction: Abs,
|
||||||
pub accent_attach: Abs,
|
pub accent_attach: Abs,
|
||||||
pub frame: Frame,
|
pub frame: Frame,
|
||||||
@ -582,3 +603,102 @@ fn kern_at_height(
|
|||||||
|
|
||||||
Some(kern.kern(i)?.scaled(ctx, font_size))
|
Some(kern.kern(i)?.scaled(ctx, font_size))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Describes in which situation a frame should use limits for attachments.
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub enum Limits {
|
||||||
|
/// Always scripts.
|
||||||
|
Never,
|
||||||
|
/// Display limits only in `display` math.
|
||||||
|
Display,
|
||||||
|
/// Always limits.
|
||||||
|
Always,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Limits {
|
||||||
|
/// The default limit configuration if the given character is the base.
|
||||||
|
pub fn for_char(c: char) -> Self {
|
||||||
|
match unicode_math_class::class(c) {
|
||||||
|
Some(MathClass::Large) => {
|
||||||
|
if is_integral_char(c) {
|
||||||
|
Limits::Never
|
||||||
|
} else {
|
||||||
|
Limits::Display
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(MathClass::Relation) => Limits::Always,
|
||||||
|
_ => Limits::Never,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default limit configuration for a math class.
|
||||||
|
pub fn for_class(class: MathClass) -> Self {
|
||||||
|
match class {
|
||||||
|
MathClass::Large => Self::Display,
|
||||||
|
MathClass::Relation => Self::Always,
|
||||||
|
_ => Self::Never,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether limits should be displayed in this context.
|
||||||
|
pub fn active(&self, styles: StyleChain) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Always => true,
|
||||||
|
Self::Display => EquationElem::size_in(styles) == MathSize::Display,
|
||||||
|
Self::Never => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines if the character is one of a variety of integral signs.
|
||||||
|
fn is_integral_char(c: char) -> bool {
|
||||||
|
('∫'..='∳').contains(&c) || ('⨋'..='⨜').contains(&c)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An OpenType substitution table that is applicable to glyph-wise substitutions.
|
||||||
|
pub enum GlyphwiseSubsts<'a> {
|
||||||
|
Single(SingleSubstitution<'a>),
|
||||||
|
Alternate(AlternateSubstitution<'a>, u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> GlyphwiseSubsts<'a> {
|
||||||
|
pub fn new(gsub: LayoutTable<'a>, feature: Feature) -> Option<Self> {
|
||||||
|
let table = gsub
|
||||||
|
.features
|
||||||
|
.find(ttf_parser::Tag(feature.tag.0))
|
||||||
|
.and_then(|feature| feature.lookup_indices.get(0))
|
||||||
|
.and_then(|index| gsub.lookups.get(index))?;
|
||||||
|
let table = table.subtables.get::<SubstitutionSubtable>(0)?;
|
||||||
|
match table {
|
||||||
|
SubstitutionSubtable::Single(single_glyphs) => {
|
||||||
|
Some(Self::Single(single_glyphs))
|
||||||
|
}
|
||||||
|
SubstitutionSubtable::Alternate(alt_glyphs) => {
|
||||||
|
Some(Self::Alternate(alt_glyphs, feature.value))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_apply(&self, glyph_id: GlyphId) -> Option<GlyphId> {
|
||||||
|
match self {
|
||||||
|
Self::Single(single) => match single {
|
||||||
|
SingleSubstitution::Format1 { coverage, delta } => coverage
|
||||||
|
.get(glyph_id)
|
||||||
|
.map(|_| GlyphId(glyph_id.0.wrapping_add(*delta as u16))),
|
||||||
|
SingleSubstitution::Format2 { coverage, substitutes } => {
|
||||||
|
coverage.get(glyph_id).and_then(|idx| substitutes.get(idx))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Self::Alternate(alternate, value) => alternate
|
||||||
|
.coverage
|
||||||
|
.get(glyph_id)
|
||||||
|
.and_then(|idx| alternate.alternate_sets.get(idx))
|
||||||
|
.and_then(|set| set.alternates.get(*value as u16)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply(&self, glyph_id: GlyphId) -> GlyphId {
|
||||||
|
self.try_apply(glyph_id).unwrap_or(glyph_id)
|
||||||
|
}
|
||||||
|
}
|
135
crates/typst-layout/src/math/lr.rs
Normal file
135
crates/typst-layout/src/math/lr.rs
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
use typst_library::diag::SourceResult;
|
||||||
|
use typst_library::foundations::{Packed, Smart, StyleChain};
|
||||||
|
use typst_library::layout::{Abs, Axis, Length, Rel};
|
||||||
|
use typst_library::math::{EquationElem, LrElem, MidElem};
|
||||||
|
use unicode_math_class::MathClass;
|
||||||
|
|
||||||
|
use super::{stretch_fragment, MathContext, MathFragment, DELIM_SHORT_FALL};
|
||||||
|
|
||||||
|
/// Lays out an [`LrElem`].
|
||||||
|
#[typst_macros::time(name = "math.lr", span = elem.span())]
|
||||||
|
pub fn layout_lr(
|
||||||
|
elem: &Packed<LrElem>,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
let mut body = elem.body();
|
||||||
|
|
||||||
|
// Extract from an EquationElem.
|
||||||
|
if let Some(equation) = body.to_packed::<EquationElem>() {
|
||||||
|
body = equation.body();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract implicit LrElem.
|
||||||
|
if let Some(lr) = body.to_packed::<LrElem>() {
|
||||||
|
if lr.size(styles).is_auto() {
|
||||||
|
body = lr.body();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut fragments = ctx.layout_into_fragments(body, styles)?;
|
||||||
|
let axis = scaled!(ctx, styles, axis_height);
|
||||||
|
let max_extent = fragments
|
||||||
|
.iter()
|
||||||
|
.map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis))
|
||||||
|
.max()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let relative_to = 2.0 * max_extent;
|
||||||
|
let height = elem.size(styles);
|
||||||
|
|
||||||
|
// Scale up fragments at both ends.
|
||||||
|
match fragments.as_mut_slice() {
|
||||||
|
[one] => scale(ctx, styles, one, relative_to, height, None),
|
||||||
|
[first, .., last] => {
|
||||||
|
scale(ctx, styles, first, relative_to, height, Some(MathClass::Opening));
|
||||||
|
scale(ctx, styles, last, relative_to, height, Some(MathClass::Closing));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle MathFragment::Variant fragments that should be scaled up.
|
||||||
|
for fragment in &mut fragments {
|
||||||
|
if let MathFragment::Variant(ref mut variant) = fragment {
|
||||||
|
if variant.mid_stretched == Some(false) {
|
||||||
|
variant.mid_stretched = Some(true);
|
||||||
|
scale(ctx, styles, fragment, relative_to, height, Some(MathClass::Large));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove weak SpacingFragment immediately after the opening or immediately
|
||||||
|
// before the closing.
|
||||||
|
let original_len = fragments.len();
|
||||||
|
let mut index = 0;
|
||||||
|
fragments.retain(|fragment| {
|
||||||
|
index += 1;
|
||||||
|
(index != 2 && index + 1 != original_len)
|
||||||
|
|| !matches!(fragment, MathFragment::Spacing(_, true))
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.extend(fragments);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lays out a [`MidElem`].
|
||||||
|
#[typst_macros::time(name = "math.mid", span = elem.span())]
|
||||||
|
pub fn layout_mid(
|
||||||
|
elem: &Packed<MidElem>,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
let mut fragments = ctx.layout_into_fragments(elem.body(), styles)?;
|
||||||
|
|
||||||
|
for fragment in &mut fragments {
|
||||||
|
match fragment {
|
||||||
|
MathFragment::Glyph(glyph) => {
|
||||||
|
let mut new = glyph.clone().into_variant();
|
||||||
|
new.mid_stretched = Some(false);
|
||||||
|
new.class = MathClass::Fence;
|
||||||
|
*fragment = MathFragment::Variant(new);
|
||||||
|
}
|
||||||
|
MathFragment::Variant(variant) => {
|
||||||
|
variant.mid_stretched = Some(false);
|
||||||
|
variant.class = MathClass::Fence;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.extend(fragments);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scale a math fragment to a height.
|
||||||
|
fn scale(
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
fragment: &mut MathFragment,
|
||||||
|
relative_to: Abs,
|
||||||
|
height: Smart<Rel<Length>>,
|
||||||
|
apply: Option<MathClass>,
|
||||||
|
) {
|
||||||
|
if matches!(
|
||||||
|
fragment.class(),
|
||||||
|
MathClass::Opening | MathClass::Closing | MathClass::Fence
|
||||||
|
) {
|
||||||
|
// This unwrap doesn't really matter. If it is None, then the fragment
|
||||||
|
// won't be stretchable anyways.
|
||||||
|
let short_fall = DELIM_SHORT_FALL.at(fragment.font_size().unwrap_or_default());
|
||||||
|
stretch_fragment(
|
||||||
|
ctx,
|
||||||
|
styles,
|
||||||
|
fragment,
|
||||||
|
Some(Axis::Y),
|
||||||
|
Some(relative_to),
|
||||||
|
height,
|
||||||
|
short_fall,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(class) = apply {
|
||||||
|
fragment.set_class(class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
333
crates/typst-layout/src/math/mat.rs
Normal file
333
crates/typst-layout/src/math/mat.rs
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
use typst_library::diag::{bail, SourceResult};
|
||||||
|
use typst_library::foundations::{Content, Packed, StyleChain};
|
||||||
|
use typst_library::layout::{
|
||||||
|
Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size,
|
||||||
|
};
|
||||||
|
use typst_library::math::{Augment, AugmentOffsets, CasesElem, MatElem, VecElem};
|
||||||
|
use typst_library::text::TextElem;
|
||||||
|
use typst_library::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape};
|
||||||
|
use typst_syntax::Span;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
alignments, delimiter_alignment, scaled_font_size, stack, style_for_denominator,
|
||||||
|
AlignmentResult, FrameFragment, GlyphFragment, LeftRightAlternator, MathContext,
|
||||||
|
Scaled, DELIM_SHORT_FALL,
|
||||||
|
};
|
||||||
|
|
||||||
|
const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
|
||||||
|
const DEFAULT_STROKE_THICKNESS: Em = Em::new(0.05);
|
||||||
|
|
||||||
|
/// Lays out a [`VecElem`].
|
||||||
|
#[typst_macros::time(name = "math.vec", span = elem.span())]
|
||||||
|
pub fn layout_vec(
|
||||||
|
elem: &Packed<VecElem>,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
let delim = elem.delim(styles);
|
||||||
|
let frame = layout_vec_body(
|
||||||
|
ctx,
|
||||||
|
styles,
|
||||||
|
elem.children(),
|
||||||
|
elem.align(styles),
|
||||||
|
elem.gap(styles).at(scaled_font_size(ctx, styles)),
|
||||||
|
LeftRightAlternator::Right,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), elem.span())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lays out a [`MatElem`].
|
||||||
|
#[typst_macros::time(name = "math.mat", span = elem.span())]
|
||||||
|
pub fn layout_mat(
|
||||||
|
elem: &Packed<MatElem>,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
let augment = elem.augment(styles);
|
||||||
|
let rows = elem.rows();
|
||||||
|
|
||||||
|
if let Some(aug) = &augment {
|
||||||
|
for &offset in &aug.hline.0 {
|
||||||
|
if offset == 0 || offset.unsigned_abs() >= rows.len() {
|
||||||
|
bail!(
|
||||||
|
elem.span(),
|
||||||
|
"cannot draw a horizontal line after row {} of a matrix with {} rows",
|
||||||
|
if offset < 0 { rows.len() as isize + offset } else { offset },
|
||||||
|
rows.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ncols = elem.rows().first().map_or(0, |row| row.len());
|
||||||
|
|
||||||
|
for &offset in &aug.vline.0 {
|
||||||
|
if offset == 0 || offset.unsigned_abs() >= ncols {
|
||||||
|
bail!(
|
||||||
|
elem.span(),
|
||||||
|
"cannot draw a vertical line after column {} of a matrix with {} columns",
|
||||||
|
if offset < 0 { ncols as isize + offset } else { offset },
|
||||||
|
ncols
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let font_size = scaled_font_size(ctx, styles);
|
||||||
|
let column_gap = elem.column_gap(styles).at(font_size);
|
||||||
|
let row_gap = elem.row_gap(styles).at(font_size);
|
||||||
|
let delim = elem.delim(styles);
|
||||||
|
let frame = layout_mat_body(
|
||||||
|
ctx,
|
||||||
|
styles,
|
||||||
|
rows,
|
||||||
|
elem.align(styles),
|
||||||
|
augment,
|
||||||
|
Axes::new(column_gap, row_gap),
|
||||||
|
elem.span(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), elem.span())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lays out a [`CasesElem`].
|
||||||
|
#[typst_macros::time(name = "math.cases", span = elem.span())]
|
||||||
|
pub fn layout_cases(
|
||||||
|
elem: &Packed<CasesElem>,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
let delim = elem.delim(styles);
|
||||||
|
let frame = layout_vec_body(
|
||||||
|
ctx,
|
||||||
|
styles,
|
||||||
|
elem.children(),
|
||||||
|
FixedAlignment::Start,
|
||||||
|
elem.gap(styles).at(scaled_font_size(ctx, styles)),
|
||||||
|
LeftRightAlternator::None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let (open, close) =
|
||||||
|
if elem.reverse(styles) { (None, delim.close()) } else { (delim.open(), None) };
|
||||||
|
|
||||||
|
layout_delimiters(ctx, styles, frame, open, close, elem.span())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout the inner contents of a vector.
|
||||||
|
fn layout_vec_body(
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
column: &[Content],
|
||||||
|
align: FixedAlignment,
|
||||||
|
row_gap: Rel<Abs>,
|
||||||
|
alternator: LeftRightAlternator,
|
||||||
|
) -> SourceResult<Frame> {
|
||||||
|
let gap = row_gap.relative_to(ctx.region.size.y);
|
||||||
|
|
||||||
|
let denom_style = style_for_denominator(styles);
|
||||||
|
let mut flat = vec![];
|
||||||
|
for child in column {
|
||||||
|
flat.push(ctx.layout_into_run(child, styles.chain(&denom_style))?);
|
||||||
|
}
|
||||||
|
// We pad ascent and descent with the ascent and descent of the paren
|
||||||
|
// to ensure that normal vectors are aligned with others unless they are
|
||||||
|
// way too big.
|
||||||
|
let paren =
|
||||||
|
GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached());
|
||||||
|
Ok(stack(flat, align, gap, 0, alternator, Some((paren.ascent, paren.descent))))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout the inner contents of a matrix.
|
||||||
|
fn layout_mat_body(
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
rows: &[Vec<Content>],
|
||||||
|
align: FixedAlignment,
|
||||||
|
augment: Option<Augment<Abs>>,
|
||||||
|
gap: Axes<Rel<Abs>>,
|
||||||
|
span: Span,
|
||||||
|
) -> SourceResult<Frame> {
|
||||||
|
let ncols = rows.first().map_or(0, |row| row.len());
|
||||||
|
let nrows = rows.len();
|
||||||
|
if ncols == 0 || nrows == 0 {
|
||||||
|
return Ok(Frame::soft(Size::zero()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let gap = gap.zip_map(ctx.region.size, Rel::relative_to);
|
||||||
|
let half_gap = gap * 0.5;
|
||||||
|
|
||||||
|
// We provide a default stroke thickness that scales
|
||||||
|
// with font size to ensure that augmentation lines
|
||||||
|
// look correct by default at all matrix sizes.
|
||||||
|
// The line cap is also set to square because it looks more "correct".
|
||||||
|
let font_size = scaled_font_size(ctx, styles);
|
||||||
|
let default_stroke_thickness = DEFAULT_STROKE_THICKNESS.at(font_size);
|
||||||
|
let default_stroke = FixedStroke {
|
||||||
|
thickness: default_stroke_thickness,
|
||||||
|
paint: TextElem::fill_in(styles).as_decoration(),
|
||||||
|
cap: LineCap::Square,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (hline, vline, stroke) = match augment {
|
||||||
|
Some(augment) => {
|
||||||
|
// We need to get stroke here for ownership.
|
||||||
|
let stroke = augment.stroke.unwrap_or_default().unwrap_or(default_stroke);
|
||||||
|
(augment.hline, augment.vline, stroke)
|
||||||
|
}
|
||||||
|
_ => (AugmentOffsets::default(), AugmentOffsets::default(), default_stroke),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Before the full matrix body can be laid out, the
|
||||||
|
// individual cells must first be independently laid out
|
||||||
|
// so we can ensure alignment across rows and columns.
|
||||||
|
|
||||||
|
// This variable stores the maximum ascent and descent for each row.
|
||||||
|
let mut heights = vec![(Abs::zero(), Abs::zero()); nrows];
|
||||||
|
|
||||||
|
// We want to transpose our data layout to columns
|
||||||
|
// before final layout. For efficiency, the columns
|
||||||
|
// variable is set up here and newly generated
|
||||||
|
// individual cells are then added to it.
|
||||||
|
let mut cols = vec![vec![]; ncols];
|
||||||
|
|
||||||
|
let denom_style = style_for_denominator(styles);
|
||||||
|
// We pad ascent and descent with the ascent and descent of the paren
|
||||||
|
// to ensure that normal matrices are aligned with others unless they are
|
||||||
|
// way too big.
|
||||||
|
let paren =
|
||||||
|
GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached());
|
||||||
|
|
||||||
|
for (row, (ascent, descent)) in rows.iter().zip(&mut heights) {
|
||||||
|
for (cell, col) in row.iter().zip(&mut cols) {
|
||||||
|
let cell = ctx.layout_into_run(cell, styles.chain(&denom_style))?;
|
||||||
|
|
||||||
|
ascent.set_max(cell.ascent().max(paren.ascent));
|
||||||
|
descent.set_max(cell.descent().max(paren.descent));
|
||||||
|
|
||||||
|
col.push(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each row, combine maximum ascent and descent into a row height.
|
||||||
|
// Sum the row heights, then add the total height of the gaps between rows.
|
||||||
|
let total_height =
|
||||||
|
heights.iter().map(|&(a, b)| a + b).sum::<Abs>() + gap.y * (nrows - 1) as f64;
|
||||||
|
|
||||||
|
// Width starts at zero because it can't be calculated until later
|
||||||
|
let mut frame = Frame::soft(Size::new(Abs::zero(), total_height));
|
||||||
|
|
||||||
|
let mut x = Abs::zero();
|
||||||
|
|
||||||
|
for (index, col) in cols.into_iter().enumerate() {
|
||||||
|
let AlignmentResult { points, width: rcol } = alignments(&col);
|
||||||
|
|
||||||
|
let mut y = Abs::zero();
|
||||||
|
|
||||||
|
for (cell, &(ascent, descent)) in col.into_iter().zip(&heights) {
|
||||||
|
let cell = cell.into_line_frame(&points, LeftRightAlternator::Right);
|
||||||
|
let pos = Point::new(
|
||||||
|
if points.is_empty() {
|
||||||
|
x + align.position(rcol - cell.width())
|
||||||
|
} else {
|
||||||
|
x
|
||||||
|
},
|
||||||
|
y + ascent - cell.ascent(),
|
||||||
|
);
|
||||||
|
|
||||||
|
frame.push_frame(pos, cell);
|
||||||
|
|
||||||
|
y += ascent + descent + gap.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance to the end of the column
|
||||||
|
x += rcol;
|
||||||
|
|
||||||
|
// If a vertical line should be inserted after this column
|
||||||
|
if vline.0.contains(&(index as isize + 1))
|
||||||
|
|| vline.0.contains(&(1 - ((ncols - index) as isize)))
|
||||||
|
{
|
||||||
|
frame.push(
|
||||||
|
Point::with_x(x + half_gap.x),
|
||||||
|
line_item(total_height, true, stroke.clone(), span),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance to the start of the next column
|
||||||
|
x += gap.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once all the columns are laid out, the total width can be calculated
|
||||||
|
let total_width = x - gap.x;
|
||||||
|
|
||||||
|
// This allows the horizontal lines to be laid out
|
||||||
|
for line in hline.0 {
|
||||||
|
let real_line =
|
||||||
|
if line < 0 { nrows - line.unsigned_abs() } else { line as usize };
|
||||||
|
let offset = (heights[0..real_line].iter().map(|&(a, b)| a + b).sum::<Abs>()
|
||||||
|
+ gap.y * (real_line - 1) as f64)
|
||||||
|
+ half_gap.y;
|
||||||
|
|
||||||
|
frame.push(
|
||||||
|
Point::with_y(offset),
|
||||||
|
line_item(total_width, false, stroke.clone(), span),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.size_mut().x = total_width;
|
||||||
|
|
||||||
|
Ok(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_item(length: Abs, vertical: bool, stroke: FixedStroke, span: Span) -> FrameItem {
|
||||||
|
let line_geom = if vertical {
|
||||||
|
Geometry::Line(Point::with_y(length))
|
||||||
|
} else {
|
||||||
|
Geometry::Line(Point::with_x(length))
|
||||||
|
};
|
||||||
|
|
||||||
|
FrameItem::Shape(
|
||||||
|
Shape {
|
||||||
|
geometry: line_geom,
|
||||||
|
fill: None,
|
||||||
|
fill_rule: FillRule::default(),
|
||||||
|
stroke: Some(stroke),
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout the outer wrapper around the body of a vector or matrix.
|
||||||
|
fn layout_delimiters(
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
mut frame: Frame,
|
||||||
|
left: Option<char>,
|
||||||
|
right: Option<char>,
|
||||||
|
span: Span,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
let font_size = scaled_font_size(ctx, styles);
|
||||||
|
let short_fall = DELIM_SHORT_FALL.at(font_size);
|
||||||
|
let axis = ctx.constants.axis_height().scaled(ctx, font_size);
|
||||||
|
let height = frame.height();
|
||||||
|
let target = height + VERTICAL_PADDING.of(height);
|
||||||
|
frame.set_baseline(height / 2.0 + axis);
|
||||||
|
|
||||||
|
if let Some(left) = left {
|
||||||
|
let mut left = GlyphFragment::new(ctx, styles, left, span)
|
||||||
|
.stretch_vertical(ctx, target, short_fall);
|
||||||
|
left.align_on_axis(ctx, delimiter_alignment(left.c));
|
||||||
|
ctx.push(left);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.push(FrameFragment::new(ctx, styles, frame));
|
||||||
|
|
||||||
|
if let Some(right) = right {
|
||||||
|
let mut right = GlyphFragment::new(ctx, styles, right, span)
|
||||||
|
.stretch_vertical(ctx, target, short_fall);
|
||||||
|
right.align_on_axis(ctx, delimiter_alignment(right.c));
|
||||||
|
ctx.push(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
703
crates/typst-layout/src/math/mod.rs
Normal file
703
crates/typst-layout/src/math/mod.rs
Normal file
@ -0,0 +1,703 @@
|
|||||||
|
#[macro_use]
|
||||||
|
mod shared;
|
||||||
|
mod accent;
|
||||||
|
mod attach;
|
||||||
|
mod cancel;
|
||||||
|
mod frac;
|
||||||
|
mod fragment;
|
||||||
|
mod lr;
|
||||||
|
mod mat;
|
||||||
|
mod root;
|
||||||
|
mod run;
|
||||||
|
mod stretch;
|
||||||
|
mod text;
|
||||||
|
mod underover;
|
||||||
|
|
||||||
|
use ttf_parser::gsub::SubstitutionSubtable;
|
||||||
|
use typst_library::diag::{bail, SourceResult};
|
||||||
|
use typst_library::engine::Engine;
|
||||||
|
use typst_library::foundations::{Content, NativeElement, Packed, Resolve, StyleChain};
|
||||||
|
use typst_library::introspection::{Counter, Locator, SplitLocator, TagElem};
|
||||||
|
use typst_library::layout::{
|
||||||
|
Abs, AlignElem, Axes, BlockElem, BoxElem, Em, FixedAlignment, Fragment, Frame, HElem,
|
||||||
|
InlineItem, OuterHAlignment, PlaceElem, Point, Region, Regions, Size, Spacing,
|
||||||
|
SpecificAlignment, VAlignment,
|
||||||
|
};
|
||||||
|
use typst_library::math::*;
|
||||||
|
use typst_library::model::ParElem;
|
||||||
|
use typst_library::routines::{Arenas, RealizationKind};
|
||||||
|
use typst_library::text::{
|
||||||
|
families, features, variant, Font, LinebreakElem, SpaceElem, TextEdgeBounds,
|
||||||
|
TextElem, TextSize,
|
||||||
|
};
|
||||||
|
use typst_library::World;
|
||||||
|
use typst_syntax::Span;
|
||||||
|
use typst_utils::Numeric;
|
||||||
|
use unicode_math_class::MathClass;
|
||||||
|
|
||||||
|
use self::fragment::{
|
||||||
|
FrameFragment, GlyphFragment, GlyphwiseSubsts, Limits, MathFragment, VariantFragment,
|
||||||
|
};
|
||||||
|
use self::run::{LeftRightAlternator, MathRun, MathRunFrameBuilder};
|
||||||
|
use self::shared::*;
|
||||||
|
use self::stretch::{stretch_fragment, stretch_glyph};
|
||||||
|
|
||||||
|
/// Layout an inline equation (in a paragraph).
|
||||||
|
#[typst_macros::time(span = elem.span())]
|
||||||
|
pub fn layout_equation_inline(
|
||||||
|
elem: &Packed<EquationElem>,
|
||||||
|
engine: &mut Engine,
|
||||||
|
locator: Locator,
|
||||||
|
styles: StyleChain,
|
||||||
|
region: Size,
|
||||||
|
) -> SourceResult<Vec<InlineItem>> {
|
||||||
|
assert!(!elem.block(styles));
|
||||||
|
|
||||||
|
let font = find_math_font(engine, styles, elem.span())?;
|
||||||
|
|
||||||
|
let mut locator = locator.split();
|
||||||
|
let mut ctx = MathContext::new(engine, &mut locator, styles, region, &font);
|
||||||
|
let run = ctx.layout_into_run(&elem.body, styles)?;
|
||||||
|
|
||||||
|
let mut items = if run.row_count() == 1 {
|
||||||
|
run.into_par_items()
|
||||||
|
} else {
|
||||||
|
vec![InlineItem::Frame(run.into_fragment(&ctx, styles).into_frame())]
|
||||||
|
};
|
||||||
|
|
||||||
|
// An empty equation should have a height, so we still create a frame
|
||||||
|
// (which is then resized in the loop).
|
||||||
|
if items.is_empty() {
|
||||||
|
items.push(InlineItem::Frame(Frame::soft(Size::zero())));
|
||||||
|
}
|
||||||
|
|
||||||
|
for item in &mut items {
|
||||||
|
let InlineItem::Frame(frame) = item else { continue };
|
||||||
|
|
||||||
|
let font_size = scaled_font_size(&ctx, styles);
|
||||||
|
let slack = ParElem::leading_in(styles) * 0.7;
|
||||||
|
|
||||||
|
let (t, b) = font.edges(
|
||||||
|
TextElem::top_edge_in(styles),
|
||||||
|
TextElem::bottom_edge_in(styles),
|
||||||
|
font_size,
|
||||||
|
TextEdgeBounds::Frame(frame),
|
||||||
|
);
|
||||||
|
|
||||||
|
let ascent = t.max(frame.ascent() - slack);
|
||||||
|
let descent = b.max(frame.descent() - slack);
|
||||||
|
frame.translate(Point::with_y(ascent - frame.baseline()));
|
||||||
|
frame.size_mut().y = ascent + descent;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout a block-level equation (in a flow).
|
||||||
|
#[typst_macros::time(span = elem.span())]
|
||||||
|
pub fn layout_equation_block(
|
||||||
|
elem: &Packed<EquationElem>,
|
||||||
|
engine: &mut Engine,
|
||||||
|
locator: Locator,
|
||||||
|
styles: StyleChain,
|
||||||
|
regions: Regions,
|
||||||
|
) -> SourceResult<Fragment> {
|
||||||
|
assert!(elem.block(styles));
|
||||||
|
|
||||||
|
let span = elem.span();
|
||||||
|
let font = find_math_font(engine, styles, span)?;
|
||||||
|
|
||||||
|
let mut locator = locator.split();
|
||||||
|
let mut ctx = MathContext::new(engine, &mut locator, styles, regions.base(), &font);
|
||||||
|
let full_equation_builder = ctx
|
||||||
|
.layout_into_run(&elem.body, styles)?
|
||||||
|
.multiline_frame_builder(&ctx, styles);
|
||||||
|
let width = full_equation_builder.size.x;
|
||||||
|
|
||||||
|
let equation_builders = if BlockElem::breakable_in(styles) {
|
||||||
|
let mut rows = full_equation_builder.frames.into_iter().peekable();
|
||||||
|
let mut equation_builders = vec![];
|
||||||
|
let mut last_first_pos = Point::zero();
|
||||||
|
let mut regions = regions;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Keep track of the position of the first row in this region,
|
||||||
|
// so that the offset can be reverted later.
|
||||||
|
let Some(&(_, first_pos)) = rows.peek() else { break };
|
||||||
|
last_first_pos = first_pos;
|
||||||
|
|
||||||
|
let mut frames = vec![];
|
||||||
|
let mut height = Abs::zero();
|
||||||
|
while let Some((sub, pos)) = rows.peek() {
|
||||||
|
let mut pos = *pos;
|
||||||
|
pos.y -= first_pos.y;
|
||||||
|
|
||||||
|
// Finish this region if the line doesn't fit. Only do it if
|
||||||
|
// we placed at least one line _or_ we still have non-last
|
||||||
|
// regions. Crucially, we don't want to infinitely create
|
||||||
|
// new regions which are too small.
|
||||||
|
if !regions.size.y.fits(sub.height() + pos.y)
|
||||||
|
&& (regions.may_progress()
|
||||||
|
|| (regions.may_break() && !frames.is_empty()))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (sub, _) = rows.next().unwrap();
|
||||||
|
height = height.max(pos.y + sub.height());
|
||||||
|
frames.push((sub, pos));
|
||||||
|
}
|
||||||
|
|
||||||
|
equation_builders
|
||||||
|
.push(MathRunFrameBuilder { frames, size: Size::new(width, height) });
|
||||||
|
regions.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append remaining rows to the equation builder of the last region.
|
||||||
|
if let Some(equation_builder) = equation_builders.last_mut() {
|
||||||
|
equation_builder.frames.extend(rows.map(|(frame, mut pos)| {
|
||||||
|
pos.y -= last_first_pos.y;
|
||||||
|
(frame, pos)
|
||||||
|
}));
|
||||||
|
|
||||||
|
let height = equation_builder
|
||||||
|
.frames
|
||||||
|
.iter()
|
||||||
|
.map(|(frame, pos)| frame.height() + pos.y)
|
||||||
|
.max()
|
||||||
|
.unwrap_or(equation_builder.size.y);
|
||||||
|
|
||||||
|
equation_builder.size.y = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that there is at least one frame, even for empty equations.
|
||||||
|
if equation_builders.is_empty() {
|
||||||
|
equation_builders
|
||||||
|
.push(MathRunFrameBuilder { frames: vec![], size: Size::zero() });
|
||||||
|
}
|
||||||
|
|
||||||
|
equation_builders
|
||||||
|
} else {
|
||||||
|
vec![full_equation_builder]
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(numbering) = (**elem).numbering(styles) else {
|
||||||
|
let frames = equation_builders
|
||||||
|
.into_iter()
|
||||||
|
.map(MathRunFrameBuilder::build)
|
||||||
|
.collect();
|
||||||
|
return Ok(Fragment::frames(frames));
|
||||||
|
};
|
||||||
|
|
||||||
|
let pod = Region::new(regions.base(), Axes::splat(false));
|
||||||
|
let counter = Counter::of(EquationElem::elem())
|
||||||
|
.display_at_loc(engine, elem.location().unwrap(), styles, numbering)?
|
||||||
|
.spanned(span);
|
||||||
|
let number =
|
||||||
|
(engine.routines.layout_frame)(engine, &counter, locator.next(&()), styles, pod)?;
|
||||||
|
|
||||||
|
static NUMBER_GUTTER: Em = Em::new(0.5);
|
||||||
|
let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles);
|
||||||
|
|
||||||
|
let number_align = match elem.number_align(styles) {
|
||||||
|
SpecificAlignment::H(h) => SpecificAlignment::Both(h, VAlignment::Horizon),
|
||||||
|
SpecificAlignment::V(v) => SpecificAlignment::Both(OuterHAlignment::End, v),
|
||||||
|
SpecificAlignment::Both(h, v) => SpecificAlignment::Both(h, v),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add equation numbers to each equation region.
|
||||||
|
let region_count = equation_builders.len();
|
||||||
|
let frames = equation_builders
|
||||||
|
.into_iter()
|
||||||
|
.map(|builder| {
|
||||||
|
if builder.frames.is_empty() && region_count > 1 {
|
||||||
|
// Don't number empty regions, but do number empty equations.
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
add_equation_number(
|
||||||
|
builder,
|
||||||
|
number.clone(),
|
||||||
|
number_align.resolve(styles),
|
||||||
|
AlignElem::alignment_in(styles).resolve(styles).x,
|
||||||
|
regions.size.x,
|
||||||
|
full_number_width,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(Fragment::frames(frames))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_math_font(
|
||||||
|
engine: &mut Engine<'_>,
|
||||||
|
styles: StyleChain,
|
||||||
|
span: Span,
|
||||||
|
) -> SourceResult<Font> {
|
||||||
|
let variant = variant(styles);
|
||||||
|
let world = engine.world;
|
||||||
|
let Some(font) = families(styles).find_map(|family| {
|
||||||
|
let id = world.book().select(family, variant)?;
|
||||||
|
let font = world.font(id)?;
|
||||||
|
let _ = font.ttf().tables().math?.constants?;
|
||||||
|
Some(font)
|
||||||
|
}) else {
|
||||||
|
bail!(span, "current font does not support math");
|
||||||
|
};
|
||||||
|
Ok(font)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_equation_number(
|
||||||
|
equation_builder: MathRunFrameBuilder,
|
||||||
|
number: Frame,
|
||||||
|
number_align: Axes<FixedAlignment>,
|
||||||
|
equation_align: FixedAlignment,
|
||||||
|
region_size_x: Abs,
|
||||||
|
full_number_width: Abs,
|
||||||
|
) -> Frame {
|
||||||
|
let first =
|
||||||
|
equation_builder.frames.first().map_or(
|
||||||
|
(equation_builder.size, Point::zero(), Abs::zero()),
|
||||||
|
|(frame, pos)| (frame.size(), *pos, frame.baseline()),
|
||||||
|
);
|
||||||
|
let last =
|
||||||
|
equation_builder.frames.last().map_or(
|
||||||
|
(equation_builder.size, Point::zero(), Abs::zero()),
|
||||||
|
|(frame, pos)| (frame.size(), *pos, frame.baseline()),
|
||||||
|
);
|
||||||
|
let line_count = equation_builder.frames.len();
|
||||||
|
let mut equation = equation_builder.build();
|
||||||
|
|
||||||
|
let width = if region_size_x.is_finite() {
|
||||||
|
region_size_x
|
||||||
|
} else {
|
||||||
|
equation.width() + 2.0 * full_number_width
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_multiline = line_count >= 2;
|
||||||
|
let resizing_offset = resize_equation(
|
||||||
|
&mut equation,
|
||||||
|
&number,
|
||||||
|
number_align,
|
||||||
|
equation_align,
|
||||||
|
width,
|
||||||
|
is_multiline,
|
||||||
|
[first, last],
|
||||||
|
);
|
||||||
|
equation.translate(Point::with_x(match (equation_align, number_align.x) {
|
||||||
|
(FixedAlignment::Start, FixedAlignment::Start) => full_number_width,
|
||||||
|
(FixedAlignment::End, FixedAlignment::End) => -full_number_width,
|
||||||
|
_ => Abs::zero(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let x = match number_align.x {
|
||||||
|
FixedAlignment::Start => Abs::zero(),
|
||||||
|
FixedAlignment::End => equation.width() - number.width(),
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
let y = {
|
||||||
|
let align_baselines = |(_, pos, baseline): (_, Point, Abs), number: &Frame| {
|
||||||
|
resizing_offset.y + pos.y + baseline - number.baseline()
|
||||||
|
};
|
||||||
|
match number_align.y {
|
||||||
|
FixedAlignment::Start => align_baselines(first, &number),
|
||||||
|
FixedAlignment::Center if !is_multiline => align_baselines(first, &number),
|
||||||
|
// In this case, the center lines (not baselines) of the number frame
|
||||||
|
// and the equation frame shall be aligned.
|
||||||
|
FixedAlignment::Center => (equation.height() - number.height()) / 2.0,
|
||||||
|
FixedAlignment::End => align_baselines(last, &number),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
equation.push_frame(Point::new(x, y), number);
|
||||||
|
equation
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resize the equation's frame accordingly so that it encompasses the number.
|
||||||
|
fn resize_equation(
|
||||||
|
equation: &mut Frame,
|
||||||
|
number: &Frame,
|
||||||
|
number_align: Axes<FixedAlignment>,
|
||||||
|
equation_align: FixedAlignment,
|
||||||
|
width: Abs,
|
||||||
|
is_multiline: bool,
|
||||||
|
[first, last]: [(Axes<Abs>, Point, Abs); 2],
|
||||||
|
) -> Point {
|
||||||
|
if matches!(number_align.y, FixedAlignment::Center if is_multiline) {
|
||||||
|
// In this case, the center lines (not baselines) of the number frame
|
||||||
|
// and the equation frame shall be aligned.
|
||||||
|
return equation.resize(
|
||||||
|
Size::new(width, equation.height().max(number.height())),
|
||||||
|
Axes::<FixedAlignment>::new(equation_align, FixedAlignment::Center),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let excess_above = Abs::zero().max({
|
||||||
|
if !is_multiline || matches!(number_align.y, FixedAlignment::Start) {
|
||||||
|
let (.., baseline) = first;
|
||||||
|
number.baseline() - baseline
|
||||||
|
} else {
|
||||||
|
Abs::zero()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let excess_below = Abs::zero().max({
|
||||||
|
if !is_multiline || matches!(number_align.y, FixedAlignment::End) {
|
||||||
|
let (size, .., baseline) = last;
|
||||||
|
(number.height() - number.baseline()) - (size.y - baseline)
|
||||||
|
} else {
|
||||||
|
Abs::zero()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// The vertical expansion is asymmetric on the top and bottom edges, so we
|
||||||
|
// first align at the top then translate the content downward later.
|
||||||
|
let resizing_offset = equation.resize(
|
||||||
|
Size::new(width, equation.height() + excess_above + excess_below),
|
||||||
|
Axes::<FixedAlignment>::new(equation_align, FixedAlignment::Start),
|
||||||
|
);
|
||||||
|
equation.translate(Point::with_y(excess_above));
|
||||||
|
resizing_offset + Point::with_y(excess_above)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The context for math layout.
|
||||||
|
struct MathContext<'a, 'v, 'e> {
|
||||||
|
// External.
|
||||||
|
engine: &'v mut Engine<'e>,
|
||||||
|
locator: &'v mut SplitLocator<'a>,
|
||||||
|
region: Region,
|
||||||
|
// Font-related.
|
||||||
|
font: &'a Font,
|
||||||
|
ttf: &'a ttf_parser::Face<'a>,
|
||||||
|
table: ttf_parser::math::Table<'a>,
|
||||||
|
constants: ttf_parser::math::Constants<'a>,
|
||||||
|
ssty_table: Option<ttf_parser::gsub::AlternateSubstitution<'a>>,
|
||||||
|
glyphwise_tables: Option<Vec<GlyphwiseSubsts<'a>>>,
|
||||||
|
space_width: Em,
|
||||||
|
// Mutable.
|
||||||
|
fragments: Vec<MathFragment>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
|
||||||
|
/// Create a new math context.
|
||||||
|
fn new(
|
||||||
|
engine: &'v mut Engine<'e>,
|
||||||
|
locator: &'v mut SplitLocator<'a>,
|
||||||
|
styles: StyleChain<'a>,
|
||||||
|
base: Size,
|
||||||
|
font: &'a Font,
|
||||||
|
) -> Self {
|
||||||
|
let math_table = font.ttf().tables().math.unwrap();
|
||||||
|
let gsub_table = font.ttf().tables().gsub;
|
||||||
|
let constants = math_table.constants.unwrap();
|
||||||
|
|
||||||
|
let ssty_table = gsub_table
|
||||||
|
.and_then(|gsub| {
|
||||||
|
gsub.features
|
||||||
|
.find(ttf_parser::Tag::from_bytes(b"ssty"))
|
||||||
|
.and_then(|feature| feature.lookup_indices.get(0))
|
||||||
|
.and_then(|index| gsub.lookups.get(index))
|
||||||
|
})
|
||||||
|
.and_then(|ssty| ssty.subtables.get::<SubstitutionSubtable>(0))
|
||||||
|
.and_then(|ssty| match ssty {
|
||||||
|
SubstitutionSubtable::Alternate(alt_glyphs) => Some(alt_glyphs),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let features = features(styles);
|
||||||
|
let glyphwise_tables = gsub_table.map(|gsub| {
|
||||||
|
features
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|feature| GlyphwiseSubsts::new(gsub, feature))
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
|
||||||
|
let ttf = font.ttf();
|
||||||
|
let space_width = ttf
|
||||||
|
.glyph_index(' ')
|
||||||
|
.and_then(|id| ttf.glyph_hor_advance(id))
|
||||||
|
.map(|advance| font.to_em(advance))
|
||||||
|
.unwrap_or(THICK);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
engine,
|
||||||
|
locator,
|
||||||
|
region: Region::new(base, Axes::splat(false)),
|
||||||
|
font,
|
||||||
|
ttf: font.ttf(),
|
||||||
|
table: math_table,
|
||||||
|
constants,
|
||||||
|
ssty_table,
|
||||||
|
glyphwise_tables,
|
||||||
|
space_width,
|
||||||
|
fragments: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a fragment.
|
||||||
|
fn push(&mut self, fragment: impl Into<MathFragment>) {
|
||||||
|
self.fragments.push(fragment.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push multiple fragments.
|
||||||
|
fn extend(&mut self, fragments: impl IntoIterator<Item = MathFragment>) {
|
||||||
|
self.fragments.extend(fragments);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout the given element and return the result as a [`MathRun`].
|
||||||
|
fn layout_into_run(
|
||||||
|
&mut self,
|
||||||
|
elem: &Content,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<MathRun> {
|
||||||
|
Ok(MathRun::new(self.layout_into_fragments(elem, styles)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout the given element and return the resulting [`MathFragment`]s.
|
||||||
|
fn layout_into_fragments(
|
||||||
|
&mut self,
|
||||||
|
elem: &Content,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<Vec<MathFragment>> {
|
||||||
|
// The element's layout_math() changes the fragments held in this
|
||||||
|
// MathContext object, but for convenience this function shouldn't change
|
||||||
|
// them, so we restore the MathContext's fragments after obtaining the
|
||||||
|
// layout result.
|
||||||
|
let prev = std::mem::take(&mut self.fragments);
|
||||||
|
self.layout_into_self(elem, styles)?;
|
||||||
|
Ok(std::mem::replace(&mut self.fragments, prev))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout the given element and return the result as a
|
||||||
|
/// unified [`MathFragment`].
|
||||||
|
fn layout_into_fragment(
|
||||||
|
&mut self,
|
||||||
|
elem: &Content,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<MathFragment> {
|
||||||
|
Ok(self.layout_into_run(elem, styles)?.into_fragment(self, styles))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout the given element and return the result as a [`Frame`].
|
||||||
|
fn layout_into_frame(
|
||||||
|
&mut self,
|
||||||
|
elem: &Content,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<Frame> {
|
||||||
|
Ok(self.layout_into_fragment(elem, styles)?.into_frame())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout arbitrary content.
|
||||||
|
fn layout_into_self(
|
||||||
|
&mut self,
|
||||||
|
content: &Content,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
let arenas = Arenas::default();
|
||||||
|
let pairs = (self.engine.routines.realize)(
|
||||||
|
RealizationKind::Math,
|
||||||
|
self.engine,
|
||||||
|
self.locator,
|
||||||
|
&arenas,
|
||||||
|
content,
|
||||||
|
styles,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let outer = styles;
|
||||||
|
for (elem, styles) in pairs {
|
||||||
|
// Hack because the font is fixed in math.
|
||||||
|
if styles != outer && TextElem::font_in(styles) != TextElem::font_in(outer) {
|
||||||
|
let frame = layout_external(elem, self, styles)?;
|
||||||
|
self.push(FrameFragment::new(self, styles, frame).with_spaced(true));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
layout_realized(elem, self, styles)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lays out a leaf element resulting from realization.
|
||||||
|
fn layout_realized(
|
||||||
|
elem: &Content,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
if let Some(elem) = elem.to_packed::<TagElem>() {
|
||||||
|
ctx.push(MathFragment::Tag(elem.tag.clone()));
|
||||||
|
} else if elem.is::<SpaceElem>() {
|
||||||
|
let font_size = scaled_font_size(ctx, styles);
|
||||||
|
ctx.push(MathFragment::Space(ctx.space_width.at(font_size)));
|
||||||
|
} else if elem.is::<LinebreakElem>() {
|
||||||
|
ctx.push(MathFragment::Linebreak);
|
||||||
|
} else if let Some(elem) = elem.to_packed::<HElem>() {
|
||||||
|
layout_h(elem, ctx, styles)?;
|
||||||
|
} else if let Some(elem) = elem.to_packed::<TextElem>() {
|
||||||
|
self::text::layout_text(elem, ctx, styles)?;
|
||||||
|
} else if let Some(elem) = elem.to_packed::<BoxElem>() {
|
||||||
|
layout_box(elem, ctx, styles)?;
|
||||||
|
} else if elem.is::<AlignPointElem>() {
|
||||||
|
ctx.push(MathFragment::Align);
|
||||||
|
} else if let Some(elem) = elem.to_packed::<ClassElem>() {
|
||||||
|
layout_class(elem, ctx, styles)?;
|
||||||
|
} else if let Some(elem) = elem.to_packed::<AccentElem>() {
|
||||||
|
self::accent::layout_accent(elem, ctx, styles)?;
|
||||||
|
} else if let Some(elem) = elem.to_packed::<AttachElem>() {
|
||||||
|
self::attach::layout_attach(elem, ctx, styles)?;
|
||||||
|
} else if let Some(elem) = elem.to_packed::<PrimesElem>() {
|
||||||
|
self::attach::layout_primes(elem, ctx, styles)?;
|
||||||
|
} else if let Some(elem) = elem.to_packed::<ScriptsElem>() {
|
||||||
|
self::attach::layout_scripts(elem, ctx, styles)?;
|
||||||
|
} else if let Some(elem) = elem.to_packed::<LimitsElem>() {
|
||||||
|
self::attach::layout_limits(elem, ctx, styles)?;
|
||||||
|
} else if let Some(elem) = elem.to_packed::<CancelElem>() {
|
||||||
|
self::cancel::layout_cancel(elem, ctx, styles)?
|
||||||
|
} else if let Some(elem) = elem.to_packed::<FracElem>() {
|
||||||
|
self::frac::layout_frac(elem, ctx, styles)?;
|
||||||
|
} else if let Some(elem) = elem.to_packed::<BinomElem>() {
|
||||||
|
self::frac::layout_binom(elem, ctx, styles)?;
|
||||||
|
} else if let Some(elem) = elem.to_packed::<LrElem>() {
|
||||||
|
self::lr::layout_lr(elem, ctx, styles)?
|
||||||
|
} else if let Some(elem) = elem.to_packed::<MidElem>() {
|
||||||
|
self::lr::layout_mid(elem, ctx, styles)?
|
||||||
|
} else if let Some(elem) = elem.to_packed::<VecElem>() {
|
||||||
|
self::mat::layout_vec(elem, ctx, styles)?
|
||||||
|
} else if let Some(elem) = elem.to_packed::<MatElem>() {
|
||||||
|
self::mat::layout_mat(elem, ctx, styles)?
|
||||||
|
} else if let Some(elem) = elem.to_packed::<CasesElem>() {
|
||||||
|
self::mat::layout_cases(elem, ctx, styles)?
|
||||||
|
} else if let Some(elem) = elem.to_packed::<OpElem>() {
|
||||||
|
layout_op(elem, ctx, styles)?
|
||||||
|
} else if let Some(elem) = elem.to_packed::<RootElem>() {
|
||||||
|
self::root::layout_root(elem, ctx, styles)?
|
||||||
|
} else if let Some(elem) = elem.to_packed::<StretchElem>() {
|
||||||
|
self::stretch::layout_stretch(elem, ctx, styles)?
|
||||||
|
} else if let Some(elem) = elem.to_packed::<UnderlineElem>() {
|
||||||
|
self::underover::layout_underline(elem, ctx, styles)?
|
||||||
|
} else if let Some(elem) = elem.to_packed::<OverlineElem>() {
|
||||||
|
self::underover::layout_overline(elem, ctx, styles)?
|
||||||
|
} else if let Some(elem) = elem.to_packed::<UnderbraceElem>() {
|
||||||
|
self::underover::layout_underbrace(elem, ctx, styles)?
|
||||||
|
} else if let Some(elem) = elem.to_packed::<OverbraceElem>() {
|
||||||
|
self::underover::layout_overbrace(elem, ctx, styles)?
|
||||||
|
} else if let Some(elem) = elem.to_packed::<UnderbracketElem>() {
|
||||||
|
self::underover::layout_underbracket(elem, ctx, styles)?
|
||||||
|
} else if let Some(elem) = elem.to_packed::<OverbracketElem>() {
|
||||||
|
self::underover::layout_overbracket(elem, ctx, styles)?
|
||||||
|
} else if let Some(elem) = elem.to_packed::<UnderparenElem>() {
|
||||||
|
self::underover::layout_underparen(elem, ctx, styles)?
|
||||||
|
} else if let Some(elem) = elem.to_packed::<OverparenElem>() {
|
||||||
|
self::underover::layout_overparen(elem, ctx, styles)?
|
||||||
|
} else if let Some(elem) = elem.to_packed::<UndershellElem>() {
|
||||||
|
self::underover::layout_undershell(elem, ctx, styles)?
|
||||||
|
} else if let Some(elem) = elem.to_packed::<OvershellElem>() {
|
||||||
|
self::underover::layout_overshell(elem, ctx, styles)?
|
||||||
|
} else {
|
||||||
|
let mut frame = layout_external(elem, ctx, styles)?;
|
||||||
|
if !frame.has_baseline() {
|
||||||
|
let axis = scaled!(ctx, styles, axis_height);
|
||||||
|
frame.set_baseline(frame.height() / 2.0 + axis);
|
||||||
|
}
|
||||||
|
ctx.push(
|
||||||
|
FrameFragment::new(ctx, styles, frame)
|
||||||
|
.with_spaced(true)
|
||||||
|
.with_ignorant(elem.is::<PlaceElem>()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lays out an [`BoxElem`].
|
||||||
|
fn layout_box(
|
||||||
|
elem: &Packed<BoxElem>,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
let local = TextElem::set_size(TextSize(scaled_font_size(ctx, styles).into())).wrap();
|
||||||
|
let frame = (ctx.engine.routines.layout_box)(
|
||||||
|
elem,
|
||||||
|
ctx.engine,
|
||||||
|
ctx.locator.next(&elem.span()),
|
||||||
|
styles.chain(&local),
|
||||||
|
ctx.region.size,
|
||||||
|
)?;
|
||||||
|
ctx.push(FrameFragment::new(ctx, styles, frame).with_spaced(true));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lays out an [`HElem`].
|
||||||
|
fn layout_h(
|
||||||
|
elem: &Packed<HElem>,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
if let Spacing::Rel(rel) = elem.amount() {
|
||||||
|
if rel.rel.is_zero() {
|
||||||
|
ctx.push(MathFragment::Spacing(
|
||||||
|
rel.abs.at(scaled_font_size(ctx, styles)),
|
||||||
|
elem.weak(styles),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lays out a [`ClassElem`].
|
||||||
|
#[typst_macros::time(name = "math.op", span = elem.span())]
|
||||||
|
fn layout_class(
|
||||||
|
elem: &Packed<ClassElem>,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
let class = *elem.class();
|
||||||
|
let style = EquationElem::set_class(Some(class)).wrap();
|
||||||
|
let mut fragment = ctx.layout_into_fragment(elem.body(), styles.chain(&style))?;
|
||||||
|
fragment.set_class(class);
|
||||||
|
fragment.set_limits(Limits::for_class(class));
|
||||||
|
ctx.push(fragment);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lays out an [`OpElem`].
|
||||||
|
#[typst_macros::time(name = "math.op", span = elem.span())]
|
||||||
|
fn layout_op(
|
||||||
|
elem: &Packed<OpElem>,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
let fragment = ctx.layout_into_fragment(elem.text(), styles)?;
|
||||||
|
let italics = fragment.italics_correction();
|
||||||
|
let accent_attach = fragment.accent_attach();
|
||||||
|
let text_like = fragment.is_text_like();
|
||||||
|
|
||||||
|
ctx.push(
|
||||||
|
FrameFragment::new(ctx, styles, fragment.into_frame())
|
||||||
|
.with_class(MathClass::Large)
|
||||||
|
.with_italics_correction(italics)
|
||||||
|
.with_accent_attach(accent_attach)
|
||||||
|
.with_text_like(text_like)
|
||||||
|
.with_limits(if elem.limits(styles) {
|
||||||
|
Limits::Display
|
||||||
|
} else {
|
||||||
|
Limits::Never
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout into a frame with normal layout.
|
||||||
|
fn layout_external(
|
||||||
|
content: &Content,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<Frame> {
|
||||||
|
let local = TextElem::set_size(TextSize(scaled_font_size(ctx, styles).into())).wrap();
|
||||||
|
(ctx.engine.routines.layout_frame)(
|
||||||
|
ctx.engine,
|
||||||
|
content,
|
||||||
|
ctx.locator.next(&content.span()),
|
||||||
|
styles.chain(&local),
|
||||||
|
ctx.region,
|
||||||
|
)
|
||||||
|
}
|
@ -1,63 +1,26 @@
|
|||||||
use crate::diag::SourceResult;
|
use typst_library::diag::SourceResult;
|
||||||
use crate::foundations::{elem, func, Content, NativeElement, Packed, StyleChain};
|
use typst_library::foundations::{Packed, StyleChain};
|
||||||
use crate::layout::{Abs, Frame, FrameItem, Point, Size};
|
use typst_library::layout::{Abs, Frame, FrameItem, Point, Size};
|
||||||
use crate::math::{
|
use typst_library::math::{EquationElem, MathSize, RootElem};
|
||||||
style_cramped, EquationElem, FrameFragment, GlyphFragment, LayoutMath, MathContext,
|
use typst_library::text::TextElem;
|
||||||
MathSize, Scaled,
|
use typst_library::visualize::{FixedStroke, Geometry};
|
||||||
};
|
|
||||||
use crate::syntax::Span;
|
|
||||||
use crate::text::TextElem;
|
|
||||||
use crate::visualize::{FixedStroke, Geometry};
|
|
||||||
|
|
||||||
/// A square root.
|
use super::{style_cramped, FrameFragment, GlyphFragment, MathContext};
|
||||||
///
|
|
||||||
/// ```example
|
|
||||||
/// $ sqrt(3 - 2 sqrt(2)) = sqrt(2) - 1 $
|
|
||||||
/// ```
|
|
||||||
#[func(title = "Square Root")]
|
|
||||||
pub fn sqrt(
|
|
||||||
/// The call span of this function.
|
|
||||||
span: Span,
|
|
||||||
/// The expression to take the square root of.
|
|
||||||
radicand: Content,
|
|
||||||
) -> Content {
|
|
||||||
RootElem::new(radicand).pack().spanned(span)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A general root.
|
/// Lays out a [`RootElem`].
|
||||||
///
|
|
||||||
/// ```example
|
|
||||||
/// $ root(3, x) $
|
|
||||||
/// ```
|
|
||||||
#[elem(LayoutMath)]
|
|
||||||
pub struct RootElem {
|
|
||||||
/// Which root of the radicand to take.
|
|
||||||
#[positional]
|
|
||||||
pub index: Option<Content>,
|
|
||||||
|
|
||||||
/// The expression to take the root of.
|
|
||||||
#[required]
|
|
||||||
pub radicand: Content,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LayoutMath for Packed<RootElem> {
|
|
||||||
#[typst_macros::time(name = "math.root", span = self.span())]
|
|
||||||
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
|
|
||||||
layout(ctx, styles, self.index(styles).as_ref(), self.radicand(), self.span())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Layout a root.
|
|
||||||
///
|
///
|
||||||
/// TeXbook page 443, page 360
|
/// TeXbook page 443, page 360
|
||||||
/// See also: <https://www.w3.org/TR/mathml-core/#radicals-msqrt-mroot>
|
/// See also: <https://www.w3.org/TR/mathml-core/#radicals-msqrt-mroot>
|
||||||
fn layout(
|
#[typst_macros::time(name = "math.root", span = elem.span())]
|
||||||
|
pub fn layout_root(
|
||||||
|
elem: &Packed<RootElem>,
|
||||||
ctx: &mut MathContext,
|
ctx: &mut MathContext,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
index: Option<&Content>,
|
|
||||||
radicand: &Content,
|
|
||||||
span: Span,
|
|
||||||
) -> SourceResult<()> {
|
) -> SourceResult<()> {
|
||||||
|
let index = elem.index(styles);
|
||||||
|
let radicand = elem.radicand();
|
||||||
|
let span = elem.span();
|
||||||
|
|
||||||
let gap = scaled!(
|
let gap = scaled!(
|
||||||
ctx, styles,
|
ctx, styles,
|
||||||
text: radical_vertical_gap,
|
text: radical_vertical_gap,
|
||||||
@ -94,6 +57,7 @@ fn layout(
|
|||||||
// Layout the index.
|
// Layout the index.
|
||||||
let sscript = EquationElem::set_size(MathSize::ScriptScript).wrap();
|
let sscript = EquationElem::set_size(MathSize::ScriptScript).wrap();
|
||||||
let index = index
|
let index = index
|
||||||
|
.as_ref()
|
||||||
.map(|elem| ctx.layout_into_frame(elem, styles.chain(&sscript)))
|
.map(|elem| ctx.layout_into_frame(elem, styles.chain(&sscript)))
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|
@ -1,16 +1,14 @@
|
|||||||
use std::iter::once;
|
use std::iter::once;
|
||||||
|
|
||||||
|
use typst_library::foundations::{Resolve, StyleChain};
|
||||||
|
use typst_library::layout::{Abs, AlignElem, Em, Frame, InlineItem, Point, Size};
|
||||||
|
use typst_library::math::{EquationElem, MathSize, MEDIUM, THICK, THIN};
|
||||||
|
use typst_library::model::ParElem;
|
||||||
use unicode_math_class::MathClass;
|
use unicode_math_class::MathClass;
|
||||||
|
|
||||||
use crate::foundations::{Resolve, StyleChain};
|
use super::{alignments, scaled_font_size, FrameFragment, MathContext, MathFragment};
|
||||||
use crate::layout::{Abs, AlignElem, Em, Frame, InlineItem, Point, Size};
|
|
||||||
use crate::math::{
|
|
||||||
alignments, scaled_font_size, spacing, EquationElem, FrameFragment, MathContext,
|
|
||||||
MathFragment, MathSize,
|
|
||||||
};
|
|
||||||
use crate::model::ParElem;
|
|
||||||
|
|
||||||
pub const TIGHT_LEADING: Em = Em::new(0.25);
|
const TIGHT_LEADING: Em = Em::new(0.25);
|
||||||
|
|
||||||
/// A linear collection of [`MathFragment`]s.
|
/// A linear collection of [`MathFragment`]s.
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
@ -423,3 +421,49 @@ impl MathRunFrameBuilder {
|
|||||||
fn affects_row_height(fragment: &MathFragment) -> bool {
|
fn affects_row_height(fragment: &MathFragment) -> bool {
|
||||||
!matches!(fragment, MathFragment::Align | MathFragment::Linebreak)
|
!matches!(fragment, MathFragment::Align | MathFragment::Linebreak)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create the spacing between two fragments in a given style.
|
||||||
|
fn spacing(
|
||||||
|
l: &MathFragment,
|
||||||
|
space: Option<MathFragment>,
|
||||||
|
r: &MathFragment,
|
||||||
|
) -> Option<MathFragment> {
|
||||||
|
use MathClass::*;
|
||||||
|
|
||||||
|
let resolve = |v: Em, size_ref: &MathFragment| -> Option<MathFragment> {
|
||||||
|
let width = size_ref.font_size().map_or(Abs::zero(), |size| v.at(size));
|
||||||
|
Some(MathFragment::Spacing(width, false))
|
||||||
|
};
|
||||||
|
let script = |f: &MathFragment| f.math_size().is_some_and(|s| s <= MathSize::Script);
|
||||||
|
|
||||||
|
match (l.class(), r.class()) {
|
||||||
|
// No spacing before punctuation; thin spacing after punctuation, unless
|
||||||
|
// in script size.
|
||||||
|
(_, Punctuation) => None,
|
||||||
|
(Punctuation, _) if !script(l) => resolve(THIN, l),
|
||||||
|
|
||||||
|
// No spacing after opening delimiters and before closing delimiters.
|
||||||
|
(Opening, _) | (_, Closing) => None,
|
||||||
|
|
||||||
|
// Thick spacing around relations, unless followed by a another relation
|
||||||
|
// or in script size.
|
||||||
|
(Relation, Relation) => None,
|
||||||
|
(Relation, _) if !script(l) => resolve(THICK, l),
|
||||||
|
(_, Relation) if !script(r) => resolve(THICK, r),
|
||||||
|
|
||||||
|
// Medium spacing around binary operators, unless in script size.
|
||||||
|
(Binary, _) if !script(l) => resolve(MEDIUM, l),
|
||||||
|
(_, Binary) if !script(r) => resolve(MEDIUM, r),
|
||||||
|
|
||||||
|
// Thin spacing around large operators, unless to the left of
|
||||||
|
// an opening delimiter. TeXBook, p170
|
||||||
|
(Large, Opening | Fence) => None,
|
||||||
|
(Large, _) => resolve(THIN, l),
|
||||||
|
(_, Large) => resolve(THIN, r),
|
||||||
|
|
||||||
|
// Spacing around spaced frames.
|
||||||
|
_ if (l.is_spaced() || r.is_spaced()) => space,
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
207
crates/typst-layout/src/math/shared.rs
Normal file
207
crates/typst-layout/src/math/shared.rs
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
use ttf_parser::math::MathValue;
|
||||||
|
use typst_library::foundations::{Style, StyleChain};
|
||||||
|
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size, VAlignment};
|
||||||
|
use typst_library::math::{EquationElem, MathSize};
|
||||||
|
use typst_library::text::TextElem;
|
||||||
|
use typst_utils::LazyHash;
|
||||||
|
|
||||||
|
use super::{LeftRightAlternator, MathContext, MathFragment, MathRun};
|
||||||
|
|
||||||
|
macro_rules! scaled {
|
||||||
|
($ctx:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => {
|
||||||
|
match typst_library::math::EquationElem::size_in($styles) {
|
||||||
|
typst_library::math::MathSize::Display => scaled!($ctx, $styles, $display),
|
||||||
|
_ => scaled!($ctx, $styles, $text),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($ctx:expr, $styles:expr, $name:ident) => {
|
||||||
|
$crate::math::Scaled::scaled(
|
||||||
|
$ctx.constants.$name(),
|
||||||
|
$ctx,
|
||||||
|
$crate::math::scaled_font_size($ctx, $styles),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! percent {
|
||||||
|
($ctx:expr, $name:ident) => {
|
||||||
|
$ctx.constants.$name() as f64 / 100.0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How much less high scaled delimiters can be than what they wrap.
|
||||||
|
pub const DELIM_SHORT_FALL: Em = Em::new(0.1);
|
||||||
|
|
||||||
|
/// Converts some unit to an absolute length with the current font & font size.
|
||||||
|
pub trait Scaled {
|
||||||
|
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scaled for i16 {
|
||||||
|
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs {
|
||||||
|
ctx.font.to_em(self).at(font_size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scaled for u16 {
|
||||||
|
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs {
|
||||||
|
ctx.font.to_em(self).at(font_size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scaled for MathValue<'_> {
|
||||||
|
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs {
|
||||||
|
self.value.scaled(ctx, font_size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the font size scaled with the `MathSize`.
|
||||||
|
pub fn scaled_font_size(ctx: &MathContext, styles: StyleChain) -> Abs {
|
||||||
|
let factor = match EquationElem::size_in(styles) {
|
||||||
|
MathSize::Display | MathSize::Text => 1.0,
|
||||||
|
MathSize::Script => percent!(ctx, script_percent_scale_down),
|
||||||
|
MathSize::ScriptScript => percent!(ctx, script_script_percent_scale_down),
|
||||||
|
};
|
||||||
|
factor * TextElem::size_in(styles)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Styles something as cramped.
|
||||||
|
pub fn style_cramped() -> LazyHash<Style> {
|
||||||
|
EquationElem::set_cramped(true).wrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The style for subscripts in the current style.
|
||||||
|
pub fn style_for_subscript(styles: StyleChain) -> [LazyHash<Style>; 2] {
|
||||||
|
[style_for_superscript(styles), EquationElem::set_cramped(true).wrap()]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The style for superscripts in the current style.
|
||||||
|
pub fn style_for_superscript(styles: StyleChain) -> LazyHash<Style> {
|
||||||
|
EquationElem::set_size(match EquationElem::size_in(styles) {
|
||||||
|
MathSize::Display | MathSize::Text => MathSize::Script,
|
||||||
|
MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript,
|
||||||
|
})
|
||||||
|
.wrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The style for numerators in the current style.
|
||||||
|
pub fn style_for_numerator(styles: StyleChain) -> LazyHash<Style> {
|
||||||
|
EquationElem::set_size(match EquationElem::size_in(styles) {
|
||||||
|
MathSize::Display => MathSize::Text,
|
||||||
|
MathSize::Text => MathSize::Script,
|
||||||
|
MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript,
|
||||||
|
})
|
||||||
|
.wrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The style for denominators in the current style.
|
||||||
|
pub fn style_for_denominator(styles: StyleChain) -> [LazyHash<Style>; 2] {
|
||||||
|
[style_for_numerator(styles), EquationElem::set_cramped(true).wrap()]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How a delimieter should be aligned when scaling.
|
||||||
|
pub fn delimiter_alignment(delimiter: char) -> VAlignment {
|
||||||
|
match delimiter {
|
||||||
|
'⌜' | '⌝' => VAlignment::Top,
|
||||||
|
'⌞' | '⌟' => VAlignment::Bottom,
|
||||||
|
_ => VAlignment::Horizon,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stack rows on top of each other.
|
||||||
|
///
|
||||||
|
/// Add a `gap` between each row and uses the baseline of the `baseline`-th
|
||||||
|
/// row for the whole frame. `alternator` controls the left/right alternating
|
||||||
|
/// alignment behavior of `AlignPointElem` in the rows.
|
||||||
|
pub fn stack(
|
||||||
|
rows: Vec<MathRun>,
|
||||||
|
align: FixedAlignment,
|
||||||
|
gap: Abs,
|
||||||
|
baseline: usize,
|
||||||
|
alternator: LeftRightAlternator,
|
||||||
|
minimum_ascent_descent: Option<(Abs, Abs)>,
|
||||||
|
) -> Frame {
|
||||||
|
let rows: Vec<_> = rows.into_iter().flat_map(|r| r.rows()).collect();
|
||||||
|
let AlignmentResult { points, width } = alignments(&rows);
|
||||||
|
let rows: Vec<_> = rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| row.into_line_frame(&points, alternator))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let padded_height = |height: Abs| {
|
||||||
|
height.max(minimum_ascent_descent.map_or(Abs::zero(), |(a, d)| a + d))
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut frame = Frame::soft(Size::new(
|
||||||
|
width,
|
||||||
|
rows.iter().map(|row| padded_height(row.height())).sum::<Abs>()
|
||||||
|
+ rows.len().saturating_sub(1) as f64 * gap,
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut y = Abs::zero();
|
||||||
|
for (i, row) in rows.into_iter().enumerate() {
|
||||||
|
let x = if points.is_empty() {
|
||||||
|
align.position(width - row.width())
|
||||||
|
} else {
|
||||||
|
Abs::zero()
|
||||||
|
};
|
||||||
|
let ascent_padded_part = minimum_ascent_descent
|
||||||
|
.map_or(Abs::zero(), |(a, _)| (a - row.ascent()))
|
||||||
|
.max(Abs::zero());
|
||||||
|
let pos = Point::new(x, y + ascent_padded_part);
|
||||||
|
if i == baseline {
|
||||||
|
frame.set_baseline(y + row.baseline() + ascent_padded_part);
|
||||||
|
}
|
||||||
|
y += padded_height(row.height()) + gap;
|
||||||
|
frame.push_frame(pos, row);
|
||||||
|
}
|
||||||
|
|
||||||
|
frame
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine the positions of the alignment points, according to the input rows combined.
|
||||||
|
pub fn alignments(rows: &[MathRun]) -> AlignmentResult {
|
||||||
|
let mut widths = Vec::<Abs>::new();
|
||||||
|
|
||||||
|
let mut pending_width = Abs::zero();
|
||||||
|
for row in rows {
|
||||||
|
let mut width = Abs::zero();
|
||||||
|
let mut alignment_index = 0;
|
||||||
|
|
||||||
|
for fragment in row.iter() {
|
||||||
|
if matches!(fragment, MathFragment::Align) {
|
||||||
|
if alignment_index < widths.len() {
|
||||||
|
widths[alignment_index].set_max(width);
|
||||||
|
} else {
|
||||||
|
widths.push(width.max(pending_width));
|
||||||
|
}
|
||||||
|
width = Abs::zero();
|
||||||
|
alignment_index += 1;
|
||||||
|
} else {
|
||||||
|
width += fragment.width();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if widths.is_empty() {
|
||||||
|
pending_width.set_max(width);
|
||||||
|
} else if alignment_index < widths.len() {
|
||||||
|
widths[alignment_index].set_max(width);
|
||||||
|
} else {
|
||||||
|
widths.push(width.max(pending_width));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut points = widths;
|
||||||
|
for i in 1..points.len() {
|
||||||
|
let prev = points[i - 1];
|
||||||
|
points[i] += prev;
|
||||||
|
}
|
||||||
|
AlignmentResult {
|
||||||
|
width: points.last().copied().unwrap_or(pending_width),
|
||||||
|
points,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AlignmentResult {
|
||||||
|
pub points: Vec<Abs>,
|
||||||
|
pub width: Abs,
|
||||||
|
}
|
@ -1,64 +1,42 @@
|
|||||||
use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
|
use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
|
||||||
use ttf_parser::LazyArray16;
|
use ttf_parser::LazyArray16;
|
||||||
|
use typst_library::diag::SourceResult;
|
||||||
|
use typst_library::foundations::{Packed, Smart, StyleChain};
|
||||||
|
use typst_library::layout::{Abs, Axis, Frame, Length, Point, Rel, Size};
|
||||||
|
use typst_library::math::StretchElem;
|
||||||
|
use typst_utils::Get;
|
||||||
|
|
||||||
use crate::diag::SourceResult;
|
use super::{
|
||||||
use crate::foundations::{elem, Content, Packed, Smart, StyleChain};
|
delimiter_alignment, scaled_font_size, GlyphFragment, MathContext, MathFragment,
|
||||||
use crate::layout::{Abs, Axis, Frame, Length, Point, Rel, Size, VAlignment};
|
Scaled, VariantFragment,
|
||||||
use crate::math::{
|
|
||||||
scaled_font_size, GlyphFragment, LayoutMath, MathContext, MathFragment, Scaled,
|
|
||||||
VariantFragment,
|
|
||||||
};
|
};
|
||||||
use crate::utils::Get;
|
|
||||||
|
|
||||||
/// Maximum number of times extenders can be repeated.
|
/// Maximum number of times extenders can be repeated.
|
||||||
const MAX_REPEATS: usize = 1024;
|
const MAX_REPEATS: usize = 1024;
|
||||||
|
|
||||||
/// Stretches a glyph.
|
/// Lays out a [`StretchElem`].
|
||||||
///
|
#[typst_macros::time(name = "math.stretch", span = elem.span())]
|
||||||
/// This function can also be used to automatically stretch the base of an
|
pub fn layout_stretch(
|
||||||
/// attachment, so that it fits the top and bottom attachments.
|
elem: &Packed<StretchElem>,
|
||||||
///
|
ctx: &mut MathContext,
|
||||||
/// Note that only some glyphs can be stretched, and which ones can depend on
|
styles: StyleChain,
|
||||||
/// the math font being used. However, most math fonts are the same in this
|
) -> SourceResult<()> {
|
||||||
/// regard.
|
let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?;
|
||||||
///
|
|
||||||
/// ```example
|
|
||||||
/// $ H stretch(=)^"define" U + p V $
|
|
||||||
/// $ f : X stretch(->>, size: #150%)_"surjective" Y $
|
|
||||||
/// $ x stretch(harpoons.ltrb, size: #3em) y
|
|
||||||
/// stretch(\[, size: #150%) z $
|
|
||||||
/// ```
|
|
||||||
#[elem(LayoutMath)]
|
|
||||||
pub struct StretchElem {
|
|
||||||
/// The glyph to stretch.
|
|
||||||
#[required]
|
|
||||||
pub body: Content,
|
|
||||||
|
|
||||||
/// The size to stretch to, relative to the maximum size of the glyph and
|
|
||||||
/// its attachments.
|
|
||||||
pub size: Smart<Rel<Length>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LayoutMath for Packed<StretchElem> {
|
|
||||||
#[typst_macros::time(name = "math.stretch", span = self.span())]
|
|
||||||
fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> {
|
|
||||||
let mut fragment = ctx.layout_into_fragment(self.body(), styles)?;
|
|
||||||
stretch_fragment(
|
stretch_fragment(
|
||||||
ctx,
|
ctx,
|
||||||
styles,
|
styles,
|
||||||
&mut fragment,
|
&mut fragment,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
self.size(styles),
|
elem.size(styles),
|
||||||
Abs::zero(),
|
Abs::zero(),
|
||||||
);
|
);
|
||||||
ctx.push(fragment);
|
ctx.push(fragment);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to stretch the given fragment by/to the amount given in stretch.
|
/// Attempts to stretch the given fragment by/to the amount given in stretch.
|
||||||
pub(super) fn stretch_fragment(
|
pub fn stretch_fragment(
|
||||||
ctx: &mut MathContext,
|
ctx: &mut MathContext,
|
||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
fragment: &mut MathFragment,
|
fragment: &mut MathFragment,
|
||||||
@ -77,9 +55,7 @@ pub(super) fn stretch_fragment(
|
|||||||
|
|
||||||
// Return if we attempt to stretch along an axis which isn't stretchable,
|
// Return if we attempt to stretch along an axis which isn't stretchable,
|
||||||
// so that the original fragment isn't modified.
|
// so that the original fragment isn't modified.
|
||||||
let Some(stretch_axis) = stretch_axis(ctx, &glyph) else {
|
let Some(stretch_axis) = stretch_axis(ctx, &glyph) else { return };
|
||||||
return;
|
|
||||||
};
|
|
||||||
let axis = axis.unwrap_or(stretch_axis);
|
let axis = axis.unwrap_or(stretch_axis);
|
||||||
if axis != stretch_axis {
|
if axis != stretch_axis {
|
||||||
return;
|
return;
|
||||||
@ -105,67 +81,10 @@ pub(super) fn stretch_fragment(
|
|||||||
*fragment = MathFragment::Variant(variant);
|
*fragment = MathFragment::Variant(variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn delimiter_alignment(delimiter: char) -> VAlignment {
|
|
||||||
match delimiter {
|
|
||||||
'\u{231c}' | '\u{231d}' => VAlignment::Top,
|
|
||||||
'\u{231e}' | '\u{231f}' => VAlignment::Bottom,
|
|
||||||
_ => VAlignment::Horizon,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return whether the glyph is stretchable and if it is, along which axis it
|
|
||||||
/// can be stretched.
|
|
||||||
fn stretch_axis(ctx: &MathContext, base: &GlyphFragment) -> Option<Axis> {
|
|
||||||
let base_id = base.id;
|
|
||||||
let vertical = ctx
|
|
||||||
.table
|
|
||||||
.variants
|
|
||||||
.and_then(|variants| variants.vertical_constructions.get(base_id))
|
|
||||||
.map(|_| Axis::Y);
|
|
||||||
let horizontal = ctx
|
|
||||||
.table
|
|
||||||
.variants
|
|
||||||
.and_then(|variants| variants.horizontal_constructions.get(base_id))
|
|
||||||
.map(|_| Axis::X);
|
|
||||||
|
|
||||||
match (vertical, horizontal) {
|
|
||||||
(vertical, None) => vertical,
|
|
||||||
(None, horizontal) => horizontal,
|
|
||||||
_ => {
|
|
||||||
// As far as we know, there aren't any glyphs that have both
|
|
||||||
// vertical and horizontal constructions. So for the time being, we
|
|
||||||
// will assume that a glyph cannot have both.
|
|
||||||
panic!("glyph {:?} has both vertical and horizontal constructions", base.c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GlyphFragment {
|
|
||||||
/// Try to stretch a glyph to a desired height.
|
|
||||||
pub fn stretch_vertical(
|
|
||||||
self,
|
|
||||||
ctx: &MathContext,
|
|
||||||
height: Abs,
|
|
||||||
short_fall: Abs,
|
|
||||||
) -> VariantFragment {
|
|
||||||
stretch_glyph(ctx, self, height, short_fall, Axis::Y)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Try to stretch a glyph to a desired width.
|
|
||||||
pub fn stretch_horizontal(
|
|
||||||
self,
|
|
||||||
ctx: &MathContext,
|
|
||||||
width: Abs,
|
|
||||||
short_fall: Abs,
|
|
||||||
) -> VariantFragment {
|
|
||||||
stretch_glyph(ctx, self, width, short_fall, Axis::X)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Try to stretch a glyph to a desired width or height.
|
/// Try to stretch a glyph to a desired width or height.
|
||||||
///
|
///
|
||||||
/// The resulting frame may not have the exact desired width.
|
/// The resulting frame may not have the exact desired width.
|
||||||
fn stretch_glyph(
|
pub fn stretch_glyph(
|
||||||
ctx: &MathContext,
|
ctx: &MathContext,
|
||||||
mut base: GlyphFragment,
|
mut base: GlyphFragment,
|
||||||
target: Abs,
|
target: Abs,
|
||||||
@ -218,6 +137,33 @@ fn stretch_glyph(
|
|||||||
assemble(ctx, base, assembly, min_overlap, target, axis)
|
assemble(ctx, base, assembly, min_overlap, target, axis)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return whether the glyph is stretchable and if it is, along which axis it
|
||||||
|
/// can be stretched.
|
||||||
|
fn stretch_axis(ctx: &MathContext, base: &GlyphFragment) -> Option<Axis> {
|
||||||
|
let base_id = base.id;
|
||||||
|
let vertical = ctx
|
||||||
|
.table
|
||||||
|
.variants
|
||||||
|
.and_then(|variants| variants.vertical_constructions.get(base_id))
|
||||||
|
.map(|_| Axis::Y);
|
||||||
|
let horizontal = ctx
|
||||||
|
.table
|
||||||
|
.variants
|
||||||
|
.and_then(|variants| variants.horizontal_constructions.get(base_id))
|
||||||
|
.map(|_| Axis::X);
|
||||||
|
|
||||||
|
match (vertical, horizontal) {
|
||||||
|
(vertical, None) => vertical,
|
||||||
|
(None, horizontal) => horizontal,
|
||||||
|
_ => {
|
||||||
|
// As far as we know, there aren't any glyphs that have both
|
||||||
|
// vertical and horizontal constructions. So for the time being, we
|
||||||
|
// will assume that a glyph cannot have both.
|
||||||
|
panic!("glyph {:?} has both vertical and horizontal constructions", base.c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Assemble a glyph from parts.
|
/// Assemble a glyph from parts.
|
||||||
fn assemble(
|
fn assemble(
|
||||||
ctx: &MathContext,
|
ctx: &MathContext,
|
||||||
@ -323,7 +269,6 @@ fn assemble(
|
|||||||
|
|
||||||
VariantFragment {
|
VariantFragment {
|
||||||
c: base.c,
|
c: base.c,
|
||||||
id: None,
|
|
||||||
frame,
|
frame,
|
||||||
font_size: base.font_size,
|
font_size: base.font_size,
|
||||||
italics_correction: Abs::zero(),
|
italics_correction: Abs::zero(),
|
344
crates/typst-layout/src/math/text.rs
Normal file
344
crates/typst-layout/src/math/text.rs
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
use std::f64::consts::SQRT_2;
|
||||||
|
|
||||||
|
use ecow::{eco_vec, EcoString};
|
||||||
|
use typst_library::diag::SourceResult;
|
||||||
|
use typst_library::foundations::{Packed, StyleChain, StyleVec};
|
||||||
|
use typst_library::layout::{Abs, Size};
|
||||||
|
use typst_library::math::{EquationElem, MathSize, MathVariant};
|
||||||
|
use typst_library::text::{
|
||||||
|
BottomEdge, BottomEdgeMetric, TextElem, TextSize, TopEdge, TopEdgeMetric,
|
||||||
|
};
|
||||||
|
use typst_syntax::{is_newline, Span};
|
||||||
|
use unicode_math_class::MathClass;
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
scaled_font_size, FrameFragment, GlyphFragment, MathContext, MathFragment, MathRun,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Lays out a [`TextElem`].
|
||||||
|
pub fn layout_text(
|
||||||
|
elem: &Packed<TextElem>,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
let text = elem.text();
|
||||||
|
let span = elem.span();
|
||||||
|
let mut chars = text.chars();
|
||||||
|
let math_size = EquationElem::size_in(styles);
|
||||||
|
|
||||||
|
let fragment: MathFragment = if let Some(mut glyph) = chars
|
||||||
|
.next()
|
||||||
|
.filter(|_| chars.next().is_none())
|
||||||
|
.map(|c| styled_char(styles, c, true))
|
||||||
|
.and_then(|c| GlyphFragment::try_new(ctx, styles, c, span))
|
||||||
|
{
|
||||||
|
// A single letter that is available in the math font.
|
||||||
|
match math_size {
|
||||||
|
MathSize::Script => {
|
||||||
|
glyph.make_scriptsize(ctx);
|
||||||
|
}
|
||||||
|
MathSize::ScriptScript => {
|
||||||
|
glyph.make_scriptscriptsize(ctx);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
if glyph.class == MathClass::Large {
|
||||||
|
let mut variant = if math_size == MathSize::Display {
|
||||||
|
let height = scaled!(ctx, styles, display_operator_min_height)
|
||||||
|
.max(SQRT_2 * glyph.height());
|
||||||
|
glyph.stretch_vertical(ctx, height, Abs::zero())
|
||||||
|
} else {
|
||||||
|
glyph.into_variant()
|
||||||
|
};
|
||||||
|
// TeXbook p 155. Large operators are always vertically centered on the axis.
|
||||||
|
variant.center_on_axis(ctx);
|
||||||
|
variant.into()
|
||||||
|
} else {
|
||||||
|
glyph.into()
|
||||||
|
}
|
||||||
|
} else if text.chars().all(|c| c.is_ascii_digit() || c == '.') {
|
||||||
|
// Numbers aren't that difficult.
|
||||||
|
let mut fragments = vec![];
|
||||||
|
for c in text.chars() {
|
||||||
|
let c = styled_char(styles, c, false);
|
||||||
|
fragments.push(GlyphFragment::new(ctx, styles, c, span).into());
|
||||||
|
}
|
||||||
|
let frame = MathRun::new(fragments).into_frame(ctx, styles);
|
||||||
|
FrameFragment::new(ctx, styles, frame).with_text_like(true).into()
|
||||||
|
} else {
|
||||||
|
let local = [
|
||||||
|
TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)),
|
||||||
|
TextElem::set_bottom_edge(BottomEdge::Metric(BottomEdgeMetric::Bounds)),
|
||||||
|
TextElem::set_size(TextSize(scaled_font_size(ctx, styles).into())),
|
||||||
|
]
|
||||||
|
.map(|p| p.wrap());
|
||||||
|
|
||||||
|
// Anything else is handled by Typst's standard text layout.
|
||||||
|
let styles = styles.chain(&local);
|
||||||
|
let text: EcoString =
|
||||||
|
text.chars().map(|c| styled_char(styles, c, false)).collect();
|
||||||
|
if text.contains(is_newline) {
|
||||||
|
let mut fragments = vec![];
|
||||||
|
for (i, piece) in text.split(is_newline).enumerate() {
|
||||||
|
if i != 0 {
|
||||||
|
fragments.push(MathFragment::Linebreak);
|
||||||
|
}
|
||||||
|
if !piece.is_empty() {
|
||||||
|
fragments.push(layout_complex_text(piece, ctx, span, styles)?.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut frame = MathRun::new(fragments).into_frame(ctx, styles);
|
||||||
|
let axis = scaled!(ctx, styles, axis_height);
|
||||||
|
frame.set_baseline(frame.height() / 2.0 + axis);
|
||||||
|
FrameFragment::new(ctx, styles, frame).into()
|
||||||
|
} else {
|
||||||
|
layout_complex_text(&text, ctx, span, styles)?.into()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.push(fragment);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout the given text string into a [`FrameFragment`].
|
||||||
|
fn layout_complex_text(
|
||||||
|
text: &str,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
span: Span,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<FrameFragment> {
|
||||||
|
// There isn't a natural width for a paragraph in a math environment;
|
||||||
|
// because it will be placed somewhere probably not at the left margin
|
||||||
|
// it will overflow. So emulate an `hbox` instead and allow the paragraph
|
||||||
|
// to extend as far as needed.
|
||||||
|
let spaced = text.graphemes(true).nth(1).is_some();
|
||||||
|
let elem = TextElem::packed(text).spanned(span);
|
||||||
|
let frame = (ctx.engine.routines.layout_inline)(
|
||||||
|
ctx.engine,
|
||||||
|
&StyleVec::wrap(eco_vec![elem]),
|
||||||
|
ctx.locator.next(&span),
|
||||||
|
styles,
|
||||||
|
false,
|
||||||
|
Size::splat(Abs::inf()),
|
||||||
|
false,
|
||||||
|
)?
|
||||||
|
.into_frame();
|
||||||
|
|
||||||
|
Ok(FrameFragment::new(ctx, styles, frame)
|
||||||
|
.with_class(MathClass::Alphabetic)
|
||||||
|
.with_text_like(true)
|
||||||
|
.with_spaced(spaced))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Select the correct styled math letter.
|
||||||
|
///
|
||||||
|
/// <https://www.w3.org/TR/mathml-core/#new-text-transform-mappings>
|
||||||
|
/// <https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols>
|
||||||
|
fn styled_char(styles: StyleChain, c: char, auto_italic: bool) -> char {
|
||||||
|
use MathVariant::*;
|
||||||
|
|
||||||
|
let variant = EquationElem::variant_in(styles);
|
||||||
|
let bold = EquationElem::bold_in(styles);
|
||||||
|
let italic = EquationElem::italic_in(styles).unwrap_or(
|
||||||
|
auto_italic
|
||||||
|
&& matches!(
|
||||||
|
c,
|
||||||
|
'a'..='z' | 'ı' | 'ȷ' | 'A'..='Z' | 'α'..='ω' |
|
||||||
|
'∂' | 'ϵ' | 'ϑ' | 'ϰ' | 'ϕ' | 'ϱ' | 'ϖ'
|
||||||
|
)
|
||||||
|
&& matches!(variant, Sans | Serif),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(c) = basic_exception(c) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(c) = latin_exception(c, variant, bold, italic) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(c) = greek_exception(c, variant, bold, italic) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
let base = match c {
|
||||||
|
'A'..='Z' => 'A',
|
||||||
|
'a'..='z' => 'a',
|
||||||
|
'Α'..='Ω' => 'Α',
|
||||||
|
'α'..='ω' => 'α',
|
||||||
|
'0'..='9' => '0',
|
||||||
|
// Hebrew Alef -> Dalet.
|
||||||
|
'\u{05D0}'..='\u{05D3}' => '\u{05D0}',
|
||||||
|
_ => return c,
|
||||||
|
};
|
||||||
|
|
||||||
|
let tuple = (variant, bold, italic);
|
||||||
|
let start = match c {
|
||||||
|
// Latin upper.
|
||||||
|
'A'..='Z' => match tuple {
|
||||||
|
(Serif, false, false) => 0x0041,
|
||||||
|
(Serif, true, false) => 0x1D400,
|
||||||
|
(Serif, false, true) => 0x1D434,
|
||||||
|
(Serif, true, true) => 0x1D468,
|
||||||
|
(Sans, false, false) => 0x1D5A0,
|
||||||
|
(Sans, true, false) => 0x1D5D4,
|
||||||
|
(Sans, false, true) => 0x1D608,
|
||||||
|
(Sans, true, true) => 0x1D63C,
|
||||||
|
(Cal, false, _) => 0x1D49C,
|
||||||
|
(Cal, true, _) => 0x1D4D0,
|
||||||
|
(Frak, false, _) => 0x1D504,
|
||||||
|
(Frak, true, _) => 0x1D56C,
|
||||||
|
(Mono, _, _) => 0x1D670,
|
||||||
|
(Bb, _, _) => 0x1D538,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Latin lower.
|
||||||
|
'a'..='z' => match tuple {
|
||||||
|
(Serif, false, false) => 0x0061,
|
||||||
|
(Serif, true, false) => 0x1D41A,
|
||||||
|
(Serif, false, true) => 0x1D44E,
|
||||||
|
(Serif, true, true) => 0x1D482,
|
||||||
|
(Sans, false, false) => 0x1D5BA,
|
||||||
|
(Sans, true, false) => 0x1D5EE,
|
||||||
|
(Sans, false, true) => 0x1D622,
|
||||||
|
(Sans, true, true) => 0x1D656,
|
||||||
|
(Cal, false, _) => 0x1D4B6,
|
||||||
|
(Cal, true, _) => 0x1D4EA,
|
||||||
|
(Frak, false, _) => 0x1D51E,
|
||||||
|
(Frak, true, _) => 0x1D586,
|
||||||
|
(Mono, _, _) => 0x1D68A,
|
||||||
|
(Bb, _, _) => 0x1D552,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Greek upper.
|
||||||
|
'Α'..='Ω' => match tuple {
|
||||||
|
(Serif, false, false) => 0x0391,
|
||||||
|
(Serif, true, false) => 0x1D6A8,
|
||||||
|
(Serif, false, true) => 0x1D6E2,
|
||||||
|
(Serif, true, true) => 0x1D71C,
|
||||||
|
(Sans, _, false) => 0x1D756,
|
||||||
|
(Sans, _, true) => 0x1D790,
|
||||||
|
(Cal | Frak | Mono | Bb, _, _) => return c,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Greek lower.
|
||||||
|
'α'..='ω' => match tuple {
|
||||||
|
(Serif, false, false) => 0x03B1,
|
||||||
|
(Serif, true, false) => 0x1D6C2,
|
||||||
|
(Serif, false, true) => 0x1D6FC,
|
||||||
|
(Serif, true, true) => 0x1D736,
|
||||||
|
(Sans, _, false) => 0x1D770,
|
||||||
|
(Sans, _, true) => 0x1D7AA,
|
||||||
|
(Cal | Frak | Mono | Bb, _, _) => return c,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Hebrew Alef -> Dalet.
|
||||||
|
'\u{05D0}'..='\u{05D3}' => 0x2135,
|
||||||
|
|
||||||
|
// Numbers.
|
||||||
|
'0'..='9' => match tuple {
|
||||||
|
(Serif, false, _) => 0x0030,
|
||||||
|
(Serif, true, _) => 0x1D7CE,
|
||||||
|
(Bb, _, _) => 0x1D7D8,
|
||||||
|
(Sans, false, _) => 0x1D7E2,
|
||||||
|
(Sans, true, _) => 0x1D7EC,
|
||||||
|
(Mono, _, _) => 0x1D7F6,
|
||||||
|
(Cal | Frak, _, _) => return c,
|
||||||
|
},
|
||||||
|
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
std::char::from_u32(start + (c as u32 - base as u32)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn basic_exception(c: char) -> Option<char> {
|
||||||
|
Some(match c {
|
||||||
|
'〈' => '⟨',
|
||||||
|
'〉' => '⟩',
|
||||||
|
'《' => '⟪',
|
||||||
|
'》' => '⟫',
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn latin_exception(
|
||||||
|
c: char,
|
||||||
|
variant: MathVariant,
|
||||||
|
bold: bool,
|
||||||
|
italic: bool,
|
||||||
|
) -> Option<char> {
|
||||||
|
use MathVariant::*;
|
||||||
|
Some(match (c, variant, bold, italic) {
|
||||||
|
('B', Cal, false, _) => 'ℬ',
|
||||||
|
('E', Cal, false, _) => 'ℰ',
|
||||||
|
('F', Cal, false, _) => 'ℱ',
|
||||||
|
('H', Cal, false, _) => 'ℋ',
|
||||||
|
('I', Cal, false, _) => 'ℐ',
|
||||||
|
('L', Cal, false, _) => 'ℒ',
|
||||||
|
('M', Cal, false, _) => 'ℳ',
|
||||||
|
('R', Cal, false, _) => 'ℛ',
|
||||||
|
('C', Frak, false, _) => 'ℭ',
|
||||||
|
('H', Frak, false, _) => 'ℌ',
|
||||||
|
('I', Frak, false, _) => 'ℑ',
|
||||||
|
('R', Frak, false, _) => 'ℜ',
|
||||||
|
('Z', Frak, false, _) => 'ℨ',
|
||||||
|
('C', Bb, ..) => 'ℂ',
|
||||||
|
('H', Bb, ..) => 'ℍ',
|
||||||
|
('N', Bb, ..) => 'ℕ',
|
||||||
|
('P', Bb, ..) => 'ℙ',
|
||||||
|
('Q', Bb, ..) => 'ℚ',
|
||||||
|
('R', Bb, ..) => 'ℝ',
|
||||||
|
('Z', Bb, ..) => 'ℤ',
|
||||||
|
('D', Bb, _, true) => 'ⅅ',
|
||||||
|
('d', Bb, _, true) => 'ⅆ',
|
||||||
|
('e', Bb, _, true) => 'ⅇ',
|
||||||
|
('i', Bb, _, true) => 'ⅈ',
|
||||||
|
('j', Bb, _, true) => 'ⅉ',
|
||||||
|
('h', Serif, false, true) => 'ℎ',
|
||||||
|
('e', Cal, false, _) => 'ℯ',
|
||||||
|
('g', Cal, false, _) => 'ℊ',
|
||||||
|
('o', Cal, false, _) => 'ℴ',
|
||||||
|
('ı', Serif, .., true) => '𝚤',
|
||||||
|
('ȷ', Serif, .., true) => '𝚥',
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn greek_exception(
|
||||||
|
c: char,
|
||||||
|
variant: MathVariant,
|
||||||
|
bold: bool,
|
||||||
|
italic: bool,
|
||||||
|
) -> Option<char> {
|
||||||
|
use MathVariant::*;
|
||||||
|
let list = match c {
|
||||||
|
'ϴ' => ['𝚹', '𝛳', '𝜭', '𝝧', '𝞡', 'ϴ'],
|
||||||
|
'∇' => ['𝛁', '𝛻', '𝜵', '𝝯', '𝞩', '∇'],
|
||||||
|
'∂' => ['𝛛', '𝜕', '𝝏', '𝞉', '𝟃', '∂'],
|
||||||
|
'ϵ' => ['𝛜', '𝜖', '𝝐', '𝞊', '𝟄', 'ϵ'],
|
||||||
|
'ϑ' => ['𝛝', '𝜗', '𝝑', '𝞋', '𝟅', 'ϑ'],
|
||||||
|
'ϰ' => ['𝛞', '𝜘', '𝝒', '𝞌', '𝟆', 'ϰ'],
|
||||||
|
'ϕ' => ['𝛟', '𝜙', '𝝓', '𝞍', '𝟇', 'ϕ'],
|
||||||
|
'ϱ' => ['𝛠', '𝜚', '𝝔', '𝞎', '𝟈', 'ϱ'],
|
||||||
|
'ϖ' => ['𝛡', '𝜛', '𝝕', '𝞏', '𝟉', 'ϖ'],
|
||||||
|
'Γ' => ['𝚪', '𝛤', '𝜞', '𝝘', '𝞒', 'ℾ'],
|
||||||
|
'γ' => ['𝛄', '𝛾', '𝜸', '𝝲', '𝞬', 'ℽ'],
|
||||||
|
'Π' => ['𝚷', '𝛱', '𝜫', '𝝥', '𝞟', 'ℿ'],
|
||||||
|
'π' => ['𝛑', '𝜋', '𝝅', '𝝿', '𝞹', 'ℼ'],
|
||||||
|
'∑' => ['∑', '∑', '∑', '∑', '∑', '⅀'],
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(match (variant, bold, italic) {
|
||||||
|
(Serif, true, false) => list[0],
|
||||||
|
(Serif, false, true) => list[1],
|
||||||
|
(Serif, true, true) => list[2],
|
||||||
|
(Sans, _, false) => list[3],
|
||||||
|
(Sans, _, true) => list[4],
|
||||||
|
(Bb, ..) => list[5],
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
327
crates/typst-layout/src/math/underover.rs
Normal file
327
crates/typst-layout/src/math/underover.rs
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
use typst_library::diag::SourceResult;
|
||||||
|
use typst_library::foundations::{Content, Packed, StyleChain};
|
||||||
|
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, FrameItem, Point, Size};
|
||||||
|
use typst_library::math::{
|
||||||
|
OverbraceElem, OverbracketElem, OverlineElem, OverparenElem, OvershellElem,
|
||||||
|
UnderbraceElem, UnderbracketElem, UnderlineElem, UnderparenElem, UndershellElem,
|
||||||
|
};
|
||||||
|
use typst_library::text::TextElem;
|
||||||
|
use typst_library::visualize::{FixedStroke, Geometry};
|
||||||
|
use typst_syntax::Span;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
scaled_font_size, stack, style_cramped, style_for_subscript, style_for_superscript,
|
||||||
|
FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, MathRun,
|
||||||
|
};
|
||||||
|
|
||||||
|
const BRACE_GAP: Em = Em::new(0.25);
|
||||||
|
const BRACKET_GAP: Em = Em::new(0.25);
|
||||||
|
const PAREN_GAP: Em = Em::new(0.25);
|
||||||
|
const SHELL_GAP: Em = Em::new(0.25);
|
||||||
|
|
||||||
|
/// A marker to distinguish under- and overlines.
|
||||||
|
enum Position {
|
||||||
|
Under,
|
||||||
|
Over,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lays out an [`UnderlineElem`].
|
||||||
|
#[typst_macros::time(name = "math.underline", span = elem.span())]
|
||||||
|
pub fn layout_underline(
|
||||||
|
elem: &Packed<UnderlineElem>,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
layout_underoverline(ctx, styles, elem.body(), elem.span(), Position::Under)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lays out an [`OverlineElem`].
|
||||||
|
#[typst_macros::time(name = "math.overline", span = elem.span())]
|
||||||
|
pub fn layout_overline(
|
||||||
|
elem: &Packed<OverlineElem>,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
layout_underoverline(ctx, styles, elem.body(), elem.span(), Position::Over)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lays out an [`UnderbraceElem`].
|
||||||
|
#[typst_macros::time(name = "math.underbrace", span = elem.span())]
|
||||||
|
pub fn layout_underbrace(
|
||||||
|
elem: &Packed<UnderbraceElem>,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
layout_underoverspreader(
|
||||||
|
ctx,
|
||||||
|
styles,
|
||||||
|
elem.body(),
|
||||||
|
&elem.annotation(styles),
|
||||||
|
'⏟',
|
||||||
|
BRACE_GAP,
|
||||||
|
Position::Under,
|
||||||
|
elem.span(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lays out an [`OverbraceElem`].
|
||||||
|
#[typst_macros::time(name = "math.overbrace", span = elem.span())]
|
||||||
|
pub fn layout_overbrace(
|
||||||
|
elem: &Packed<OverbraceElem>,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
layout_underoverspreader(
|
||||||
|
ctx,
|
||||||
|
styles,
|
||||||
|
elem.body(),
|
||||||
|
&elem.annotation(styles),
|
||||||
|
'⏞',
|
||||||
|
BRACE_GAP,
|
||||||
|
Position::Over,
|
||||||
|
elem.span(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lays out an [`UnderbracketElem`].
|
||||||
|
#[typst_macros::time(name = "math.underbracket", span = elem.span())]
|
||||||
|
pub fn layout_underbracket(
|
||||||
|
elem: &Packed<UnderbracketElem>,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
layout_underoverspreader(
|
||||||
|
ctx,
|
||||||
|
styles,
|
||||||
|
elem.body(),
|
||||||
|
&elem.annotation(styles),
|
||||||
|
'⎵',
|
||||||
|
BRACKET_GAP,
|
||||||
|
Position::Under,
|
||||||
|
elem.span(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lays out an [`OverbracketElem`].
|
||||||
|
#[typst_macros::time(name = "math.overbracket", span = elem.span())]
|
||||||
|
pub fn layout_overbracket(
|
||||||
|
elem: &Packed<OverbracketElem>,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
layout_underoverspreader(
|
||||||
|
ctx,
|
||||||
|
styles,
|
||||||
|
elem.body(),
|
||||||
|
&elem.annotation(styles),
|
||||||
|
'⎴',
|
||||||
|
BRACKET_GAP,
|
||||||
|
Position::Over,
|
||||||
|
elem.span(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lays out an [`UnderparenElem`].
|
||||||
|
#[typst_macros::time(name = "math.underparen", span = elem.span())]
|
||||||
|
pub fn layout_underparen(
|
||||||
|
elem: &Packed<UnderparenElem>,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
layout_underoverspreader(
|
||||||
|
ctx,
|
||||||
|
styles,
|
||||||
|
elem.body(),
|
||||||
|
&elem.annotation(styles),
|
||||||
|
'⏝',
|
||||||
|
PAREN_GAP,
|
||||||
|
Position::Under,
|
||||||
|
elem.span(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lays out an [`OverparenElem`].
|
||||||
|
#[typst_macros::time(name = "math.overparen", span = elem.span())]
|
||||||
|
pub fn layout_overparen(
|
||||||
|
elem: &Packed<OverparenElem>,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
layout_underoverspreader(
|
||||||
|
ctx,
|
||||||
|
styles,
|
||||||
|
elem.body(),
|
||||||
|
&elem.annotation(styles),
|
||||||
|
'⏜',
|
||||||
|
PAREN_GAP,
|
||||||
|
Position::Over,
|
||||||
|
elem.span(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lays out an [`UndershellElem`].
|
||||||
|
#[typst_macros::time(name = "math.undershell", span = elem.span())]
|
||||||
|
pub fn layout_undershell(
|
||||||
|
elem: &Packed<UndershellElem>,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
layout_underoverspreader(
|
||||||
|
ctx,
|
||||||
|
styles,
|
||||||
|
elem.body(),
|
||||||
|
&elem.annotation(styles),
|
||||||
|
'⏡',
|
||||||
|
SHELL_GAP,
|
||||||
|
Position::Under,
|
||||||
|
elem.span(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lays out an [`OvershellElem`].
|
||||||
|
#[typst_macros::time(name = "math.overshell", span = elem.span())]
|
||||||
|
pub fn layout_overshell(
|
||||||
|
elem: &Packed<OvershellElem>,
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
layout_underoverspreader(
|
||||||
|
ctx,
|
||||||
|
styles,
|
||||||
|
elem.body(),
|
||||||
|
&elem.annotation(styles),
|
||||||
|
'⏠',
|
||||||
|
SHELL_GAP,
|
||||||
|
Position::Over,
|
||||||
|
elem.span(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// layout under- or overlined content.
|
||||||
|
fn layout_underoverline(
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
body: &Content,
|
||||||
|
span: Span,
|
||||||
|
position: Position,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
let (extra_height, content, line_pos, content_pos, baseline, bar_height, line_adjust);
|
||||||
|
match position {
|
||||||
|
Position::Under => {
|
||||||
|
let sep = scaled!(ctx, styles, underbar_extra_descender);
|
||||||
|
bar_height = scaled!(ctx, styles, underbar_rule_thickness);
|
||||||
|
let gap = scaled!(ctx, styles, underbar_vertical_gap);
|
||||||
|
extra_height = sep + bar_height + gap;
|
||||||
|
|
||||||
|
content = ctx.layout_into_fragment(body, styles)?;
|
||||||
|
|
||||||
|
line_pos = Point::with_y(content.height() + gap + bar_height / 2.0);
|
||||||
|
content_pos = Point::zero();
|
||||||
|
baseline = content.ascent();
|
||||||
|
line_adjust = -content.italics_correction();
|
||||||
|
}
|
||||||
|
Position::Over => {
|
||||||
|
let sep = scaled!(ctx, styles, overbar_extra_ascender);
|
||||||
|
bar_height = scaled!(ctx, styles, overbar_rule_thickness);
|
||||||
|
let gap = scaled!(ctx, styles, overbar_vertical_gap);
|
||||||
|
extra_height = sep + bar_height + gap;
|
||||||
|
|
||||||
|
let cramped = style_cramped();
|
||||||
|
content = ctx.layout_into_fragment(body, styles.chain(&cramped))?;
|
||||||
|
|
||||||
|
line_pos = Point::with_y(sep + bar_height / 2.0);
|
||||||
|
content_pos = Point::with_y(extra_height);
|
||||||
|
baseline = content.ascent() + extra_height;
|
||||||
|
line_adjust = Abs::zero();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = content.width();
|
||||||
|
let height = content.height() + extra_height;
|
||||||
|
let size = Size::new(width, height);
|
||||||
|
let line_width = width + line_adjust;
|
||||||
|
|
||||||
|
let content_class = content.class();
|
||||||
|
let content_is_text_like = content.is_text_like();
|
||||||
|
let content_italics_correction = content.italics_correction();
|
||||||
|
let mut frame = Frame::soft(size);
|
||||||
|
frame.set_baseline(baseline);
|
||||||
|
frame.push_frame(content_pos, content.into_frame());
|
||||||
|
frame.push(
|
||||||
|
line_pos,
|
||||||
|
FrameItem::Shape(
|
||||||
|
Geometry::Line(Point::with_x(line_width)).stroked(FixedStroke {
|
||||||
|
paint: TextElem::fill_in(styles).as_decoration(),
|
||||||
|
thickness: bar_height,
|
||||||
|
..FixedStroke::default()
|
||||||
|
}),
|
||||||
|
span,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.push(
|
||||||
|
FrameFragment::new(ctx, styles, frame)
|
||||||
|
.with_class(content_class)
|
||||||
|
.with_text_like(content_is_text_like)
|
||||||
|
.with_italics_correction(content_italics_correction),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout an over- or underbrace-like object.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn layout_underoverspreader(
|
||||||
|
ctx: &mut MathContext,
|
||||||
|
styles: StyleChain,
|
||||||
|
body: &Content,
|
||||||
|
annotation: &Option<Content>,
|
||||||
|
c: char,
|
||||||
|
gap: Em,
|
||||||
|
position: Position,
|
||||||
|
span: Span,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
let font_size = scaled_font_size(ctx, styles);
|
||||||
|
let gap = gap.at(font_size);
|
||||||
|
let body = ctx.layout_into_run(body, styles)?;
|
||||||
|
let body_class = body.class();
|
||||||
|
let body = body.into_fragment(ctx, styles);
|
||||||
|
let glyph = GlyphFragment::new(ctx, styles, c, span);
|
||||||
|
let stretched = glyph.stretch_horizontal(ctx, body.width(), Abs::zero());
|
||||||
|
|
||||||
|
let mut rows = vec![];
|
||||||
|
let baseline = match position {
|
||||||
|
Position::Under => {
|
||||||
|
rows.push(MathRun::new(vec![body]));
|
||||||
|
rows.push(stretched.into());
|
||||||
|
if let Some(annotation) = annotation {
|
||||||
|
let under_style = style_for_subscript(styles);
|
||||||
|
let annotation_styles = styles.chain(&under_style);
|
||||||
|
rows.push(ctx.layout_into_run(annotation, annotation_styles)?);
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
|
Position::Over => {
|
||||||
|
if let Some(annotation) = annotation {
|
||||||
|
let over_style = style_for_superscript(styles);
|
||||||
|
let annotation_styles = styles.chain(&over_style);
|
||||||
|
rows.push(ctx.layout_into_run(annotation, annotation_styles)?);
|
||||||
|
}
|
||||||
|
rows.push(stretched.into());
|
||||||
|
rows.push(MathRun::new(vec![body]));
|
||||||
|
rows.len() - 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let frame = stack(
|
||||||
|
rows,
|
||||||
|
FixedAlignment::Center,
|
||||||
|
gap,
|
||||||
|
baseline,
|
||||||
|
LeftRightAlternator::Right,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
ctx.push(FrameFragment::new(ctx, styles, frame).with_class(body_class));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1,78 +1,14 @@
|
|||||||
use crate::diag::SourceResult;
|
use typst_library::diag::SourceResult;
|
||||||
use crate::engine::Engine;
|
use typst_library::engine::Engine;
|
||||||
use crate::foundations::{
|
use typst_library::foundations::{Packed, Resolve, StyleChain};
|
||||||
elem, Content, NativeElement, Packed, Resolve, Show, StyleChain,
|
use typst_library::introspection::Locator;
|
||||||
|
use typst_library::layout::{
|
||||||
|
Abs, Fragment, Frame, PadElem, Point, Regions, Rel, Sides, Size,
|
||||||
};
|
};
|
||||||
use crate::introspection::Locator;
|
|
||||||
use crate::layout::{
|
|
||||||
layout_fragment, Abs, BlockElem, Fragment, Frame, Length, Point, Regions, Rel, Sides,
|
|
||||||
Size,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Adds spacing around content.
|
|
||||||
///
|
|
||||||
/// The spacing can be specified for each side individually, or for all sides at
|
|
||||||
/// once by specifying a positional argument.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```example
|
|
||||||
/// #set align(center)
|
|
||||||
///
|
|
||||||
/// #pad(x: 16pt, image("typing.jpg"))
|
|
||||||
/// _Typing speeds can be
|
|
||||||
/// measured in words per minute._
|
|
||||||
/// ```
|
|
||||||
#[elem(title = "Padding", Show)]
|
|
||||||
pub struct PadElem {
|
|
||||||
/// The padding at the left side.
|
|
||||||
#[parse(
|
|
||||||
let all = args.named("rest")?.or(args.find()?);
|
|
||||||
let x = args.named("x")?.or(all);
|
|
||||||
let y = args.named("y")?.or(all);
|
|
||||||
args.named("left")?.or(x)
|
|
||||||
)]
|
|
||||||
pub left: Rel<Length>,
|
|
||||||
|
|
||||||
/// The padding at the top side.
|
|
||||||
#[parse(args.named("top")?.or(y))]
|
|
||||||
pub top: Rel<Length>,
|
|
||||||
|
|
||||||
/// The padding at the right side.
|
|
||||||
#[parse(args.named("right")?.or(x))]
|
|
||||||
pub right: Rel<Length>,
|
|
||||||
|
|
||||||
/// The padding at the bottom side.
|
|
||||||
#[parse(args.named("bottom")?.or(y))]
|
|
||||||
pub bottom: Rel<Length>,
|
|
||||||
|
|
||||||
/// A shorthand to set `left` and `right` to the same value.
|
|
||||||
#[external]
|
|
||||||
pub x: Rel<Length>,
|
|
||||||
|
|
||||||
/// A shorthand to set `top` and `bottom` to the same value.
|
|
||||||
#[external]
|
|
||||||
pub y: Rel<Length>,
|
|
||||||
|
|
||||||
/// A shorthand to set all four sides to the same value.
|
|
||||||
#[external]
|
|
||||||
pub rest: Rel<Length>,
|
|
||||||
|
|
||||||
/// The content to pad at the sides.
|
|
||||||
#[required]
|
|
||||||
pub body: Content,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Show for Packed<PadElem> {
|
|
||||||
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
|
|
||||||
Ok(BlockElem::multi_layouter(self.clone(), layout_pad)
|
|
||||||
.pack()
|
|
||||||
.spanned(self.span()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Layout the padded content.
|
/// Layout the padded content.
|
||||||
#[typst_macros::time(span = elem.span())]
|
#[typst_macros::time(span = elem.span())]
|
||||||
fn layout_pad(
|
pub fn layout_pad(
|
||||||
elem: &Packed<PadElem>,
|
elem: &Packed<PadElem>,
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
locator: Locator,
|
locator: Locator,
|
||||||
@ -90,7 +26,7 @@ fn layout_pad(
|
|||||||
let pod = regions.map(&mut backlog, |size| shrink(size, &padding));
|
let pod = regions.map(&mut backlog, |size| shrink(size, &padding));
|
||||||
|
|
||||||
// Layout child into padded regions.
|
// Layout child into padded regions.
|
||||||
let mut fragment = layout_fragment(engine, &elem.body, locator, styles, pod)?;
|
let mut fragment = crate::layout_fragment(engine, &elem.body, locator, styles, pod)?;
|
||||||
|
|
||||||
for frame in &mut fragment {
|
for frame in &mut fragment {
|
||||||
grow(frame, &padding);
|
grow(frame, &padding);
|
||||||
@ -100,13 +36,13 @@ fn layout_pad(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Shrink a region size by an inset relative to the size itself.
|
/// Shrink a region size by an inset relative to the size itself.
|
||||||
pub(crate) fn shrink(size: Size, inset: &Sides<Rel<Abs>>) -> Size {
|
pub fn shrink(size: Size, inset: &Sides<Rel<Abs>>) -> Size {
|
||||||
size - inset.sum_by_axis().relative_to(size)
|
size - inset.sum_by_axis().relative_to(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shrink the components of possibly multiple `Regions` by an inset relative to
|
/// Shrink the components of possibly multiple `Regions` by an inset relative to
|
||||||
/// the regions themselves.
|
/// the regions themselves.
|
||||||
pub(crate) fn shrink_multiple(
|
pub fn shrink_multiple(
|
||||||
size: &mut Size,
|
size: &mut Size,
|
||||||
full: &mut Abs,
|
full: &mut Abs,
|
||||||
backlog: &mut [Abs],
|
backlog: &mut [Abs],
|
||||||
@ -141,7 +77,7 @@ pub(crate) fn shrink_multiple(
|
|||||||
/// <=> w - p.rel * w - p.abs = s
|
/// <=> w - p.rel * w - p.abs = s
|
||||||
/// <=> (1 - p.rel) * w = s + p.abs
|
/// <=> (1 - p.rel) * w = s + p.abs
|
||||||
/// <=> w = (s + p.abs) / (1 - p.rel)
|
/// <=> w = (s + p.abs) / (1 - p.rel)
|
||||||
pub(crate) fn grow(frame: &mut Frame, inset: &Sides<Rel<Abs>>) {
|
pub fn grow(frame: &mut Frame, inset: &Sides<Rel<Abs>>) {
|
||||||
// Apply the padding inversely such that the grown size padded
|
// Apply the padding inversely such that the grown size padded
|
||||||
// yields the frame's size.
|
// yields the frame's size.
|
||||||
let padded = frame
|
let padded = frame
|
@ -1,9 +1,9 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use crate::foundations::StyleChain;
|
use typst_library::foundations::StyleChain;
|
||||||
use crate::introspection::{Locator, SplitLocator, Tag, TagElem};
|
use typst_library::introspection::{Locator, SplitLocator, Tag, TagElem};
|
||||||
use crate::layout::{PagebreakElem, Parity};
|
use typst_library::layout::{PagebreakElem, Parity};
|
||||||
use crate::realize::Pair;
|
use typst_library::routines::Pair;
|
||||||
|
|
||||||
/// An item in page layout.
|
/// An item in page layout.
|
||||||
pub enum Item<'a> {
|
pub enum Item<'a> {
|
@ -1,8 +1,9 @@
|
|||||||
|
use typst_library::diag::SourceResult;
|
||||||
|
use typst_library::engine::Engine;
|
||||||
|
use typst_library::introspection::{ManualPageCounter, Tag};
|
||||||
|
use typst_library::layout::{Frame, FrameItem, Page, Point};
|
||||||
|
|
||||||
use super::LayoutedPage;
|
use super::LayoutedPage;
|
||||||
use crate::diag::SourceResult;
|
|
||||||
use crate::engine::Engine;
|
|
||||||
use crate::introspection::{ManualPageCounter, Tag};
|
|
||||||
use crate::layout::{Frame, FrameItem, Page, Point};
|
|
||||||
|
|
||||||
/// Piece together the inner page frame and the marginals. We can only do this
|
/// Piece together the inner page frame and the marginals. We can only do this
|
||||||
/// at the very end because inside/outside margins require knowledge of the
|
/// at the very end because inside/outside margins require knowledge of the
|
@ -5,25 +5,25 @@ mod finalize;
|
|||||||
mod run;
|
mod run;
|
||||||
|
|
||||||
use comemo::{Tracked, TrackedMut};
|
use comemo::{Tracked, TrackedMut};
|
||||||
|
use typst_library::diag::SourceResult;
|
||||||
|
use typst_library::engine::{Engine, Route, Sink, Traced};
|
||||||
|
use typst_library::foundations::{Content, StyleChain};
|
||||||
|
use typst_library::introspection::{
|
||||||
|
Introspector, Locator, ManualPageCounter, SplitLocator, TagElem,
|
||||||
|
};
|
||||||
|
use typst_library::layout::{FrameItem, Page, Point};
|
||||||
|
use typst_library::model::{Document, DocumentInfo};
|
||||||
|
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
|
||||||
|
use typst_library::World;
|
||||||
|
|
||||||
use self::collect::{collect, Item};
|
use self::collect::{collect, Item};
|
||||||
use self::finalize::finalize;
|
use self::finalize::finalize;
|
||||||
use self::run::{layout_blank_page, layout_page_run, LayoutedPage};
|
use self::run::{layout_blank_page, layout_page_run, LayoutedPage};
|
||||||
use crate::diag::SourceResult;
|
|
||||||
use crate::engine::{Engine, Route, Sink, Traced};
|
|
||||||
use crate::foundations::{Content, StyleChain};
|
|
||||||
use crate::introspection::{
|
|
||||||
Introspector, Locator, ManualPageCounter, SplitLocator, TagElem,
|
|
||||||
};
|
|
||||||
use crate::layout::{FrameItem, Page, Point};
|
|
||||||
use crate::model::{Document, DocumentInfo};
|
|
||||||
use crate::realize::{realize, Arenas, Pair, RealizationKind};
|
|
||||||
use crate::World;
|
|
||||||
|
|
||||||
/// Layout content into a document.
|
/// Layout content into a document.
|
||||||
///
|
///
|
||||||
/// This first performs root-level realization and then lays out the resulting
|
/// This first performs root-level realization and then lays out the resulting
|
||||||
/// elements. In contrast to [`layout_fragment`](crate::layout::layout_fragment),
|
/// elements. In contrast to [`layout_fragment`](crate::layout_fragment),
|
||||||
/// this does not take regions since the regions are defined by the page
|
/// this does not take regions since the regions are defined by the page
|
||||||
/// configuration in the content and style chain.
|
/// configuration in the content and style chain.
|
||||||
#[typst_macros::time(name = "document")]
|
#[typst_macros::time(name = "document")]
|
||||||
@ -33,6 +33,7 @@ pub fn layout_document(
|
|||||||
styles: StyleChain,
|
styles: StyleChain,
|
||||||
) -> SourceResult<Document> {
|
) -> SourceResult<Document> {
|
||||||
layout_document_impl(
|
layout_document_impl(
|
||||||
|
engine.routines,
|
||||||
engine.world,
|
engine.world,
|
||||||
engine.introspector,
|
engine.introspector,
|
||||||
engine.traced,
|
engine.traced,
|
||||||
@ -45,7 +46,9 @@ pub fn layout_document(
|
|||||||
|
|
||||||
/// The internal implementation of `layout_document`.
|
/// The internal implementation of `layout_document`.
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn layout_document_impl(
|
fn layout_document_impl(
|
||||||
|
routines: &Routines,
|
||||||
world: Tracked<dyn World + '_>,
|
world: Tracked<dyn World + '_>,
|
||||||
introspector: Tracked<Introspector>,
|
introspector: Tracked<Introspector>,
|
||||||
traced: Tracked<Traced>,
|
traced: Tracked<Traced>,
|
||||||
@ -56,6 +59,7 @@ fn layout_document_impl(
|
|||||||
) -> SourceResult<Document> {
|
) -> SourceResult<Document> {
|
||||||
let mut locator = Locator::root().split();
|
let mut locator = Locator::root().split();
|
||||||
let mut engine = Engine {
|
let mut engine = Engine {
|
||||||
|
routines,
|
||||||
world,
|
world,
|
||||||
introspector,
|
introspector,
|
||||||
traced,
|
traced,
|
||||||
@ -70,7 +74,7 @@ fn layout_document_impl(
|
|||||||
|
|
||||||
let arenas = Arenas::default();
|
let arenas = Arenas::default();
|
||||||
let mut info = DocumentInfo::default();
|
let mut info = DocumentInfo::default();
|
||||||
let mut children = realize(
|
let mut children = (engine.routines.realize)(
|
||||||
RealizationKind::Root(&mut info),
|
RealizationKind::Root(&mut info),
|
||||||
&mut engine,
|
&mut engine,
|
||||||
&mut locator,
|
&mut locator,
|
@ -1,22 +1,25 @@
|
|||||||
use comemo::{Track, Tracked, TrackedMut};
|
use comemo::{Track, Tracked, TrackedMut};
|
||||||
|
use typst_library::diag::SourceResult;
|
||||||
use crate::diag::SourceResult;
|
use typst_library::engine::{Engine, Route, Sink, Traced};
|
||||||
use crate::engine::{Engine, Route, Sink, Traced};
|
use typst_library::foundations::{
|
||||||
use crate::foundations::{Content, NativeElement, Resolve, Smart, StyleChain, Styles};
|
Content, NativeElement, Resolve, Smart, StyleChain, Styles,
|
||||||
use crate::introspection::{
|
};
|
||||||
|
use typst_library::introspection::{
|
||||||
Counter, CounterDisplayElem, CounterKey, Introspector, Locator, LocatorLink, TagElem,
|
Counter, CounterDisplayElem, CounterKey, Introspector, Locator, LocatorLink, TagElem,
|
||||||
};
|
};
|
||||||
use crate::layout::{
|
use typst_library::layout::{
|
||||||
layout_flow, layout_frame, Abs, AlignElem, Alignment, Axes, Binding, ColumnsElem,
|
Abs, AlignElem, Alignment, Axes, Binding, ColumnsElem, Dir, Frame, HAlignment,
|
||||||
Dir, Frame, HAlignment, Length, OuterVAlignment, PageElem, Paper, Region, Regions,
|
Length, OuterVAlignment, PageElem, Paper, Region, Regions, Rel, Sides, Size,
|
||||||
Rel, Sides, Size, VAlignment,
|
VAlignment,
|
||||||
};
|
};
|
||||||
use crate::model::Numbering;
|
use typst_library::model::Numbering;
|
||||||
use crate::realize::Pair;
|
use typst_library::routines::{Pair, Routines};
|
||||||
use crate::text::TextElem;
|
use typst_library::text::TextElem;
|
||||||
use crate::utils::Numeric;
|
use typst_library::visualize::Paint;
|
||||||
use crate::visualize::Paint;
|
use typst_library::World;
|
||||||
use crate::World;
|
use typst_utils::Numeric;
|
||||||
|
|
||||||
|
use crate::flow::layout_flow;
|
||||||
|
|
||||||
/// A mostly finished layout for one page. Needs only knowledge of its exact
|
/// A mostly finished layout for one page. Needs only knowledge of its exact
|
||||||
/// page number to be finalized into a `Page`. (Because the margins can depend
|
/// page number to be finalized into a `Page`. (Because the margins can depend
|
||||||
@ -54,6 +57,7 @@ pub fn layout_page_run(
|
|||||||
initial: StyleChain,
|
initial: StyleChain,
|
||||||
) -> SourceResult<Vec<LayoutedPage>> {
|
) -> SourceResult<Vec<LayoutedPage>> {
|
||||||
layout_page_run_impl(
|
layout_page_run_impl(
|
||||||
|
engine.routines,
|
||||||
engine.world,
|
engine.world,
|
||||||
engine.introspector,
|
engine.introspector,
|
||||||
engine.traced,
|
engine.traced,
|
||||||
@ -69,6 +73,7 @@ pub fn layout_page_run(
|
|||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn layout_page_run_impl(
|
fn layout_page_run_impl(
|
||||||
|
routines: &Routines,
|
||||||
world: Tracked<dyn World + '_>,
|
world: Tracked<dyn World + '_>,
|
||||||
introspector: Tracked<Introspector>,
|
introspector: Tracked<Introspector>,
|
||||||
traced: Tracked<Traced>,
|
traced: Tracked<Traced>,
|
||||||
@ -81,6 +86,7 @@ fn layout_page_run_impl(
|
|||||||
let link = LocatorLink::new(locator);
|
let link = LocatorLink::new(locator);
|
||||||
let mut locator = Locator::link(&link).split();
|
let mut locator = Locator::link(&link).split();
|
||||||
let mut engine = Engine {
|
let mut engine = Engine {
|
||||||
|
routines,
|
||||||
world,
|
world,
|
||||||
introspector,
|
introspector,
|
||||||
traced,
|
traced,
|
||||||
@ -177,7 +183,7 @@ fn layout_page_run_impl(
|
|||||||
let mut layout_marginal = |content: &Option<Content>, area, align| {
|
let mut layout_marginal = |content: &Option<Content>, area, align| {
|
||||||
let Some(content) = content else { return Ok(None) };
|
let Some(content) = content else { return Ok(None) };
|
||||||
let aligned = content.clone().styled(AlignElem::set_alignment(align));
|
let aligned = content.clone().styled(AlignElem::set_alignment(align));
|
||||||
layout_frame(
|
crate::layout_frame(
|
||||||
&mut engine,
|
&mut engine,
|
||||||
&aligned,
|
&aligned,
|
||||||
locator.next(&content.span()),
|
locator.next(&content.span()),
|
60
crates/typst-layout/src/repeat.rs
Normal file
60
crates/typst-layout/src/repeat.rs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
use typst_library::diag::{bail, SourceResult};
|
||||||
|
use typst_library::engine::Engine;
|
||||||
|
use typst_library::foundations::{Packed, Resolve, StyleChain};
|
||||||
|
use typst_library::introspection::Locator;
|
||||||
|
use typst_library::layout::{
|
||||||
|
Abs, AlignElem, Axes, Frame, Point, Region, RepeatElem, Size,
|
||||||
|
};
|
||||||
|
use typst_utils::Numeric;
|
||||||
|
|
||||||
|
/// Layout the repeated content.
|
||||||
|
#[typst_macros::time(span = elem.span())]
|
||||||
|
pub fn layout_repeat(
|
||||||
|
elem: &Packed<RepeatElem>,
|
||||||
|
engine: &mut Engine,
|
||||||
|
locator: Locator,
|
||||||
|
styles: StyleChain,
|
||||||
|
region: Region,
|
||||||
|
) -> SourceResult<Frame> {
|
||||||
|
let pod = Region::new(region.size, Axes::new(false, false));
|
||||||
|
let piece = crate::layout_frame(engine, &elem.body, locator, styles, pod)?;
|
||||||
|
let size = Size::new(region.size.x, piece.height());
|
||||||
|
|
||||||
|
if !size.is_finite() {
|
||||||
|
bail!(elem.span(), "repeat with no size restrictions");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut frame = Frame::soft(size);
|
||||||
|
if piece.has_baseline() {
|
||||||
|
frame.set_baseline(piece.baseline());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut gap = elem.gap(styles).resolve(styles);
|
||||||
|
let fill = region.size.x;
|
||||||
|
let width = piece.width();
|
||||||
|
|
||||||
|
// count * width + (count - 1) * gap = fill, but count is an integer so
|
||||||
|
// we need to round down and get the remainder.
|
||||||
|
let count = ((fill + gap) / (width + gap)).floor();
|
||||||
|
let remaining = (fill + gap) % (width + gap);
|
||||||
|
|
||||||
|
let justify = elem.justify(styles);
|
||||||
|
if justify {
|
||||||
|
gap += remaining / (count - 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let align = AlignElem::alignment_in(styles).resolve(styles);
|
||||||
|
let mut offset = Abs::zero();
|
||||||
|
if count == 1.0 || !justify {
|
||||||
|
offset += align.x.position(remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
if width > Abs::zero() {
|
||||||
|
for _ in 0..(count as usize).min(1000) {
|
||||||
|
frame.push_frame(Point::with_x(offset), piece.clone());
|
||||||
|
offset += piece.width() + gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(frame)
|
||||||
|
}
|
@ -1,145 +1,202 @@
|
|||||||
use std::f64::consts::SQRT_2;
|
use std::f64::consts::SQRT_2;
|
||||||
|
|
||||||
use crate::diag::SourceResult;
|
use kurbo::ParamCurveExtrema;
|
||||||
use crate::engine::Engine;
|
use typst_library::diag::{bail, SourceResult};
|
||||||
use crate::foundations::{
|
use typst_library::engine::Engine;
|
||||||
elem, Cast, Content, NativeElement, Packed, Show, Smart, StyleChain,
|
use typst_library::foundations::{Content, Packed, Resolve, Smart, StyleChain};
|
||||||
|
use typst_library::introspection::Locator;
|
||||||
|
use typst_library::layout::{
|
||||||
|
Abs, Axes, Corner, Corners, Frame, FrameItem, Length, Point, Ratio, Region, Rel,
|
||||||
|
Sides, Size,
|
||||||
};
|
};
|
||||||
use crate::introspection::Locator;
|
use typst_library::visualize::{
|
||||||
use crate::layout::{
|
CircleElem, EllipseElem, FillRule, FixedStroke, Geometry, LineElem, Paint, Path,
|
||||||
layout_frame, Abs, Axes, BlockElem, Corner, Corners, Frame, FrameItem, Length, Point,
|
PathElem, PathVertex, PolygonElem, RectElem, Shape, SquareElem, Stroke,
|
||||||
Ratio, Region, Rel, Sides, Size, Sizing,
|
|
||||||
};
|
};
|
||||||
use crate::syntax::Span;
|
use typst_syntax::Span;
|
||||||
use crate::utils::Get;
|
use typst_utils::{Get, Numeric};
|
||||||
use crate::visualize::{FixedStroke, Paint, Path, Stroke};
|
|
||||||
|
|
||||||
/// A rectangle with optional content.
|
/// Layout the line.
|
||||||
///
|
#[typst_macros::time(span = elem.span())]
|
||||||
/// # Example
|
pub fn layout_line(
|
||||||
/// ```example
|
elem: &Packed<LineElem>,
|
||||||
/// // Without content.
|
_: &mut Engine,
|
||||||
/// #rect(width: 35%, height: 30pt)
|
_: Locator,
|
||||||
///
|
styles: StyleChain,
|
||||||
/// // With content.
|
region: Region,
|
||||||
/// #rect[
|
) -> SourceResult<Frame> {
|
||||||
/// Automatically sized \
|
let resolve = |axes: Axes<Rel<Abs>>| axes.zip_map(region.size, Rel::relative_to);
|
||||||
/// to fit the content.
|
let start = resolve(elem.start(styles));
|
||||||
/// ]
|
let delta = elem.end(styles).map(|end| resolve(end) - start).unwrap_or_else(|| {
|
||||||
/// ```
|
let length = elem.length(styles);
|
||||||
#[elem(title = "Rectangle", Show)]
|
let angle = elem.angle(styles);
|
||||||
pub struct RectElem {
|
let x = angle.cos() * length;
|
||||||
/// The rectangle's width, relative to its parent container.
|
let y = angle.sin() * length;
|
||||||
pub width: Smart<Rel<Length>>,
|
resolve(Axes::new(x, y))
|
||||||
|
});
|
||||||
|
|
||||||
/// The rectangle's height, relative to its parent container.
|
let stroke = elem.stroke(styles).unwrap_or_default();
|
||||||
pub height: Sizing,
|
let size = start.max(start + delta).max(Size::zero());
|
||||||
|
|
||||||
/// How to fill the rectangle.
|
if !size.is_finite() {
|
||||||
///
|
bail!(elem.span(), "cannot create line with infinite length");
|
||||||
/// When setting a fill, the default stroke disappears. To create a
|
}
|
||||||
/// rectangle with both fill and stroke, you have to configure both.
|
|
||||||
///
|
|
||||||
/// ```example
|
|
||||||
/// #rect(fill: blue)
|
|
||||||
/// ```
|
|
||||||
pub fill: Option<Paint>,
|
|
||||||
|
|
||||||
/// How to stroke the rectangle. This can be:
|
let mut frame = Frame::soft(size);
|
||||||
///
|
let shape = Geometry::Line(delta.to_point()).stroked(stroke);
|
||||||
/// - `{none}` to disable stroking
|
frame.push(start.to_point(), FrameItem::Shape(shape, elem.span()));
|
||||||
/// - `{auto}` for a stroke of `{1pt + black}` if and if only if no fill is
|
Ok(frame)
|
||||||
/// given.
|
|
||||||
/// - Any kind of [stroke]
|
|
||||||
/// - A dictionary describing the stroke for each side individually. The
|
|
||||||
/// dictionary can contain the following keys in order of precedence:
|
|
||||||
/// - `top`: The top stroke.
|
|
||||||
/// - `right`: The right stroke.
|
|
||||||
/// - `bottom`: The bottom stroke.
|
|
||||||
/// - `left`: The left stroke.
|
|
||||||
/// - `x`: The horizontal stroke.
|
|
||||||
/// - `y`: The vertical stroke.
|
|
||||||
/// - `rest`: The stroke on all sides except those for which the
|
|
||||||
/// dictionary explicitly sets a size.
|
|
||||||
///
|
|
||||||
/// ```example
|
|
||||||
/// #stack(
|
|
||||||
/// dir: ltr,
|
|
||||||
/// spacing: 1fr,
|
|
||||||
/// rect(stroke: red),
|
|
||||||
/// rect(stroke: 2pt),
|
|
||||||
/// rect(stroke: 2pt + red),
|
|
||||||
/// )
|
|
||||||
/// ```
|
|
||||||
#[resolve]
|
|
||||||
#[fold]
|
|
||||||
pub stroke: Smart<Sides<Option<Option<Stroke>>>>,
|
|
||||||
|
|
||||||
/// How much to round the rectangle's corners, relative to the minimum of
|
|
||||||
/// the width and height divided by two. This can be:
|
|
||||||
///
|
|
||||||
/// - A relative length for a uniform corner radius.
|
|
||||||
/// - A dictionary: With a dictionary, the stroke for each side can be set
|
|
||||||
/// individually. The dictionary can contain the following keys in order
|
|
||||||
/// of precedence:
|
|
||||||
/// - `top-left`: The top-left corner radius.
|
|
||||||
/// - `top-right`: The top-right corner radius.
|
|
||||||
/// - `bottom-right`: The bottom-right corner radius.
|
|
||||||
/// - `bottom-left`: The bottom-left corner radius.
|
|
||||||
/// - `left`: The top-left and bottom-left corner radii.
|
|
||||||
/// - `top`: The top-left and top-right corner radii.
|
|
||||||
/// - `right`: The top-right and bottom-right corner radii.
|
|
||||||
/// - `bottom`: The bottom-left and bottom-right corner radii.
|
|
||||||
/// - `rest`: The radii for all corners except those for which the
|
|
||||||
/// dictionary explicitly sets a size.
|
|
||||||
///
|
|
||||||
/// ```example
|
|
||||||
/// #set rect(stroke: 4pt)
|
|
||||||
/// #rect(
|
|
||||||
/// radius: (
|
|
||||||
/// left: 5pt,
|
|
||||||
/// top-right: 20pt,
|
|
||||||
/// bottom-right: 10pt,
|
|
||||||
/// ),
|
|
||||||
/// stroke: (
|
|
||||||
/// left: red,
|
|
||||||
/// top: yellow,
|
|
||||||
/// right: green,
|
|
||||||
/// bottom: blue,
|
|
||||||
/// ),
|
|
||||||
/// )
|
|
||||||
/// ```
|
|
||||||
#[resolve]
|
|
||||||
#[fold]
|
|
||||||
pub radius: Corners<Option<Rel<Length>>>,
|
|
||||||
|
|
||||||
/// How much to pad the rectangle's content.
|
|
||||||
/// See the [box's documentation]($box.outset) for more details.
|
|
||||||
#[resolve]
|
|
||||||
#[fold]
|
|
||||||
#[default(Sides::splat(Some(Abs::pt(5.0).into())))]
|
|
||||||
pub inset: Sides<Option<Rel<Length>>>,
|
|
||||||
|
|
||||||
/// How much to expand the rectangle's size without affecting the layout.
|
|
||||||
/// See the [box's documentation]($box.outset) for more details.
|
|
||||||
#[resolve]
|
|
||||||
#[fold]
|
|
||||||
pub outset: Sides<Option<Rel<Length>>>,
|
|
||||||
|
|
||||||
/// The content to place into the rectangle.
|
|
||||||
///
|
|
||||||
/// When this is omitted, the rectangle takes on a default size of at most
|
|
||||||
/// `{45pt}` by `{30pt}`.
|
|
||||||
#[positional]
|
|
||||||
#[borrowed]
|
|
||||||
pub body: Option<Content>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Show for Packed<RectElem> {
|
/// Layout the path.
|
||||||
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
#[typst_macros::time(span = elem.span())]
|
||||||
Ok(BlockElem::single_layouter(
|
pub fn layout_path(
|
||||||
self.clone(),
|
elem: &Packed<PathElem>,
|
||||||
|elem, engine, locator, styles, region| {
|
_: &mut Engine,
|
||||||
|
_: Locator,
|
||||||
|
styles: StyleChain,
|
||||||
|
region: Region,
|
||||||
|
) -> SourceResult<Frame> {
|
||||||
|
let resolve = |axes: Axes<Rel<Length>>| {
|
||||||
|
axes.resolve(styles).zip_map(region.size, Rel::relative_to).to_point()
|
||||||
|
};
|
||||||
|
|
||||||
|
let vertices = elem.vertices();
|
||||||
|
let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect();
|
||||||
|
|
||||||
|
let mut size = Size::zero();
|
||||||
|
if points.is_empty() {
|
||||||
|
return Ok(Frame::soft(size));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only create a path if there are more than zero points.
|
||||||
|
// Construct a closed path given all points.
|
||||||
|
let mut path = Path::new();
|
||||||
|
path.move_to(points[0]);
|
||||||
|
|
||||||
|
let mut add_cubic = |from_point: Point,
|
||||||
|
to_point: Point,
|
||||||
|
from: PathVertex,
|
||||||
|
to: PathVertex| {
|
||||||
|
let from_control_point = resolve(from.control_point_from()) + from_point;
|
||||||
|
let to_control_point = resolve(to.control_point_to()) + to_point;
|
||||||
|
path.cubic_to(from_control_point, to_control_point, to_point);
|
||||||
|
|
||||||
|
let p0 = kurbo::Point::new(from_point.x.to_raw(), from_point.y.to_raw());
|
||||||
|
let p1 = kurbo::Point::new(
|
||||||
|
from_control_point.x.to_raw(),
|
||||||
|
from_control_point.y.to_raw(),
|
||||||
|
);
|
||||||
|
let p2 =
|
||||||
|
kurbo::Point::new(to_control_point.x.to_raw(), to_control_point.y.to_raw());
|
||||||
|
let p3 = kurbo::Point::new(to_point.x.to_raw(), to_point.y.to_raw());
|
||||||
|
let extrema = kurbo::CubicBez::new(p0, p1, p2, p3).bounding_box();
|
||||||
|
size.x.set_max(Abs::raw(extrema.x1));
|
||||||
|
size.y.set_max(Abs::raw(extrema.y1));
|
||||||
|
};
|
||||||
|
|
||||||
|
for (vertex_window, point_window) in vertices.windows(2).zip(points.windows(2)) {
|
||||||
|
let from = vertex_window[0];
|
||||||
|
let to = vertex_window[1];
|
||||||
|
let from_point = point_window[0];
|
||||||
|
let to_point = point_window[1];
|
||||||
|
|
||||||
|
add_cubic(from_point, to_point, from, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
if elem.closed(styles) {
|
||||||
|
let from = *vertices.last().unwrap(); // We checked that we have at least one element.
|
||||||
|
let to = vertices[0];
|
||||||
|
let from_point = *points.last().unwrap();
|
||||||
|
let to_point = points[0];
|
||||||
|
|
||||||
|
add_cubic(from_point, to_point, from, to);
|
||||||
|
path.close_path();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare fill and stroke.
|
||||||
|
let fill = elem.fill(styles);
|
||||||
|
let fill_rule = elem.fill_rule(styles);
|
||||||
|
let stroke = match elem.stroke(styles) {
|
||||||
|
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
|
||||||
|
Smart::Auto => None,
|
||||||
|
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut frame = Frame::soft(size);
|
||||||
|
let shape = Shape {
|
||||||
|
geometry: Geometry::Path(path),
|
||||||
|
stroke,
|
||||||
|
fill,
|
||||||
|
fill_rule,
|
||||||
|
};
|
||||||
|
frame.push(Point::zero(), FrameItem::Shape(shape, elem.span()));
|
||||||
|
Ok(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout the polygon.
|
||||||
|
#[typst_macros::time(span = elem.span())]
|
||||||
|
pub fn layout_polygon(
|
||||||
|
elem: &Packed<PolygonElem>,
|
||||||
|
_: &mut Engine,
|
||||||
|
_: Locator,
|
||||||
|
styles: StyleChain,
|
||||||
|
region: Region,
|
||||||
|
) -> SourceResult<Frame> {
|
||||||
|
let points: Vec<Point> = elem
|
||||||
|
.vertices()
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.resolve(styles).zip_map(region.size, Rel::relative_to).to_point())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let size = points.iter().fold(Point::zero(), |max, c| c.max(max)).to_size();
|
||||||
|
if !size.is_finite() {
|
||||||
|
bail!(elem.span(), "cannot create polygon with infinite size");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut frame = Frame::hard(size);
|
||||||
|
|
||||||
|
// Only create a path if there are more than zero points.
|
||||||
|
if points.is_empty() {
|
||||||
|
return Ok(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare fill and stroke.
|
||||||
|
let fill = elem.fill(styles);
|
||||||
|
let fill_rule = elem.fill_rule(styles);
|
||||||
|
let stroke = match elem.stroke(styles) {
|
||||||
|
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
|
||||||
|
Smart::Auto => None,
|
||||||
|
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Construct a closed path given all points.
|
||||||
|
let mut path = Path::new();
|
||||||
|
path.move_to(points[0]);
|
||||||
|
for &point in &points[1..] {
|
||||||
|
path.line_to(point);
|
||||||
|
}
|
||||||
|
path.close_path();
|
||||||
|
|
||||||
|
let shape = Shape {
|
||||||
|
geometry: Geometry::Path(path),
|
||||||
|
stroke,
|
||||||
|
fill,
|
||||||
|
fill_rule,
|
||||||
|
};
|
||||||
|
frame.push(Point::zero(), FrameItem::Shape(shape, elem.span()));
|
||||||
|
Ok(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lay out the rectangle.
|
||||||
|
#[typst_macros::time(span = elem.span())]
|
||||||
|
pub fn layout_rect(
|
||||||
|
elem: &Packed<RectElem>,
|
||||||
|
engine: &mut Engine,
|
||||||
|
locator: Locator,
|
||||||
|
styles: StyleChain,
|
||||||
|
region: Region,
|
||||||
|
) -> SourceResult<Frame> {
|
||||||
layout_shape(
|
layout_shape(
|
||||||
engine,
|
engine,
|
||||||
locator,
|
locator,
|
||||||
@ -154,107 +211,22 @@ impl Show for Packed<RectElem> {
|
|||||||
elem.radius(styles),
|
elem.radius(styles),
|
||||||
elem.span(),
|
elem.span(),
|
||||||
)
|
)
|
||||||
},
|
|
||||||
)
|
|
||||||
.with_width(self.width(styles))
|
|
||||||
.with_height(self.height(styles))
|
|
||||||
.pack()
|
|
||||||
.spanned(self.span()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A square with optional content.
|
/// Lay out the square.
|
||||||
///
|
#[typst_macros::time(span = elem.span())]
|
||||||
/// # Example
|
pub fn layout_square(
|
||||||
/// ```example
|
elem: &Packed<SquareElem>,
|
||||||
/// // Without content.
|
engine: &mut Engine,
|
||||||
/// #square(size: 40pt)
|
locator: Locator,
|
||||||
///
|
styles: StyleChain,
|
||||||
/// // With content.
|
region: Region,
|
||||||
/// #square[
|
) -> SourceResult<Frame> {
|
||||||
/// Automatically \
|
|
||||||
/// sized to fit.
|
|
||||||
/// ]
|
|
||||||
/// ```
|
|
||||||
#[elem(Show)]
|
|
||||||
pub struct SquareElem {
|
|
||||||
/// The square's side length. This is mutually exclusive with `width` and
|
|
||||||
/// `height`.
|
|
||||||
#[external]
|
|
||||||
pub size: Smart<Length>,
|
|
||||||
|
|
||||||
/// The square's width. This is mutually exclusive with `size` and `height`.
|
|
||||||
///
|
|
||||||
/// In contrast to `size`, this can be relative to the parent container's
|
|
||||||
/// width.
|
|
||||||
#[parse(
|
|
||||||
let size = args.named::<Smart<Length>>("size")?.map(|s| s.map(Rel::from));
|
|
||||||
match size {
|
|
||||||
None => args.named("width")?,
|
|
||||||
size => size,
|
|
||||||
}
|
|
||||||
)]
|
|
||||||
pub width: Smart<Rel<Length>>,
|
|
||||||
|
|
||||||
/// The square's height. This is mutually exclusive with `size` and `width`.
|
|
||||||
///
|
|
||||||
/// In contrast to `size`, this can be relative to the parent container's
|
|
||||||
/// height.
|
|
||||||
#[parse(match size {
|
|
||||||
None => args.named("height")?,
|
|
||||||
size => size.map(Into::into),
|
|
||||||
})]
|
|
||||||
pub height: Sizing,
|
|
||||||
|
|
||||||
/// How to fill the square. See the [rectangle's documentation]($rect.fill)
|
|
||||||
/// for more details.
|
|
||||||
pub fill: Option<Paint>,
|
|
||||||
|
|
||||||
/// How to stroke the square. See the
|
|
||||||
/// [rectangle's documentation]($rect.stroke) for more details.
|
|
||||||
#[resolve]
|
|
||||||
#[fold]
|
|
||||||
pub stroke: Smart<Sides<Option<Option<Stroke>>>>,
|
|
||||||
|
|
||||||
/// How much to round the square's corners. See the
|
|
||||||
/// [rectangle's documentation]($rect.radius) for more details.
|
|
||||||
#[resolve]
|
|
||||||
#[fold]
|
|
||||||
pub radius: Corners<Option<Rel<Length>>>,
|
|
||||||
|
|
||||||
/// How much to pad the square's content. See the
|
|
||||||
/// [box's documentation]($box.inset) for more details.
|
|
||||||
#[resolve]
|
|
||||||
#[fold]
|
|
||||||
#[default(Sides::splat(Some(Abs::pt(5.0).into())))]
|
|
||||||
pub inset: Sides<Option<Rel<Length>>>,
|
|
||||||
|
|
||||||
/// How much to expand the square's size without affecting the layout. See
|
|
||||||
/// the [box's documentation]($box.outset) for more details.
|
|
||||||
#[resolve]
|
|
||||||
#[fold]
|
|
||||||
pub outset: Sides<Option<Rel<Length>>>,
|
|
||||||
|
|
||||||
/// The content to place into the square. The square expands to fit this
|
|
||||||
/// content, keeping the 1-1 aspect ratio.
|
|
||||||
///
|
|
||||||
/// When this is omitted, the square takes on a default size of at most
|
|
||||||
/// `{30pt}`.
|
|
||||||
#[positional]
|
|
||||||
#[borrowed]
|
|
||||||
pub body: Option<Content>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Show for Packed<SquareElem> {
|
|
||||||
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
|
||||||
Ok(BlockElem::single_layouter(
|
|
||||||
self.clone(),
|
|
||||||
|elem, engine, locator, styles, regions| {
|
|
||||||
layout_shape(
|
layout_shape(
|
||||||
engine,
|
engine,
|
||||||
locator,
|
locator,
|
||||||
styles,
|
styles,
|
||||||
regions,
|
region,
|
||||||
ShapeKind::Square,
|
ShapeKind::Square,
|
||||||
elem.body(styles),
|
elem.body(styles),
|
||||||
elem.fill(styles),
|
elem.fill(styles),
|
||||||
@ -264,79 +236,22 @@ impl Show for Packed<SquareElem> {
|
|||||||
elem.radius(styles),
|
elem.radius(styles),
|
||||||
elem.span(),
|
elem.span(),
|
||||||
)
|
)
|
||||||
},
|
|
||||||
)
|
|
||||||
.with_width(self.width(styles))
|
|
||||||
.with_height(self.height(styles))
|
|
||||||
.pack()
|
|
||||||
.spanned(self.span()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An ellipse with optional content.
|
/// Lay out the ellipse.
|
||||||
///
|
#[typst_macros::time(span = elem.span())]
|
||||||
/// # Example
|
pub fn layout_ellipse(
|
||||||
/// ```example
|
elem: &Packed<EllipseElem>,
|
||||||
/// // Without content.
|
engine: &mut Engine,
|
||||||
/// #ellipse(width: 35%, height: 30pt)
|
locator: Locator,
|
||||||
///
|
styles: StyleChain,
|
||||||
/// // With content.
|
region: Region,
|
||||||
/// #ellipse[
|
) -> SourceResult<Frame> {
|
||||||
/// #set align(center)
|
|
||||||
/// Automatically sized \
|
|
||||||
/// to fit the content.
|
|
||||||
/// ]
|
|
||||||
/// ```
|
|
||||||
#[elem(Show)]
|
|
||||||
pub struct EllipseElem {
|
|
||||||
/// The ellipse's width, relative to its parent container.
|
|
||||||
pub width: Smart<Rel<Length>>,
|
|
||||||
|
|
||||||
/// The ellipse's height, relative to its parent container.
|
|
||||||
pub height: Sizing,
|
|
||||||
|
|
||||||
/// How to fill the ellipse. See the [rectangle's documentation]($rect.fill)
|
|
||||||
/// for more details.
|
|
||||||
pub fill: Option<Paint>,
|
|
||||||
|
|
||||||
/// How to stroke the ellipse. See the
|
|
||||||
/// [rectangle's documentation]($rect.stroke) for more details.
|
|
||||||
#[resolve]
|
|
||||||
#[fold]
|
|
||||||
pub stroke: Smart<Option<Stroke>>,
|
|
||||||
|
|
||||||
/// How much to pad the ellipse's content. See the
|
|
||||||
/// [box's documentation]($box.inset) for more details.
|
|
||||||
#[resolve]
|
|
||||||
#[fold]
|
|
||||||
#[default(Sides::splat(Some(Abs::pt(5.0).into())))]
|
|
||||||
pub inset: Sides<Option<Rel<Length>>>,
|
|
||||||
|
|
||||||
/// How much to expand the ellipse's size without affecting the layout. See
|
|
||||||
/// the [box's documentation]($box.outset) for more details.
|
|
||||||
#[resolve]
|
|
||||||
#[fold]
|
|
||||||
pub outset: Sides<Option<Rel<Length>>>,
|
|
||||||
|
|
||||||
/// The content to place into the ellipse.
|
|
||||||
///
|
|
||||||
/// When this is omitted, the ellipse takes on a default size of at most
|
|
||||||
/// `{45pt}` by `{30pt}`.
|
|
||||||
#[positional]
|
|
||||||
#[borrowed]
|
|
||||||
pub body: Option<Content>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Show for Packed<EllipseElem> {
|
|
||||||
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
|
||||||
Ok(BlockElem::single_layouter(
|
|
||||||
self.clone(),
|
|
||||||
|elem, engine, locator, styles, regions| {
|
|
||||||
layout_shape(
|
layout_shape(
|
||||||
engine,
|
engine,
|
||||||
locator,
|
locator,
|
||||||
styles,
|
styles,
|
||||||
regions,
|
region,
|
||||||
ShapeKind::Ellipse,
|
ShapeKind::Ellipse,
|
||||||
elem.body(styles),
|
elem.body(styles),
|
||||||
elem.fill(styles),
|
elem.fill(styles),
|
||||||
@ -346,104 +261,22 @@ impl Show for Packed<EllipseElem> {
|
|||||||
Corners::splat(None),
|
Corners::splat(None),
|
||||||
elem.span(),
|
elem.span(),
|
||||||
)
|
)
|
||||||
},
|
|
||||||
)
|
|
||||||
.with_width(self.width(styles))
|
|
||||||
.with_height(self.height(styles))
|
|
||||||
.pack()
|
|
||||||
.spanned(self.span()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A circle with optional content.
|
/// Lay out the circle.
|
||||||
///
|
#[typst_macros::time(span = elem.span())]
|
||||||
/// # Example
|
pub fn layout_circle(
|
||||||
/// ```example
|
elem: &Packed<CircleElem>,
|
||||||
/// // Without content.
|
engine: &mut Engine,
|
||||||
/// #circle(radius: 25pt)
|
locator: Locator,
|
||||||
///
|
styles: StyleChain,
|
||||||
/// // With content.
|
region: Region,
|
||||||
/// #circle[
|
) -> SourceResult<Frame> {
|
||||||
/// #set align(center + horizon)
|
|
||||||
/// Automatically \
|
|
||||||
/// sized to fit.
|
|
||||||
/// ]
|
|
||||||
/// ```
|
|
||||||
#[elem(Show)]
|
|
||||||
pub struct CircleElem {
|
|
||||||
/// The circle's radius. This is mutually exclusive with `width` and
|
|
||||||
/// `height`.
|
|
||||||
#[external]
|
|
||||||
pub radius: Length,
|
|
||||||
|
|
||||||
/// The circle's width. This is mutually exclusive with `radius` and
|
|
||||||
/// `height`.
|
|
||||||
///
|
|
||||||
/// In contrast to `radius`, this can be relative to the parent container's
|
|
||||||
/// width.
|
|
||||||
#[parse(
|
|
||||||
let size = args
|
|
||||||
.named::<Smart<Length>>("radius")?
|
|
||||||
.map(|s| s.map(|r| 2.0 * Rel::from(r)));
|
|
||||||
match size {
|
|
||||||
None => args.named("width")?,
|
|
||||||
size => size,
|
|
||||||
}
|
|
||||||
)]
|
|
||||||
pub width: Smart<Rel<Length>>,
|
|
||||||
|
|
||||||
/// The circle's height. This is mutually exclusive with `radius` and
|
|
||||||
/// `width`.
|
|
||||||
///
|
|
||||||
/// In contrast to `radius`, this can be relative to the parent container's
|
|
||||||
/// height.
|
|
||||||
#[parse(match size {
|
|
||||||
None => args.named("height")?,
|
|
||||||
size => size.map(Into::into),
|
|
||||||
})]
|
|
||||||
pub height: Sizing,
|
|
||||||
|
|
||||||
/// How to fill the circle. See the [rectangle's documentation]($rect.fill)
|
|
||||||
/// for more details.
|
|
||||||
pub fill: Option<Paint>,
|
|
||||||
|
|
||||||
/// How to stroke the circle. See the
|
|
||||||
/// [rectangle's documentation]($rect.stroke) for more details.
|
|
||||||
#[resolve]
|
|
||||||
#[fold]
|
|
||||||
#[default(Smart::Auto)]
|
|
||||||
pub stroke: Smart<Option<Stroke>>,
|
|
||||||
|
|
||||||
/// How much to pad the circle's content. See the
|
|
||||||
/// [box's documentation]($box.inset) for more details.
|
|
||||||
#[resolve]
|
|
||||||
#[fold]
|
|
||||||
#[default(Sides::splat(Some(Abs::pt(5.0).into())))]
|
|
||||||
pub inset: Sides<Option<Rel<Length>>>,
|
|
||||||
|
|
||||||
/// How much to expand the circle's size without affecting the layout. See
|
|
||||||
/// the [box's documentation]($box.outset) for more details.
|
|
||||||
#[resolve]
|
|
||||||
#[fold]
|
|
||||||
pub outset: Sides<Option<Rel<Length>>>,
|
|
||||||
|
|
||||||
/// The content to place into the circle. The circle expands to fit this
|
|
||||||
/// content, keeping the 1-1 aspect ratio.
|
|
||||||
#[positional]
|
|
||||||
#[borrowed]
|
|
||||||
pub body: Option<Content>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Show for Packed<CircleElem> {
|
|
||||||
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
|
||||||
Ok(BlockElem::single_layouter(
|
|
||||||
self.clone(),
|
|
||||||
|elem, engine, locator, styles, regions| {
|
|
||||||
layout_shape(
|
layout_shape(
|
||||||
engine,
|
engine,
|
||||||
locator,
|
locator,
|
||||||
styles,
|
styles,
|
||||||
regions,
|
region,
|
||||||
ShapeKind::Circle,
|
ShapeKind::Circle,
|
||||||
elem.body(styles),
|
elem.body(styles),
|
||||||
elem.fill(styles),
|
elem.fill(styles),
|
||||||
@ -453,17 +286,34 @@ impl Show for Packed<CircleElem> {
|
|||||||
Corners::splat(None),
|
Corners::splat(None),
|
||||||
elem.span(),
|
elem.span(),
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
)
|
|
||||||
.with_width(self.width(styles))
|
/// A category of shape.
|
||||||
.with_height(self.height(styles))
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
.pack()
|
enum ShapeKind {
|
||||||
.spanned(self.span()))
|
/// A rectangle with equal side lengths.
|
||||||
|
Square,
|
||||||
|
/// A quadrilateral with four right angles.
|
||||||
|
Rect,
|
||||||
|
/// An ellipse with coinciding foci.
|
||||||
|
Circle,
|
||||||
|
/// A curve around two focal points.
|
||||||
|
Ellipse,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShapeKind {
|
||||||
|
/// Whether this shape kind is curvy.
|
||||||
|
fn is_round(self) -> bool {
|
||||||
|
matches!(self, Self::Circle | Self::Ellipse)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this shape kind has equal side length.
|
||||||
|
fn is_quadratic(self) -> bool {
|
||||||
|
matches!(self, Self::Square | Self::Circle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Layout a shape.
|
/// Layout a shape.
|
||||||
#[typst_macros::time(span = span)]
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn layout_shape(
|
fn layout_shape(
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
@ -491,23 +341,23 @@ fn layout_shape(
|
|||||||
// Take the inset, if any, into account.
|
// Take the inset, if any, into account.
|
||||||
let mut pod = region;
|
let mut pod = region;
|
||||||
if has_inset {
|
if has_inset {
|
||||||
pod.size = crate::layout::shrink(region.size, &inset);
|
pod.size = crate::pad::shrink(region.size, &inset);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layout the child.
|
// Layout the child.
|
||||||
frame = layout_frame(engine, child, locator.relayout(), styles, pod)?;
|
frame = crate::layout_frame(engine, child, locator.relayout(), styles, pod)?;
|
||||||
|
|
||||||
// If the child is a square or circle, relayout with full expansion into
|
// If the child is a square or circle, relayout with full expansion into
|
||||||
// square region to make sure the result is really quadratic.
|
// square region to make sure the result is really quadratic.
|
||||||
if kind.is_quadratic() {
|
if kind.is_quadratic() {
|
||||||
let length = frame.size().max_by_side().min(pod.size.min_by_side());
|
let length = frame.size().max_by_side().min(pod.size.min_by_side());
|
||||||
let quad_pod = Region::new(Size::splat(length), Axes::splat(true));
|
let quad_pod = Region::new(Size::splat(length), Axes::splat(true));
|
||||||
frame = layout_frame(engine, child, locator, styles, quad_pod)?;
|
frame = crate::layout_frame(engine, child, locator, styles, quad_pod)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply the inset.
|
// Apply the inset.
|
||||||
if has_inset {
|
if has_inset {
|
||||||
crate::layout::grow(&mut frame, &inset);
|
crate::pad::grow(&mut frame, &inset);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// The default size that a shape takes on if it has no child and
|
// The default size that a shape takes on if it has no child and
|
||||||
@ -535,10 +385,16 @@ fn layout_shape(
|
|||||||
let outset = outset.unwrap_or_default().relative_to(frame.size());
|
let outset = outset.unwrap_or_default().relative_to(frame.size());
|
||||||
let size = frame.size() + outset.sum_by_axis();
|
let size = frame.size() + outset.sum_by_axis();
|
||||||
let pos = Point::new(-outset.left, -outset.top);
|
let pos = Point::new(-outset.left, -outset.top);
|
||||||
let shape = ellipse(size, fill, stroke.left);
|
let shape = Shape {
|
||||||
|
geometry: Geometry::Path(Path::ellipse(size)),
|
||||||
|
fill,
|
||||||
|
stroke: stroke.left,
|
||||||
|
fill_rule: FillRule::default(),
|
||||||
|
};
|
||||||
frame.prepend(pos, FrameItem::Shape(shape, span));
|
frame.prepend(pos, FrameItem::Shape(shape, span));
|
||||||
} else {
|
} else {
|
||||||
frame.fill_and_stroke(
|
fill_and_stroke(
|
||||||
|
&mut frame,
|
||||||
fill,
|
fill,
|
||||||
&stroke,
|
&stroke,
|
||||||
&outset.unwrap_or_default(),
|
&outset.unwrap_or_default(),
|
||||||
@ -551,128 +407,8 @@ fn layout_shape(
|
|||||||
Ok(frame)
|
Ok(frame)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A category of shape.
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
|
||||||
pub enum ShapeKind {
|
|
||||||
/// A rectangle with equal side lengths.
|
|
||||||
Square,
|
|
||||||
/// A quadrilateral with four right angles.
|
|
||||||
Rect,
|
|
||||||
/// An ellipse with coinciding foci.
|
|
||||||
Circle,
|
|
||||||
/// A curve around two focal points.
|
|
||||||
Ellipse,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ShapeKind {
|
|
||||||
/// Whether this shape kind is curvy.
|
|
||||||
fn is_round(self) -> bool {
|
|
||||||
matches!(self, Self::Circle | Self::Ellipse)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether this shape kind has equal side length.
|
|
||||||
fn is_quadratic(self) -> bool {
|
|
||||||
matches!(self, Self::Square | Self::Circle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A geometric shape with optional fill and stroke.
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
|
||||||
pub struct Shape {
|
|
||||||
/// The shape's geometry.
|
|
||||||
pub geometry: Geometry,
|
|
||||||
/// The shape's background fill.
|
|
||||||
pub fill: Option<Paint>,
|
|
||||||
/// The shape's fill rule.
|
|
||||||
pub fill_rule: FillRule,
|
|
||||||
/// The shape's border stroke.
|
|
||||||
pub stroke: Option<FixedStroke>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A path filling rule.
|
|
||||||
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)]
|
|
||||||
pub enum FillRule {
|
|
||||||
/// Specifies that "inside" is computed by a non-zero sum of signed edge crossings.
|
|
||||||
#[default]
|
|
||||||
NonZero,
|
|
||||||
/// Specifies that "inside" is computed by an odd number of edge crossings.
|
|
||||||
EvenOdd,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A shape's geometry.
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
|
||||||
pub enum Geometry {
|
|
||||||
/// A line to a point (relative to its position).
|
|
||||||
Line(Point),
|
|
||||||
/// A rectangle with its origin in the topleft corner.
|
|
||||||
Rect(Size),
|
|
||||||
/// A bezier path.
|
|
||||||
Path(Path),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Geometry {
|
|
||||||
/// Fill the geometry without a stroke.
|
|
||||||
pub fn filled(self, fill: Paint) -> Shape {
|
|
||||||
Shape {
|
|
||||||
geometry: self,
|
|
||||||
fill: Some(fill),
|
|
||||||
fill_rule: FillRule::default(),
|
|
||||||
stroke: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stroke the geometry without a fill.
|
|
||||||
pub fn stroked(self, stroke: FixedStroke) -> Shape {
|
|
||||||
Shape {
|
|
||||||
geometry: self,
|
|
||||||
fill: None,
|
|
||||||
fill_rule: FillRule::default(),
|
|
||||||
stroke: Some(stroke),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The bounding box of the geometry.
|
|
||||||
pub fn bbox_size(&self) -> Size {
|
|
||||||
match self {
|
|
||||||
Self::Line(line) => Size::new(line.x, line.y),
|
|
||||||
Self::Rect(s) => *s,
|
|
||||||
Self::Path(p) => p.bbox_size(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Produce a shape that approximates an axis-aligned ellipse.
|
|
||||||
pub(crate) fn ellipse(
|
|
||||||
size: Size,
|
|
||||||
fill: Option<Paint>,
|
|
||||||
stroke: Option<FixedStroke>,
|
|
||||||
) -> Shape {
|
|
||||||
// https://stackoverflow.com/a/2007782
|
|
||||||
let z = Abs::zero();
|
|
||||||
let rx = size.x / 2.0;
|
|
||||||
let ry = size.y / 2.0;
|
|
||||||
let m = 0.551784;
|
|
||||||
let mx = m * rx;
|
|
||||||
let my = m * ry;
|
|
||||||
let point = |x, y| Point::new(x + rx, y + ry);
|
|
||||||
|
|
||||||
let mut path = Path::new();
|
|
||||||
path.move_to(point(-rx, z));
|
|
||||||
path.cubic_to(point(-rx, -my), point(-mx, -ry), point(z, -ry));
|
|
||||||
path.cubic_to(point(mx, -ry), point(rx, -my), point(rx, z));
|
|
||||||
path.cubic_to(point(rx, my), point(mx, ry), point(z, ry));
|
|
||||||
path.cubic_to(point(-mx, ry), point(-rx, my), point(-rx, z));
|
|
||||||
|
|
||||||
Shape {
|
|
||||||
geometry: Geometry::Path(path),
|
|
||||||
stroke,
|
|
||||||
fill,
|
|
||||||
fill_rule: FillRule::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new rectangle as a path.
|
/// Creates a new rectangle as a path.
|
||||||
pub(crate) fn clip_rect(
|
pub fn clip_rect(
|
||||||
size: Size,
|
size: Size,
|
||||||
radius: &Corners<Rel<Abs>>,
|
radius: &Corners<Rel<Abs>>,
|
||||||
stroke: &Sides<Option<FixedStroke>>,
|
stroke: &Sides<Option<FixedStroke>>,
|
||||||
@ -708,11 +444,30 @@ pub(crate) fn clip_rect(
|
|||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a fill and stroke with optional radius and outset to the frame.
|
||||||
|
pub fn fill_and_stroke(
|
||||||
|
frame: &mut Frame,
|
||||||
|
fill: Option<Paint>,
|
||||||
|
stroke: &Sides<Option<FixedStroke>>,
|
||||||
|
outset: &Sides<Rel<Abs>>,
|
||||||
|
radius: &Corners<Rel<Abs>>,
|
||||||
|
span: Span,
|
||||||
|
) {
|
||||||
|
let outset = outset.relative_to(frame.size());
|
||||||
|
let size = frame.size() + outset.sum_by_axis();
|
||||||
|
let pos = Point::new(-outset.left, -outset.top);
|
||||||
|
frame.prepend_multiple(
|
||||||
|
styled_rect(size, radius, fill, stroke)
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| (pos, FrameItem::Shape(x, span))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a styled rectangle with shapes.
|
/// Create a styled rectangle with shapes.
|
||||||
/// - use rect primitive for simple rectangles
|
/// - use rect primitive for simple rectangles
|
||||||
/// - stroke sides if possible
|
/// - stroke sides if possible
|
||||||
/// - use fill for sides for best looks
|
/// - use fill for sides for best looks
|
||||||
pub(crate) fn styled_rect(
|
pub fn styled_rect(
|
||||||
size: Size,
|
size: Size,
|
||||||
radius: &Corners<Rel<Abs>>,
|
radius: &Corners<Rel<Abs>>,
|
||||||
fill: Option<Paint>,
|
fill: Option<Paint>,
|
@ -1,99 +1,17 @@
|
|||||||
use std::fmt::{self, Debug, Formatter};
|
use typst_library::diag::{bail, SourceResult};
|
||||||
|
use typst_library::engine::Engine;
|
||||||
|
use typst_library::foundations::{Content, Packed, Resolve, StyleChain, StyledElem};
|
||||||
|
use typst_library::introspection::{Locator, SplitLocator};
|
||||||
|
use typst_library::layout::{
|
||||||
|
Abs, AlignElem, Axes, Axis, Dir, FixedAlignment, Fr, Fragment, Frame, HElem, Point,
|
||||||
|
Regions, Size, Spacing, StackChild, StackElem, VElem,
|
||||||
|
};
|
||||||
use typst_syntax::Span;
|
use typst_syntax::Span;
|
||||||
|
use typst_utils::{Get, Numeric};
|
||||||
use crate::diag::{bail, SourceResult};
|
|
||||||
use crate::engine::Engine;
|
|
||||||
use crate::foundations::{
|
|
||||||
cast, elem, Content, NativeElement, Packed, Resolve, Show, StyleChain, StyledElem,
|
|
||||||
};
|
|
||||||
use crate::introspection::{Locator, SplitLocator};
|
|
||||||
use crate::layout::{
|
|
||||||
layout_fragment, Abs, AlignElem, Axes, Axis, BlockElem, Dir, FixedAlignment, Fr,
|
|
||||||
Fragment, Frame, HElem, Point, Regions, Size, Spacing, VElem,
|
|
||||||
};
|
|
||||||
use crate::utils::{Get, Numeric};
|
|
||||||
|
|
||||||
/// Arranges content and spacing horizontally or vertically.
|
|
||||||
///
|
|
||||||
/// The stack places a list of items along an axis, with optional spacing
|
|
||||||
/// between each item.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```example
|
|
||||||
/// #stack(
|
|
||||||
/// dir: ttb,
|
|
||||||
/// rect(width: 40pt),
|
|
||||||
/// rect(width: 120pt),
|
|
||||||
/// rect(width: 90pt),
|
|
||||||
/// )
|
|
||||||
/// ```
|
|
||||||
#[elem(Show)]
|
|
||||||
pub struct StackElem {
|
|
||||||
/// The direction along which the items are stacked. Possible values are:
|
|
||||||
///
|
|
||||||
/// - `{ltr}`: Left to right.
|
|
||||||
/// - `{rtl}`: Right to left.
|
|
||||||
/// - `{ttb}`: Top to bottom.
|
|
||||||
/// - `{btt}`: Bottom to top.
|
|
||||||
///
|
|
||||||
/// You can use the `start` and `end` methods to obtain the initial and
|
|
||||||
/// final points (respectively) of a direction, as `alignment`. You can also
|
|
||||||
/// use the `axis` method to determine whether a direction is
|
|
||||||
/// `{"horizontal"}` or `{"vertical"}`. The `inv` method returns a
|
|
||||||
/// direction's inverse direction.
|
|
||||||
///
|
|
||||||
/// For example, `{ttb.start()}` is `top`, `{ttb.end()}` is `bottom`,
|
|
||||||
/// `{ttb.axis()}` is `{"vertical"}` and `{ttb.inv()}` is equal to `btt`.
|
|
||||||
#[default(Dir::TTB)]
|
|
||||||
pub dir: Dir,
|
|
||||||
|
|
||||||
/// Spacing to insert between items where no explicit spacing was provided.
|
|
||||||
pub spacing: Option<Spacing>,
|
|
||||||
|
|
||||||
/// The children to stack along the axis.
|
|
||||||
#[variadic]
|
|
||||||
pub children: Vec<StackChild>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Show for Packed<StackElem> {
|
|
||||||
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
|
|
||||||
Ok(BlockElem::multi_layouter(self.clone(), layout_stack)
|
|
||||||
.pack()
|
|
||||||
.spanned(self.span()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A child of a stack element.
|
|
||||||
#[derive(Clone, PartialEq, Hash)]
|
|
||||||
pub enum StackChild {
|
|
||||||
/// Spacing between other children.
|
|
||||||
Spacing(Spacing),
|
|
||||||
/// Arbitrary block-level content.
|
|
||||||
Block(Content),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for StackChild {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Spacing(kind) => kind.fmt(f),
|
|
||||||
Self::Block(block) => block.fmt(f),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cast! {
|
|
||||||
StackChild,
|
|
||||||
self => match self {
|
|
||||||
Self::Spacing(spacing) => spacing.into_value(),
|
|
||||||
Self::Block(content) => content.into_value(),
|
|
||||||
},
|
|
||||||
v: Spacing => Self::Spacing(v),
|
|
||||||
v: Content => Self::Block(v),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Layout the stack.
|
/// Layout the stack.
|
||||||
#[typst_macros::time(span = elem.span())]
|
#[typst_macros::time(span = elem.span())]
|
||||||
fn layout_stack(
|
pub fn layout_stack(
|
||||||
elem: &Packed<StackElem>,
|
elem: &Packed<StackElem>,
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
locator: Locator,
|
locator: Locator,
|
||||||
@ -257,7 +175,7 @@ impl<'a> StackLayouter<'a> {
|
|||||||
}
|
}
|
||||||
.resolve(styles);
|
.resolve(styles);
|
||||||
|
|
||||||
let fragment = layout_fragment(
|
let fragment = crate::layout_fragment(
|
||||||
engine,
|
engine,
|
||||||
block,
|
block,
|
||||||
self.locator.next(&block.span()),
|
self.locator.next(&block.span()),
|
246
crates/typst-layout/src/transforms.rs
Normal file
246
crates/typst-layout/src/transforms.rs
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
use once_cell::unsync::Lazy;
|
||||||
|
use typst_library::diag::{bail, SourceResult};
|
||||||
|
use typst_library::engine::Engine;
|
||||||
|
use typst_library::foundations::{Content, Packed, Resolve, Smart, StyleChain};
|
||||||
|
use typst_library::introspection::Locator;
|
||||||
|
use typst_library::layout::{
|
||||||
|
Abs, Axes, FixedAlignment, Frame, MoveElem, Point, Ratio, Region, Rel, RotateElem,
|
||||||
|
ScaleAmount, ScaleElem, Size, SkewElem, Transform,
|
||||||
|
};
|
||||||
|
use typst_utils::Numeric;
|
||||||
|
|
||||||
|
/// Layout the moved content.
|
||||||
|
#[typst_macros::time(span = elem.span())]
|
||||||
|
pub fn layout_move(
|
||||||
|
elem: &Packed<MoveElem>,
|
||||||
|
engine: &mut Engine,
|
||||||
|
locator: Locator,
|
||||||
|
styles: StyleChain,
|
||||||
|
region: Region,
|
||||||
|
) -> SourceResult<Frame> {
|
||||||
|
let mut frame = crate::layout_frame(engine, &elem.body, locator, styles, region)?;
|
||||||
|
let delta = Axes::new(elem.dx(styles), elem.dy(styles)).resolve(styles);
|
||||||
|
let delta = delta.zip_map(region.size, Rel::relative_to);
|
||||||
|
frame.translate(delta.to_point());
|
||||||
|
Ok(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout the rotated content.
|
||||||
|
#[typst_macros::time(span = elem.span())]
|
||||||
|
pub fn layout_rotate(
|
||||||
|
elem: &Packed<RotateElem>,
|
||||||
|
engine: &mut Engine,
|
||||||
|
locator: Locator,
|
||||||
|
styles: StyleChain,
|
||||||
|
region: Region,
|
||||||
|
) -> SourceResult<Frame> {
|
||||||
|
let angle = elem.angle(styles);
|
||||||
|
let align = elem.origin(styles).resolve(styles);
|
||||||
|
|
||||||
|
// Compute the new region's approximate size.
|
||||||
|
let is_finite = region.size.is_finite();
|
||||||
|
let size = if is_finite {
|
||||||
|
compute_bounding_box(region.size, Transform::rotate(-angle)).1
|
||||||
|
} else {
|
||||||
|
Size::splat(Abs::inf())
|
||||||
|
};
|
||||||
|
|
||||||
|
measure_and_layout(
|
||||||
|
engine,
|
||||||
|
locator,
|
||||||
|
region,
|
||||||
|
size,
|
||||||
|
styles,
|
||||||
|
elem.body(),
|
||||||
|
Transform::rotate(angle),
|
||||||
|
align,
|
||||||
|
elem.reflow(styles),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout the scaled content.
|
||||||
|
#[typst_macros::time(span = elem.span())]
|
||||||
|
pub fn layout_scale(
|
||||||
|
elem: &Packed<ScaleElem>,
|
||||||
|
engine: &mut Engine,
|
||||||
|
locator: Locator,
|
||||||
|
styles: StyleChain,
|
||||||
|
region: Region,
|
||||||
|
) -> SourceResult<Frame> {
|
||||||
|
// Compute the new region's approximate size.
|
||||||
|
let scale = resolve_scale(elem, engine, locator.relayout(), region.size, styles)?;
|
||||||
|
let size = region
|
||||||
|
.size
|
||||||
|
.zip_map(scale, |r, s| if r.is_finite() { Ratio::new(1.0 / s).of(r) } else { r })
|
||||||
|
.map(Abs::abs);
|
||||||
|
|
||||||
|
measure_and_layout(
|
||||||
|
engine,
|
||||||
|
locator,
|
||||||
|
region,
|
||||||
|
size,
|
||||||
|
styles,
|
||||||
|
elem.body(),
|
||||||
|
Transform::scale(scale.x, scale.y),
|
||||||
|
elem.origin(styles).resolve(styles),
|
||||||
|
elem.reflow(styles),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves scale parameters, preserving aspect ratio if one of the scales
|
||||||
|
/// is set to `auto`.
|
||||||
|
fn resolve_scale(
|
||||||
|
elem: &Packed<ScaleElem>,
|
||||||
|
engine: &mut Engine,
|
||||||
|
locator: Locator,
|
||||||
|
container: Size,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<Axes<Ratio>> {
|
||||||
|
fn resolve_axis(
|
||||||
|
axis: Smart<ScaleAmount>,
|
||||||
|
body: impl Fn() -> SourceResult<Abs>,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<Smart<Ratio>> {
|
||||||
|
Ok(match axis {
|
||||||
|
Smart::Auto => Smart::Auto,
|
||||||
|
Smart::Custom(amt) => Smart::Custom(match amt {
|
||||||
|
ScaleAmount::Ratio(ratio) => ratio,
|
||||||
|
ScaleAmount::Length(length) => {
|
||||||
|
let length = length.resolve(styles);
|
||||||
|
Ratio::new(length / body()?)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = Lazy::new(|| {
|
||||||
|
let pod = Region::new(container, Axes::splat(false));
|
||||||
|
let frame = crate::layout_frame(engine, &elem.body, locator, styles, pod)?;
|
||||||
|
SourceResult::Ok(frame.size())
|
||||||
|
});
|
||||||
|
|
||||||
|
let x = resolve_axis(
|
||||||
|
elem.x(styles),
|
||||||
|
|| size.as_ref().map(|size| size.x).map_err(Clone::clone),
|
||||||
|
styles,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let y = resolve_axis(
|
||||||
|
elem.y(styles),
|
||||||
|
|| size.as_ref().map(|size| size.y).map_err(Clone::clone),
|
||||||
|
styles,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
match (x, y) {
|
||||||
|
(Smart::Auto, Smart::Auto) => {
|
||||||
|
bail!(elem.span(), "x and y cannot both be auto")
|
||||||
|
}
|
||||||
|
(Smart::Custom(x), Smart::Custom(y)) => Ok(Axes::new(x, y)),
|
||||||
|
(Smart::Auto, Smart::Custom(v)) | (Smart::Custom(v), Smart::Auto) => {
|
||||||
|
Ok(Axes::splat(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout the skewed content.
|
||||||
|
#[typst_macros::time(span = elem.span())]
|
||||||
|
pub fn layout_skew(
|
||||||
|
elem: &Packed<SkewElem>,
|
||||||
|
engine: &mut Engine,
|
||||||
|
locator: Locator,
|
||||||
|
styles: StyleChain,
|
||||||
|
region: Region,
|
||||||
|
) -> SourceResult<Frame> {
|
||||||
|
let ax = elem.ax(styles);
|
||||||
|
let ay = elem.ay(styles);
|
||||||
|
let align = elem.origin(styles).resolve(styles);
|
||||||
|
|
||||||
|
// Compute the new region's approximate size.
|
||||||
|
let size = if region.size.is_finite() {
|
||||||
|
compute_bounding_box(region.size, Transform::skew(ax, ay)).1
|
||||||
|
} else {
|
||||||
|
Size::splat(Abs::inf())
|
||||||
|
};
|
||||||
|
|
||||||
|
measure_and_layout(
|
||||||
|
engine,
|
||||||
|
locator,
|
||||||
|
region,
|
||||||
|
size,
|
||||||
|
styles,
|
||||||
|
elem.body(),
|
||||||
|
Transform::skew(ax, ay),
|
||||||
|
align,
|
||||||
|
elem.reflow(styles),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies a transformation to a frame, reflowing the layout if necessary.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn measure_and_layout(
|
||||||
|
engine: &mut Engine,
|
||||||
|
locator: Locator,
|
||||||
|
region: Region,
|
||||||
|
size: Size,
|
||||||
|
styles: StyleChain,
|
||||||
|
body: &Content,
|
||||||
|
transform: Transform,
|
||||||
|
align: Axes<FixedAlignment>,
|
||||||
|
reflow: bool,
|
||||||
|
) -> SourceResult<Frame> {
|
||||||
|
if reflow {
|
||||||
|
// Measure the size of the body.
|
||||||
|
let pod = Region::new(size, Axes::splat(false));
|
||||||
|
let frame = crate::layout_frame(engine, body, locator.relayout(), styles, pod)?;
|
||||||
|
|
||||||
|
// Actually perform the layout.
|
||||||
|
let pod = Region::new(frame.size(), Axes::splat(true));
|
||||||
|
let mut frame = crate::layout_frame(engine, body, locator, styles, pod)?;
|
||||||
|
let Axes { x, y } = align.zip_map(frame.size(), FixedAlignment::position);
|
||||||
|
|
||||||
|
// Compute the transform.
|
||||||
|
let ts = Transform::translate(x, y)
|
||||||
|
.pre_concat(transform)
|
||||||
|
.pre_concat(Transform::translate(-x, -y));
|
||||||
|
|
||||||
|
// Compute the bounding box and offset and wrap in a new frame.
|
||||||
|
let (offset, size) = compute_bounding_box(frame.size(), ts);
|
||||||
|
frame.transform(ts);
|
||||||
|
frame.translate(offset);
|
||||||
|
frame.set_size(size);
|
||||||
|
Ok(frame)
|
||||||
|
} else {
|
||||||
|
// Layout the body.
|
||||||
|
let mut frame = crate::layout_frame(engine, body, locator, styles, region)?;
|
||||||
|
let Axes { x, y } = align.zip_map(frame.size(), FixedAlignment::position);
|
||||||
|
|
||||||
|
// Compute the transform.
|
||||||
|
let ts = Transform::translate(x, y)
|
||||||
|
.pre_concat(transform)
|
||||||
|
.pre_concat(Transform::translate(-x, -y));
|
||||||
|
|
||||||
|
// Apply the transform.
|
||||||
|
frame.transform(ts);
|
||||||
|
Ok(frame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the bounding box and offset of a transformed area.
|
||||||
|
fn compute_bounding_box(size: Size, ts: Transform) -> (Point, Size) {
|
||||||
|
let top_left = Point::zero().transform_inf(ts);
|
||||||
|
let top_right = Point::with_x(size.x).transform_inf(ts);
|
||||||
|
let bottom_left = Point::with_y(size.y).transform_inf(ts);
|
||||||
|
let bottom_right = size.to_point().transform_inf(ts);
|
||||||
|
|
||||||
|
// We first compute the new bounding box of the rotated area.
|
||||||
|
let min_x = top_left.x.min(top_right.x).min(bottom_left.x).min(bottom_right.x);
|
||||||
|
let min_y = top_left.y.min(top_right.y).min(bottom_left.y).min(bottom_right.y);
|
||||||
|
let max_x = top_left.x.max(top_right.x).max(bottom_left.x).max(bottom_right.x);
|
||||||
|
let max_y = top_left.y.max(top_right.y).max(bottom_left.y).max(bottom_right.y);
|
||||||
|
|
||||||
|
// Then we compute the new size of the area.
|
||||||
|
let width = max_x - min_x;
|
||||||
|
let height = max_y - min_y;
|
||||||
|
|
||||||
|
(Point::new(-min_x, -min_y), Size::new(width.abs(), height.abs()))
|
||||||
|
}
|
72
crates/typst-library/Cargo.toml
Normal file
72
crates/typst-library/Cargo.toml
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
[package]
|
||||||
|
name = "typst-library"
|
||||||
|
description = "Typst's standard library."
|
||||||
|
version = { workspace = true }
|
||||||
|
rust-version = { workspace = true }
|
||||||
|
authors = { workspace = true }
|
||||||
|
edition = { workspace = true }
|
||||||
|
homepage = { workspace = true }
|
||||||
|
repository = { workspace = true }
|
||||||
|
license = { workspace = true }
|
||||||
|
categories = { workspace = true }
|
||||||
|
keywords = { workspace = true }
|
||||||
|
readme = { workspace = true }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
typst-assets = { workspace = true }
|
||||||
|
typst-macros = { workspace = true }
|
||||||
|
typst-syntax = { workspace = true }
|
||||||
|
typst-timing = { workspace = true }
|
||||||
|
typst-utils = { workspace = true }
|
||||||
|
az = { workspace = true }
|
||||||
|
bitflags = { workspace = true }
|
||||||
|
bumpalo = { workspace = true }
|
||||||
|
chinese-number = { workspace = true }
|
||||||
|
ciborium = { workspace = true }
|
||||||
|
comemo = { workspace = true }
|
||||||
|
csv = { workspace = true }
|
||||||
|
ecow = { workspace = true }
|
||||||
|
flate2 = { workspace = true }
|
||||||
|
fontdb = { workspace = true }
|
||||||
|
hayagriva = { workspace = true }
|
||||||
|
icu_properties = { workspace = true }
|
||||||
|
icu_provider = { workspace = true }
|
||||||
|
icu_provider_blob = { workspace = true }
|
||||||
|
image = { workspace = true }
|
||||||
|
indexmap = { workspace = true }
|
||||||
|
kamadak-exif = { workspace = true }
|
||||||
|
kurbo = { workspace = true }
|
||||||
|
lipsum = { workspace = true }
|
||||||
|
once_cell = { workspace = true }
|
||||||
|
palette = { workspace = true }
|
||||||
|
phf = { workspace = true }
|
||||||
|
png = { workspace = true }
|
||||||
|
qcms = { workspace = true }
|
||||||
|
rayon = { workspace = true }
|
||||||
|
regex = { workspace = true }
|
||||||
|
roxmltree = { workspace = true }
|
||||||
|
rust_decimal = { workspace = true }
|
||||||
|
rustybuzz = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
serde_yaml = { workspace = true }
|
||||||
|
siphasher = { workspace = true }
|
||||||
|
smallvec = { workspace = true }
|
||||||
|
syntect = { workspace = true }
|
||||||
|
time = { workspace = true }
|
||||||
|
toml = { workspace = true }
|
||||||
|
ttf-parser = { workspace = true }
|
||||||
|
two-face = { workspace = true }
|
||||||
|
typed-arena = { workspace = true }
|
||||||
|
unicode-math-class = { workspace = true }
|
||||||
|
unicode-segmentation = { workspace = true }
|
||||||
|
unscanny = { workspace = true }
|
||||||
|
usvg = { workspace = true }
|
||||||
|
wasmi = { workspace = true }
|
||||||
|
xmlwriter = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
typst-dev-assets = { workspace = true }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
@ -8,9 +8,9 @@ use std::string::FromUtf8Error;
|
|||||||
|
|
||||||
use comemo::Tracked;
|
use comemo::Tracked;
|
||||||
use ecow::{eco_vec, EcoVec};
|
use ecow::{eco_vec, EcoVec};
|
||||||
|
use typst_syntax::package::{PackageSpec, PackageVersion};
|
||||||
|
use typst_syntax::{Span, Spanned, SyntaxError};
|
||||||
|
|
||||||
use crate::syntax::package::{PackageSpec, PackageVersion};
|
|
||||||
use crate::syntax::{Span, Spanned, SyntaxError};
|
|
||||||
use crate::{World, WorldExt};
|
use crate::{World, WorldExt};
|
||||||
|
|
||||||
/// Early-return with a [`StrResult`] or [`SourceResult`].
|
/// Early-return with a [`StrResult`] or [`SourceResult`].
|
@ -6,15 +6,19 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
|||||||
use comemo::{Track, Tracked, TrackedMut, Validate};
|
use comemo::{Track, Tracked, TrackedMut, Validate};
|
||||||
use ecow::EcoVec;
|
use ecow::EcoVec;
|
||||||
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
|
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
|
||||||
|
use typst_syntax::{FileId, Span};
|
||||||
|
|
||||||
use crate::diag::{bail, HintedStrResult, SourceDiagnostic, SourceResult, StrResult};
|
use crate::diag::{bail, HintedStrResult, SourceDiagnostic, SourceResult, StrResult};
|
||||||
use crate::foundations::{Styles, Value};
|
use crate::foundations::{Styles, Value};
|
||||||
use crate::introspection::Introspector;
|
use crate::introspection::Introspector;
|
||||||
use crate::syntax::{FileId, Span};
|
use crate::routines::Routines;
|
||||||
use crate::World;
|
use crate::World;
|
||||||
|
|
||||||
/// Holds all data needed during compilation.
|
/// Holds all data needed during compilation.
|
||||||
pub struct Engine<'a> {
|
pub struct Engine<'a> {
|
||||||
|
/// Defines implementation of various Typst compiler routines as a table of
|
||||||
|
/// function pointers.
|
||||||
|
pub routines: &'a Routines,
|
||||||
/// The compilation environment.
|
/// The compilation environment.
|
||||||
pub world: Tracked<'a, dyn World + 'a>,
|
pub world: Tracked<'a, dyn World + 'a>,
|
||||||
/// Provides access to information about the document.
|
/// Provides access to information about the document.
|
||||||
@ -51,7 +55,9 @@ impl Engine<'_> {
|
|||||||
U: Send,
|
U: Send,
|
||||||
F: Fn(&mut Engine, T) -> U + Send + Sync,
|
F: Fn(&mut Engine, T) -> U + Send + Sync,
|
||||||
{
|
{
|
||||||
let Engine { world, introspector, traced, ref route, .. } = *self;
|
let Engine {
|
||||||
|
world, introspector, traced, ref route, routines, ..
|
||||||
|
} = *self;
|
||||||
|
|
||||||
// We collect into a vector and then call `into_par_iter` instead of
|
// We collect into a vector and then call `into_par_iter` instead of
|
||||||
// using `par_bridge` because it does not retain the ordering.
|
// using `par_bridge` because it does not retain the ordering.
|
||||||
@ -68,6 +74,7 @@ impl Engine<'_> {
|
|||||||
traced,
|
traced,
|
||||||
sink: sink.track_mut(),
|
sink: sink.track_mut(),
|
||||||
route: route.clone(),
|
route: route.clone(),
|
||||||
|
routines,
|
||||||
};
|
};
|
||||||
(f(&mut engine, value), sink)
|
(f(&mut engine, value), sink)
|
||||||
})
|
})
|
||||||
@ -173,7 +180,7 @@ impl Sink {
|
|||||||
/// Add a warning.
|
/// Add a warning.
|
||||||
pub fn warn(&mut self, warning: SourceDiagnostic) {
|
pub fn warn(&mut self, warning: SourceDiagnostic) {
|
||||||
// Check if warning is a duplicate.
|
// Check if warning is a duplicate.
|
||||||
let hash = crate::utils::hash128(&(&warning.span, &warning.message));
|
let hash = typst_utils::hash128(&(&warning.span, &warning.message));
|
||||||
if self.warnings_set.insert(hash) {
|
if self.warnings_set.insert(hash) {
|
||||||
self.warnings.push(warning);
|
self.warnings.push(warning);
|
||||||
}
|
}
|
@ -1,12 +1,12 @@
|
|||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Debug, Formatter};
|
||||||
|
|
||||||
use ecow::{eco_format, eco_vec, EcoString, EcoVec};
|
use ecow::{eco_format, eco_vec, EcoString, EcoVec};
|
||||||
|
use typst_syntax::{Span, Spanned};
|
||||||
|
|
||||||
use crate::diag::{bail, error, At, SourceDiagnostic, SourceResult, StrResult};
|
use crate::diag::{bail, error, At, SourceDiagnostic, SourceResult, StrResult};
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
cast, func, repr, scope, ty, Array, Dict, FromValue, IntoValue, Repr, Str, Value,
|
cast, func, repr, scope, ty, Array, Dict, FromValue, IntoValue, Repr, Str, Value,
|
||||||
};
|
};
|
||||||
use crate::syntax::{Span, Spanned};
|
|
||||||
|
|
||||||
/// Captured arguments to a function.
|
/// Captured arguments to a function.
|
||||||
///
|
///
|
@ -7,15 +7,14 @@ use comemo::Tracked;
|
|||||||
use ecow::{eco_format, EcoString, EcoVec};
|
use ecow::{eco_format, EcoString, EcoVec};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
use typst_syntax::{Span, Spanned};
|
||||||
|
|
||||||
use crate::diag::{bail, At, HintedStrResult, SourceDiagnostic, SourceResult, StrResult};
|
use crate::diag::{bail, At, HintedStrResult, SourceDiagnostic, SourceResult, StrResult};
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
use crate::eval::ops;
|
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
cast, func, repr, scope, ty, Args, Bytes, CastInfo, Context, Dict, FromValue, Func,
|
cast, func, ops, repr, scope, ty, Args, Bytes, CastInfo, Context, Dict, FromValue,
|
||||||
IntoValue, Reflect, Repr, Str, Value, Version,
|
Func, IntoValue, Reflect, Repr, Str, Value, Version,
|
||||||
};
|
};
|
||||||
use crate::syntax::{Span, Spanned};
|
|
||||||
|
|
||||||
/// Create a new [`Array`] from values.
|
/// Create a new [`Array`] from values.
|
||||||
#[macro_export]
|
#[macro_export]
|
@ -1,6 +1,7 @@
|
|||||||
use ecow::EcoString;
|
|
||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Debug, Formatter};
|
||||||
|
|
||||||
|
use ecow::EcoString;
|
||||||
|
|
||||||
use crate::diag::HintedStrResult;
|
use crate::diag::HintedStrResult;
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
ty, CastInfo, Fold, FromValue, IntoValue, Reflect, Repr, Resolve, StyleChain, Type,
|
ty, CastInfo, Fold, FromValue, IntoValue, Reflect, Repr, Resolve, StyleChain, Type,
|
@ -5,10 +5,10 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
use serde::{Serialize, Serializer};
|
use serde::{Serialize, Serializer};
|
||||||
|
use typst_utils::LazyHash;
|
||||||
|
|
||||||
use crate::diag::{bail, StrResult};
|
use crate::diag::{bail, StrResult};
|
||||||
use crate::foundations::{cast, func, scope, ty, Array, Reflect, Repr, Str, Value};
|
use crate::foundations::{cast, func, scope, ty, Array, Reflect, Repr, Str, Value};
|
||||||
use crate::utils::LazyHash;
|
|
||||||
|
|
||||||
/// A sequence of bytes.
|
/// A sequence of bytes.
|
||||||
///
|
///
|
@ -4,13 +4,12 @@ use std::cmp;
|
|||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
use az::SaturatingAs;
|
use az::SaturatingAs;
|
||||||
|
use typst_syntax::{Span, Spanned};
|
||||||
|
use typst_utils::{round_int_with_precision, round_with_precision};
|
||||||
|
|
||||||
use crate::diag::{bail, At, HintedString, SourceResult, StrResult};
|
use crate::diag::{bail, At, HintedString, SourceResult, StrResult};
|
||||||
use crate::eval::ops;
|
use crate::foundations::{cast, func, ops, Decimal, IntoValue, Module, Scope, Value};
|
||||||
use crate::foundations::{cast, func, Decimal, IntoValue, Module, Scope, Value};
|
|
||||||
use crate::layout::{Angle, Fr, Length, Ratio};
|
use crate::layout::{Angle, Fr, Length, Ratio};
|
||||||
use crate::syntax::{Span, Spanned};
|
|
||||||
use crate::utils::{round_int_with_precision, round_with_precision};
|
|
||||||
|
|
||||||
/// A module with calculation definitions.
|
/// A module with calculation definitions.
|
||||||
pub fn module() -> Module {
|
pub fn module() -> Module {
|
@ -1,3 +1,7 @@
|
|||||||
|
#[rustfmt::skip]
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use typst_macros::{cast, Cast};
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
@ -5,15 +9,11 @@ use std::ops::Add;
|
|||||||
|
|
||||||
use ecow::eco_format;
|
use ecow::eco_format;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
use typst_syntax::{Span, Spanned};
|
||||||
use unicode_math_class::MathClass;
|
use unicode_math_class::MathClass;
|
||||||
|
|
||||||
use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult};
|
use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult};
|
||||||
use crate::foundations::{array, repr, NativeElement, Packed, Repr, Str, Type, Value};
|
use crate::foundations::{array, repr, NativeElement, Packed, Repr, Str, Type, Value};
|
||||||
use crate::syntax::{Span, Spanned};
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[doc(inline)]
|
|
||||||
pub use typst_macros::{cast, Cast};
|
|
||||||
|
|
||||||
/// Determine details of a type.
|
/// Determine details of a type.
|
||||||
///
|
///
|
@ -10,6 +10,8 @@ use comemo::Tracked;
|
|||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
use serde::{Serialize, Serializer};
|
use serde::{Serialize, Serializer};
|
||||||
use smallvec::smallvec;
|
use smallvec::smallvec;
|
||||||
|
use typst_syntax::Span;
|
||||||
|
use typst_utils::{fat, singleton, LazyHash, SmallBitSet};
|
||||||
|
|
||||||
use crate::diag::{SourceResult, StrResult};
|
use crate::diag::{SourceResult, StrResult};
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
@ -21,9 +23,7 @@ use crate::foundations::{
|
|||||||
use crate::introspection::Location;
|
use crate::introspection::Location;
|
||||||
use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides};
|
use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides};
|
||||||
use crate::model::{Destination, EmphElem, LinkElem, StrongElem};
|
use crate::model::{Destination, EmphElem, LinkElem, StrongElem};
|
||||||
use crate::syntax::Span;
|
|
||||||
use crate::text::UnderlineElem;
|
use crate::text::UnderlineElem;
|
||||||
use crate::utils::{fat, singleton, LazyHash, SmallBitSet};
|
|
||||||
|
|
||||||
/// A piece of document content.
|
/// A piece of document content.
|
||||||
///
|
///
|
@ -5,10 +5,11 @@ use std::str::FromStr;
|
|||||||
|
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
use rust_decimal::MathematicalOps;
|
use rust_decimal::MathematicalOps;
|
||||||
|
use typst_syntax::{ast, Span, Spanned};
|
||||||
|
|
||||||
use crate::diag::{warning, At, SourceResult};
|
use crate::diag::{warning, At, SourceResult};
|
||||||
use crate::foundations::{cast, func, repr, scope, ty, Engine, Repr, Str};
|
use crate::engine::Engine;
|
||||||
use crate::syntax::{ast, Span, Spanned};
|
use crate::foundations::{cast, func, repr, scope, ty, Repr, Str};
|
||||||
use crate::World;
|
use crate::World;
|
||||||
|
|
||||||
/// A fixed-point decimal number type.
|
/// A fixed-point decimal number type.
|
||||||
@ -447,8 +448,9 @@ cast! {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use typst_utils::hash128;
|
||||||
|
|
||||||
use super::Decimal;
|
use super::Decimal;
|
||||||
use crate::utils::hash128;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_decimals_with_equal_scales_hash_identically() {
|
fn test_decimals_with_equal_scales_hash_identically() {
|
@ -6,13 +6,13 @@ use std::sync::Arc;
|
|||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
use typst_syntax::is_ident;
|
||||||
|
use typst_utils::ArcExt;
|
||||||
|
|
||||||
use crate::diag::{Hint, HintedStrResult, StrResult};
|
use crate::diag::{Hint, HintedStrResult, StrResult};
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
array, cast, func, repr, scope, ty, Array, Module, Repr, Str, Value,
|
array, cast, func, repr, scope, ty, Array, Module, Repr, Str, Value,
|
||||||
};
|
};
|
||||||
use crate::syntax::is_ident;
|
|
||||||
use crate::utils::ArcExt;
|
|
||||||
|
|
||||||
/// Create a new [`Dict`] from key-value pairs.
|
/// Create a new [`Dict`] from key-value pairs.
|
||||||
#[macro_export]
|
#[macro_export]
|
@ -7,6 +7,9 @@ use std::ptr::NonNull;
|
|||||||
use ecow::EcoString;
|
use ecow::EcoString;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use typst_macros::elem;
|
||||||
|
use typst_utils::Static;
|
||||||
|
|
||||||
use crate::diag::SourceResult;
|
use crate::diag::SourceResult;
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
@ -15,10 +18,6 @@ use crate::foundations::{
|
|||||||
StyleChain, Styles, Value,
|
StyleChain, Styles, Value,
|
||||||
};
|
};
|
||||||
use crate::text::{Lang, Region};
|
use crate::text::{Lang, Region};
|
||||||
use crate::utils::Static;
|
|
||||||
|
|
||||||
#[doc(inline)]
|
|
||||||
pub use typst_macros::elem;
|
|
||||||
|
|
||||||
/// A document element.
|
/// A document element.
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
@ -2,9 +2,9 @@ use std::num::ParseFloatError;
|
|||||||
|
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
|
|
||||||
use crate::diag::StrResult;
|
use crate::diag::{bail, StrResult};
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
bail, cast, func, repr, scope, ty, Bytes, Decimal, Endianness, Repr, Str,
|
cast, func, repr, scope, ty, Bytes, Decimal, Endianness, Repr, Str,
|
||||||
};
|
};
|
||||||
use crate::layout::Ratio;
|
use crate::layout::Ratio;
|
||||||
|
|
@ -1,9 +1,14 @@
|
|||||||
|
#[doc(inline)]
|
||||||
|
pub use typst_macros::func;
|
||||||
|
|
||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Debug, Formatter};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use comemo::{Tracked, TrackedMut};
|
use comemo::{Tracked, TrackedMut};
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
use typst_syntax::{ast, Span, SyntaxNode};
|
||||||
|
use typst_utils::{singleton, LazyHash, Static};
|
||||||
|
|
||||||
use crate::diag::{bail, SourceResult, StrResult};
|
use crate::diag::{bail, SourceResult, StrResult};
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
@ -11,11 +16,6 @@ use crate::foundations::{
|
|||||||
cast, repr, scope, ty, Args, CastInfo, Content, Context, Element, IntoArgs, Scope,
|
cast, repr, scope, ty, Args, CastInfo, Content, Context, Element, IntoArgs, Scope,
|
||||||
Selector, Type, Value,
|
Selector, Type, Value,
|
||||||
};
|
};
|
||||||
use crate::syntax::{ast, Span, SyntaxNode};
|
|
||||||
use crate::utils::{singleton, LazyHash, Static};
|
|
||||||
|
|
||||||
#[doc(inline)]
|
|
||||||
pub use typst_macros::func;
|
|
||||||
|
|
||||||
/// A mapping from argument values to a return value.
|
/// A mapping from argument values to a return value.
|
||||||
///
|
///
|
||||||
@ -296,9 +296,10 @@ impl Func {
|
|||||||
args.finish()?;
|
args.finish()?;
|
||||||
Ok(Value::Content(value))
|
Ok(Value::Content(value))
|
||||||
}
|
}
|
||||||
Repr::Closure(closure) => crate::eval::call_closure(
|
Repr::Closure(closure) => (engine.routines.eval_closure)(
|
||||||
self,
|
self,
|
||||||
closure,
|
closure,
|
||||||
|
engine.routines,
|
||||||
engine.world,
|
engine.world,
|
||||||
engine.introspector,
|
engine.introspector,
|
||||||
engine.traced,
|
engine.traced,
|
@ -2,9 +2,9 @@ use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError
|
|||||||
|
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
|
|
||||||
use crate::diag::StrResult;
|
use crate::diag::{bail, StrResult};
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
bail, cast, func, repr, scope, ty, Bytes, Cast, Decimal, Repr, Str, Value,
|
cast, func, repr, scope, ty, Bytes, Cast, Decimal, Repr, Str, Value,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A whole number.
|
/// A whole number.
|
||||||
@ -402,13 +402,16 @@ macro_rules! unsigned_int {
|
|||||||
($($ty:ty)*) => {
|
($($ty:ty)*) => {
|
||||||
$(cast! {
|
$(cast! {
|
||||||
$ty,
|
$ty,
|
||||||
self => if let Ok(int) = i64::try_from(self) {
|
self => {
|
||||||
|
#[allow(irrefutable_let_patterns)]
|
||||||
|
if let Ok(int) = i64::try_from(self) {
|
||||||
Value::Int(int)
|
Value::Int(int)
|
||||||
} else {
|
} else {
|
||||||
// Some u64 are too large to be cast as i64
|
// Some u64 are too large to be cast as i64
|
||||||
// In that case, we accept that there may be a
|
// In that case, we accept that there may be a
|
||||||
// precision loss, and use a floating point number
|
// precision loss, and use a floating point number
|
||||||
Value::Float(self as _)
|
Value::Float(self as _)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
v: i64 => v.try_into().map_err(|_| {
|
v: i64 => v.try_into().map_err(|_| {
|
||||||
if v < 0 {
|
if v < 0 {
|
@ -1,7 +1,7 @@
|
|||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
|
use typst_utils::PicoStr;
|
||||||
|
|
||||||
use crate::foundations::{func, scope, ty, Repr};
|
use crate::foundations::{func, scope, ty, Repr};
|
||||||
use crate::utils::PicoStr;
|
|
||||||
|
|
||||||
/// A label for an element.
|
/// A label for an element.
|
||||||
///
|
///
|
@ -1,11 +1,10 @@
|
|||||||
//! Foundational types and functions.
|
//! Foundational types and functions.
|
||||||
|
|
||||||
pub mod calc;
|
pub mod calc;
|
||||||
|
pub mod ops;
|
||||||
pub mod repr;
|
pub mod repr;
|
||||||
pub mod sys;
|
pub mod sys;
|
||||||
|
|
||||||
pub use typst_macros::{scope, ty};
|
|
||||||
|
|
||||||
mod args;
|
mod args;
|
||||||
mod array;
|
mod array;
|
||||||
mod auto;
|
mod auto;
|
||||||
@ -24,7 +23,6 @@ mod float;
|
|||||||
mod func;
|
mod func;
|
||||||
mod int;
|
mod int;
|
||||||
mod label;
|
mod label;
|
||||||
mod methods;
|
|
||||||
mod module;
|
mod module;
|
||||||
mod none;
|
mod none;
|
||||||
mod plugin;
|
mod plugin;
|
||||||
@ -32,6 +30,7 @@ mod scope;
|
|||||||
mod selector;
|
mod selector;
|
||||||
mod str;
|
mod str;
|
||||||
mod styles;
|
mod styles;
|
||||||
|
mod symbol;
|
||||||
mod ty;
|
mod ty;
|
||||||
mod value;
|
mod value;
|
||||||
mod version;
|
mod version;
|
||||||
@ -53,7 +52,6 @@ pub use self::float::*;
|
|||||||
pub use self::func::*;
|
pub use self::func::*;
|
||||||
pub use self::int::*;
|
pub use self::int::*;
|
||||||
pub use self::label::*;
|
pub use self::label::*;
|
||||||
pub(crate) use self::methods::*;
|
|
||||||
pub use self::module::*;
|
pub use self::module::*;
|
||||||
pub use self::none::*;
|
pub use self::none::*;
|
||||||
pub use self::plugin::*;
|
pub use self::plugin::*;
|
||||||
@ -62,9 +60,11 @@ pub use self::scope::*;
|
|||||||
pub use self::selector::*;
|
pub use self::selector::*;
|
||||||
pub use self::str::*;
|
pub use self::str::*;
|
||||||
pub use self::styles::*;
|
pub use self::styles::*;
|
||||||
|
pub use self::symbol::*;
|
||||||
pub use self::ty::*;
|
pub use self::ty::*;
|
||||||
pub use self::value::*;
|
pub use self::value::*;
|
||||||
pub use self::version::*;
|
pub use self::version::*;
|
||||||
|
pub use typst_macros::{scope, ty};
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
@ -75,11 +75,11 @@ pub use {
|
|||||||
};
|
};
|
||||||
|
|
||||||
use ecow::EcoString;
|
use ecow::EcoString;
|
||||||
|
use typst_syntax::Spanned;
|
||||||
|
|
||||||
use crate::diag::{bail, SourceResult, StrResult};
|
use crate::diag::{bail, SourceResult, StrResult};
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
use crate::eval::EvalMode;
|
use crate::routines::EvalMode;
|
||||||
use crate::syntax::Spanned;
|
|
||||||
|
|
||||||
/// Foundational types and functions.
|
/// Foundational types and functions.
|
||||||
///
|
///
|
||||||
@ -108,6 +108,7 @@ pub(super) fn define(global: &mut Scope, inputs: Dict) {
|
|||||||
global.define_type::<Selector>();
|
global.define_type::<Selector>();
|
||||||
global.define_type::<Datetime>();
|
global.define_type::<Datetime>();
|
||||||
global.define_type::<Decimal>();
|
global.define_type::<Decimal>();
|
||||||
|
global.define_type::<Symbol>();
|
||||||
global.define_type::<Duration>();
|
global.define_type::<Duration>();
|
||||||
global.define_type::<Version>();
|
global.define_type::<Version>();
|
||||||
global.define_type::<Plugin>();
|
global.define_type::<Plugin>();
|
||||||
@ -297,5 +298,5 @@ pub fn eval(
|
|||||||
for (key, value) in dict {
|
for (key, value) in dict {
|
||||||
scope.define_spanned(key, value, span);
|
scope.define_spanned(key, value, span);
|
||||||
}
|
}
|
||||||
crate::eval::eval_string(engine.world, &text, span, mode, scope)
|
(engine.routines.eval_string)(engine.routines, engine.world, &text, span, mode, scope)
|
||||||
}
|
}
|
@ -2,10 +2,10 @@ use std::fmt::{self, Debug, Formatter};
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use ecow::{eco_format, EcoString};
|
use ecow::{eco_format, EcoString};
|
||||||
|
use typst_syntax::FileId;
|
||||||
|
|
||||||
use crate::diag::StrResult;
|
use crate::diag::StrResult;
|
||||||
use crate::foundations::{repr, ty, Content, Scope, Value};
|
use crate::foundations::{repr, ty, Content, Scope, Value};
|
||||||
use crate::syntax::FileId;
|
|
||||||
|
|
||||||
/// An evaluated module, either built-in or resulting from a file.
|
/// An evaluated module, either built-in or resulting from a file.
|
||||||
///
|
///
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user