diff options
| author | Laurenz <laurmaedje@gmail.com> | 2024-08-27 14:36:48 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2024-08-27 14:38:00 +0200 |
| commit | 7eb3b3f0f7d61116ebe7f7da1952e742d94849fb (patch) | |
| tree | 335d2351fe545b657c40e905778186dd97920e6a | |
| parent | b5ef9244eb2b546b47530d40860e9e04256aafb2 (diff) | |
Better lists
| -rw-r--r-- | crates/typst/src/layout/sides.rs | 11 | ||||
| -rw-r--r-- | crates/typst/src/model/enum.rs | 128 | ||||
| -rw-r--r-- | crates/typst/src/model/figure.rs | 8 | ||||
| -rw-r--r-- | crates/typst/src/model/list.rs | 90 | ||||
| -rw-r--r-- | crates/typst/src/model/outline.rs | 10 | ||||
| -rw-r--r-- | crates/typst/src/model/terms.rs | 112 | ||||
| -rw-r--r-- | crates/typst/src/realize/mod.rs | 6 | ||||
| -rw-r--r-- | crates/typst/src/text/mod.rs | 6 |
8 files changed, 195 insertions, 176 deletions
diff --git a/crates/typst/src/layout/sides.rs b/crates/typst/src/layout/sides.rs index 75d4e25b..1c9125ab 100644 --- a/crates/typst/src/layout/sides.rs +++ b/crates/typst/src/layout/sides.rs @@ -41,6 +41,17 @@ impl<T> Sides<T> { } } + /// Create an instance where a single has the value and all others are + /// default. + pub fn one(value: T, side: Side) -> Self + where + T: Default, + { + let mut this = Self::default(); + this.set(side, value); + this + } + /// Map the individual fields with `f`. pub fn map<F, U>(self, mut f: F) -> Sides<U> where diff --git a/crates/typst/src/model/enum.rs b/crates/typst/src/model/enum.rs index 336a8fc9..ac9addd1 100644 --- a/crates/typst/src/model/enum.rs +++ b/crates/typst/src/model/enum.rs @@ -6,16 +6,16 @@ use smallvec::{smallvec, SmallVec}; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, Array, Content, Context, NativeElement, Packed, Show, Smart, - StyleChain, Styles, + cast, elem, scope, Array, Content, Context, NativeElement, Packed, Show, ShowSet, + Smart, StyleChain, Styles, }; -use crate::introspection::Locator; +use crate::introspection::{Locatable, Locator, LocatorLink}; use crate::layout::{ - Alignment, Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlignment, - Length, Regions, Sizing, VAlignment, VElem, + layout_fragment, layout_frame, Abs, Axes, BlockElem, Em, Fragment, HElem, Length, + Region, Regions, Sides, Size, VElem, }; use crate::model::{Numbering, NumberingPattern, ParElem}; -use crate::text::TextElem; +use crate::text::{isolate, TextElem}; /// A numbered list. /// @@ -73,7 +73,7 @@ use crate::text::TextElem; /// Enumeration items can contain multiple paragraphs and other block-level /// content. All content that is indented more than an item's marker becomes /// part of that item. -#[elem(scope, title = "Numbered List", Show)] +#[elem(scope, title = "Numbered List", Locatable, Show, ShowSet)] pub struct EnumElem { /// If this is `{false}`, the items are spaced apart with /// [enum spacing]($enum.spacing). If it is `{true}`, they use normal @@ -146,14 +146,15 @@ pub struct EnumElem { #[default(false)] pub full: bool, - /// The indentation of each item. + /// The indentation of the enumeration. #[resolve] + #[default(Em::new(1.75).into())] pub indent: Length, - /// The space between the numbering and the body of each item. + /// The spacing between the number and the body of each item. #[resolve] #[default(Em::new(0.5).into())] - pub body_indent: Length, + pub number_gap: Length, /// The spacing between the items of the enumeration. /// @@ -162,32 +163,6 @@ pub struct EnumElem { /// (non-tight) enumerations. pub spacing: Smart<Length>, - /// The alignment that enum numbers should have. - /// - /// By default, this is set to `{end + top}`, which aligns enum numbers - /// towards end of the current text direction (in left-to-right script, - /// for example, this is the same as `{right}`) and at the top of the line. - /// The choice of `{end}` for horizontal alignment of enum numbers is - /// usually preferred over `{start}`, as numbers then grow away from the - /// text instead of towards it, avoiding certain visual issues. This option - /// lets you override this behaviour, however. (Also to note is that the - /// [unordered list]($list) uses a different method for this, by giving the - /// `marker` content an alignment directly.). - /// - /// ````example - /// #set enum(number-align: start + bottom) - /// - /// Here are some powers of two: - /// 1. One - /// 2. Two - /// 4. Four - /// 8. Eight - /// 16. Sixteen - /// 32. Thirty two - /// ```` - #[default(HAlignment::End + VAlignment::Top)] - pub number_align: Alignment, - /// The numbered list's items. /// /// When using the enum syntax, adjacent items are automatically collected @@ -218,7 +193,11 @@ impl EnumElem { impl Show for Packed<EnumElem> { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let indent = self.indent(styles); + let inset = Sides::one(Some(indent.into()), TextElem::dir_in(styles).start()); + let mut realized = BlockElem::multi_layouter(self.clone(), layout_enum) + .with_inset(inset) .pack() .spanned(self.span()); @@ -232,6 +211,14 @@ impl Show for Packed<EnumElem> { } } +impl ShowSet for Packed<EnumElem> { + fn show_set(&self, _: StyleChain) -> Styles { + let mut out = Styles::new(); + out.set(ParElem::set_first_line_indent(Length::zero())); + out + } +} + /// Layout the enumeration. #[typst_macros::time(span = elem.span())] fn layout_enum( @@ -242,9 +229,10 @@ fn layout_enum( regions: Regions, ) -> SourceResult<Fragment> { let numbering = elem.numbering(styles); + let full = elem.full(styles); let indent = elem.indent(styles); - let body_indent = elem.body_indent(styles); - let gutter = elem.spacing(styles).unwrap_or_else(|| { + let number_gap = elem.number_gap(styles); + let spacing = elem.spacing(styles).unwrap_or_else(|| { if elem.tight(styles) { ParElem::leading_in(styles).into() } else { @@ -252,21 +240,19 @@ fn layout_enum( } }); - let mut cells = vec![]; - let mut locator = locator.split(); + let spacing_elem = VElem::block_spacing(spacing.into()).pack(); + let number_gap_elem = HElem::new(number_gap.into()).pack(); + let mut number = elem.start(styles); let mut parents = EnumElem::parents_in(styles); + let mut seq = vec![]; - 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 (i, child) in elem.children.iter().enumerate() { + if i > 0 { + seq.push(spacing_elem.clone()); + } - for item in elem.children() { - number = item.number(styles).unwrap_or(number); + number = child.number(styles).unwrap_or(number); let context = Context::new(None, Some(styles)); let resolved = if full { @@ -283,34 +269,28 @@ fn layout_enum( } }; - // 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()), - )); + let number_width = layout_frame( + engine, + &resolved, + Locator::link(&LocatorLink::measure(elem.location().unwrap())), + styles, + Region::new(Size::splat(Abs::inf()), Axes::splat(false)), + )? + .width(); + + let dedent = -(number_width + number_gap).min(indent); + let dedent_elem = HElem::new(dedent.into()).pack(); + + seq.push(dedent_elem); + isolate(&mut seq, resolved, styles); + seq.push(number_gap_elem.clone()); + seq.push(child.body.clone().styled(EnumElem::set_parents(smallvec![number]))); + 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) + let realized = Content::sequence(seq); + layout_fragment(engine, &realized, locator, styles, regions) } /// An enumeration item. diff --git a/crates/typst/src/model/figure.rs b/crates/typst/src/model/figure.rs index 164d1b48..49160d0d 100644 --- a/crates/typst/src/model/figure.rs +++ b/crates/typst/src/model/figure.rs @@ -339,10 +339,10 @@ impl ShowSet for Packed<FigureElem> { fn show_set(&self, _: StyleChain) -> Styles { // Still allows breakable figures with // `show figure: set block(breakable: true)`. - let mut map = Styles::new(); - map.set(BlockElem::set_breakable(false)); - map.set(AlignElem::set_alignment(Alignment::CENTER)); - map + let mut out = Styles::new(); + out.set(BlockElem::set_breakable(false)); + out.set(AlignElem::set_alignment(Alignment::CENTER)); + out } } diff --git a/crates/typst/src/model/list.rs b/crates/typst/src/model/list.rs index a0d76770..0c633f70 100644 --- a/crates/typst/src/model/list.rs +++ b/crates/typst/src/model/list.rs @@ -4,15 +4,15 @@ use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, scope, Array, Content, Context, Depth, Func, NativeElement, Packed, Show, - Smart, StyleChain, Styles, Value, + ShowSet, Smart, StyleChain, Styles, Value, }; -use crate::introspection::Locator; +use crate::introspection::{Locatable, Locator, LocatorLink}; use crate::layout::{ - Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlignment, Length, - Regions, Sizing, VAlignment, VElem, + layout_fragment, layout_frame, Abs, Axes, BlockElem, Em, Fragment, HElem, Length, + Region, Regions, Sides, Size, VElem, }; use crate::model::ParElem; -use crate::text::TextElem; +use crate::text::{isolate, TextElem}; /// A bullet list. /// @@ -45,7 +45,7 @@ use crate::text::TextElem; /// followed by a space to create a list item. A list item can contain multiple /// paragraphs and other block-level content. All content that is indented /// more than an item's marker becomes part of that item. -#[elem(scope, title = "Bullet List", Show)] +#[elem(scope, title = "Bullet List", Locatable, Show, ShowSet)] pub struct ListElem { /// If this is `{false}`, the items are spaced apart with /// [list spacing]($list.spacing). If it is `{true}`, they use normal @@ -98,14 +98,15 @@ pub struct ListElem { ]))] pub marker: ListMarker, - /// The indent of each item. + /// The indentation of the list. #[resolve] + #[default(Em::new(1.75).into())] pub indent: Length, /// The spacing between the marker and the body of each item. #[resolve] #[default(Em::new(0.5).into())] - pub body_indent: Length, + pub marker_gap: Length, /// The spacing between the items of the list. /// @@ -142,7 +143,11 @@ impl ListElem { impl Show for Packed<ListElem> { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let indent = self.indent(styles); + let inset = Sides::one(Some(indent.into()), TextElem::dir_in(styles).start()); + let mut realized = BlockElem::multi_layouter(self.clone(), layout_list) + .with_inset(inset) .pack() .spanned(self.span()); @@ -156,6 +161,14 @@ impl Show for Packed<ListElem> { } } +impl ShowSet for Packed<ListElem> { + fn show_set(&self, _: StyleChain) -> Styles { + let mut out = Styles::new(); + out.set(ParElem::set_first_line_indent(Length::zero())); + out + } +} + /// Layout the list. #[typst_macros::time(span = elem.span())] fn layout_list( @@ -165,9 +178,11 @@ fn layout_list( styles: StyleChain, regions: Regions, ) -> SourceResult<Fragment> { + let Depth(depth) = ListElem::depth_in(styles); + let marker = elem.marker(styles).resolve(engine, styles, depth)?; let indent = elem.indent(styles); - let body_indent = elem.body_indent(styles); - let gutter = elem.spacing(styles).unwrap_or_else(|| { + let marker_gap = elem.marker_gap(styles); + let spacing = elem.spacing(styles).unwrap_or_else(|| { if elem.tight(styles) { ParElem::leading_in(styles).into() } else { @@ -175,39 +190,36 @@ fn layout_list( } }); - 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 marker_width = layout_frame( + engine, + &marker, + Locator::link(&LocatorLink::measure(elem.location().unwrap())), + styles, + Region::new(Size::splat(Abs::inf()), Axes::splat(false)), + )? + .width(); - let mut cells = vec![]; - let mut locator = locator.split(); + let dedent = -(marker_width + marker_gap).min(indent); - 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 spacing_elem = VElem::block_spacing(spacing.into()).pack(); + let dedent_elem = HElem::new(dedent.into()).pack(); + let marker_gap_elem = HElem::new(marker_gap.into()).pack(); - 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()); + let mut seq = vec![]; + for (i, child) in elem.children.iter().enumerate() { + if i > 0 { + seq.push(spacing_elem.clone()); + } + + seq.push(dedent_elem.clone()); + isolate(&mut seq, marker.clone(), styles); + seq.push(marker_gap_elem.clone()); + seq.push(child.body.clone()); + } - layouter.layout(engine) + let depth = ListElem::set_depth(Depth(1)).wrap(); + let realized = Content::sequence(seq); + layout_fragment(engine, &realized, locator, styles.chain(&depth), regions) } /// A bullet list item. diff --git a/crates/typst/src/model/outline.rs b/crates/typst/src/model/outline.rs index b90c4e3b..adae5216 100644 --- a/crates/typst/src/model/outline.rs +++ b/crates/typst/src/model/outline.rs @@ -10,12 +10,12 @@ use crate::foundations::{ NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles, }; use crate::introspection::{Counter, CounterKey, Locatable}; -use crate::layout::{BoxElem, Em, Fr, HElem, HideElem, Length, Rel, RepeatElem, Spacing}; +use crate::layout::{BoxElem, Fr, HElem, HideElem, Length, Rel, RepeatElem, Spacing}; use crate::model::{ Destination, HeadingElem, NumberingPattern, ParElem, ParbreakElem, Refable, }; use crate::syntax::Span; -use crate::text::{LinebreakElem, LocalName, SpaceElem, TextElem}; +use crate::text::{isolate, LinebreakElem, LocalName, SpaceElem, TextElem}; use crate::utils::NonZeroExt; /// A table of contents, figures, or other elements. @@ -263,7 +263,7 @@ impl ShowSet for Packed<OutlineElem> { let mut out = Styles::new(); out.set(HeadingElem::set_outlined(false)); out.set(HeadingElem::set_numbering(None)); - out.set(ParElem::set_first_line_indent(Em::new(0.0).into())); + out.set(ParElem::set_first_line_indent(Length::zero())); out } } @@ -500,10 +500,10 @@ impl Show for Packed<OutlineEntry> { }; // The body text remains overridable. - crate::text::isolate( + isolate( + &mut seq, self.body().clone().linked(Destination::Location(location)), styles, - &mut seq, ); // Add filler symbols between the section name and page number. diff --git a/crates/typst/src/model/terms.rs b/crates/typst/src/model/terms.rs index 98fe3ff1..f44d3fce 100644 --- a/crates/typst/src/model/terms.rs +++ b/crates/typst/src/model/terms.rs @@ -1,13 +1,15 @@ use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain, - Styles, + cast, elem, scope, Array, Content, NativeElement, Packed, Show, ShowSet, Smart, + StyleChain, Styles, +}; +use crate::introspection::{Locatable, Locator}; +use crate::layout::{ + layout_fragment, BlockElem, Em, Fragment, HElem, Length, Regions, Sides, VElem, }; -use crate::layout::{Dir, Em, HElem, Length, Sides, StackChild, StackElem, VElem}; use crate::model::ParElem; -use crate::text::TextElem; -use crate::utils::Numeric; +use crate::text::{isolate, TextElem}; /// A list of terms and their descriptions. /// @@ -25,7 +27,7 @@ use crate::utils::Numeric; /// # Syntax /// This function also has dedicated syntax: Starting a line with a slash, /// followed by a term, a colon and a description creates a term list item. -#[elem(scope, title = "Term List", Show)] +#[elem(scope, title = "Term List", Locatable, Show, ShowSet)] pub struct TermsElem { /// If this is `{false}`, the items are spaced apart with /// [term list spacing]($terms.spacing). If it is `{true}`, they use normal @@ -65,20 +67,15 @@ pub struct TermsElem { #[borrowed] pub separator: Content, - /// The indentation of each item. - pub indent: Length, - - /// The hanging indent of the description. - /// - /// This is in addition to the whole item's `indent`. + /// The indentation of the term list. /// /// ```example - /// #set terms(hanging-indent: 0pt) + /// #set terms(indent: 0pt) /// / Term: This term list does not - /// make use of hanging indents. + /// make use of indents. /// ``` - #[default(Em::new(2.0).into())] - pub hanging_indent: Length, + #[default(Em::new(1.75).into())] + pub indent: Length, /// The spacing between the items of the term list. /// @@ -111,42 +108,13 @@ impl TermsElem { impl Show for Packed<TermsElem> { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { - let separator = self.separator(styles); let indent = self.indent(styles); - let hanging_indent = self.hanging_indent(styles); - let gutter = self.spacing(styles).unwrap_or_else(|| { - if self.tight(styles) { - ParElem::leading_in(styles).into() - } else { - ParElem::spacing_in(styles).into() - } - }); - - let pad = hanging_indent + indent; - let unpad = (!hanging_indent.is_zero()) - .then(|| HElem::new((-hanging_indent).into()).pack()); - - let mut children = vec![]; - for child in self.children().iter() { - let mut seq = vec![]; - seq.extend(unpad.clone()); - seq.push(child.term().clone().strong()); - seq.push((*separator).clone()); - seq.push(child.description().clone()); - children.push(StackChild::Block(Content::sequence(seq))); - } + let inset = Sides::one(Some(indent.into()), TextElem::dir_in(styles).start()); - let mut padding = Sides::default(); - if TextElem::dir_in(styles) == Dir::LTR { - padding.left = pad.into(); - } else { - padding.right = pad.into(); - } - - let mut realized = StackElem::new(children) - .with_spacing(Some(gutter.into())) + let mut realized = BlockElem::multi_layouter(self.clone(), layout_terms) + .with_inset(inset) .pack() - .padded(padding); + .spanned(self.span()); if self.tight(styles) { let leading = ParElem::leading_in(styles); @@ -158,6 +126,52 @@ impl Show for Packed<TermsElem> { } } +impl ShowSet for Packed<TermsElem> { + fn show_set(&self, _: StyleChain) -> Styles { + let mut out = Styles::new(); + out.set(ParElem::set_first_line_indent(Length::zero())); + out + } +} + +/// Layout the term list. +#[typst_macros::time(span = elem.span())] +fn layout_terms( + elem: &Packed<TermsElem>, + engine: &mut Engine, + locator: Locator, + styles: StyleChain, + regions: Regions, +) -> SourceResult<Fragment> { + let separator = elem.separator(styles); + let indent = elem.indent(styles); + let spacing = elem.spacing(styles).unwrap_or_else(|| { + if elem.tight(styles) { + ParElem::leading_in(styles).into() + } else { + ParElem::spacing_in(styles).into() + } + }); + + let spacing_elem = VElem::block_spacing(spacing.into()).pack(); + let dedent_elem = HElem::new((-indent).into()).pack(); + + let mut seq = vec![]; + for (i, child) in elem.children.iter().enumerate() { + if i > 0 { + seq.push(spacing_elem.clone()); + } + + seq.push(dedent_elem.clone()); + isolate(&mut seq, child.term().clone().strong(), styles); + seq.push(separator.clone()); + seq.push(child.description().clone()); + } + + let realized = Content::sequence(seq); + layout_fragment(engine, &realized, locator, styles, regions) +} + /// A term list item. #[elem(name = "item", title = "Term List Item")] pub struct TermItem { diff --git a/crates/typst/src/realize/mod.rs b/crates/typst/src/realize/mod.rs index a154fce1..56954942 100644 --- a/crates/typst/src/realize/mod.rs +++ b/crates/typst/src/realize/mod.rs @@ -207,8 +207,10 @@ impl<'a, 'v> Builder<'a, 'v> { .store(VElem::par_spacing(ParElem::spacing_in(styles).into()).pack()) }); - if content.is::<TagElem>() - || content.is::<PlaceElem>() + if content.is::<TagElem>() { + self.sink.push(content, styles); + self.last_was_par = last_was_par; + } else if content.is::<PlaceElem>() || content.is::<FlushElem>() || content.is::<ColbreakElem>() { diff --git a/crates/typst/src/text/mod.rs b/crates/typst/src/text/mod.rs index d1e841af..b641b743 100644 --- a/crates/typst/src/text/mod.rs +++ b/crates/typst/src/text/mod.rs @@ -1319,12 +1319,12 @@ cast! { }, } -/// Pushes `text` wrapped in LRE/RLE + PDF to `out`. -pub(crate) fn isolate(text: Content, styles: StyleChain, out: &mut Vec<Content>) { +/// Pushes `content` wrapped in LRE/RLE + PDF to `out`. +pub(crate) fn isolate(out: &mut Vec<Content>, content: Content, styles: StyleChain) { out.push(TextElem::packed(match TextElem::dir_in(styles) { Dir::RTL => "\u{202B}", _ => "\u{202A}", })); - out.push(text); + out.push(content); out.push(TextElem::packed("\u{202C}")); } |
