diff --git a/Cargo.lock b/Cargo.lock index 0f5f255ff..3d9e7d000 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2668,68 +2668,16 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" name = "typst" version = "0.12.0" dependencies = [ - "arrayvec", - "az", - "bitflags 2.6.0", - "bumpalo", - "chinese-number", - "ciborium", "comemo", - "csv", "ecow", - "flate2", - "fontdb", - "hayagriva", - "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-eval", + "typst-layout", + "typst-library", "typst-macros", + "typst-realize", "typst-syntax", "typst-timing", "typst-utils", - "unicode-bidi", - "unicode-math-class", - "unicode-script", - "unicode-segmentation", - "unscanny", - "usvg", - "wasmi", - "xmlwriter", ] [[package]] @@ -2769,7 +2717,7 @@ dependencies = [ "tempfile", "toml", "typst", - "typst-assets", + "typst-eval", "typst-kit", "typst-macros", "typst-pdf", @@ -2791,7 +2739,6 @@ name = "typst-docs" version = "0.12.0" dependencies = [ "clap", - "comemo", "ecow", "heck", "once_cell", @@ -2809,6 +2756,24 @@ dependencies = [ "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]] name = "typst-fuzz" version = "0.12.0" @@ -2834,6 +2799,7 @@ dependencies = [ "typst", "typst-assets", "typst-dev-assets", + "typst-eval", "unscanny", ] @@ -2850,13 +2816,103 @@ dependencies = [ "once_cell", "openssl", "tar", - "typst", "typst-assets", + "typst-library", + "typst-syntax", "typst-timing", "typst-utils", "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]] name = "typst-macros" version = "0.12.0" @@ -2885,14 +2941,32 @@ dependencies = [ "subsetter", "svg2pdf", "ttf-parser", - "typst", "typst-assets", + "typst-library", "typst-macros", + "typst-syntax", "typst-timing", - "unscanny", + "typst-utils", "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]] name = "typst-render" version = "0.12.0" @@ -2902,13 +2976,11 @@ dependencies = [ "image", "pixglyph", "resvg", - "roxmltree", "tiny-skia", "ttf-parser", - "typst", + "typst-library", "typst-macros", "typst-timing", - "usvg", ] [[package]] @@ -2920,9 +2992,10 @@ dependencies = [ "ecow", "flate2", "ttf-parser", - "typst", + "typst-library", "typst-macros", "typst-timing", + "typst-utils", "xmlparser", "xmlwriter", ] @@ -2935,6 +3008,7 @@ dependencies = [ "once_cell", "serde", "toml", + "typst-timing", "typst-utils", "unicode-ident", "unicode-math-class", @@ -2956,10 +3030,10 @@ dependencies = [ "rayon", "regex", "tiny-skia", - "ttf-parser", "typst", "typst-assets", "typst-dev-assets", + "typst-library", "typst-pdf", "typst-render", "typst-svg", @@ -2974,7 +3048,6 @@ dependencies = [ "parking_lot", "serde", "serde_json", - "typst-syntax", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index bbbc98d12..753ca0f2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,10 +18,14 @@ readme = "README.md" [workspace.dependencies] typst = { path = "crates/typst", 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-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-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-svg = { path = "crates/typst-svg", version = "0.12.0" } typst-syntax = { path = "crates/typst-syntax", version = "0.12.0" } @@ -145,5 +149,8 @@ strip = true [workspace.lints.clippy] blocks_in_conditions = "allow" +comparison_chain = "allow" +manual_range_contains = "allow" mutable_key_type = "allow" uninlined_format_args = "warn" +wildcard_in_or_patterns = "allow" diff --git a/crates/typst-cli/Cargo.toml b/crates/typst-cli/Cargo.toml index 1c622d1e2..855d7beeb 100644 --- a/crates/typst-cli/Cargo.toml +++ b/crates/typst-cli/Cargo.toml @@ -19,7 +19,7 @@ doc = false [dependencies] typst = { workspace = true } -typst-assets = { workspace = true, features = ["fonts"] } +typst-eval = { workspace = true } typst-kit = { workspace = true } typst-macros = { workspace = true } typst-pdf = { workspace = true } @@ -28,8 +28,8 @@ typst-svg = { workspace = true } typst-timing = { workspace = true } chrono = { workspace = true } clap = { workspace = true } -color-print = { workspace = true } codespan-reporting = { workspace = true } +color-print = { workspace = true } comemo = { workspace = true } dirs = { workspace = true } ecow = { workspace = true } diff --git a/crates/typst-cli/src/main.rs b/crates/typst-cli/src/main.rs index b14e687ee..464ad6240 100644 --- a/crates/typst-cli/src/main.rs +++ b/crates/typst-cli/src/main.rs @@ -94,9 +94,10 @@ fn print_error(msg: &str) -> io::Result<()> { #[cfg(not(feature = "self-update"))] mod update { - use crate::args::UpdateCommand; use typst::diag::{bail, StrResult}; + use crate::args::UpdateCommand; + pub fn update(_: &UpdateCommand) -> StrResult<()> { bail!( "self-updating is not enabled for this executable, \ diff --git a/crates/typst-cli/src/query.rs b/crates/typst-cli/src/query.rs index ef3c79514..90d99a5a2 100644 --- a/crates/typst-cli/src/query.rs +++ b/crates/typst-cli/src/query.rs @@ -2,11 +2,11 @@ use comemo::Track; use ecow::{eco_format, EcoString}; use serde::Serialize; use typst::diag::{bail, HintedStrResult, StrResult, Warned}; -use typst::eval::{eval_string, EvalMode}; use typst::foundations::{Content, IntoValue, LocatableSelector, Scope}; use typst::model::Document; use typst::syntax::Span; use typst::World; +use typst_eval::{eval_string, EvalMode}; use crate::args::{QueryCommand, SerializationFormat}; use crate::compile::print_diagnostics; @@ -56,6 +56,7 @@ fn retrieve( document: &Document, ) -> HintedStrResult> { let selector = eval_string( + &typst::ROUTINES, world.track(), &command.selector, Span::detached(), diff --git a/crates/typst-cli/src/timings.rs b/crates/typst-cli/src/timings.rs index df7665b7a..4446bbf98 100644 --- a/crates/typst-cli/src/timings.rs +++ b/crates/typst-cli/src/timings.rs @@ -72,7 +72,8 @@ impl Timer { let writer = BufWriter::with_capacity(1 << 20, file); 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) diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs index bb491c029..b790f41af 100644 --- a/crates/typst-cli/src/world.rs +++ b/crates/typst-cli/src/world.rs @@ -16,7 +16,7 @@ use typst::utils::LazyHash; use typst::{Library, World}; use typst_kit::fonts::{FontSlot, Fonts}; use typst_kit::package::PackageStorage; -use typst_timing::{timed, TimingScope}; +use typst_timing::timed; use crate::args::{Input, SharedArgs}; use crate::compile::ExportCache; @@ -285,8 +285,6 @@ impl FileSlot { self.source.get_or_init( || read(self.id, project_root, package_storage), |data, prev| { - let name = if prev.is_some() { "reparsing file" } else { "parsing file" }; - let _scope = TimingScope::new(name, None); let text = decode_utf8(&data)?; if let Some(mut prev) = prev { prev.replace(text); diff --git a/crates/typst-eval/Cargo.toml b/crates/typst-eval/Cargo.toml new file mode 100644 index 000000000..12a6a6a46 --- /dev/null +++ b/crates/typst-eval/Cargo.toml @@ -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 diff --git a/crates/typst/src/eval/access.rs b/crates/typst-eval/src/access.rs similarity index 91% rename from crates/typst/src/eval/access.rs rename to crates/typst-eval/src/access.rs index ab0a64912..9bcac4d68 100644 --- a/crates/typst/src/eval/access.rs +++ b/crates/typst-eval/src/access.rs @@ -1,9 +1,9 @@ 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::eval::{Eval, Vm}; -use crate::foundations::{call_method_access, is_accessor_method, Dict, Value}; -use crate::syntax::ast::{self, AstNode}; +use crate::{call_method_access, is_accessor_method, Eval, Vm}; /// Access an expression mutably. pub(crate) trait Access { @@ -85,7 +85,7 @@ pub(crate) fn access_dict<'a>( Value::Symbol(_) | Value::Content(_) | Value::Module(_) | Value::Func(_) ) { 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"); } else { // type supports static fields, which don't yet have diff --git a/crates/typst/src/eval/binding.rs b/crates/typst-eval/src/binding.rs similarity index 96% rename from crates/typst/src/eval/binding.rs rename to crates/typst-eval/src/binding.rs index 18468b9ee..f3802f079 100644 --- a/crates/typst/src/eval/binding.rs +++ b/crates/typst-eval/src/binding.rs @@ -1,11 +1,11 @@ use std::collections::HashSet; 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::eval::{Access, Eval, Vm}; -use crate::foundations::{Array, Dict, Value}; -use crate::syntax::ast::{self, AstNode}; +use crate::{Access, Eval, Vm}; impl Eval for ast::LetBinding<'_> { type Output = Value; diff --git a/crates/typst/src/eval/call.rs b/crates/typst-eval/src/call.rs similarity index 96% rename from crates/typst/src/eval/call.rs rename to crates/typst-eval/src/call.rs index 4331e187e..9dfb7693c 100644 --- a/crates/typst/src/eval/call.rs +++ b/crates/typst-eval/src/call.rs @@ -1,23 +1,24 @@ use comemo::{Tracked, TrackedMut}; use ecow::{eco_format, EcoString, EcoVec}; - -use crate::diag::{ +use typst_library::diag::{ bail, error, At, HintedStrResult, HintedString, SourceDiagnostic, SourceResult, Trace, Tracepoint, }; -use crate::engine::{Engine, Sink, Traced}; -use crate::eval::{Access, Eval, FlowEvent, Route, Vm}; -use crate::foundations::{ - call_method_mut, is_mutating_method, Arg, Args, Bytes, Capturer, Closure, Content, - Context, Func, IntoValue, NativeElement, Scope, Scopes, Value, +use typst_library::engine::{Engine, Sink, Traced}; +use typst_library::foundations::{ + Arg, Args, Bytes, Capturer, Closure, Content, Context, Func, IntoValue, + NativeElement, Scope, Scopes, Value, }; -use crate::introspection::Introspector; -use crate::math::LrElem; -use crate::syntax::ast::{self, AstNode, Ident}; -use crate::syntax::{Span, Spanned, SyntaxNode}; -use crate::text::TextElem; -use crate::utils::LazyHash; -use crate::World; +use typst_library::introspection::Introspector; +use typst_library::math::LrElem; +use typst_library::routines::Routines; +use typst_library::text::TextElem; +use typst_library::World; +use typst_syntax::ast::{self, AstNode, Ident}; +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<'_> { type Output = Value; @@ -159,9 +160,10 @@ impl Eval for ast::Closure<'_> { /// Call the function in the context with the arguments. #[comemo::memoize] #[allow(clippy::too_many_arguments)] -pub(crate) fn call_closure( +pub fn eval_closure( func: &Func, closure: &LazyHash, + routines: &Routines, world: Tracked, introspector: Tracked, traced: Tracked, @@ -182,6 +184,7 @@ pub(crate) fn call_closure( // Prepare the engine. let engine = Engine { + routines, world, introspector, traced, @@ -210,7 +213,7 @@ pub(crate) fn call_closure( vm.define(ident, args.expect::(&ident)?) } pattern => { - crate::eval::destructure( + crate::destructure( &mut vm, pattern, args.expect::("pattern parameter")?, @@ -583,8 +586,9 @@ impl<'a> CapturesVisitor<'a> { #[cfg(test)] mod tests { + use typst_syntax::parse; + use super::*; - use crate::syntax::parse; #[track_caller] fn test(text: &str, result: &[&str]) { diff --git a/crates/typst/src/eval/code.rs b/crates/typst-eval/src/code.rs similarity index 97% rename from crates/typst/src/eval/code.rs rename to crates/typst-eval/src/code.rs index 7cfb7f593..918d9d2a4 100644 --- a/crates/typst/src/eval/code.rs +++ b/crates/typst-eval/src/code.rs @@ -1,11 +1,12 @@ use ecow::{eco_vec, EcoVec}; - -use crate::diag::{bail, error, At, SourceResult}; -use crate::eval::{ops, CapturesVisitor, Eval, Vm}; -use crate::foundations::{ - Array, Capturer, Closure, Content, ContextElem, Dict, Func, NativeElement, Str, Value, +use typst_library::diag::{bail, error, At, SourceResult}; +use typst_library::foundations::{ + ops, 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<'_> { type Output = Value; diff --git a/crates/typst/src/eval/flow.rs b/crates/typst-eval/src/flow.rs similarity index 96% rename from crates/typst/src/eval/flow.rs rename to crates/typst-eval/src/flow.rs index a68be95ba..5c9d2a006 100644 --- a/crates/typst/src/eval/flow.rs +++ b/crates/typst-eval/src/flow.rs @@ -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 crate::diag::{bail, error, At, SourceDiagnostic, SourceResult}; -use crate::eval::{destructure, ops, Eval, Vm}; -use crate::foundations::{IntoValue, Value}; -use crate::syntax::ast::{self, AstNode}; -use crate::syntax::{Span, SyntaxKind, SyntaxNode}; +use crate::{destructure, Eval, Vm}; /// The maximum number of loop iterations. const MAX_ITERATIONS: usize = 10_000; diff --git a/crates/typst/src/eval/import.rs b/crates/typst-eval/src/import.rs similarity index 92% rename from crates/typst/src/eval/import.rs rename to crates/typst-eval/src/import.rs index d02938cc1..316fbf87b 100644 --- a/crates/typst/src/eval/import.rs +++ b/crates/typst-eval/src/import.rs @@ -1,13 +1,15 @@ use comemo::TrackedMut; 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, 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; +use crate::{eval, Eval, Vm}; impl Eval for ast::ModuleImport<'_> { type Output = Value; @@ -171,8 +173,9 @@ pub fn import( /// Import an external package. fn import_package(vm: &mut Vm, spec: PackageSpec, span: Span) -> SourceResult { // Evaluate the manifest. + let world = vm.world(); 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 manifest: PackageManifest = toml::from_str(string) .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 SourceResult SourceResult { // Evaluate the file. let point = || Tracepoint::Import; eval( - world, + vm.engine.routines, + vm.engine.world, vm.engine.traced, TrackedMut::reborrow_mut(&mut vm.engine.sink), vm.engine.route.track(), diff --git a/crates/typst/src/eval/mod.rs b/crates/typst-eval/src/lib.rs similarity index 83% rename from crates/typst/src/eval/mod.rs rename to crates/typst-eval/src/lib.rs index dc8a18020..a5c0c7e30 100644 --- a/crates/typst/src/eval/mod.rs +++ b/crates/typst-eval/src/lib.rs @@ -1,4 +1,4 @@ -//! Evaluation of markup and code. +//! Typst's code interpreter. pub(crate) mod ops; @@ -10,31 +10,35 @@ mod flow; mod import; mod markup; mod math; +mod methods; mod rules; mod vm; pub use self::call::*; pub use self::import::*; pub use self::vm::*; +pub use typst_library::routines::EvalMode; -pub(crate) use self::access::*; -pub(crate) use self::binding::*; -pub(crate) use self::flow::*; +use self::access::*; +use self::binding::*; +use self::flow::*; +use self::methods::*; use comemo::{Track, Tracked, TrackedMut}; - -use crate::diag::{bail, SourceResult}; -use crate::engine::{Engine, Route, Sink, Traced}; -use crate::foundations::{Cast, Context, Module, NativeElement, Scope, Scopes, Value}; -use crate::introspection::Introspector; -use crate::math::EquationElem; -use crate::syntax::{ast, parse, parse_code, parse_math, Source, Span}; -use crate::World; +use typst_library::diag::{bail, SourceResult}; +use typst_library::engine::{Engine, Route, Sink, Traced}; +use typst_library::foundations::{Context, Module, NativeElement, Scope, Scopes, Value}; +use typst_library::introspection::Introspector; +use typst_library::math::EquationElem; +use typst_library::routines::Routines; +use typst_library::World; +use typst_syntax::{ast, parse, parse_code, parse_math, Source, Span}; /// Evaluate a source file and return the resulting module. #[comemo::memoize] #[typst_macros::time(name = "eval", span = source.root().span())] pub fn eval( + routines: &Routines, world: Tracked, traced: Tracked, sink: TrackedMut, @@ -50,6 +54,7 @@ pub fn eval( // Prepare the engine. let introspector = Introspector::default(); let engine = Engine { + routines, world, introspector: introspector.track(), traced, @@ -94,6 +99,7 @@ pub fn eval( /// Everything in the output is associated with the given `span`. #[comemo::memoize] pub fn eval_string( + routines: &Routines, world: Tracked, string: &str, span: Span, @@ -119,6 +125,7 @@ pub fn eval_string( let introspector = Introspector::default(); let traced = Traced::default(); let engine = Engine { + routines, world, introspector: introspector.track(), traced: traced.track(), @@ -153,17 +160,6 @@ pub fn eval_string( 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. pub trait Eval { /// The output of evaluating the expression. diff --git a/crates/typst/src/eval/markup.rs b/crates/typst-eval/src/markup.rs similarity index 95% rename from crates/typst/src/eval/markup.rs rename to crates/typst-eval/src/markup.rs index 42fede1c0..e28eb9ddb 100644 --- a/crates/typst/src/eval/markup.rs +++ b/crates/typst-eval/src/markup.rs @@ -1,18 +1,18 @@ -use crate::diag::{warning, At, SourceResult}; -use crate::eval::{Eval, Vm}; -use crate::foundations::{ - Content, Label, NativeElement, Repr, Smart, Unlabellable, Value, +use typst_library::diag::{warning, At, SourceResult}; +use typst_library::foundations::{ + Content, Label, NativeElement, Repr, Smart, Symbol, Unlabellable, Value, }; -use crate::math::EquationElem; -use crate::model::{ +use typst_library::math::EquationElem; +use typst_library::model::{ EmphElem, EnumItem, HeadingElem, LinkElem, ListItem, ParbreakElem, RefElem, StrongElem, Supplement, TermItem, Url, }; -use crate::symbols::Symbol; -use crate::syntax::ast::{self, AstNode}; -use crate::text::{ +use typst_library::text::{ LinebreakElem, RawContent, RawElem, SmartQuoteElem, SpaceElem, TextElem, }; +use typst_syntax::ast::{self, AstNode}; + +use crate::{Eval, Vm}; impl Eval for ast::Markup<'_> { type Output = Content; diff --git a/crates/typst/src/eval/math.rs b/crates/typst-eval/src/math.rs similarity index 90% rename from crates/typst/src/eval/math.rs rename to crates/typst-eval/src/math.rs index d1a667a58..c61a32514 100644 --- a/crates/typst/src/eval/math.rs +++ b/crates/typst-eval/src/math.rs @@ -1,12 +1,13 @@ 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::{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; +use crate::{Eval, Vm}; impl Eval for ast::Math<'_> { type Output = Content; diff --git a/crates/typst/src/foundations/methods.rs b/crates/typst-eval/src/methods.rs similarity index 95% rename from crates/typst/src/foundations/methods.rs rename to crates/typst-eval/src/methods.rs index 945b7c507..7cb36a00f 100644 --- a/crates/typst/src/foundations/methods.rs +++ b/crates/typst-eval/src/methods.rs @@ -1,8 +1,8 @@ //! Handles special built-in methods on values. -use crate::diag::{At, SourceResult}; -use crate::foundations::{Args, Str, Type, Value}; -use crate::syntax::Span; +use typst_library::diag::{At, SourceResult}; +use typst_library::foundations::{Args, Str, Type, Value}; +use typst_syntax::Span; /// Whether a specific method is mutating. pub(crate) fn is_mutating_method(method: &str) -> bool { diff --git a/crates/typst-eval/src/ops.rs b/crates/typst-eval/src/ops.rs new file mode 100644 index 000000000..ebbd67430 --- /dev/null +++ b/crates/typst-eval/src/ops.rs @@ -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 { + 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 { + 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, +) -> SourceResult { + 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, +) -> SourceResult { + 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) +} diff --git a/crates/typst/src/eval/rules.rs b/crates/typst-eval/src/rules.rs similarity index 91% rename from crates/typst/src/eval/rules.rs rename to crates/typst-eval/src/rules.rs index 1748bbd72..646354d4b 100644 --- a/crates/typst/src/eval/rules.rs +++ b/crates/typst-eval/src/rules.rs @@ -1,11 +1,12 @@ -use crate::diag::{warning, At, SourceResult}; -use crate::eval::{Eval, Vm}; -use crate::foundations::{ +use typst_library::diag::{warning, At, SourceResult}; +use typst_library::foundations::{ Element, Fields, Func, Recipe, Selector, ShowableSelector, Styles, Transformation, }; -use crate::layout::BlockElem; -use crate::model::ParElem; -use crate::syntax::ast::{self, AstNode}; +use typst_library::layout::BlockElem; +use typst_library::model::ParElem; +use typst_syntax::ast::{self, AstNode}; + +use crate::{Eval, Vm}; impl Eval for ast::SetRule<'_> { type Output = Styles; diff --git a/crates/typst/src/eval/vm.rs b/crates/typst-eval/src/vm.rs similarity index 84% rename from crates/typst/src/eval/vm.rs rename to crates/typst-eval/src/vm.rs index 4d346870c..1c8331b66 100644 --- a/crates/typst/src/eval/vm.rs +++ b/crates/typst-eval/src/vm.rs @@ -1,15 +1,15 @@ 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::eval::FlowEvent; -use crate::foundations::{Context, IntoValue, Scopes, Value}; -use crate::syntax::ast::{self, AstNode}; -use crate::syntax::Span; -use crate::World; +use crate::FlowEvent; /// 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. pub struct Vm<'a> { /// The underlying virtual typesetter. diff --git a/crates/typst-ide/Cargo.toml b/crates/typst-ide/Cargo.toml index 4e87f99b1..3c98e9b8a 100644 --- a/crates/typst-ide/Cargo.toml +++ b/crates/typst-ide/Cargo.toml @@ -14,6 +14,7 @@ readme = { workspace = true } [dependencies] typst = { workspace = true } +typst-eval = { workspace = true } comemo = { workspace = true } ecow = { workspace = true } if_chain = { workspace = true } diff --git a/crates/typst-ide/src/analyze.rs b/crates/typst-ide/src/analyze.rs index c37795562..75ffaede5 100644 --- a/crates/typst-ide/src/analyze.rs +++ b/crates/typst-ide/src/analyze.rs @@ -1,12 +1,12 @@ use comemo::Track; use ecow::{eco_vec, EcoString, EcoVec}; use typst::engine::{Engine, Route, Sink, Traced}; -use typst::eval::Vm; use typst::foundations::{Context, Label, Scopes, Styles, Value}; use typst::introspection::Introspector; use typst::model::{BibliographyElem, Document}; use typst::syntax::{ast, LinkedNode, Span, SyntaxKind}; use typst::World; +use typst_eval::Vm; /// Try to determine a set of possible values for an expression. pub fn analyze_expr( @@ -58,6 +58,7 @@ pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option { let traced = Traced::default(); let mut sink = Sink::new(); let engine = Engine { + routines: &typst::ROUTINES, world: world.track(), introspector: introspector.track(), traced: traced.track(), @@ -73,7 +74,7 @@ pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option { Span::detached(), ); - typst::eval::import(&mut vm, source, source_span, true) + typst_eval::import(&mut vm, source, source_span, true) .ok() .map(Value::Module) } diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index d534f55cc..cde4d1e39 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -1353,7 +1353,6 @@ impl<'a> CompletionContext<'a> { #[cfg(test)] mod tests { - use super::autocomplete; use crate::tests::TestWorld; diff --git a/crates/typst-ide/src/lib.rs b/crates/typst-ide/src/lib.rs index 63ba6f75f..f09e6ac57 100644 --- a/crates/typst-ide/src/lib.rs +++ b/crates/typst-ide/src/lib.rs @@ -191,7 +191,7 @@ mod tests { // 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 // that it multiplies to nice round numbers. - let mut lib = Library::default(); + let mut lib = typst::Library::default(); lib.styles .set(PageElem::set_width(Smart::Custom(Abs::pt(120.0).into()))); lib.styles.set(PageElem::set_height(Smart::Auto)); diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs index 532cda396..df93d1dc5 100644 --- a/crates/typst-ide/src/tooltip.rs +++ b/crates/typst-ide/src/tooltip.rs @@ -3,13 +3,13 @@ use std::fmt::Write; use ecow::{eco_format, EcoString}; use if_chain::if_chain; use typst::engine::Sink; -use typst::eval::CapturesVisitor; use typst::foundations::{repr, Capturer, CastInfo, Repr, Value}; use typst::layout::Length; use typst::model::Document; use typst::syntax::{ast, LinkedNode, Side, Source, SyntaxKind}; use typst::utils::{round_with_precision, Numeric}; use typst::World; +use typst_eval::CapturesVisitor; use crate::{analyze_expr, analyze_labels, plain_docs_sentence, summarize_font_family}; diff --git a/crates/typst-kit/Cargo.toml b/crates/typst-kit/Cargo.toml index 13d0a34be..266eba0b4 100644 --- a/crates/typst-kit/Cargo.toml +++ b/crates/typst-kit/Cargo.toml @@ -11,13 +11,14 @@ license = { workspace = true } readme = { workspace = true } [dependencies] -typst = { workspace = true } typst-assets = { workspace = true, optional = true } +typst-library = { workspace = true } +typst-syntax = { workspace = true } typst-timing = { workspace = true } typst-utils = { workspace = true } +dirs = { workspace = true, optional = true } ecow = { workspace = true } env_proxy = { workspace = true, optional = true } -dirs = { workspace = true, optional = true } flate2 = { workspace = true, optional = true } fontdb = { workspace = true, optional = true } native-tls = { workspace = true, optional = true } diff --git a/crates/typst-kit/src/fonts.rs b/crates/typst-kit/src/fonts.rs index 8c8981a1a..83e13fd8f 100644 --- a/crates/typst-kit/src/fonts.rs +++ b/crates/typst-kit/src/fonts.rs @@ -8,12 +8,12 @@ //! - For math: New Computer Modern Math //! - For code: Deja Vu Sans Mono -use std::path::PathBuf; +use std::fs; +use std::path::{Path, PathBuf}; use std::sync::OnceLock; -use std::{fs, path::Path}; use fontdb::{Database, Source}; -use typst::text::{Font, FontBook, FontInfo}; +use typst_library::text::{Font, FontBook, FontInfo}; use typst_timing::TimingScope; /// Holds details about the location of a font and lazily the font itself. @@ -46,7 +46,7 @@ impl FontSlot { pub fn get(&self) -> Option { self.font .get_or_init(|| { - let _scope = TimingScope::new("load font", None); + let _scope = TimingScope::new("load font"); let data = fs::read( self.path .as_ref() @@ -196,7 +196,7 @@ impl FontSearcher { #[cfg(feature = "embed-fonts")] fn add_embedded(&mut self) { 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() { self.book.push(font.info().clone()); self.fonts.push(FontSlot { diff --git a/crates/typst-kit/src/package.rs b/crates/typst-kit/src/package.rs index 839780106..412c7982f 100644 --- a/crates/typst-kit/src/package.rs +++ b/crates/typst-kit/src/package.rs @@ -5,8 +5,8 @@ use std::path::{Path, PathBuf}; use ecow::eco_format; use once_cell::sync::OnceCell; -use typst::diag::{bail, PackageError, PackageResult, StrResult}; -use typst::syntax::package::{ +use typst_library::diag::{bail, PackageError, PackageResult, StrResult}; +use typst_syntax::package::{ PackageInfo, PackageSpec, PackageVersion, VersionlessPackageSpec, }; diff --git a/crates/typst-layout/Cargo.toml b/crates/typst-layout/Cargo.toml new file mode 100644 index 000000000..4c133a4ec --- /dev/null +++ b/crates/typst-layout/Cargo.toml @@ -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 diff --git a/crates/typst-layout/src/flow/block.rs b/crates/typst-layout/src/flow/block.rs new file mode 100644 index 000000000..1dd988120 --- /dev/null +++ b/crates/typst-layout/src/flow/block.rs @@ -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, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Region, +) -> SourceResult { + // 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, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, +) -> SourceResult { + // 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>, + 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>, + 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..]) +} diff --git a/crates/typst/src/layout/flow/collect.rs b/crates/typst-layout/src/flow/collect.rs similarity index 94% rename from crates/typst/src/layout/flow/collect.rs rename to crates/typst-layout/src/flow/collect.rs index ffb45fda2..aee5d5081 100644 --- a/crates/typst/src/layout/flow/collect.rs +++ b/crates/typst-layout/src/flow/collect.rs @@ -6,22 +6,23 @@ use bumpalo::boxed::Box as BumpBox; use bumpalo::Bump; use comemo::{Track, Tracked, TrackedMut}; use once_cell::unsync::Lazy; - -use crate::diag::{bail, SourceResult}; -use crate::engine::{Engine, Route, Sink, Traced}; -use crate::foundations::{Packed, Resolve, Smart, StyleChain}; -use crate::introspection::{ +use typst_library::diag::{bail, SourceResult}; +use typst_library::engine::{Engine, Route, Sink, Traced}; +use typst_library::foundations::{Packed, Resolve, Smart, StyleChain}; +use typst_library::introspection::{ Introspector, Location, Locator, LocatorLink, SplitLocator, Tag, TagElem, }; -use crate::layout::{ - layout_frame, Abs, AlignElem, Alignment, Axes, BlockElem, ColbreakElem, - FixedAlignment, FlushElem, Fr, Fragment, Frame, PagebreakElem, PlaceElem, - PlacementScope, Ratio, Region, Regions, Rel, Size, Sizing, Spacing, VElem, +use typst_library::layout::{ + Abs, AlignElem, Alignment, Axes, BlockElem, ColbreakElem, FixedAlignment, FlushElem, + Fr, Fragment, Frame, PagebreakElem, PlaceElem, PlacementScope, Ratio, Region, + Regions, Rel, Size, Sizing, Spacing, VElem, }; -use crate::model::ParElem; -use crate::realize::Pair; -use crate::text::TextElem; -use crate::World; +use typst_library::model::ParElem; +use typst_library::routines::{Pair, Routines}; +use typst_library::text::TextElem; +use typst_library::World; + +use super::{layout_multi_block, layout_single_block}; /// Collects all elements of the flow into prepared children. These are much /// simpler to handle than the raw elements. @@ -110,7 +111,7 @@ impl<'a> Collector<'a, '_, '_> { let spacing = ParElem::spacing_in(styles); let costs = TextElem::costs_in(styles); - let lines = crate::layout::layout_inline( + let lines = crate::layout_inline( self.engine, &elem.children, self.locator.next(&elem.span()), @@ -332,6 +333,7 @@ impl SingleChild<'_> { // Vertical expansion is only kept if this block is the only child. region.expand.y &= self.alone; layout_single_impl( + engine.routines, engine.world, engine.introspector, engine.traced, @@ -350,6 +352,7 @@ impl SingleChild<'_> { #[comemo::memoize] #[allow(clippy::too_many_arguments)] fn layout_single_impl( + routines: &Routines, world: Tracked, introspector: Tracked, traced: Tracked, @@ -363,6 +366,7 @@ fn layout_single_impl( let link = LocatorLink::new(locator); let locator = Locator::link(&link); let mut engine = Engine { + routines, world, introspector, traced, @@ -370,7 +374,7 @@ fn layout_single_impl( 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)) } @@ -425,6 +429,7 @@ impl<'a> MultiChild<'a> { // Vertical expansion is only kept if this block is the only child. regions.expand.y &= self.alone; layout_multi_impl( + engine.routines, engine.world, engine.introspector, engine.traced, @@ -443,6 +448,7 @@ impl<'a> MultiChild<'a> { #[comemo::memoize] #[allow(clippy::too_many_arguments)] fn layout_multi_impl( + routines: &Routines, world: Tracked, introspector: Tracked, traced: Tracked, @@ -456,6 +462,7 @@ fn layout_multi_impl( let link = LocatorLink::new(locator); let locator = Locator::link(&link); let mut engine = Engine { + routines, world, introspector, traced, @@ -463,13 +470,12 @@ fn layout_multi_impl( route: Route::extend(route), }; - elem.layout_multiple(&mut engine, locator, styles, regions) - .map(|mut fragment| { - for frame in &mut fragment { - frame.post_process(styles); - } - fragment - }) + layout_multi_block(elem, &mut engine, locator, styles, regions).map(|mut fragment| { + for frame in &mut fragment { + frame.post_process(styles); + } + fragment + }) } /// The spilled remains of a `MultiChild` that broke across two regions. @@ -571,7 +577,7 @@ impl PlacedChild<'_> { let align = self.alignment.unwrap_or_else(|| Alignment::CENTER); let aligned = AlignElem::set_alignment(align).wrap(); - let mut frame = layout_frame( + let mut frame = crate::layout_frame( engine, &self.elem.body, self.locator.relayout(), @@ -614,7 +620,7 @@ impl CachedCell { T: Clone, 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(); if let Some((hash, output)) = &*slot { diff --git a/crates/typst/src/layout/flow/compose.rs b/crates/typst-layout/src/flow/compose.rs similarity index 98% rename from crates/typst/src/layout/flow/compose.rs rename to crates/typst-layout/src/flow/compose.rs index a262d5c1f..932ccc9ad 100644 --- a/crates/typst/src/layout/flow/compose.rs +++ b/crates/typst-layout/src/flow/compose.rs @@ -1,22 +1,23 @@ use std::num::NonZeroUsize; -use super::{distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Stop, Work}; -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{Content, NativeElement, Packed, Resolve, Smart}; -use crate::introspection::{ +use typst_library::diag::SourceResult; +use typst_library::engine::Engine; +use typst_library::foundations::{Content, NativeElement, Packed, Resolve, Smart}; +use typst_library::introspection::{ Counter, CounterDisplayElem, CounterState, CounterUpdate, Location, Locator, SplitLocator, Tag, }; -use crate::layout::{ - layout_fragment, layout_frame, Abs, Axes, Dir, FixedAlignment, Fragment, Frame, - FrameItem, OuterHAlignment, PlacementScope, Point, Region, Regions, Rel, Size, +use typst_library::layout::{ + Abs, Axes, Dir, FixedAlignment, Fragment, Frame, FrameItem, OuterHAlignment, + PlacementScope, Point, Region, Regions, Rel, Size, }; -use crate::model::{ +use typst_library::model::{ FootnoteElem, FootnoteEntry, LineNumberingScope, Numbering, ParLineMarker, }; -use crate::syntax::Span; -use crate::utils::NonZeroExt; +use typst_syntax::Span; +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 /// columns/subregions. @@ -517,7 +518,7 @@ fn layout_footnote_separator( config: &Config, base: Size, ) -> SourceResult { - layout_frame( + crate::layout_frame( engine, &config.footnote.separator, Locator::root(), @@ -534,7 +535,7 @@ fn layout_footnote( pod: Regions, ) -> SourceResult { let loc = elem.location().unwrap(); - layout_fragment( + crate::layout_fragment( engine, &FootnoteEntry::new(elem.clone()).pack(), Locator::synthesize(loc), @@ -785,7 +786,7 @@ fn layout_line_number_reset( let counter = Counter::of(ParLineMarker::elem()); let update = CounterUpdate::Set(CounterState::init(false)); let content = counter.update(Span::detached(), update); - layout_frame( + crate::layout_frame( engine, &content, locator.next(&()), @@ -821,7 +822,7 @@ fn layout_line_number( ]); // Layout the number. - let mut frame = layout_frame( + let mut frame = crate::layout_frame( engine, &content, locator.next(&()), diff --git a/crates/typst/src/layout/flow/distribute.rs b/crates/typst-layout/src/flow/distribute.rs similarity index 99% rename from crates/typst/src/layout/flow/distribute.rs rename to crates/typst-layout/src/flow/distribute.rs index 3fa166266..1852f7ca9 100644 --- a/crates/typst/src/layout/flow/distribute.rs +++ b/crates/typst-layout/src/flow/distribute.rs @@ -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::{ Child, Composer, FlowResult, LineChild, MultiChild, MultiSpill, PlacedChild, 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 /// region and returns the resulting frame. diff --git a/crates/typst/src/layout/flow/mod.rs b/crates/typst-layout/src/flow/mod.rs similarity index 90% rename from crates/typst/src/layout/flow/mod.rs rename to crates/typst-layout/src/flow/mod.rs index 66ec8e97c..7cbec59af 100644 --- a/crates/typst/src/layout/flow/mod.rs +++ b/crates/typst-layout/src/flow/mod.rs @@ -1,9 +1,12 @@ //! Layout of content into a [`Frame`] or [`Fragment`]. +mod block; mod collect; mod compose; mod distribute; +pub(crate) use self::block::unbreakable_pod; + use std::collections::HashSet; use std::num::NonZeroUsize; use std::rc::Rc; @@ -11,26 +14,40 @@ use std::rc::Rc; use bumpalo::Bump; use comemo::{Track, Tracked, TrackedMut}; 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::{ collect, Child, LineChild, MultiChild, MultiSpill, PlacedChild, SingleChild, }; use self::compose::{compose, Composer}; use self::distribute::distribute; -use crate::diag::{bail, At, SourceDiagnostic, SourceResult}; -use crate::engine::{Engine, Route, Sink, Traced}; -use crate::foundations::{Content, Packed, Resolve, StyleChain}; -use crate::introspection::{ - Introspector, Location, Locator, LocatorLink, SplitLocator, Tag, -}; -use crate::layout::{ - Abs, Dir, Em, Fragment, Frame, PageElem, PlacementScope, Region, Regions, Rel, Size, -}; -use crate::model::{FootnoteElem, FootnoteEntry, LineNumberingScope, ParLine}; -use crate::realize::{realize, Arenas, Pair, RealizationKind}; -use crate::text::TextElem; -use crate::utils::{NonZeroExt, Numeric}; -use crate::World; + +/// 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 { + layout_fragment(engine, content, locator, styles, region.into()) + .map(Fragment::into_frame) +} /// Lays out content into multiple regions. /// @@ -43,6 +60,7 @@ pub fn layout_fragment( regions: Regions, ) -> SourceResult { layout_fragment_impl( + engine.routines, engine.world, engine.introspector, 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 /// 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, engine: &mut Engine, - content: &Content, locator: Locator, styles: StyleChain, regions: Regions, - count: NonZeroUsize, - gutter: Rel, ) -> SourceResult { layout_fragment_impl( + engine.routines, engine.world, engine.introspector, engine.traced, TrackedMut::reborrow_mut(&mut engine.sink), engine.route.track(), - content, + &elem.body, locator.track(), styles, regions, - count, - gutter, + elem.count(styles), + 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 { - layout_fragment(engine, content, locator, styles, region.into()) - .map(Fragment::into_frame) -} - /// The cached, internal implementation of [`layout_fragment`]. #[comemo::memoize] #[allow(clippy::too_many_arguments)] fn layout_fragment_impl( + routines: &Routines, world: Tracked, introspector: Tracked, traced: Tracked, @@ -123,6 +130,7 @@ fn layout_fragment_impl( let link = LocatorLink::new(locator); let mut locator = Locator::link(&link).split(); let mut engine = Engine { + routines, world, introspector, traced, @@ -133,7 +141,7 @@ fn layout_fragment_impl( engine.route.check_layout_depth().at(content.span())?; let arenas = Arenas::default(); - let children = realize( + let children = (engine.routines.realize)( RealizationKind::Container, &mut engine, &mut locator, diff --git a/crates/typst/src/layout/grid/cells.rs b/crates/typst-layout/src/grid/cells.rs similarity index 90% rename from crates/typst/src/layout/grid/cells.rs rename to crates/typst-layout/src/grid/cells.rs index 64234aafb..175e21833 100644 --- a/crates/typst/src/layout/grid/cells.rs +++ b/crates/typst-layout/src/grid/cells.rs @@ -1,161 +1,89 @@ use std::num::NonZeroUsize; use std::sync::Arc; -use comemo::Track; use ecow::eco_format; - -use super::lines::Line; -use super::repeated::{Footer, Header, Repeatable}; -use crate::diag::{bail, At, Hint, HintedStrResult, HintedString, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ - Array, CastInfo, Content, Context, Fold, FromValue, Func, IntoValue, Reflect, - Resolve, Smart, StyleChain, Value, +use typst_library::diag::{bail, At, Hint, HintedStrResult, HintedString, SourceResult}; +use typst_library::engine::Engine; +use typst_library::foundations::{Content, Smart, StyleChain}; +use typst_library::introspection::Locator; +use typst_library::layout::{ + Abs, Alignment, Axes, Celled, Fragment, Length, Regions, Rel, ResolvedCelled, Sides, + Sizing, }; -use crate::introspection::Locator; -use crate::layout::{ - layout_fragment, Abs, Alignment, Axes, Fragment, Length, LinePosition, Regions, Rel, - Sides, Sizing, -}; -use crate::syntax::Span; -use crate::utils::NonZeroExt; -use crate::visualize::{Paint, Stroke}; +use typst_library::visualize::{Paint, Stroke}; +use typst_syntax::Span; +use typst_utils::NonZeroExt; -/// A value that can be configured per cell. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum Celled { - /// 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), -} +use super::{Footer, Header, Line, Repeatable}; -impl Celled { - /// Resolve the value based on the cell position. - pub fn resolve( - &self, - engine: &mut Engine, - styles: StyleChain, +/// 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, - ) -> SourceResult { - Ok(match self { - Self::Value(value) => value.clone(), - Self::Func(func) => func - .call(engine, Context::new(None, Some(styles)).track(), [x, y])? - .cast() - .at(func.span())?, - Self::Array(array) => x - .checked_rem(array.len()) - .and_then(|i| array.get(i)) - .cloned() - .unwrap_or_default(), - }) - } -} - -impl Default for Celled { - fn default() -> Self { - Self::Value(T::default()) - } -} - -impl Reflect for Celled { - 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 IntoValue for Celled { - 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 FromValue for Celled { - fn from_value(value: Value) -> HintedStrResult { - match value { - Value::Func(v) => Ok(Self::Func(v)), - Value::Array(array) => Ok(Self::Array( - array.into_iter().map(T::from_value).collect::>()?, - )), - v if T::castable(&v) => Ok(Self::Value(T::from_value(v)?)), - v => Err(Self::error(&v)), - } - } -} - -impl Fold for Celled { - fn fold(self, outer: Self) -> Self { - match (self, outer) { - (Self::Value(inner), Self::Value(outer)) => Self::Value(inner.fold(outer)), - (self_, _) => self_, - } - } -} - -impl Resolve for Celled { - type Output = ResolvedCelled; - - 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(Celled); - -impl ResolvedCelled -where - T: FromValue + Resolve, - ::Output: Default + Clone, -{ - /// Resolve the value based on the cell position. - pub fn resolve( - &self, - engine: &mut Engine, + fill: &Option, + align: Smart, + inset: Sides>>, + stroke: Sides>>>>, + breakable: bool, + locator: Locator<'a>, styles: StyleChain, - x: usize, - y: usize, - ) -> SourceResult { - Ok(match &self.0 { - Celled::Value(value) => value.clone(), - Celled::Func(func) => func - .call(engine, Context::new(None, Some(styles)).track(), [x, y])? - .cast::() - .at(func.span())? - .resolve(styles), - Celled::Array(array) => x - .checked_rem(array.len()) - .and_then(|i| array.get(i)) - .cloned() - .unwrap_or_default(), - }) - } + ) -> Cell<'a>; + + /// Returns this cell's column override. + fn x(&self, styles: StyleChain) -> Smart; + + /// Returns this cell's row override. + fn y(&self, styles: StyleChain) -> Smart; + + /// 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 item, possibly affected by automatic cell positioning. Can be either +/// a line or a cell. +pub enum ResolvableGridItem { + /// A horizontal line in the grid. + HLine { + /// The row above which the horizontal line is drawn. + y: Smart, + start: usize, + end: Option, + stroke: Option>>, + /// 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, + start: usize, + end: Option, + stroke: Option>>, + /// 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. @@ -221,12 +149,24 @@ impl<'a> Cell<'a> { if disambiguator > 0 { 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. -pub(super) enum Entry<'a> { +pub enum Entry<'a> { /// An entry which holds a cell. Cell(Cell<'a>), /// 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 { - /// A horizontal line in the grid. - HLine { - /// The row above which the horizontal line is drawn. - y: Smart, - start: usize, - end: Option, - stroke: Option>>, - /// 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, - start: usize, - end: Option, - stroke: Option>>, - /// 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. pub enum ResolvableGridChild { Header { repeat: bool, span: Span, items: I }, @@ -286,65 +193,28 @@ pub enum ResolvableGridChild { Item(ResolvableGridItem), } -/// 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, - align: Smart, - inset: Sides>>, - stroke: Sides>>>>, - breakable: bool, - locator: Locator<'a>, - styles: StyleChain, - ) -> Cell<'a>; - - /// Returns this cell's column override. - fn x(&self, styles: StyleChain) -> Smart; - - /// Returns this cell's row override. - fn y(&self, styles: StyleChain) -> Smart; - - /// 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. pub struct CellGrid<'a> { /// The grid cells. - pub(super) entries: Vec>, + pub entries: Vec>, /// The column tracks including gutter tracks. - pub(super) cols: Vec, + pub cols: Vec, /// The row tracks including gutter tracks. - pub(super) rows: Vec, + pub rows: Vec, /// The vertical lines before each column, or on the end border. /// Gutter columns are not included. /// Contains up to 'cols_without_gutter.len() + 1' vectors of lines. - pub(super) vlines: Vec>, + pub vlines: Vec>, /// The horizontal lines on top of each row, or on the bottom border. /// Gutter rows are not included. /// Contains up to 'rows_without_gutter.len() + 1' vectors of lines. - pub(super) hlines: Vec>, + pub hlines: Vec>, /// The repeatable header of this grid. - pub(super) header: Option>, + pub header: Option>, /// The repeatable footer of this grid. - pub(super) footer: Option>, + pub footer: Option>, /// Whether this grid has gutters. - pub(super) has_gutter: bool, + pub has_gutter: bool, } impl<'a> CellGrid<'a> { @@ -1125,7 +995,7 @@ impl<'a> CellGrid<'a> { } /// Generates the cell grid, given the tracks and resolved entries. - pub(super) fn new_internal( + pub fn new_internal( tracks: Axes<&[Sizing]>, gutter: Axes<&[Sizing]>, vlines: Vec>, @@ -1194,7 +1064,7 @@ impl<'a> CellGrid<'a> { /// /// Returns `None` if it's a gutter cell. #[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!(y < self.rows.len()); @@ -1216,7 +1086,7 @@ impl<'a> CellGrid<'a> { /// /// Returns `None` if it's a gutter cell or merged position. #[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) } @@ -1228,7 +1098,7 @@ impl<'a> CellGrid<'a> { /// - If it is a merged cell, returns the parent cell's position. /// - If it is a gutter cell, returns None. #[track_caller] - pub(super) fn parent_cell_position(&self, x: usize, y: usize) -> Option> { + pub fn parent_cell_position(&self, x: usize, y: usize) -> Option> { self.entry(x, y).map(|entry| match entry { Entry::Cell(_) => Axes::new(x, y), Entry::Merged { parent } => { @@ -1260,7 +1130,7 @@ impl<'a> CellGrid<'a> { /// position, if it is gutter), if it exists; otherwise returns None (it's /// gutter and no cell spans it). #[track_caller] - pub(super) fn effective_parent_cell_position( + pub fn effective_parent_cell_position( &self, x: usize, y: usize, @@ -1283,14 +1153,14 @@ impl<'a> CellGrid<'a> { /// Checks if the track with the given index is gutter. /// Does not check if the index is a valid track. #[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 } /// Returns the effective colspan of a cell, considering the gutters it /// might span if the grid has gutters. #[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 { 2 * cell.colspan.get() - 1 } else { @@ -1301,7 +1171,7 @@ impl<'a> CellGrid<'a> { /// Returns the effective rowspan of a cell, considering the gutters it /// might span if the grid has gutters. #[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 { 2 * cell.rowspan.get() - 1 } else { diff --git a/crates/typst/src/layout/grid/layout.rs b/crates/typst-layout/src/grid/layouter.rs similarity index 99% rename from crates/typst/src/layout/grid/layout.rs rename to crates/typst-layout/src/grid/layouter.rs index 53eda0f0a..7c94617dc 100644 --- a/crates/typst/src/layout/grid/layout.rs +++ b/crates/typst-layout/src/grid/layouter.rs @@ -1,22 +1,21 @@ use std::fmt::Debug; -use super::lines::{ - generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, LinePosition, - LineSegment, +use typst_library::diag::{bail, SourceResult}; +use typst_library::engine::Engine; +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 super::rowspans::{Rowspan, UnbreakableRowGroup}; -use crate::diag::{bail, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{Resolve, StyleChain}; -use crate::layout::{ - Abs, Axes, Cell, CellGrid, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, - Region, Regions, Rel, Size, Sizing, +use typst_library::text::TextElem; +use typst_library::visualize::Geometry; +use typst_syntax::Span; +use typst_utils::{MaybeReverseIter, Numeric}; + +use super::{ + generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Cell, CellGrid, + 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. pub struct GridLayouter<'a> { diff --git a/crates/typst/src/layout/grid/lines.rs b/crates/typst-layout/src/grid/lines.rs similarity index 98% rename from crates/typst/src/layout/grid/lines.rs rename to crates/typst-layout/src/grid/lines.rs index 660811c7b..3e89612a1 100644 --- a/crates/typst/src/layout/grid/lines.rs +++ b/crates/typst-layout/src/grid/lines.rs @@ -1,12 +1,11 @@ use std::num::NonZeroUsize; use std::sync::Arc; -use super::cells::CellGrid; -use super::layout::RowPiece; -use super::repeated::Repeatable; -use crate::foundations::{AlternativeFold, Fold}; -use crate::layout::Abs; -use crate::visualize::Stroke; +use typst_library::foundations::{AlternativeFold, Fold}; +use typst_library::layout::Abs; +use typst_library::visualize::Stroke; + +use super::{CellGrid, LinePosition, Repeatable, RowPiece}; /// Represents an explicit grid line (horizontal or vertical) specified by the /// user. @@ -38,22 +37,10 @@ pub struct Line { 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 /// on the highest priority configuration that defined the segment's stroke. #[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 /// stroke setting, so it should have the lowest priority. GridStroke = 0, @@ -71,19 +58,19 @@ pub(super) enum StrokePriority { /// Data for a particular line segment in the grid as generated by /// `generate_line_segments`. #[derive(Debug, PartialEq, Eq)] -pub(super) struct LineSegment { +pub struct LineSegment { /// The stroke with which to draw this segment. - pub(super) stroke: Arc>, + pub stroke: Arc>, /// 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 /// table in the current page; for a horizontal line segment, this is the /// offset since the start border of the table. - pub(super) offset: Abs, + pub offset: Abs, /// The length of this segment. - pub(super) length: Abs, + pub length: Abs, /// The segment's drawing priority, indicating on top of which other /// 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 @@ -119,7 +106,7 @@ pub(super) struct LineSegment { /// number, and they must be iterable over pairs of (number, size). For /// vertical lines, for instance, `tracks` would describe the rows in the /// 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, tracks: I, index: usize, @@ -269,7 +256,7 @@ where current_segment = 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 // current segment, as it was interrupted. offset += size; @@ -289,7 +276,9 @@ where // closure, the current segment will necessarily be 'None', // so the iterator will necessarily end (that is, we will return None) // 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 /// described in the docs for `generate_line_segment`. -pub(super) fn vline_stroke_at_row( +pub fn vline_stroke_at_row( grid: &CellGrid, x: 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 /// sorted by increasing `y`. -pub(super) fn hline_stroke_at_column( +pub fn hline_stroke_at_column( grid: &CellGrid, rows: &[RowPiece], local_top_y: Option, @@ -599,12 +588,14 @@ pub(super) fn hline_stroke_at_column( #[cfg(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::Cell; 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> { Cell { diff --git a/crates/typst-layout/src/grid/mod.rs b/crates/typst-layout/src/grid/mod.rs new file mode 100644 index 000000000..769bef8c5 --- /dev/null +++ b/crates/typst-layout/src/grid/mod.rs @@ -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, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, +) -> SourceResult { + 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, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, +) -> SourceResult { + 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> { + 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> { + 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 { + fn resolve_cell<'a>( + mut self, + x: usize, + y: usize, + fill: &Option, + align: Smart, + inset: Sides>>, + stroke: Sides>>>>, + 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 { + (**self).x(styles) + } + + fn y(&self, styles: StyleChain) -> Smart { + (**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 { + fn resolve_cell<'a>( + mut self, + x: usize, + y: usize, + fill: &Option, + align: Smart, + inset: Sides>>, + stroke: Sides>>>>, + 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 { + (**self).x(styles) + } + + fn y(&self, styles: StyleChain) -> Smart { + (**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) + } +} diff --git a/crates/typst/src/layout/grid/repeated.rs b/crates/typst-layout/src/grid/repeated.rs similarity index 92% rename from crates/typst/src/layout/grid/repeated.rs rename to crates/typst-layout/src/grid/repeated.rs index f187d0bc4..972179da8 100644 --- a/crates/typst/src/layout/grid/repeated.rs +++ b/crates/typst-layout/src/grid/repeated.rs @@ -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 crate::diag::SourceResult; -use crate::engine::Engine; -use crate::layout::{Abs, Axes, Frame, GridLayouter, Regions}; /// 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. - pub(super) end: usize, + pub end: usize, } /// A repeatable grid footer. Stops at the last row. -pub(super) struct Footer { +pub struct Footer { /// The first row included in this footer. - pub(super) start: usize, + pub start: usize, } /// A possibly repeatable grid object. /// It still exists even when not repeatable, but must not have additional /// considerations by grid layout, other than for consistency (such as making /// a certain group of rows unbreakable). -pub(super) enum Repeatable { +pub enum Repeatable { Repeated(T), NotRepeated(T), } @@ -27,7 +29,7 @@ pub(super) enum Repeatable { impl Repeatable { /// Gets the value inside this repeatable, regardless of whether /// it repeats. - pub(super) fn unwrap(&self) -> &T { + pub fn unwrap(&self) -> &T { match self { Self::Repeated(repeated) => repeated, Self::NotRepeated(not_repeated) => not_repeated, @@ -35,7 +37,7 @@ impl Repeatable { } /// 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 { Self::Repeated(repeated) => Some(repeated), Self::NotRepeated(_) => None, @@ -46,7 +48,7 @@ impl Repeatable { impl<'a> GridLayouter<'a> { /// Layouts the header's rows. /// Skips regions as necessary. - pub(super) fn layout_header( + pub fn layout_header( &mut self, header: &Header, engine: &mut Engine, @@ -90,7 +92,7 @@ impl<'a> GridLayouter<'a> { } /// Simulate the header's group of rows. - pub(super) fn simulate_header( + pub fn simulate_header( &self, header: &Header, regions: &Regions<'_>, @@ -112,7 +114,7 @@ impl<'a> GridLayouter<'a> { } /// Updates `self.footer_height` by simulating the footer, and skips to fitting region. - pub(super) fn prepare_footer( + pub fn prepare_footer( &mut self, footer: &Footer, engine: &mut Engine, @@ -146,7 +148,7 @@ impl<'a> GridLayouter<'a> { /// Lays out all rows in the footer. /// They are unbreakable. - pub(super) fn layout_footer( + pub fn layout_footer( &mut self, footer: &Footer, engine: &mut Engine, @@ -167,7 +169,7 @@ impl<'a> GridLayouter<'a> { } // Simulate the footer's group of rows. - pub(super) fn simulate_footer( + pub fn simulate_footer( &self, footer: &Footer, regions: &Regions<'_>, diff --git a/crates/typst/src/layout/grid/rowspans.rs b/crates/typst-layout/src/grid/rowspans.rs similarity index 97% rename from crates/typst/src/layout/grid/rowspans.rs rename to crates/typst-layout/src/grid/rowspans.rs index 6381ba668..03b4103fd 100644 --- a/crates/typst/src/layout/grid/rowspans.rs +++ b/crates/typst-layout/src/grid/rowspans.rs @@ -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 crate::diag::SourceResult; -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; +use super::{Cell, GridLayouter}; /// All information needed to layout a single rowspan. -pub(super) struct Rowspan { +pub struct Rowspan { /// First column of this rowspan. - pub(super) x: usize, + pub x: usize, /// First row of this rowspan. - pub(super) y: usize, + pub y: usize, /// The disambiguator for laying out the cells. - pub(super) disambiguator: usize, + pub disambiguator: usize, /// 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. /// This is true e.g. in headers and footers, regardless of what the user /// 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. - pub(super) dx: Abs, + pub dx: Abs, /// 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. - pub(super) first_region: usize, + pub first_region: usize, /// The full height in the first region this rowspan appears in, for /// relative sizing. - pub(super) region_full: Abs, + pub region_full: Abs, /// The vertical space available for this rowspan in each region. - pub(super) heights: Vec, + pub heights: Vec, /// The index of the largest resolved spanned row so far. /// 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 /// header, will no longer contribute height to this rowspan. /// /// This is `None` if no spanned rows were resolved in `finish_region` yet. - pub(super) max_resolved_row: Option, + pub max_resolved_row: Option, } /// The output of the simulation of an unbreakable row group. #[derive(Default)] -pub(super) struct UnbreakableRowGroup { +pub struct UnbreakableRowGroup { /// The rows in this group of unbreakable rows. /// 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. - pub(super) height: Abs, + pub height: Abs, } /// 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. - pub(super) width: Abs, + pub width: Abs, /// The available height for the cell in its first region. /// 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. /// /// When this is `None`, the `custom_backlog` field should be used instead. /// That's because, otherwise, this field would have to contain a reference /// to the `custom_backlog` field, which isn't possible in Rust without /// 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 /// 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 /// backlog), then this vector will store the new backlog. - pub(super) custom_backlog: Vec, + pub custom_backlog: Vec, /// The full height of the first region of the cell. /// 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, /// if any. - pub(super) last: Option, + pub last: Option, /// The total height of previous rows spanned by the cell in the current /// 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. /// They are skipped for measurement purposes. - pub(super) frames_in_previous_regions: usize, + pub frames_in_previous_regions: usize, } 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 /// spanned rows, which is only possible after laying out the last row /// spanned by the rowspan (or some row immediately after the last one). - pub(super) fn layout_rowspan( + pub fn layout_rowspan( &mut self, rowspan_data: Rowspan, 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. /// 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. // For that reason, we must reverse the column order when using 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 /// enough space for them. This can be needed, for example, if there's an /// unbreakable rowspan crossing those rows. - pub(super) fn check_for_unbreakable_rows( + pub fn check_for_unbreakable_rows( &mut self, current_row: usize, engine: &mut Engine, @@ -292,7 +292,7 @@ impl<'a> GridLayouter<'a> { /// /// This is used to figure out how much height the next unbreakable row /// group (if any) needs. - pub(super) fn simulate_unbreakable_row_group( + pub fn simulate_unbreakable_row_group( &self, first_row: usize, amount_unbreakable_rows: Option, @@ -359,7 +359,7 @@ impl<'a> GridLayouter<'a> { /// Checks if one or more of the cells at the given row are unbreakable. /// If so, returns the largest rowspan among the unbreakable cells; /// 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()) .filter_map(|x| self.grid.cell(x, y)) .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. - pub(super) fn prepare_auto_row_cell_measurement( + pub fn prepare_auto_row_cell_measurement( &self, parent: Axes, cell: &Cell, @@ -577,7 +577,7 @@ impl<'a> GridLayouter<'a> { /// expand the auto row based on the rowspan's demanded size, or `false` /// otherwise. #[allow(clippy::too_many_arguments)] - pub(super) fn prepare_rowspan_sizes( + pub fn prepare_rowspan_sizes( &self, auto_row_y: usize, sizes: &mut Vec, @@ -667,7 +667,7 @@ impl<'a> GridLayouter<'a> { /// in each region and the pending rowspans' data (parent Y, rowspan amount /// and vector of requested sizes). #[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, y: usize, resolved: &mut Vec, diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs new file mode 100644 index 000000000..84a602823 --- /dev/null +++ b/crates/typst-layout/src/image.rs @@ -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, + engine: &mut Engine, + _: Locator, + styles: StyleChain, + region: Region, +) -> SourceResult { + 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(">(), + ) + .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 { + 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"), + }, + }, + }) +} diff --git a/crates/typst-layout/src/inline/box.rs b/crates/typst-layout/src/inline/box.rs new file mode 100644 index 000000000..30572e4e6 --- /dev/null +++ b/crates/typst-layout/src/inline/box.rs @@ -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, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Size, +) -> SourceResult { + // 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) +} diff --git a/crates/typst/src/layout/inline/collect.rs b/crates/typst-layout/src/inline/collect.rs similarity index 97% rename from crates/typst/src/layout/inline/collect.rs rename to crates/typst-layout/src/inline/collect.rs index af14b1527..fbcddee5c 100644 --- a/crates/typst/src/layout/inline/collect.rs +++ b/crates/typst-layout/src/inline/collect.rs @@ -1,17 +1,18 @@ -use super::*; -use crate::diag::bail; -use crate::foundations::{Packed, Resolve}; -use crate::introspection::{SplitLocator, Tag, TagElem}; -use crate::layout::{ +use typst_library::diag::bail; +use typst_library::foundations::{Packed, Resolve}; +use typst_library::introspection::{SplitLocator, Tag, TagElem}; +use typst_library::layout::{ Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing, Spacing, }; -use crate::syntax::Span; -use crate::text::{ +use typst_library::text::{ is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes, 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 // paragraph's full text. @@ -222,7 +223,7 @@ pub fn collect<'a>( if let Sizing::Fr(v) = elem.width(styles) { collector.push_item(Item::Fractional(v, Some((elem, loc, styles)))); } 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)); } } else if let Some(elem) = child.to_packed::() { diff --git a/crates/typst-layout/src/inline/deco.rs b/crates/typst-layout/src/inline/deco.rs new file mode 100644 index 000000000..c01b369b2 --- /dev/null +++ b/crates/typst-layout/src/inline/deco.rs @@ -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(); + } +} diff --git a/crates/typst/src/layout/inline/finalize.rs b/crates/typst-layout/src/inline/finalize.rs similarity index 92% rename from crates/typst/src/layout/inline/finalize.rs rename to crates/typst-layout/src/inline/finalize.rs index 082e36137..599ace9de 100644 --- a/crates/typst/src/layout/inline/finalize.rs +++ b/crates/typst-layout/src/inline/finalize.rs @@ -1,6 +1,7 @@ +use typst_library::introspection::SplitLocator; +use typst_utils::Numeric; + use super::*; -use crate::introspection::SplitLocator; -use crate::utils::Numeric; /// Turns the selected lines into frames. #[typst_macros::time] diff --git a/crates/typst/src/layout/inline/line.rs b/crates/typst-layout/src/inline/line.rs similarity index 98% rename from crates/typst/src/layout/inline/line.rs rename to crates/typst-layout/src/inline/line.rs index a512c32d9..596e109ee 100644 --- a/crates/typst/src/layout/inline/line.rs +++ b/crates/typst-layout/src/inline/line.rs @@ -1,14 +1,15 @@ use std::fmt::{self, Debug, Formatter}; 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 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 HYPHEN: char = '-'; @@ -510,7 +511,7 @@ pub fn commit( if let Some((elem, loc, styles)) = elem { let region = Size::new(amount, full); 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))); push(&mut offset, frame.post_processed(*styles)); } 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 // and to be valid (to have a location). 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); marker.set_location(loc); diff --git a/crates/typst/src/layout/inline/linebreak.rs b/crates/typst-layout/src/inline/linebreak.rs similarity index 99% rename from crates/typst/src/layout/inline/linebreak.rs rename to crates/typst-layout/src/inline/linebreak.rs index aa62d487c..7fc8b3683 100644 --- a/crates/typst/src/layout/inline/linebreak.rs +++ b/crates/typst-layout/src/inline/linebreak.rs @@ -8,14 +8,14 @@ use icu_provider_adapters::fork::ForkByKeyProvider; use icu_provider_blob::BlobDataProvider; use icu_segmenter::LineSegmenter; 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 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. type Cost = f64; diff --git a/crates/typst/src/layout/inline/mod.rs b/crates/typst-layout/src/inline/mod.rs similarity index 80% rename from crates/typst/src/layout/inline/mod.rs rename to crates/typst-layout/src/inline/mod.rs index 275cb3326..658e30846 100644 --- a/crates/typst/src/layout/inline/mod.rs +++ b/crates/typst-layout/src/inline/mod.rs @@ -1,13 +1,27 @@ +#[path = "box.rs"] +mod box_; mod collect; +mod deco; mod finalize; mod line; mod linebreak; mod prepare; mod shaping; +pub use self::box_::layout_box; + 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::deco::decorate; use self::finalize::finalize; use self::line::{commit, line, Line}; use self::linebreak::{linebreak, Breakpoint}; @@ -16,19 +30,12 @@ use self::shaping::{ cjk_punct_style, is_of_cj_script, shape_range, ShapedGlyph, ShapedText, 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. type Range = std::ops::Range; /// Layouts content inline. -pub(crate) fn layout_inline( +pub fn layout_inline( engine: &mut Engine, children: &StyleVec, locator: Locator, @@ -39,6 +46,7 @@ pub(crate) fn layout_inline( ) -> SourceResult { layout_inline_impl( children, + engine.routines, engine.world, engine.introspector, engine.traced, @@ -57,6 +65,7 @@ pub(crate) fn layout_inline( #[allow(clippy::too_many_arguments)] fn layout_inline_impl( children: &StyleVec, + routines: &Routines, world: Tracked, introspector: Tracked, traced: Tracked, @@ -71,6 +80,7 @@ fn layout_inline_impl( let link = LocatorLink::new(locator); let locator = Locator::link(&link); let mut engine = Engine { + routines, world, introspector, traced, diff --git a/crates/typst/src/layout/inline/prepare.rs b/crates/typst-layout/src/inline/prepare.rs similarity index 97% rename from crates/typst/src/layout/inline/prepare.rs rename to crates/typst-layout/src/inline/prepare.rs index 3ac155b5e..2dd79aecf 100644 --- a/crates/typst/src/layout/inline/prepare.rs +++ b/crates/typst-layout/src/inline/prepare.rs @@ -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 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 /// is already preshaped. diff --git a/crates/typst/src/layout/inline/shaping.rs b/crates/typst-layout/src/inline/shaping.rs similarity index 98% rename from crates/typst/src/layout/inline/shaping.rs rename to crates/typst-layout/src/inline/shaping.rs index 5b95dadb7..bd803b521 100644 --- a/crates/typst/src/layout/inline/shaping.rs +++ b/crates/typst-layout/src/inline/shaping.rs @@ -7,19 +7,19 @@ use az::SaturatingAs; use ecow::EcoString; use rustybuzz::{BufferFlags, ShapePlan, UnicodeBuffer}; 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_script::{Script, UnicodeScript}; -use super::{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; +use super::{decorate, Item, Range, SpanMapper}; /// The result of shaping text. /// diff --git a/crates/typst-layout/src/lib.rs b/crates/typst-layout/src/lib.rs new file mode 100644 index 000000000..7069fc4dd --- /dev/null +++ b/crates/typst-layout/src/lib.rs @@ -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}; diff --git a/crates/typst-layout/src/lists.rs b/crates/typst-layout/src/lists.rs new file mode 100644 index 000000000..08c2a2f45 --- /dev/null +++ b/crates/typst-layout/src/lists.rs @@ -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, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, +) -> SourceResult { + 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, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, +) -> SourceResult { + 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) +} diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs new file mode 100644 index 000000000..9fa7a5a08 --- /dev/null +++ b/crates/typst-layout/src/math/accent.rs @@ -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, + 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(()) +} diff --git a/crates/typst/src/math/attach.rs b/crates/typst-layout/src/math/attach.rs similarity index 60% rename from crates/typst/src/math/attach.rs rename to crates/typst-layout/src/math/attach.rs index 9eb0c824c..0f9090f77 100644 --- a/crates/typst/src/math/attach.rs +++ b/crates/typst-layout/src/math/attach.rs @@ -1,14 +1,16 @@ -use unicode_math_class::MathClass; - -use crate::diag::SourceResult; -use crate::foundations::{elem, Content, Packed, Smart, StyleChain}; -use crate::layout::{Abs, Axis, Corner, Frame, Length, Point, Rel, Size}; -use crate::math::{ - stretch_fragment, style_for_subscript, style_for_superscript, EquationElem, - FrameFragment, LayoutMath, MathContext, MathFragment, MathSize, Scaled, StretchElem, +use typst_library::diag::SourceResult; +use typst_library::foundations::{Packed, Smart, StyleChain}; +use typst_library::layout::{Abs, Axis, Corner, Frame, Length, Point, Rel, Size}; +use typst_library::math::{ + AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem, +}; +use typst_library::text::TextElem; +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 { ($e: ident, $attr: ident) => { @@ -16,300 +18,140 @@ macro_rules! measure { }; } -/// A base with optional attachments. -/// -/// ```example -/// $ attach( -/// Pi, t: alpha, b: beta, -/// tl: 1, tr: 2+3, bl: 4+5, br: 6, -/// ) $ -/// ``` -#[elem(LayoutMath)] -pub struct AttachElem { - /// The base to which things are attached. - #[required] - pub base: Content, +/// Lays out an [`AttachElem`]. +#[typst_macros::time(name = "math.attach", span = elem.span())] +pub fn layout_attach( + elem: &Packed, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + let merged = elem.merge_base(); + let elem = merged.as_ref().unwrap_or(elem); + let stretch = stretch_size(styles, elem); - /// 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, + let mut base = ctx.layout_into_fragment(elem.base(), styles)?; + let sup_style = style_for_superscript(styles); + let sup_style_chain = styles.chain(&sup_style); + let tl = elem.tl(sup_style_chain); + let tr = elem.tr(sup_style_chain); + let primed = tr.as_ref().is_some_and(|content| content.is::()); + let t = elem.t(sup_style_chain); - /// 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, + let sub_style = style_for_subscript(styles); + let sub_style_chain = styles.chain(&sub_style); + let bl = elem.bl(sub_style_chain); + let br = elem.br(sub_style_chain); + let b = elem.b(sub_style_chain); - /// The top-left attachment (before the base). - pub tl: Option, + let limits = base.limits().active(styles); + let (t, tr) = match (t, tr) { + (Some(t), Some(tr)) if primed && !limits => (None, Some(tr + t)), + (Some(t), None) if !limits => (None, Some(t)), + (t, tr) => (t, tr), + }; + let (b, br) = if limits || br.is_some() { (b, br) } else { (None, b) }; - /// The bottom-left attachment (before base). - pub bl: Option, - - /// The top-right attachment (after the base). - pub tr: Option, - - /// The bottom-right attachment (after the base). - pub br: Option, -} - -impl LayoutMath for Packed { - #[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 mut base = ctx.layout_into_fragment(elem.base(), styles)?; - let sup_style = style_for_superscript(styles); - let sup_style_chain = styles.chain(&sup_style); - let tl = elem.tl(sup_style_chain); - let tr = elem.tr(sup_style_chain); - let primed = tr.as_ref().is_some_and(|content| content.is::()); - let t = elem.t(sup_style_chain); - - let sub_style = style_for_subscript(styles); - let sub_style_chain = styles.chain(&sub_style); - let bl = elem.bl(sub_style_chain); - let br = elem.br(sub_style_chain); - let b = elem.b(sub_style_chain); - - let limits = base.limits().active(styles); - let (t, tr) = match (t, tr) { - (Some(t), Some(tr)) if primed && !limits => (None, Some(tr + t)), - (Some(t), None) if !limits => (None, Some(t)), - (t, tr) => (t, tr), + macro_rules! layout { + ($content:ident, $style_chain:ident) => { + $content + .map(|elem| ctx.layout_into_fragment(&elem, $style_chain)) + .transpose() }; - let (b, br) = if limits || br.is_some() { (b, br) } else { (None, b) }; + } - macro_rules! layout { - ($content:ident, $style_chain:ident) => { - $content - .map(|elem| ctx.layout_into_fragment(&elem, $style_chain)) - .transpose() + // Layout the top and bottom attachments early so we can measure their + // widths, in order to calculate what the stretch size is relative to. + let t = layout!(t, sup_style_chain)?; + let b = layout!(b, sub_style_chain)?; + if let Some(stretch) = stretch { + let relative_to_width = measure!(t, width).max(measure!(b, width)); + stretch_fragment( + ctx, + styles, + &mut base, + Some(Axis::X), + Some(relative_to_width), + stretch, + Abs::zero(), + ); + } + + let fragments = [ + layout!(tl, sup_style_chain)?, + t, + layout!(tr, sup_style_chain)?, + layout!(bl, sub_style_chain)?, + b, + layout!(br, sub_style_chain)?, + ]; + + layout_attachments(ctx, styles, base, fragments) +} + +/// Lays out a [`PrimeElem`]. +#[typst_macros::time(name = "math.primes", span = elem.span())] +pub fn layout_primes( + elem: &Packed, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + match *elem.count() { + count @ 1..=4 => { + let c = match count { + 1 => '′', + 2 => '″', + 3 => '‴', + 4 => '⁗', + _ => unreachable!(), }; + let f = ctx.layout_into_fragment(&TextElem::packed(c), styles)?; + ctx.push(f); } + count => { + // Custom amount of primes + let prime = + ctx.layout_into_fragment(&TextElem::packed('′'), styles)?.into_frame(); + let width = prime.width() * (count + 1) as f64 / 2.0; + let mut frame = Frame::soft(Size::new(width, prime.height())); + frame.set_baseline(prime.ascent()); - // Layout the top and bottom attachments early so we can measure their - // widths, in order to calculate what the stretch size is relative to. - let t = layout!(t, sup_style_chain)?; - let b = layout!(b, sub_style_chain)?; - if let Some(stretch) = stretch { - let relative_to_width = measure!(t, width).max(measure!(b, width)); - stretch_fragment( - ctx, - styles, - &mut base, - Some(Axis::X), - Some(relative_to_width), - stretch, - Abs::zero(), - ); - } - - let fragments = [ - layout!(tl, sup_style_chain)?, - t, - layout!(tr, sup_style_chain)?, - layout!(bl, sub_style_chain)?, - b, - layout!(br, sub_style_chain)?, - ]; - - layout_attachments(ctx, styles, base, fragments) - } -} - -/// Grouped primes. -/// -/// ```example -/// $ a'''_b = a^'''_b $ -/// ``` -/// -/// # Syntax -/// This function has dedicated syntax: use apostrophes instead of primes. They -/// 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 { - #[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 => { - let c = match count { - 1 => '′', - 2 => '″', - 3 => '‴', - 4 => '⁗', - _ => unreachable!(), - }; - let f = ctx.layout_into_fragment(&TextElem::packed(c), styles)?; - ctx.push(f); + for i in 0..count { + frame.push_frame( + Point::new(prime.width() * (i as f64 / 2.0), Abs::zero()), + prime.clone(), + ) } - count => { - // Custom amount of primes - let prime = ctx - .layout_into_fragment(&TextElem::packed('′'), styles)? - .into_frame(); - let width = prime.width() * (count + 1) as f64 / 2.0; - let mut frame = Frame::soft(Size::new(width, prime.height())); - frame.set_baseline(prime.ascent()); - - for i in 0..count { - frame.push_frame( - Point::new(prime.width() * (i as f64 / 2.0), Abs::zero()), - prime.clone(), - ) - } - ctx.push(FrameFragment::new(ctx, styles, frame).with_text_like(true)); - } - } - Ok(()) - } -} - -/// Forces a base to display attachments as scripts. -/// -/// ```example -/// $ scripts(sum)_1^2 != sum_1^2 $ -/// ``` -#[elem(LayoutMath)] -pub struct ScriptsElem { - /// The base to attach the scripts to. - #[required] - pub body: Content, -} - -impl LayoutMath for Packed { - #[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); - ctx.push(fragment); - Ok(()) - } -} - -/// Forces a base to display attachments as limits. -/// -/// ```example -/// $ limits(A)_1^2 != A_1^2 $ -/// ``` -#[elem(LayoutMath)] -pub struct LimitsElem { - /// The base to attach the limits to. - #[required] - 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 { - #[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); - ctx.push(fragment); - 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, + ctx.push(FrameFragment::new(ctx, styles, frame).with_text_like(true)); } } + Ok(()) } -/// If an AttachElem's base is also an AttachElem, merge attachments into the -/// base AttachElem where possible. -fn merge_base(elem: &Packed) -> Option> { - // Extract from an EquationElem. - let mut base = elem.base(); - if let Some(equation) = base.to_packed::() { - base = equation.body(); - } +/// Lays out a [`ScriptsElem`]. +#[typst_macros::time(name = "math.scripts", span = elem.span())] +pub fn layout_scripts( + elem: &Packed, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?; + fragment.set_limits(Limits::Never); + ctx.push(fragment); + Ok(()) +} - // Move attachments from elem into base where possible. - if let Some(base) = base.to_packed::() { - 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 +/// Lays out a [`LimitsElem`]. +#[typst_macros::time(name = "math.limits", span = elem.span())] +pub fn layout_limits( + elem: &Packed, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + let limits = if elem.inline(styles) { Limits::Always } else { Limits::Display }; + let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?; + fragment.set_limits(limits); + ctx.push(fragment); + Ok(()) } /// Get the size to stretch the base to, if the attach argument is true. @@ -326,7 +168,7 @@ fn stretch_size( base.to_packed::().map(|stretch| stretch.size(styles)) } -/// Layout the attachments. +/// Lay out the attachments. fn layout_attachments( ctx: &mut MathContext, styles: StyleChain, @@ -671,8 +513,3 @@ fn math_kern( // result in glyphs colliding. 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) -} diff --git a/crates/typst-layout/src/math/cancel.rs b/crates/typst-layout/src/math/cancel.rs new file mode 100644 index 000000000..994e0e2f7 --- /dev/null +++ b/crates/typst-layout/src/math/cancel.rs @@ -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, + 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, + stroke: FixedStroke, + invert: bool, + angle: &Smart, + body_size: Size, + styles: StyleChain, + span: Span, +) -> SourceResult { + 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) +} diff --git a/crates/typst/src/math/frac.rs b/crates/typst-layout/src/math/frac.rs similarity index 59% rename from crates/typst/src/math/frac.rs rename to crates/typst-layout/src/math/frac.rs index 744a15c5e..50686333f 100644 --- a/crates/typst/src/math/frac.rs +++ b/crates/typst-layout/src/math/frac.rs @@ -1,90 +1,47 @@ -use crate::diag::{bail, SourceResult}; -use crate::foundations::{elem, Content, Packed, StyleChain, Value}; -use crate::layout::{Em, Frame, FrameItem, Point, Size}; -use crate::math::{ +use typst_library::diag::SourceResult; +use typst_library::foundations::{Content, Packed, StyleChain}; +use typst_library::layout::{Em, Frame, FrameItem, Point, Size}; +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, - 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); -/// A mathematical fraction. -/// -/// # Example -/// ```example -/// $ 1/2 < (x+1)/2 $ -/// $ ((x+1)) / 2 = frac(a, b) $ -/// ``` -/// -/// # 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, +/// Lays out a [`FracElem`]. +#[typst_macros::time(name = "math.frac", span = elem.span())] +pub fn layout_frac( + elem: &Packed, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + layout_frac_like( + ctx, + styles, + elem.num(), + std::slice::from_ref(elem.denom()), + false, + elem.span(), + ) } -impl LayoutMath for Packed { - #[typst_macros::time(name = "math.frac", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - layout( - ctx, - styles, - self.num(), - std::slice::from_ref(self.denom()), - false, - self.span(), - ) - } -} - -/// A binomial expression. -/// -/// # Example -/// ```example -/// $ binom(n, k) $ -/// $ binom(n, k_1, k_2, k_3, ..., k_m) $ -/// ``` -#[elem(title = "Binomial", LayoutMath)] -pub struct BinomElem { - /// The binomial's upper index. - #[required] - pub upper: Content, - - /// The binomial's lower index. - #[required] - #[variadic] - #[parse( - let values = args.all::>()?; - 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, -} - -impl LayoutMath for Packed { - #[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()) - } +/// Lays out a [`BinomElem`]. +#[typst_macros::time(name = "math.binom", span = elem.span())] +pub fn layout_binom( + elem: &Packed, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + layout_frac_like(ctx, styles, elem.upper(), elem.lower(), true, elem.span()) } /// Layout a fraction or binomial. -fn layout( +fn layout_frac_like( ctx: &mut MathContext, styles: StyleChain, num: &Content, diff --git a/crates/typst/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs similarity index 78% rename from crates/typst/src/math/fragment.rs rename to crates/typst-layout/src/math/fragment.rs index a3fcc9c64..19a4494ef 100644 --- a/crates/typst/src/math/fragment.rs +++ b/crates/typst-layout/src/math/fragment.rs @@ -1,22 +1,25 @@ use std::fmt::{self, Debug, Formatter}; +use rustybuzz::Feature; 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 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 crate::foundations::StyleChain; -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; +use super::{scaled_font_size, stretch_glyph, MathContext, Scaled}; #[derive(Debug, Clone)] pub enum MathFragment { @@ -350,7 +353,6 @@ impl GlyphFragment { pub fn into_variant(self) -> VariantFragment { VariantFragment { c: self.c, - id: Some(self.id), font_size: self.font_size, italics_correction: self.italics_correction, accent_attach: self.accent_attach, @@ -406,6 +408,26 @@ impl GlyphFragment { 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 { @@ -417,7 +439,6 @@ impl Debug for GlyphFragment { #[derive(Clone)] pub struct VariantFragment { pub c: char, - pub id: Option, pub italics_correction: Abs, pub accent_attach: Abs, pub frame: Frame, @@ -582,3 +603,102 @@ fn kern_at_height( 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 { + 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::(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 { + 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) + } +} diff --git a/crates/typst-layout/src/math/lr.rs b/crates/typst-layout/src/math/lr.rs new file mode 100644 index 000000000..aba9012f2 --- /dev/null +++ b/crates/typst-layout/src/math/lr.rs @@ -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, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + let mut body = elem.body(); + + // Extract from an EquationElem. + if let Some(equation) = body.to_packed::() { + body = equation.body(); + } + + // Extract implicit LrElem. + if let Some(lr) = body.to_packed::() { + 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, + 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>, + apply: Option, +) { + 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); + } + } +} diff --git a/crates/typst-layout/src/math/mat.rs b/crates/typst-layout/src/math/mat.rs new file mode 100644 index 000000000..6c8b04553 --- /dev/null +++ b/crates/typst-layout/src/math/mat.rs @@ -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, + 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, + 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, + 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, + alternator: LeftRightAlternator, +) -> SourceResult { + 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], + align: FixedAlignment, + augment: Option>, + gap: Axes>, + span: Span, +) -> SourceResult { + 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::() + 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::() + + 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, + right: Option, + 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(()) +} diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs new file mode 100644 index 000000000..b3dde977c --- /dev/null +++ b/crates/typst-layout/src/math/mod.rs @@ -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, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + region: Size, +) -> SourceResult> { + 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, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, +) -> SourceResult { + 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 { + 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, + 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, + equation_align: FixedAlignment, + width: Abs, + is_multiline: bool, + [first, last]: [(Axes, 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::::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::::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>, + glyphwise_tables: Option>>, + space_width: Em, + // Mutable. + fragments: Vec, +} + +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::(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) { + self.fragments.push(fragment.into()); + } + + /// Push multiple fragments. + fn extend(&mut self, fragments: impl IntoIterator) { + 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 { + 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> { + // 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 { + 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 { + 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::() { + ctx.push(MathFragment::Tag(elem.tag.clone())); + } else if elem.is::() { + let font_size = scaled_font_size(ctx, styles); + ctx.push(MathFragment::Space(ctx.space_width.at(font_size))); + } else if elem.is::() { + ctx.push(MathFragment::Linebreak); + } else if let Some(elem) = elem.to_packed::() { + layout_h(elem, ctx, styles)?; + } else if let Some(elem) = elem.to_packed::() { + self::text::layout_text(elem, ctx, styles)?; + } else if let Some(elem) = elem.to_packed::() { + layout_box(elem, ctx, styles)?; + } else if elem.is::() { + ctx.push(MathFragment::Align); + } else if let Some(elem) = elem.to_packed::() { + layout_class(elem, ctx, styles)?; + } else if let Some(elem) = elem.to_packed::() { + self::accent::layout_accent(elem, ctx, styles)?; + } else if let Some(elem) = elem.to_packed::() { + self::attach::layout_attach(elem, ctx, styles)?; + } else if let Some(elem) = elem.to_packed::() { + self::attach::layout_primes(elem, ctx, styles)?; + } else if let Some(elem) = elem.to_packed::() { + self::attach::layout_scripts(elem, ctx, styles)?; + } else if let Some(elem) = elem.to_packed::() { + self::attach::layout_limits(elem, ctx, styles)?; + } else if let Some(elem) = elem.to_packed::() { + self::cancel::layout_cancel(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::() { + self::frac::layout_frac(elem, ctx, styles)?; + } else if let Some(elem) = elem.to_packed::() { + self::frac::layout_binom(elem, ctx, styles)?; + } else if let Some(elem) = elem.to_packed::() { + self::lr::layout_lr(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::() { + self::lr::layout_mid(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::() { + self::mat::layout_vec(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::() { + self::mat::layout_mat(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::() { + self::mat::layout_cases(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::() { + layout_op(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::() { + self::root::layout_root(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::() { + self::stretch::layout_stretch(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::() { + self::underover::layout_underline(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::() { + self::underover::layout_overline(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::() { + self::underover::layout_underbrace(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::() { + self::underover::layout_overbrace(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::() { + self::underover::layout_underbracket(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::() { + self::underover::layout_overbracket(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::() { + self::underover::layout_underparen(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::() { + self::underover::layout_overparen(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::() { + self::underover::layout_undershell(elem, ctx, styles)? + } else if let Some(elem) = elem.to_packed::() { + 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::()), + ); + } + + Ok(()) +} + +/// Lays out an [`BoxElem`]. +fn layout_box( + elem: &Packed, + 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, + 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, + 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, + 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 { + 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, + ) +} diff --git a/crates/typst/src/math/root.rs b/crates/typst-layout/src/math/root.rs similarity index 74% rename from crates/typst/src/math/root.rs rename to crates/typst-layout/src/math/root.rs index 66707c59d..0bb2f5393 100644 --- a/crates/typst/src/math/root.rs +++ b/crates/typst-layout/src/math/root.rs @@ -1,63 +1,26 @@ -use crate::diag::SourceResult; -use crate::foundations::{elem, func, Content, NativeElement, Packed, StyleChain}; -use crate::layout::{Abs, Frame, FrameItem, Point, Size}; -use crate::math::{ - style_cramped, EquationElem, FrameFragment, GlyphFragment, LayoutMath, MathContext, - MathSize, Scaled, -}; -use crate::syntax::Span; -use crate::text::TextElem; -use crate::visualize::{FixedStroke, Geometry}; +use typst_library::diag::SourceResult; +use typst_library::foundations::{Packed, StyleChain}; +use typst_library::layout::{Abs, Frame, FrameItem, Point, Size}; +use typst_library::math::{EquationElem, MathSize, RootElem}; +use typst_library::text::TextElem; +use typst_library::visualize::{FixedStroke, Geometry}; -/// A square root. -/// -/// ```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) -} +use super::{style_cramped, FrameFragment, GlyphFragment, MathContext}; -/// A general root. -/// -/// ```example -/// $ root(3, x) $ -/// ``` -#[elem(LayoutMath)] -pub struct RootElem { - /// Which root of the radicand to take. - #[positional] - pub index: Option, - - /// The expression to take the root of. - #[required] - pub radicand: Content, -} - -impl LayoutMath for Packed { - #[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. +/// Lays out a [`RootElem`]. /// /// TeXbook page 443, page 360 /// See also: -fn layout( +#[typst_macros::time(name = "math.root", span = elem.span())] +pub fn layout_root( + elem: &Packed, ctx: &mut MathContext, styles: StyleChain, - index: Option<&Content>, - radicand: &Content, - span: Span, ) -> SourceResult<()> { + let index = elem.index(styles); + let radicand = elem.radicand(); + let span = elem.span(); + let gap = scaled!( ctx, styles, text: radical_vertical_gap, @@ -94,6 +57,7 @@ fn layout( // Layout the index. let sscript = EquationElem::set_size(MathSize::ScriptScript).wrap(); let index = index + .as_ref() .map(|elem| ctx.layout_into_frame(elem, styles.chain(&sscript))) .transpose()?; diff --git a/crates/typst/src/math/row.rs b/crates/typst-layout/src/math/run.rs similarity index 86% rename from crates/typst/src/math/row.rs rename to crates/typst-layout/src/math/run.rs index a8422b1e6..8f12c5098 100644 --- a/crates/typst/src/math/row.rs +++ b/crates/typst-layout/src/math/run.rs @@ -1,16 +1,14 @@ 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 crate::foundations::{Resolve, StyleChain}; -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; +use super::{alignments, scaled_font_size, FrameFragment, MathContext, MathFragment}; -pub const TIGHT_LEADING: Em = Em::new(0.25); +const TIGHT_LEADING: Em = Em::new(0.25); /// A linear collection of [`MathFragment`]s. #[derive(Debug, Default, Clone)] @@ -423,3 +421,49 @@ impl MathRunFrameBuilder { fn affects_row_height(fragment: &MathFragment) -> bool { !matches!(fragment, MathFragment::Align | MathFragment::Linebreak) } + +/// Create the spacing between two fragments in a given style. +fn spacing( + l: &MathFragment, + space: Option, + r: &MathFragment, +) -> Option { + use MathClass::*; + + let resolve = |v: Em, size_ref: &MathFragment| -> Option { + 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, + } +} diff --git a/crates/typst-layout/src/math/shared.rs b/crates/typst-layout/src/math/shared.rs new file mode 100644 index 000000000..13477c10b --- /dev/null +++ b/crates/typst-layout/src/math/shared.rs @@ -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