From a9d40afcc267bc13d85fc5320047908a77e8e7ff Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Sun, 25 Aug 2024 02:59:29 +0800 Subject: [PATCH 01/43] dev: add some black magics --- utils/configs.typ | 305 ++++++++++++++++++++++++++++++++++++++++++++++ utils/magic.typ | 63 ++++++++++ 2 files changed, 368 insertions(+) create mode 100644 utils/configs.typ create mode 100644 utils/magic.typ diff --git a/utils/configs.typ b/utils/configs.typ new file mode 100644 index 000000000..a988ac8d4 --- /dev/null +++ b/utils/configs.typ @@ -0,0 +1,305 @@ +/// The private configurations of the theme. +#let store(..args) = { + assert(args.pos().len() == 0, message: "Unexpected positional arguments.") + return (store: args.named()) +} + +/// The common configurations of the slides. +/// +/// - handout (bool): Whether to enable the handout mode. It retains only the last subslide of each slide in handout mode. +/// +/// - cover (function): The function to cover content. The default value is `hide` function. +/// +/// - slide-level (int): The level of the slides. The default value is `2`, which means the level 1 and 2 headings will be treated as slides. +/// +/// - slide (function): The function to create a new slide. +/// +/// - new-section-slide (function): The function to create a new slide for a new section. The default value is `none`. +/// +/// - new-subsection-slide (function): The function to create a new slide for a new subsection. The default value is `none`. +/// +/// - new-subsubsection-slide (function): The function to create a new slide for a new subsubsection. The default value is `none`. +/// +/// - new-subsubsubsection-slide (function): The function to create a new slide for a new subsubsubsection. The default value is `none`. +/// +/// - zero-margin-header (bool): Whether to show the full header (with negative padding). The default value is `true`. +/// +/// - zero-margin-footer (bool): Whether to show the full footer (with negative padding). The default value is `true`. +/// +/// - datetime-format (string): The format of the datetime. +/// +/// - with-pdfpc-file-label (bool): Whether to add `` label for querying. +/// +/// You can export the .pdfpc file directly using: `typst query --root . ./example.typ --field value --one "" > ./example.pdfpc` +/// +/// ------------------------------------------------------------ +/// The following configurations are some black magics for better slides writing, +/// maybe will be deprecated in the future. +/// ------------------------------------------------------------ +/// +/// - show-notes-on-second-screen (none, alignment): Whether to show the speaker notes on the second screen. +/// +/// Currently, the alignment can be `none` and `right`. +/// +/// - horizontal-line-to-pagebreak (bool): Whether to convert horizontal lines to page breaks. +/// +/// You can use markdown-like syntax `---` to divide slides. +/// +/// - reset-footnote-number-per-slide (bool): Whether to reset the footnote number per slide. +/// +/// - nontight-list-enum-and-terms (bool): Whether to make `tight` argument always be `false` for list, enum, and terms. The default value is `true`. +/// +/// - align-list-marker-with-baseline (bool): Whether to align the list marker with the baseline. The default value is `true`. +/// +/// - scale-list-items (none, float): Whether to scale the list items recursively. The default value is `none`. +#let common( + handout: false, + cover: hide, + slide-level: 2, + slide: none, + new-section-slide: none, + new-subsection-slide: none, + new-subsubsection-slide: none, + new-subsubsubsection-slide: none, + zero-margin-header: true, + zero-margin-footer: true, + datetime-format: auto, + with-pdfpc-file-label: true, + // some black magics for better slides writing, + // maybe will be deprecated in the future + show-notes-on-second-screen: none, + horizontal-line-to-pagebreak: true, + reset-footnote-number-per-slide: true, + nontight-list-enum-and-terms: true, + align-list-marker-with-baseline: true, + scale-list-items: none, + ..args, +) = { + assert(args.pos().len() == 0, message: "Unexpected positional arguments.") + return ( + handout: handout, + cover: cover, + slide-level: slide-level, + slide: slide, + new-section-slide: new-section-slide, + new-subsection-slide: new-subsection-slide, + new-subsubsection-slide: new-subsubsection-slide, + new-subsubsubsection-slide: new-subsubsubsection-slide, + zero-margin-header: zero-margin-header, + zero-margin-footer: zero-margin-footer, + datetime-format: datetime-format, + with-pdfpc-file-label: with-pdfpc-file-label, + show-notes-on-second-screen: show-notes-on-second-screen, + horizontal-line-to-pagebreak: horizontal-line-to-pagebreak, + reset-footnote-number-per-slide: reset-footnote-number-per-slide, + nontight-list-enum-and-terms: nontight-list-enum-and-terms, + align-list-marker-with-baseline: align-list-marker-with-baseline, + scale-list-items: scale-list-items, + ) + args.named() +} + + +/// The configuration of important information of the presentation. +/// +/// #example(``` +/// configs.info( +/// title: "Title", +/// subtitle: "Subtitle", +/// author: "Author", +/// date: datetime.today(), +/// institution: "Institution", +/// ) +/// ```) +/// +/// - title (content): The title of the presentation, which will be displayed in the title slide. +/// - short-title (content, auto): The short title of the presentation, which will be displayed in the footer of the slides usally. +/// +/// If you set it to `auto`, it will be the same as the title. +/// +/// - subtitle (content): The subtitle of the presentation. +/// +/// - short-subtitle (content, auto): The short subtitle of the presentation, which will be displayed in the footer of the slides usally. +/// +/// If you set it to `auto`, it will be the same as the subtitle. +/// +/// - author (content): The author of the presentation. +/// +/// - date (datetime, content): The date of the presentation. +/// +/// You can use `datetime.today()` to get the current date. +/// +/// - institution (content): The institution of the presentation. +/// +/// - logo (content): The logo of the institution. +#let info( + title: none, + short-title: auto, + subtitle: none, + short-subtitle: auto, + author: none, + date: none, + institution: none, + logo: none, + ..args, +) = { + assert(args.pos().len() == 0, message: "Unexpected positional arguments.") + if short-title == auto { + short-title = title + } + if short-subtitle == auto { + short-subtitle = subtitle + } + return ( + info: ( + title: title, + short-title: short-title, + subtitle: subtitle, + short-subtitle: short-subtitle, + author: author, + date: date, + institution: institution, + logo: logo, + ) + args.named(), + ) +} + + +/// The configuration of the colors used in the theme. +/// +/// #example(``` +/// configs.colors( +/// primary: rgb("#04364A"), +/// secondary: rgb("#176B87"), +/// tertiary: rgb("#448C95"), +/// neutral: rgb("#303030"), +/// neutral-darkest: rgb("#000000"), +/// ) +/// ```) +/// +/// IMPORTANT: The colors should be defined in the *RGB* format at most cases. +/// +/// There are four main colors in the theme: primary, secondary, tertiary, and neutral, +/// and each of them has a light, lighter, lightest, dark, darker, and darkest version. +#let colors( + neutral: rgb("#303030"), + neutral-light: rgb("#a0a0a0"), + neutral-lighter: rgb("#d0d0d0"), + neutral-lightest: rgb("#ffffff"), + neutral-dark: rgb("#202020"), + neutral-darker: rgb("#101010"), + neutral-darkest: rgb("#000000"), + primary: rgb("#303030"), + primary-light: rgb("#a0a0a0"), + primary-lighter: rgb("#d0d0d0"), + primary-lightest: rgb("#ffffff"), + primary-dark: rgb("#202020"), + primary-darker: rgb("#101010"), + primary-darkest: rgb("#000000"), + secondary: rgb("#303030"), + secondary-light: rgb("#a0a0a0"), + secondary-lighter: rgb("#d0d0d0"), + secondary-lightest: rgb("#ffffff"), + secondary-dark: rgb("#202020"), + secondary-darker: rgb("#101010"), + secondary-darkest: rgb("#000000"), + tertiary: rgb("#303030"), + tertiary-light: rgb("#a0a0a0"), + tertiary-lighter: rgb("#d0d0d0"), + tertiary-lightest: rgb("#ffffff"), + tertiary-dark: rgb("#202020"), + tertiary-darker: rgb("#101010"), + tertiary-darkest: rgb("#000000"), + ..args, +) = { + assert(args.pos().len() == 0, message: "Unexpected positional arguments.") + return ( + colors: ( + neutral: neutral, + neutral-light: neutral-light, + neutral-lighter: neutral-lighter, + neutral-lightest: neutral-lightest, + neutral-dark: neutral-dark, + neutral-darker: neutral-darker, + neutral-darkest: neutral-darkest, + primary: primary, + primary-light: primary-light, + primary-lighter: primary-lighter, + primary-lightest: primary-lightest, + primary-dark: primary-dark, + primary-darker: primary-darker, + primary-darkest: primary-darkest, + secondary: secondary, + secondary-light: secondary-light, + secondary-lighter: secondary-lighter, + secondary-lightest: secondary-lightest, + secondary-dark: secondary-dark, + secondary-darker: secondary-darker, + secondary-darkest: secondary-darkest, + tertiary: tertiary, + tertiary-light: tertiary-light, + tertiary-lighter: tertiary-lighter, + tertiary-lightest: tertiary-lightest, + tertiary-dark: tertiary-dark, + tertiary-darker: tertiary-darker, + tertiary-darkest: tertiary-darkest, + ) + args.named(), + ) +} + +/// The configuration of the page layout. +/// +/// It is equivalent to the `#set page()` rule in Touying. +/// +/// #example(``` +/// configs.page( +/// paper: "presentation-16-9", +/// header: none, +/// footer: none, +/// fill: rgb("#ffffff"), +/// margin: (x: 3em, y: 2.8em), +/// ) +/// ```) +/// +/// - paper (string): A standard paper size to set width and height. The default value is "presentation-16-9". +/// +/// You can also use `aspect-ratio` to set the aspect ratio of the paper. +/// +/// - header (content): The page's header. Fills the top margin of each page. +/// +/// - footer (content): The page's footer. Fills the bottom margin of each page. +/// +/// - fill (color): The background color of the page. The default value is `rgb("#ffffff")`. +/// +/// - margin (length, dictionary): The margin of the page. The default value is `(x: 3em, y: 2.8em)`. +/// - A single length: The same margin on all sides. +/// - A dictionary: With a dictionary, the margins can be set individually. The dictionary can contain the following keys in order of precedence: +/// - top: The top margin. +/// - right: The right margin. +/// - bottom: The bottom margin. +/// - left: The left margin. +/// - inside: The margin at the inner side of the page (where the binding is). +/// - outside: The margin at the outer side of the page (opposite to the binding). +/// - x: The horizontal margins. +/// - y: The vertical margins. +/// - rest: The margins on all sides except those for which the dictionary explicitly sets a size. +/// +/// The values for left and right are mutually exclusive with the values for inside and outside. +#let page( + paper: "presentation-16-9", + header: none, + footer: none, + fill: rgb("#ffffff"), + margin: (x: 3em, y: 2.8em), + ..args, +) = { + assert(args.pos().len() == 0, message: "Unexpected positional arguments.") + return ( + page: ( + paper: paper, + header: header, + footer: footer, + fill: fill, + margin: margin, + ) + args.named(), + ) +} diff --git a/utils/magic.typ b/utils/magic.typ new file mode 100644 index 000000000..53e4d8f46 --- /dev/null +++ b/utils/magic.typ @@ -0,0 +1,63 @@ +/// Align the list marker with the baseline of the first line of the list item. +/// +/// Usage: `#show: align-list-marker-with-baseline` +#let align-list-marker-with-baseline(body) = { + show list.item: it => { + let current-marker = if type(list.marker) == array { + list.marker.at(0) + } else { + list.marker + } + let hanging-indent = measure(current-marker).width + .6em + .3pt + set terms(hanging-indent: hanging-indent) + if type(list.marker) == array { + terms.item( + current-marker, + { + // set the value of list.marker in a loop + set list(marker: list.marker.slice(1) + (list.marker.at(0),)) + it.body + }, + ) + } else { + terms.item(current-marker, it.body) + } + } + body +} + +/// Scale the font size of the list items. +/// +/// Usage: `#show: scale-list-items.with(scale: .75)` +/// +/// - `scale` (number): The ratio of the font size of the current level to the font size of the upper level. +#let scale-list-items( + scale: .75, + body, +) = { + show list.where().or(enum.where().or(terms)): it => { + show list.where().or(enum.where().or(terms)): set text(scale * 1em) + it + } + body +} + +/// Make the list, enum, or terms nontight by default. +/// +/// Usage: `#show list: nontight(list)` +#let nontight(lst) = { + let fields = lst.fields() + fields.remove("children") + fields.tight = false + return (lst.func())(..fields, ..lst.children) +} + +/// Make the list, enum, and terms nontight by default. +/// +/// Usage: `#show: nontight-list-enum-and-terms` +#let nontight-list-enum-and-terms(body) = { + show list: nontight(list) + show enum: nontight(enum) + show terms: nontight(terms) + body +} \ No newline at end of file From f90a93651b05d4c5e0c064f683d77c7cbc9a3021 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Sun, 25 Aug 2024 15:55:35 +0800 Subject: [PATCH 02/43] feat: bibliography-as-footnote --- lib.typ | 1 + utils/magic.typ | 86 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/lib.typ b/lib.typ index 9b7d2e85a..5a127fb84 100644 --- a/lib.typ +++ b/lib.typ @@ -1,6 +1,7 @@ #import "slide.typ": pause, meanwhile, slides-end, touying-equation, touying-mitex, touying-reducer, uncover, only, alternatives-match, alternatives, alternatives-fn, alternatives-cases #import "utils/utils.typ" #import "utils/states.typ" +#import "utils/magic.typ" #import "utils/pdfpc.typ" #import "utils/components.typ" #import "themes/themes.typ" \ No newline at end of file diff --git a/utils/magic.typ b/utils/magic.typ index 53e4d8f46..a7d92ab46 100644 --- a/utils/magic.typ +++ b/utils/magic.typ @@ -1,3 +1,8 @@ +// --------------------------------------------------------------------- +// List, Enum, and Terms +// --------------------------------------------------------------------- + + /// Align the list marker with the baseline of the first line of the list item. /// /// Usage: `#show: align-list-marker-with-baseline` @@ -60,4 +65,85 @@ show enum: nontight(enum) show terms: nontight(terms) body +} + + + +// --------------------------------------------------------------------- +// Bibliography +// --------------------------------------------------------------------- + +#let bibliography-counter = counter("footer-bibliography-counter") +#let bibliography-state = state("footer-bibliography-state", ()) +#let bibliography-map = state("footer-bibliography-map", (:)) + +/// Display the bibliography as footnote. +/// +/// Usage: `#show: magic.bibliography-as-footnote.with(bibliography("ref.bib"))` +/// +/// Notice: You cannot use the same key twice in the same document, unless you use the escape option like `@key[-]`. +/// +/// - numbering (string): The numbering format of the bibliography in the footnote. +/// +/// - escape (content): The escape string which will be used to escape the cite key, in order to avoid the conflict of the same key. +/// +/// - bibliography (bibliography): The bibliography argument. You should use the `bibliography` function to define the bibliography like `bibliography("ref.bib")`. +#let bibliography-as-footnote(numbering: "[1]", escape: [-], bibliography, body) = { + show cite: it => if it.supplement != escape { + box({ + place(hide(it)) + context { + let bibitem = bibliography-state.final().at(bibliography-counter.get().at(0)) + footnote(numbering: numbering, bibitem) + bibliography-map.update(map => { + map.insert(str(it.key), bibitem) + map + }) + } + bibliography-counter.step() + }) + } else { + footnote(numbering: numbering, context bibliography-map.final().at(str(it.key))) + } + + // Record the bibliography items. + { + show grid: it => { + bibliography-state.update( + range(it.children.len()).filter(i => calc.rem(i, 2) == 1).map(i => it.children.at(i).body), + ) + } + place(hide(bibliography)) + } + + body +} + +/// Display the bibliography. +/// +/// You can avoid `multiple bibliographies are not yet supported` error by using this function. +/// +/// Usage: `#magic.bibliography()` +#let bibliography(title: auto) = { + context { + let title = title + let bibitems = bibliography-state.final() + if title == auto { + if text.lang == "zh" { + title = "参考文献" + } else { + title = "Bibliography" + } + } + if title != none { + heading(title) + v(.45em) + } + grid( + columns: (auto, 1fr), + column-gutter: .7em, + row-gutter: 1.2em, + ..range(bibitems.len()).map(i => (numbering("[1]", i + 1), bibitems.at(i))).flatten(), + ) + } } \ No newline at end of file From 7f8ace5e50cf07d1b9c312540cacf58df5228f39 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Sun, 25 Aug 2024 19:57:35 +0800 Subject: [PATCH 03/43] refactor: rename utils to src --- lib.typ | 10 +++++----- slide.typ | 6 +++--- {utils => src}/components.typ | 0 {utils => src}/configs.typ | 0 {utils => src}/magic.typ | 0 {utils => src}/pdfpc.typ | 0 {utils => src}/states.typ | 0 {utils => src}/utils.typ | 0 themes/aqua.typ | 6 +++--- themes/dewdrop.typ | 4 ++-- themes/metropolis.typ | 6 +++--- themes/simple.typ | 4 ++-- themes/university.typ | 6 +++--- 13 files changed, 21 insertions(+), 21 deletions(-) rename {utils => src}/components.typ (100%) rename {utils => src}/configs.typ (100%) rename {utils => src}/magic.typ (100%) rename {utils => src}/pdfpc.typ (100%) rename {utils => src}/states.typ (100%) rename {utils => src}/utils.typ (100%) diff --git a/lib.typ b/lib.typ index 5a127fb84..5df7dd72f 100644 --- a/lib.typ +++ b/lib.typ @@ -1,7 +1,7 @@ #import "slide.typ": pause, meanwhile, slides-end, touying-equation, touying-mitex, touying-reducer, uncover, only, alternatives-match, alternatives, alternatives-fn, alternatives-cases -#import "utils/utils.typ" -#import "utils/states.typ" -#import "utils/magic.typ" -#import "utils/pdfpc.typ" -#import "utils/components.typ" +#import "src/utils.typ" +#import "src/states.typ" +#import "src/magic.typ" +#import "src/pdfpc.typ" +#import "src/components.typ" #import "themes/themes.typ" \ No newline at end of file diff --git a/slide.typ b/slide.typ index 20cbb64be..728d14bda 100644 --- a/slide.typ +++ b/slide.typ @@ -1,6 +1,6 @@ -#import "utils/utils.typ" -#import "utils/states.typ" -#import "utils/pdfpc.typ" +#import "src/utils.typ" +#import "src/states.typ" +#import "src/pdfpc.typ" // touying pause mark #let pause = [#metadata((kind: "touying-pause"))] diff --git a/utils/components.typ b/src/components.typ similarity index 100% rename from utils/components.typ rename to src/components.typ diff --git a/utils/configs.typ b/src/configs.typ similarity index 100% rename from utils/configs.typ rename to src/configs.typ diff --git a/utils/magic.typ b/src/magic.typ similarity index 100% rename from utils/magic.typ rename to src/magic.typ diff --git a/utils/pdfpc.typ b/src/pdfpc.typ similarity index 100% rename from utils/pdfpc.typ rename to src/pdfpc.typ diff --git a/utils/states.typ b/src/states.typ similarity index 100% rename from utils/states.typ rename to src/states.typ diff --git a/utils/utils.typ b/src/utils.typ similarity index 100% rename from utils/utils.typ rename to src/utils.typ diff --git a/themes/aqua.typ b/themes/aqua.typ index 6d588772f..bba297d18 100644 --- a/themes/aqua.typ +++ b/themes/aqua.typ @@ -1,7 +1,7 @@ #import "../slide.typ": s -#import "../utils/utils.typ" -#import "../utils/states.typ" -#import "../utils/components.typ" +#import "../src/utils.typ" +#import "../src/states.typ" +#import "../src/components.typ" #let title-slide(self: none, ..args) = { self = utils.empty-page(self) diff --git a/themes/dewdrop.typ b/themes/dewdrop.typ index 2c3d5f354..91635111a 100644 --- a/themes/dewdrop.typ +++ b/themes/dewdrop.typ @@ -2,8 +2,8 @@ // The typst version was written by https://github.com/OrangeX4 #import "../slide.typ": s -#import "../utils/utils.typ" -#import "../utils/states.typ" +#import "../src/utils.typ" +#import "../src/states.typ" #let slide( self: none, diff --git a/themes/metropolis.typ b/themes/metropolis.typ index ea651feab..68d628704 100644 --- a/themes/metropolis.typ +++ b/themes/metropolis.typ @@ -8,9 +8,9 @@ // #set par(justify: true) #import "../slide.typ": s -#import "../utils/utils.typ" -#import "../utils/states.typ" -#import "../utils/components.typ" +#import "../src/utils.typ" +#import "../src/states.typ" +#import "../src/components.typ" #let _saved-align = align diff --git a/themes/simple.typ b/themes/simple.typ index 042137c47..0d2f08905 100644 --- a/themes/simple.typ +++ b/themes/simple.typ @@ -2,8 +2,8 @@ // Author: Andreas Kröpelin #import "../slide.typ": s -#import "../utils/utils.typ" -#import "../utils/states.typ" +#import "../src/utils.typ" +#import "../src/states.typ" #let slide(self: none, title: none, footer: auto, ..args) = { if footer != auto { diff --git a/themes/university.typ b/themes/university.typ index 122577a8a..20ca52e36 100644 --- a/themes/university.typ +++ b/themes/university.typ @@ -3,9 +3,9 @@ // Originally contributed by Pol Dellaiera - https://github.com/drupol #import "../slide.typ": s -#import "../utils/utils.typ" -#import "../utils/states.typ" -#import "../utils/components.typ" +#import "../src/utils.typ" +#import "../src/states.typ" +#import "../src/components.typ" #let slide( self: none, From 765a4c44ae9c59f3361c7860a7b3a4f63a487bfd Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Mon, 26 Aug 2024 02:04:59 +0800 Subject: [PATCH 04/43] dev: complete split-content-into-slides --- src/configs.typ | 53 ++++++++---- src/core.typ | 225 ++++++++++++++++++++++++++++++++++++++++++++++++ src/utils.typ | 200 ++++++++++++++++++++++++++++++++---------- 3 files changed, 412 insertions(+), 66 deletions(-) create mode 100644 src/core.typ diff --git a/src/configs.typ b/src/configs.typ index a988ac8d4..d70d198f9 100644 --- a/src/configs.typ +++ b/src/configs.typ @@ -1,5 +1,7 @@ +#import "utils.typ": merge-dicts + /// The private configurations of the theme. -#let store(..args) = { +#let config-store(..args) = { assert(args.pos().len() == 0, message: "Unexpected positional arguments.") return (store: args.named()) } @@ -12,7 +14,7 @@ /// /// - slide-level (int): The level of the slides. The default value is `2`, which means the level 1 and 2 headings will be treated as slides. /// -/// - slide (function): The function to create a new slide. +/// - slide-fn (function): The function to create a new slide. /// /// - new-section-slide (function): The function to create a new slide for a new section. The default value is `none`. /// @@ -28,6 +30,8 @@ /// /// - datetime-format (string): The format of the datetime. /// +/// - auto-offset-for-heading (bool): Whether to add an offset relative to slide-level for headings. +/// /// - with-pdfpc-file-label (bool): Whether to add `` label for querying. /// /// You can export the .pdfpc file directly using: `typst query --root . ./example.typ --field value --one "" > ./example.pdfpc` @@ -52,18 +56,19 @@ /// - align-list-marker-with-baseline (bool): Whether to align the list marker with the baseline. The default value is `true`. /// /// - scale-list-items (none, float): Whether to scale the list items recursively. The default value is `none`. -#let common( +#let config-common( handout: false, cover: hide, slide-level: 2, - slide: none, - new-section-slide: none, - new-subsection-slide: none, - new-subsubsection-slide: none, - new-subsubsubsection-slide: none, + slide-fn: none, + new-section-slide-fn: none, + new-subsection-slide-fn: none, + new-subsubsection-slide-fn: none, + new-subsubsubsection-slide-fn: none, zero-margin-header: true, zero-margin-footer: true, datetime-format: auto, + auto-offset-for-heading: true, with-pdfpc-file-label: true, // some black magics for better slides writing, // maybe will be deprecated in the future @@ -80,14 +85,15 @@ handout: handout, cover: cover, slide-level: slide-level, - slide: slide, - new-section-slide: new-section-slide, - new-subsection-slide: new-subsection-slide, - new-subsubsection-slide: new-subsubsection-slide, - new-subsubsubsection-slide: new-subsubsubsection-slide, + slide-fn: slide-fn, + new-section-slide-fn: new-section-slide-fn, + new-subsection-slide-fn: new-subsection-slide-fn, + new-subsubsection-slide-fn: new-subsubsection-slide-fn, + new-subsubsubsection-slide-fn: new-subsubsubsection-slide-fn, zero-margin-header: zero-margin-header, zero-margin-footer: zero-margin-footer, datetime-format: datetime-format, + auto-offset-for-heading: auto-offset-for-heading, with-pdfpc-file-label: with-pdfpc-file-label, show-notes-on-second-screen: show-notes-on-second-screen, horizontal-line-to-pagebreak: horizontal-line-to-pagebreak, @@ -102,7 +108,7 @@ /// The configuration of important information of the presentation. /// /// #example(``` -/// configs.info( +/// config-info( /// title: "Title", /// subtitle: "Subtitle", /// author: "Author", @@ -131,7 +137,7 @@ /// - institution (content): The institution of the presentation. /// /// - logo (content): The logo of the institution. -#let info( +#let config-info( title: none, short-title: auto, subtitle: none, @@ -167,7 +173,7 @@ /// The configuration of the colors used in the theme. /// /// #example(``` -/// configs.colors( +/// config-colors( /// primary: rgb("#04364A"), /// secondary: rgb("#176B87"), /// tertiary: rgb("#448C95"), @@ -180,7 +186,7 @@ /// /// There are four main colors in the theme: primary, secondary, tertiary, and neutral, /// and each of them has a light, lighter, lightest, dark, darker, and darkest version. -#let colors( +#let config-colors( neutral: rgb("#303030"), neutral-light: rgb("#a0a0a0"), neutral-lighter: rgb("#d0d0d0"), @@ -251,7 +257,7 @@ /// It is equivalent to the `#set page()` rule in Touying. /// /// #example(``` -/// configs.page( +/// config-page( /// paper: "presentation-16-9", /// header: none, /// footer: none, @@ -284,7 +290,7 @@ /// - rest: The margins on all sides except those for which the dictionary explicitly sets a size. /// /// The values for left and right are mutually exclusive with the values for inside and outside. -#let page( +#let config-page( paper: "presentation-16-9", header: none, footer: none, @@ -303,3 +309,12 @@ ) + args.named(), ) } + + +/// The default configurations +#let default-config = merge-dicts( + config-common(), + config-info(), + config-colors(), + config-page(), +) \ No newline at end of file diff --git a/src/core.typ b/src/core.typ new file mode 100644 index 000000000..823b8c21e --- /dev/null +++ b/src/core.typ @@ -0,0 +1,225 @@ +#import "utils.typ" +#import "configs.typ": * + + +/// Wrapper for a slide function +/// +/// - `fn` (self => { .. }): The function that will be called to render the slide +#let touying-slide-wrapper(fn) = utils.label-it( + metadata(( + kind: "touying-slide-wrapper", + fn: fn, + )), + "touying-temporary-mark", +) + + +/// Recall a slide by its label +/// +/// - `lbl` (str): The label of the slide to recall +#let touying-recall(lbl) = utils.label-it( + metadata(( + kind: "touying-slide-recaller", + label: if type(lbl) == label { + str(lbl) + } else { + lbl + }, + )), + "touying-temporary-mark", +) + + +/// Call touying slide function +#let call-slide-fn(self, fn, body) = { + let slide-wrapper = fn(body) + assert( + utils.is-kind(slide-wrapper, "touying-slide-wrapper"), + message: "you must use `touying-slide-wrapper` in your slide function", + ) + return (slide-wrapper.value.fn)(self) +} + + +/// Use headings to split a content block into slides +#let split-content-into-slides(self: none, recaller-map: (:), body) = { + // Extract arguments + assert(type(self) == dictionary, message: "`self` must be a dictionary") + assert("slide-level" in self and type(self.slide-level) == int, message: "`self.slide-level` must be an integer") + assert("slide-fn" in self and type(self.slide-fn) == function, message: "`self.slide-fn` must be a function") + let slide-level = self.slide-level + let slide-fn = self.slide-fn + let horizontal-line-to-pagebreak = self.at("horizontal-line-to-pagebreak", default: true) + let children = if utils.is-sequence(body) { + body.children + } else { + (body,) + } + let get-last-heading-depth(current-headings) = { + if current-headings != () { + current-headings.at(-1).depth + } else { + 0 + } + } + let get-last-heading-label(current-headings) = { + if current-headings != () { + if current-headings.at(-1).has("label") { + str(current-headings.at(-1).label) + } + } + } + let call-slide-fn-and-reset(self, slide-fn, current-slide-cont, recaller-map) = { + let cont = call-slide-fn(self, slide-fn, current-slide-cont) + let last-heading-label = get-last-heading-label(self.headings) + if last-heading-label != none { + recaller-map.insert(last-heading-label, cont) + } + (cont, recaller-map, (), (), true) + } + // The empty content list + let empty-contents = ([], [ ], parbreak(), linebreak()) + // The headings that we currently have + let current-headings = () + // Recaller map + let recaller-map = recaller-map + // The current slide we are building + let current-slide = () + // The current slide content + let cont = none + // Is the first part should be a slide + let first-slide = false + + + // Is we have a horizontal line + let horizontal-line = false + // Iterate over the children + for child in children { + // Handle horizontal-line + // split content when we have a horizontal line + if horizontal-line-to-pagebreak and horizontal-line and child not in ([—], [–], [-]) { + current-slide = utils.trim(current-slide) + (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings), + slide-fn, + current-slide.sum(default: none), + recaller-map, + ) + cont + } + // Main logic + if utils.is-kind(child, "touying-slide-wrapper") { + current-slide = utils.trim(current-slide) + if current-slide != () { + (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings), + slide-fn, + current-slide.sum(default: none), + recaller-map, + ) + cont + } + (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings), + child.value.fn, + current-slide.sum(default: none), + recaller-map, + ) + cont + } else if utils.is-kind(child, "touying-slide-recaller") { + current-slide = utils.trim(current-slide) + if current-slide != () or current-headings != () { + (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings), + slide-fn, + current-slide.sum(default: none), + recaller-map, + ) + cont + } + let lbl = child.value.label + assert(lbl in recaller-map, message: "label not found in the recaller map for slides") + // recall the slide + recaller-map.at(lbl) + } else if child == pagebreak() { + // split content when we have a pagebreak + current-slide = utils.trim(current-slide) + (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings), + slide-fn, + current-slide.sum(default: none), + recaller-map, + ) + cont + } else if horizontal-line-to-pagebreak and child == [—] { + horizontal-line = true + continue + } else if horizontal-line-to-pagebreak and horizontal-line and child in ([–], [-]) { + continue + } else if utils.is-heading(child, depth: slide-level) { + let last-heading-depth = get-last-heading-depth(current-headings) + if child.depth <= last-heading-depth { + current-slide = utils.trim(current-slide) + (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings), + slide-fn, + current-slide.sum(default: none), + recaller-map, + ) + cont + } + current-headings.push(child) + first-slide = true + } else { + let child = if utils.is-sequence(child) { + // Split the content into slides recursively + split-content-into-slides(self: self, recaller-map: recaller-map, child) + } else if utils.is-styled(child) { + // Split the content into slides recursively for styled content + utils.reconstruct-styled(child, split-content-into-slides(self: self, recaller-map: recaller-map, child.child)) + } else { + child + } + if first-slide { + // Add the child to the current slide + current-slide.push(child) + } else { + child + } + } + } + + // Handle the last slide + current-slide = utils.trim(current-slide) + if current-slide != () or current-headings != () { + (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings), + slide-fn, + current-slide.sum(default: none), + recaller-map, + ) + cont + } +} + +#show: split-content-into-slides.with(self: default-config + config-common(slide-fn: body => touying-slide-wrapper(self => [ + #set page(paper: "presentation-16-9") + + #self.headings + + #body +]))) + += sdfsdf + +== recall + +sdf + +== recall + +sdf + += sdfsdfsdf + +sdfddd diff --git a/src/utils.typ b/src/utils.typ index 0c0f4aa77..e19bb3ff7 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -1,3 +1,156 @@ +/// Add a dictionary to another dictionary recursively +/// +/// Example: `add-dicts((a: (b: 1), (a: (c: 2))` returns `(a: (b: 1, c: 2)` +#let add-dicts(dict-a, dict-b) = { + let res = dict-a + for key in dict-b.keys() { + if key in res and type(res.at(key)) == dictionary and type(dict-b.at(key)) == dictionary { + res.insert(key, add-dicts(res.at(key), dict-b.at(key))) + } else { + res.insert(key, dict-b.at(key)) + } + } + return res +} + + +/// Merge some dictionaries recursively +/// +/// Example: `merge-dicts((a: (b: 1)), (a: (c: 2)))` returns `(a: (b: 1, c: 2))` +#let merge-dicts(init-dict, ..dicts) = { + assert(dicts.named().len() == 0, message: "You must provide dictionaries as positional arguments") + let res = init-dict + for dict in dicts.pos() { + res = add-dicts(res, dict) + } + return res +} + + +/// Remove leading and trailing empty elements from an array of content +/// +/// - `empty-contents` is a array of content that is considered empty +/// +/// Example: `trim(([], [ ], parbreak(), linebreak(), [a], [ ], [b], [c], linebreak(), parbreak(), [ ], [ ]))` returns `([a], [ ], [b], [c])` +#let trim(arr, empty-contents: ([], [ ], parbreak(), linebreak())) = { + let i = 0 + let j = arr.len() - 1 + while i != arr.len() and arr.at(i) in empty-contents { + i += 1 + } + while j != i - 1 and arr.at(j) in empty-contents { + j -= 1 + } + arr.slice(i, j + 1) +} + + +/// Add a label to a content +/// +/// Example: `label-it("key", [a])` is equivalent to `[a ]` +/// +/// - `it` is the content to label +/// +/// - `label-name` is the name of the label, or a label +#let label-it(it, label-name) = { + if type(label-name) == label { + [#it#label-name] + } else { + [#it#label(label-name)] + } +} + +/// Reconstruct a content with a new body +/// +/// - `body-name` is the property name of the body field +/// +/// - `named` is a boolean indicating whether the fields should be named +/// +/// - `it` is the content to reconstruct +/// +/// - `new-body` is the new body you want to replace the old body with +#let reconstruct(body-name: "body", named: false, it, new-body) = { + let fields = it.fields() + let label = fields.remove("label", default: none) + let _ = fields.remove(body-name, default: none) + if named { + if label != none { + return label-it(label, (it.func())(..fields, new-body)) + } else { + return (it.func())(..fields, new-body) + } + } else { + if label != none { + return label-it(label, (it.func())(..fields.values(), new-body)) + } else { + return (it.func())(..fields.values(), new-body) + } + } +} + + +/// Reconstruct a table-like content with new children +/// +/// - `named` is a boolean indicating whether the fields should be named +/// +/// - `it` is the content to reconstruct +/// +/// - `new-children` is the new children you want to replace the old children with +#let reconstruct-table-like(named: true, it, new-children) = { + reconstruct(body-name: "children", named: named, it, new-children) +} + + +#let typst-builtin-sequence = ([A] + [ ] + [B]).func() + +/// Determine if a content is a sequence +/// +/// Example: `is-sequence([a])` returns `true` +#let is-sequence(it) = { + type(it) == content and it.func() == typst-builtin-sequence +} + + +#let typst-builtin-styled = [#set text(fill: red)].func() + +/// Determine if a content is styled +/// +/// Example: `is-styled(text(fill: red)[Red])` returns `true` +#let is-styled(it) = { + type(it) == content and it.func() == typst-builtin-styled +} + + +/// Reconstruct a styled content with a new body +/// +/// - `it` is the content to reconstruct +/// +/// - `new-child` is the new child you want to replace the old body with +#let reconstruct-styled(it, new-child) = { + typst-builtin-styled(new-child, it.styles) +} + + +/// Determine if a content is a metadata +/// +/// Example: `is-metadata(metadata((a: 1)))` returns `true` +#let is-metadata(it) = { + type(it) == content and it.func() == metadata +} + + +/// Determine if a content is a metadata with a specific kind +#let is-kind(it, kind) = { + is-metadata(it) and type(it.value) == dictionary and it.value.at("kind", default: none) == kind +} + + +/// Determine if a content is a heading in a specific depth +#let is-heading(it, depth: 9999) = { + type(it) == content and it.func() == heading and it.depth <= depth +} + + // OOP: call it or display it #let call-or-display(self, it) = { if type(it) == function { @@ -38,15 +191,6 @@ return methods } -// touying slide wrapper mark -#let touying-slide-wrapper(name, fn, ..args) = [ - #metadata(( - kind: "touying-slide-wrapper", - name: name, - fn: fn, - args: args, - )) -] // touying wrapper mark #let touying-wrapper(fn, with-visible-subslides: false, ..args) = [ @@ -100,14 +244,6 @@ } } -// Utils: trim -#let trim(arr) = { - let i = 0 - while arr.len() != i and arr.at(i) in ([], [ ], parbreak(), linebreak()) { - i += 1 - } - arr.slice(i) -} // Utils: bookmark #let bookmark(level: 1, numbering: none, outlined: true, body) = { @@ -116,36 +252,6 @@ } } -// Type: is sequence -#let typst-builtin-sequence = ([A] + [ ] + [B]).func() -#let is-sequence(it) = { - type(it) == content and it.func() == typst-builtin-sequence -} - -// Type: is styled -#let typst-builtin-styled = [#set text(fill: red)].func() -#let is-styled(it) = { - type(it) == content and it.func() == typst-builtin-styled -} - -#let reconstruct(body-name: "body", named: false, it, body) = { - let fields = it.fields() - let _ = fields.remove("label", default: none) - let _ = fields.remove(body-name, default: none) - if named { - return (it.func())(..fields, body) - } else { - return (it.func())(..fields.values(), body) - } -} - -#let heading-depth(it) = { - if it.has("depth") { - it.depth - } else { - it.level - } -} // Convert content to markup text, partly from From 3b43399f6d0f0ed4d7d7245fa58f359333a6da56 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Mon, 26 Aug 2024 02:26:08 +0800 Subject: [PATCH 05/43] dev: add new-section-slide-fn support --- src/core.typ | 80 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 12 deletions(-) diff --git a/src/core.typ b/src/core.typ index 823b8c21e..f348a6b46 100644 --- a/src/core.typ +++ b/src/core.typ @@ -49,6 +49,10 @@ assert("slide-fn" in self and type(self.slide-fn) == function, message: "`self.slide-fn` must be a function") let slide-level = self.slide-level let slide-fn = self.slide-fn + let new-section-slide-fn = self.at("new-section-slide-fn", default: none) + let new-subsection-slide-fn = self.at("new-subsection-slide-fn", default: none) + let new-subsubsection-slide-fn = self.at("new-subsubsection-slide-fn", default: none) + let new-subsubsubsection-slide-fn = self.at("new-subsubsubsection-slide-fn", default: none) let horizontal-line-to-pagebreak = self.at("horizontal-line-to-pagebreak", default: true) let children = if utils.is-sequence(body) { body.children @@ -158,18 +162,60 @@ continue } else if utils.is-heading(child, depth: slide-level) { let last-heading-depth = get-last-heading-depth(current-headings) - if child.depth <= last-heading-depth { - current-slide = utils.trim(current-slide) + current-slide = utils.trim(current-slide) + if child.depth <= last-heading-depth or current-slide != () or (child.depth == 1 and new-section-slide-fn != none) or ( + child.depth == 2 and new-subsection-slide-fn != none + ) or (child.depth == 3 and new-subsubsection-slide-fn != none) or ( + child.depth == 4 and new-subsubsubsection-slide-fn != none + ) { + if current-slide != () or current-headings != () { + (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings), + slide-fn, + current-slide.sum(default: none), + recaller-map, + ) + cont + } + } + + current-headings.push(child) + first-slide = true + + if child.depth == 1 and new-section-slide-fn != none { (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( self + (headings: current-headings), - slide-fn, - current-slide.sum(default: none), + new-section-slide-fn, + child.body, + recaller-map, + ) + cont + } else if child.depth == 2 and new-subsection-slide-fn != none { + (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings), + new-subsection-slide-fn, + child.body, + recaller-map, + ) + cont + } else if child.depth == 3 and new-subsubsection-slide-fn != none { + (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings), + new-subsubsection-slide-fn, + child.body, + recaller-map, + ) + cont + } else if child.depth == 4 and new-subsubsubsection-slide-fn != none { + (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings), + new-subsubsubsection-slide-fn, + child.body, recaller-map, ) cont } - current-headings.push(child) - first-slide = true + } else { let child = if utils.is-sequence(child) { // Split the content into slides recursively @@ -202,15 +248,25 @@ } } -#show: split-content-into-slides.with(self: default-config + config-common(slide-fn: body => touying-slide-wrapper(self => [ - #set page(paper: "presentation-16-9") +#show: split-content-into-slides.with(self: default-config + config-common( + slide-fn: body => touying-slide-wrapper(self => [ + #set page(paper: "presentation-16-9") + + #self.headings + + #body + ]), + new-section-slide-fn: title => touying-slide-wrapper(self => [ + #set page(paper: "presentation-16-9") + #set text(green) - #self.headings + #self.headings - #body -]))) + #title + ]), +)) -= sdfsdf += Title == recall From 0e8c5e91f3805c2cb0dcaca388bb3f8323e61e82 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Mon, 26 Aug 2024 19:31:40 +0800 Subject: [PATCH 06/43] dev: add touying-set-config and appendix --- src/configs.typ | 16 +++++++++---- src/core.typ | 61 +++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/configs.typ b/src/configs.typ index d70d198f9..dfce4f36b 100644 --- a/src/configs.typ +++ b/src/configs.typ @@ -24,12 +24,16 @@ /// /// - new-subsubsubsection-slide (function): The function to create a new slide for a new subsubsubsection. The default value is `none`. /// +/// - datetime-format (string): The format of the datetime. +/// +/// - appendix (bool): Is touying in the appendix mode. The last-slide-counter will be frozen in the appendix mode. +/// +/// - freeze-slide-counter (bool): Whether to freeze the slide counter. The default value is `false`. +/// /// - zero-margin-header (bool): Whether to show the full header (with negative padding). The default value is `true`. /// /// - zero-margin-footer (bool): Whether to show the full footer (with negative padding). The default value is `true`. /// -/// - datetime-format (string): The format of the datetime. -/// /// - auto-offset-for-heading (bool): Whether to add an offset relative to slide-level for headings. /// /// - with-pdfpc-file-label (bool): Whether to add `` label for querying. @@ -65,9 +69,11 @@ new-subsection-slide-fn: none, new-subsubsection-slide-fn: none, new-subsubsubsection-slide-fn: none, + datetime-format: auto, + appendix: false, + freeze-slide-counter: false, zero-margin-header: true, zero-margin-footer: true, - datetime-format: auto, auto-offset-for-heading: true, with-pdfpc-file-label: true, // some black magics for better slides writing, @@ -90,9 +96,11 @@ new-subsection-slide-fn: new-subsection-slide-fn, new-subsubsection-slide-fn: new-subsubsection-slide-fn, new-subsubsubsection-slide-fn: new-subsubsubsection-slide-fn, + datetime-format: datetime-format, + appendix: appendix, + freeze-slide-counter: freeze-slide-counter, zero-margin-header: zero-margin-header, zero-margin-footer: zero-margin-footer, - datetime-format: datetime-format, auto-offset-for-heading: auto-offset-for-heading, with-pdfpc-file-label: with-pdfpc-file-label, show-notes-on-second-screen: show-notes-on-second-screen, diff --git a/src/core.typ b/src/core.typ index f348a6b46..a352eafaa 100644 --- a/src/core.typ +++ b/src/core.typ @@ -14,6 +14,24 @@ ) +/// Set config +#let touying-set-config(config, body) = utils.label-it( + metadata(( + kind: "touying-set-config", + config: config, + body: body, + )), + "touying-temporary-mark", +) + + +/// Appendix for the presentation. The last-slide-counter will be frozen at the last slide before the appendix. +#let appendix(body) = touying-set-config( + (appendix: true), + body, +) + + /// Recall a slide by its label /// /// - `lbl` (str): The label of the slide to recall @@ -42,7 +60,7 @@ /// Use headings to split a content block into slides -#let split-content-into-slides(self: none, recaller-map: (:), body) = { +#let split-content-into-slides(self: none, recaller-map: (:), first-slide: false, body) = { // Extract arguments assert(type(self) == dictionary, message: "`self` must be a dictionary") assert("slide-level" in self and type(self.slide-level) == int, message: "`self.slide-level` must be an integer") @@ -92,7 +110,7 @@ // The current slide content let cont = none // Is the first part should be a slide - let first-slide = false + let first-slide = first-slide // Is we have a horizontal line @@ -163,11 +181,11 @@ } else if utils.is-heading(child, depth: slide-level) { let last-heading-depth = get-last-heading-depth(current-headings) current-slide = utils.trim(current-slide) - if child.depth <= last-heading-depth or current-slide != () or (child.depth == 1 and new-section-slide-fn != none) or ( - child.depth == 2 and new-subsection-slide-fn != none - ) or (child.depth == 3 and new-subsubsection-slide-fn != none) or ( - child.depth == 4 and new-subsubsubsection-slide-fn != none - ) { + if child.depth <= last-heading-depth or current-slide != () or ( + child.depth == 1 and new-section-slide-fn != none + ) or (child.depth == 2 and new-subsection-slide-fn != none) or ( + child.depth == 3 and new-subsubsection-slide-fn != none + ) or (child.depth == 4 and new-subsubsubsection-slide-fn != none) { if current-slide != () or current-headings != () { (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( self + (headings: current-headings), @@ -216,6 +234,23 @@ cont } + } else if utils.is-kind(child, "touying-set-config") { + if current-slide != () or current-headings != () { + (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings), + slide-fn, + current-slide.sum(default: none), + recaller-map, + ) + cont + } + // Appendix content + split-content-into-slides( + self: self + child.value.config, + recaller-map: recaller-map, + first-slide: true, + child.value.body, + ) } else { let child = if utils.is-sequence(child) { // Split the content into slides recursively @@ -254,6 +289,8 @@ #self.headings + #self.appendix + #body ]), new-section-slide-fn: title => touying-slide-wrapper(self => [ @@ -262,17 +299,21 @@ #self.headings + #self.appendix + #title ]), )) = Title -== recall +== recall1 -sdf +sdfdf + +sdfsdfdsfsdfsdf -== recall +== recall2 sdf From d6f260be3910f903d55a03a73cc7ac47fd2793ee Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Thu, 29 Aug 2024 00:26:20 +0800 Subject: [PATCH 07/43] dev: migrate slide.typ --- lib.typ | 7 +- src/configs.typ | 90 ++++- src/exports.typ | 8 + src/slide.typ | 750 +++++++++++++++++++++++++++++++++++ src/{core.typ => slides.typ} | 79 ++-- src/utils.typ | 11 - themes/default.typ | 32 +- themes/themes.typ | 10 +- 8 files changed, 897 insertions(+), 90 deletions(-) create mode 100644 src/exports.typ create mode 100644 src/slide.typ rename src/{core.typ => slides.typ} (86%) diff --git a/lib.typ b/lib.typ index 5df7dd72f..0ececc9df 100644 --- a/lib.typ +++ b/lib.typ @@ -1,7 +1,2 @@ -#import "slide.typ": pause, meanwhile, slides-end, touying-equation, touying-mitex, touying-reducer, uncover, only, alternatives-match, alternatives, alternatives-fn, alternatives-cases -#import "src/utils.typ" -#import "src/states.typ" -#import "src/magic.typ" -#import "src/pdfpc.typ" -#import "src/components.typ" +#import "src/exports.typ": * #import "themes/themes.typ" \ No newline at end of file diff --git a/src/configs.typ b/src/configs.typ index dfce4f36b..c3e67ce1a 100644 --- a/src/configs.typ +++ b/src/configs.typ @@ -1,4 +1,5 @@ -#import "utils.typ": merge-dicts +#import "utils.typ" +#import "slide.typ": touying-slide-wrapper, touying-slide /// The private configurations of the theme. #let config-store(..args) = { @@ -62,9 +63,13 @@ /// - scale-list-items (none, float): Whether to scale the list items recursively. The default value is `none`. #let config-common( handout: false, - cover: hide, slide-level: 2, - slide-fn: none, + slide-fn: ( + repeat: auto, + setting: body => body, + composer: auto, + ..bodies, + ) => touying-slide-wrapper(self => touying-slide(self: self, repeat: repeat, setting: setting, composer: composer, ..bodies)), new-section-slide-fn: none, new-subsection-slide-fn: none, new-subsubsection-slide-fn: none, @@ -76,8 +81,41 @@ zero-margin-footer: true, auto-offset-for-heading: true, with-pdfpc-file-label: true, + enable-mark-warning: true, // some black magics for better slides writing, // maybe will be deprecated in the future + frozen-states: (), + default-frozen-states: ( + // ctheorems state + state("thm", + ( + "counters": ("heading": ()), + "latest": () + ) + ), + ), + frozen-counters: (), + default-frozen-counters: (counter(math.equation), counter(figure.where(kind: table)), counter(figure.where(kind: image))), + first-slide-number: 1, + preamble: none, + default-preamble: self => { + if self.at("enable-mark-warning", default: true) { + context { + let marks = query() + if marks.len() > 0 { + let page-num = marks.at(0).location().page() + let kind = marks.at(0).value.kind + panic("Unsupported mark `" + kind + "` at page " + str(page-num) + ". You can't use it inside some layout functions like `grid`. You may want to use the callback-style `uncover` function instead. Or you may want to use #slide[][] for a two-column layout. ") + } + } + } + }, + page-preamble: none, + default-page-preamble: self => { + if self.at("reset-footnote-number-per-slide", default: true) { + counter(footnote).update(0) + } + }, show-notes-on-second-screen: none, horizontal-line-to-pagebreak: true, reset-footnote-number-per-slide: true, @@ -89,7 +127,6 @@ assert(args.pos().len() == 0, message: "Unexpected positional arguments.") return ( handout: handout, - cover: cover, slide-level: slide-level, slide-fn: slide-fn, new-section-slide-fn: new-section-slide-fn, @@ -103,6 +140,16 @@ zero-margin-footer: zero-margin-footer, auto-offset-for-heading: auto-offset-for-heading, with-pdfpc-file-label: with-pdfpc-file-label, + enable-mark-warning: enable-mark-warning, + frozen-states: frozen-states, + frozen-counters: frozen-counters, + default-frozen-states: default-frozen-states, + default-frozen-counters: default-frozen-counters, + first-slide-number: first-slide-number, + preamble: preamble, + default-preamble: default-preamble, + page-preamble: page-preamble, + default-page-preamble: default-page-preamble, show-notes-on-second-screen: show-notes-on-second-screen, horizontal-line-to-pagebreak: horizontal-line-to-pagebreak, reset-footnote-number-per-slide: reset-footnote-number-per-slide, @@ -113,6 +160,36 @@ } +/// The configuration of the methods +#let config-methods( + cover: utils.wrap-method(hide), + // dynamic control + uncover: utils.uncover, + only: utils.only, + alternatives-match: utils.alternatives-match, + alternatives: utils.alternatives, + alternatives-fn: utils.alternatives-fn, + alternatives-cases: utils.alternatives-cases, + // alert interface + alert: utils.wrap-method(text.with(weight: "bold")), + ..args, +) = { + assert(args.pos().len() == 0, message: "Unexpected positional arguments.") + return ( + methods: ( + cover: cover, + uncover: uncover, + only: only, + alternatives-match: alternatives-match, + alternatives: alternatives, + alternatives-fn: alternatives-fn, + alternatives-cases: alternatives-cases, + alert: alert, + ) + args.named(), + ) +} + + /// The configuration of important information of the presentation. /// /// #example(``` @@ -320,9 +397,10 @@ /// The default configurations -#let default-config = merge-dicts( +#let default-config = utils.merge-dicts( config-common(), + config-methods(), config-info(), config-colors(), - config-page(), + config-page(), ) \ No newline at end of file diff --git a/src/exports.typ b/src/exports.typ new file mode 100644 index 000000000..dfcf64560 --- /dev/null +++ b/src/exports.typ @@ -0,0 +1,8 @@ +#import "slide.typ": pause, meanwhile, touying-equation, touying-mitex, touying-reducer, uncover, only, alternatives-match, alternatives, alternatives-fn, alternatives-cases +#import "slides.typ": touying-set-config, appendix, touying-recall, touying-slides +#import "configs.typ": * +#import "utils.typ" +#import "states.typ" +#import "magic.typ" +#import "pdfpc.typ" +#import "components.typ" \ No newline at end of file diff --git a/src/slide.typ b/src/slide.typ new file mode 100644 index 000000000..ca3b7c3a6 --- /dev/null +++ b/src/slide.typ @@ -0,0 +1,750 @@ +#import "utils.typ" +#import "states.typ" +#import "pdfpc.typ" + +/// ------------------------------------------------ +/// Slide +/// ------------------------------------------------ + +// touying function wrapper mark +#let touying-fn-wrapper(fn, with-visible-subslides: false, ..args) = label-it( + metadata(( + kind: "touying-fn-wrapper", + fn: fn, + args: args, + with-visible-subslides: with-visible-subslides, + )), + "touying-temporary-mark", +) + +/// Wrapper for a slide function +/// +/// - `fn` (self => { .. }): The function that will be called to render the slide +#let touying-slide-wrapper(fn) = utils.label-it( + metadata(( + kind: "touying-slide-wrapper", + fn: fn, + )), + "touying-temporary-mark", +) + +// touying pause mark +#let pause = [#metadata((kind: "touying-pause"))] +// touying meanwhile mark +#let meanwhile = [#metadata((kind: "touying-meanwhile"))] +// dynamic control mark +#let uncover = touying-fn-wrapper.with(utils.uncover, with-visible-subslides: true) +#let only = touying-fn-wrapper.with(utils.only, with-visible-subslides: true) +#let alternatives-match = touying-fn-wrapper.with(utils.alternatives-match) +#let alternatives = touying-fn-wrapper.with(utils.alternatives) +#let alternatives-fn = touying-fn-wrapper.with(utils.alternatives-fn) +#let alternatives-cases = touying-fn-wrapper.with(utils.alternatives-cases) +// touying equation mark +#let touying-equation(block: true, numbering: none, supplement: auto, scope: (:), body) = utils.label( + metadata(( + kind: "touying-equation", + block: block, + numbering: numbering, + supplement: supplement, + scope: scope, + body: { + if type(body) == function { + body + } else if type(body) == str { + body + } else if type(body) == content and body.has("text") { + body.text + } else { + panic("Unsupported type: " + str(type(body))) + } + }, + )), + "touying-temporary-mark", +) + +// touying mitex mark +#let touying-mitex(block: true, numbering: none, supplement: auto, mitex, body) = utils.label( + metadata(( + kind: "touying-mitex", + block: block, + numbering: numbering, + supplement: supplement, + mitex: mitex, + body: { + if type(body) == function { + body + } else if type(body) == str { + body + } else if type(body) == content and body.has("text") { + body.text + } else { + panic("Unsupported type: " + str(type(body))) + } + }, + )), + "touying-temporary-mark", +) + +// touying reducer mark +#let touying-reducer(reduce: arr => arr.sum(), cover: arr => none, ..args) = utils.label-it( + metadata(( + kind: "touying-reducer", + reduce: reduce, + cover: cover, + kwargs: args.named(), + args: args.pos(), + )), + "touying-temporary-mark", +) + +// parse touying equation, and get the repetitions +#let _parse-touying-equation(self: none, need-cover: true, base: 1, index: 1, eqt) = { + let result-arr = () + // repetitions + let repetitions = base + let max-repetitions = repetitions + // get cover function from self + let cover = self.methods.cover.with(self: self) + // get eqt body + let it = eqt.body + // if it is a function, then call it with self + if type(it) == function { + it = it(self) + } + assert(type(it) == str, message: "Unsupported type: " + str(type(it))) + // parse the content + let result = () + let cover-arr = () + let children = it + .split(regex("(#meanwhile;?)|(meanwhile)")) + .intersperse("touying-meanwhile") + .map(s => s.split(regex("(#pause;?)|(pause)")).intersperse("touying-pause")) + .flatten() + .map(s => s.split(regex("(\\\\\\s)|(\\\\\\n)")).intersperse("\\\n")) + .flatten() + .map(s => s.split(regex("&")).intersperse("&")) + .flatten() + for child in children { + if child == "touying-pause" { + repetitions += 1 + } else if child == "touying-meanwhile" { + // clear the cover-arr when encounter #meanwhile + if cover-arr.len() != 0 { + result.push("cover(" + cover-arr.sum() + ")") + cover-arr = () + } + // then reset the repetitions + max-repetitions = calc.max(max-repetitions, repetitions) + repetitions = 1 + } else if child == "\\\n" or child == "&" { + // clear the cover-arr when encounter linebreak or parbreak + if cover-arr.len() != 0 { + result.push("cover(" + cover-arr.sum() + ")") + cover-arr = () + } + result.push(child) + } else { + if repetitions <= index or not need-cover { + result.push(child) + } else { + cover-arr.push(child) + } + } + } + // clear the cover-arr when end + if cover-arr.len() != 0 { + result.push("cover(" + cover-arr.sum() + ")") + cover-arr = () + } + result-arr.push( + math.equation( + block: eqt.block, + numbering: eqt.numbering, + supplement: eqt.supplement, + eval( + "$" + result.sum(default: "") + "$", + scope: eqt.scope + ( + cover: (..args) => { + let cover = eqt.scope.at("cover", default: cover) + if args.pos().len() != 0 { + cover(args.pos().first()) + } + }, + ), + ), + ), + ) + max-repetitions = calc.max(max-repetitions, repetitions) + return (result-arr, max-repetitions) +} + +// parse touying mitex, and get the repetitions +#let _parse-touying-mitex(self: none, need-cover: true, base: 1, index: 1, eqt) = { + let result-arr = () + // repetitions + let repetitions = base + let max-repetitions = repetitions + // get eqt body + let it = eqt.body + // if it is a function, then call it with self + if type(it) == function { + it = it(self) + } + assert(type(it) == str, message: "Unsupported type: " + str(type(it))) + // parse the content + let result = () + let cover-arr = () + let children = it + .split(regex("\\\\meanwhile")) + .intersperse("touying-meanwhile") + .map(s => s.split(regex("\\\\pause")).intersperse("touying-pause")) + .flatten() + .map(s => s.split(regex("(\\\\\\\\\s)|(\\\\\\\\\n)")).intersperse("\\\\\n")) + .flatten() + .map(s => s.split(regex("&")).intersperse("&")) + .flatten() + for child in children { + if child == "touying-pause" { + repetitions += 1 + } else if child == "touying-meanwhile" { + // clear the cover-arr when encounter #meanwhile + if cover-arr.len() != 0 { + result.push("\\phantom{" + cover-arr.sum() + "}") + cover-arr = () + } + // then reset the repetitions + max-repetitions = calc.max(max-repetitions, repetitions) + repetitions = 1 + } else if child == "\\\n" or child == "&" { + // clear the cover-arr when encounter linebreak or parbreak + if cover-arr.len() != 0 { + result.push("\\phantom{" + cover-arr.sum() + "}") + cover-arr = () + } + result.push(child) + } else { + if repetitions <= index or not need-cover { + result.push(child) + } else { + cover-arr.push(child) + } + } + } + // clear the cover-arr when end + if cover-arr.len() != 0 { + result.push("\\phantom{" + cover-arr.sum() + "}") + cover-arr = () + } + result-arr.push( + (eqt.mitex)( + block: eqt.block, + numbering: eqt.numbering, + supplement: eqt.supplement, + result.sum(default: ""), + ), + ) + max-repetitions = calc.max(max-repetitions, repetitions) + return (result-arr, max-repetitions) +} + +// parse touying reducer, and get the repetitions +#let _parse-touying-reducer(self: none, base: 1, index: 1, reducer) = { + let result-arr = () + // repetitions + let repetitions = base + let max-repetitions = repetitions + // get cover function from self + let cover = reducer.cover + // parse the content + let result = () + let cover-arr = () + for child in reducer.args.flatten() { + if type(child) == content and child.func() == metadata and type(child.value) == dictionary { + let kind = child.value.at("kind", default: none) + if kind == "touying-pause" { + repetitions += 1 + } else if kind == "touying-meanwhile" { + // clear the cover-arr when encounter #meanwhile + if cover-arr.len() != 0 { + result.push(cover(cover-arr.sum())) + cover-arr = () + } + // then reset the repetitions + max-repetitions = calc.max(max-repetitions, repetitions) + repetitions = 1 + } else { + if repetitions <= index { + result.push(child) + } else { + cover-arr.push(child) + } + } + } else { + if repetitions <= index { + result.push(child) + } else { + cover-arr.push(child) + } + } + } + // clear the cover-arr when end + if cover-arr.len() != 0 { + let r = cover(cover-arr) + if type(r) == array { + result += r + } else { + result.push(r) + } + cover-arr = () + } + result-arr.push( + (reducer.reduce)( + ..reducer.kwargs, + result, + ), + ) + max-repetitions = calc.max(max-repetitions, repetitions) + return (result-arr, max-repetitions) +} + +// parse content into results and repetitions +#let _parse-content-into-results-and-repetitions(self: none, need-cover: true, base: 1, index: 1, ..bodies) = { + let bodies = bodies.pos() + let result-arr = () + // repetitions + let repetitions = base + let max-repetitions = repetitions + // get cover function from self + let cover = self.methods.cover.with(self: self) + for it in bodies { + // if it is a function, then call it with self + if type(it) == function { + // subslide index + it = it(self) + } + // parse the content + let result = () + let cover-arr = () + let children = if utils.is-sequence(it) { + it.children + } else { + (it,) + } + for child in children { + if type(child) == content and child.func() == metadata and type(child.value) == dictionary { + let kind = child.value.at("kind", default: none) + if kind == "touying-pause" { + repetitions += 1 + } else if kind == "touying-meanwhile" { + // clear the cover-arr when encounter #meanwhile + if cover-arr.len() != 0 { + result.push(cover(cover-arr.sum())) + cover-arr = () + } + // then reset the repetitions + max-repetitions = calc.max(max-repetitions, repetitions) + repetitions = 1 + } else if kind == "touying-equation" { + // handle touying-equation + let (conts, nextrepetitions) = _parse-touying-equation( + self: self, + need-cover: repetitions <= index, + base: repetitions, + index: index, + child.value, + ) + let cont = conts.first() + if repetitions <= index or not need-cover { + result.push(cont) + } else { + cover-arr.push(cont) + } + repetitions = nextrepetitions + } else if kind == "touying-mitex" { + // handle touying-mitex + let (conts, nextrepetitions) = _parse-touying-mitex( + self: self, + need-cover: repetitions <= index, + base: repetitions, + index: index, + child.value, + ) + let cont = conts.first() + if repetitions <= index or not need-cover { + result.push(cont) + } else { + cover-arr.push(cont) + } + repetitions = nextrepetitions + } else if kind == "touying-reducer" { + // handle touying-reducer + let (conts, nextrepetitions) = _parse-touying-reducer( + self: self, + base: repetitions, + index: index, + child.value, + ) + let cont = conts.first() + if repetitions <= index or not need-cover { + result.push(cont) + } else { + cover-arr.push(cont) + } + repetitions = nextrepetitions + } else if kind == "touying-fn-wrapper" { + // handle touying-fn-wrapper + self.subslide = index + if repetitions <= index or not need-cover { + result.push((child.value.fn)(self: self, ..child.value.args)) + } else { + cover-arr.push((child.value.fn)(self: self, ..child.value.args)) + } + if child.value.with-visible-subslides { + let visible-subslides = child.value.args.pos().at(0) + max-repetitions = calc.max(max-repetitions, utils.last-required-subslide(visible-subslides)) + } + } else { + if repetitions <= index or not need-cover { + result.push(child) + } else { + cover-arr.push(child) + } + } + } else if child == linebreak() or child == parbreak() { + // clear the cover-arr when encounter linebreak or parbreak + if cover-arr.len() != 0 { + result.push(cover(cover-arr.sum())) + cover-arr = () + } + result.push(child) + } else if utils.is-sequence(child) { + // handle the sequence + let (conts, nextrepetitions) = _parse-content-into-results-and-repetitions( + self: self, + need-cover: repetitions <= index, + base: repetitions, + index: index, + child, + ) + let cont = conts.first() + if repetitions <= index or not need-cover { + result.push(cont) + } else { + cover-arr.push(cont) + } + repetitions = nextrepetitions + } else if utils.is-styled(child) { + // handle styled + let (conts, nextrepetitions) = _parse-content-into-results-and-repetitions( + self: self, + need-cover: repetitions <= index, + base: repetitions, + index: index, + child.child, + ) + let cont = conts.first() + if repetitions <= index or not need-cover { + result.push(utils.typst-builtin-styled(cont, child.styles)) + } else { + cover-arr.push(utils.typst-builtin-styled(cont, child.styles)) + } + repetitions = nextrepetitions + } else if type(child) == content and child.func() in (list.item, enum.item, align) { + // handle the list item + let (conts, nextrepetitions) = _parse-content-into-results-and-repetitions( + self: self, + need-cover: repetitions <= index, + base: repetitions, + index: index, + child.body, + ) + let cont = conts.first() + if repetitions <= index or not need-cover { + result.push(utils.reconstruct(child, cont)) + } else { + cover-arr.push(utils.reconstruct(child, cont)) + } + repetitions = nextrepetitions + } else if type(child) == content and child.func() in (pad,) { + // handle the pad + let (conts, nextrepetitions) = _parse-content-into-results-and-repetitions( + self: self, + need-cover: repetitions <= index, + base: repetitions, + index: index, + child.body, + ) + let cont = conts.first() + if repetitions <= index or not need-cover { + result.push(utils.reconstruct(named: true, child, cont)) + } else { + cover-arr.push(utils.reconstruct(named: true, child, cont)) + } + repetitions = nextrepetitions + } else if type(child) == content and child.func() == terms.item { + // handle the terms item + let (conts, nextrepetitions) = _parse-content-into-results-and-repetitions( + self: self, + need-cover: repetitions <= index, + base: repetitions, + index: index, + child.description, + ) + let cont = conts.first() + if repetitions <= index or not need-cover { + result.push(terms.item(child.term, cont)) + } else { + cover-arr.push(terms.item(child.term, cont)) + } + repetitions = nextrepetitions + } else { + if repetitions <= index or not need-cover { + result.push(child) + } else { + cover-arr.push(child) + } + } + } + // clear the cover-arr when end + if cover-arr.len() != 0 { + result.push(cover(cover-arr.sum())) + cover-arr = () + } + result-arr.push(result.sum(default: [])) + } + max-repetitions = calc.max(max-repetitions, repetitions) + return (result-arr, max-repetitions) +} + +// get negative pad for header and footer +#let _get-negative-pad(self) = { + let margin = self.page.margin + if type(margin) != dictionary and type(margin) != length and type(margin) != relative { + return it => it + } + let cell = block.with(width: 100%, height: 100%, above: 0pt, below: 0pt, breakable: false) + if type(margin) == length or type(margin) == relative { + return it => pad(x: -margin, cell(it)) + } + let pad-args = (:) + if "x" in margin { + pad-args.x = -margin.x + } + if "left" in margin { + pad-args.left = -margin.left + } + if "right" in margin { + pad-args.right = -margin.right + } + if "rest" in margin { + pad-args.rest = -margin.rest + } + it => pad(..pad-args, cell(it)) +} + +// get page extra args for show-notes-on-second-screen +#let _get-page-extra-args(self) = { + if self.show-notes-on-second-screen == right { + let margin = self.page.margin + assert( + self.page.paper == "presentation-16-9" or self.page.paper == "presentation-4-3", + message: "The paper of page should be presentation-16-9 or presentation-4-3", + ) + let page-width = if self.page.paper == "presentation-16-9" { + 841.89pt + } else { + 793.7pt + } + if type(margin) != dictionary and type(margin) != length and type(margin) != relative { + return (:) + } + if type(margin) == length or type(margin) == relative { + margin = (x: margin, y: margin) + } + if "right" not in margin { + assert("x" in margin, message: "The margin should have right or x") + margin.right = margin.x + } + margin.right += page-width + return (margin: margin, width: 2 * page-width) + } else { + return (:) + } +} + +#let _get-header-footer(self) = { + let header = utils.call-or-display(self, self.page.at("header", default: none)) + let footer = utils.call-or-display(self, self.page.at("footer", default: none)) + // speaker note + if self.show-notes-on-second-screen == right { + assert( + self.page.paper == "presentation-16-9" or self.page.paper == "presentation-4-3", + message: "The paper of page should be presentation-16-9 or presentation-4-3", + ) + let page-width = if self.page.paper == "presentation-16-9" { + 841.89pt + } else { + 793.7pt + } + let page-height = if self.page.paper == "presentation-16-9" { + 473.56pt + } else { + 595.28pt + } + footer += place( + left + bottom, + dx: page-width, + block( + fill: rgb("#E6E6E6"), + width: page-width, + height: page-height, + { + set align(left + top) + set text(size: 24pt, fill: black, weight: "regular") + block( + width: 100%, + height: 88pt, + inset: (left: 32pt, top: 16pt), + outset: 0pt, + fill: rgb("#CCCCCC"), + { + states.current-section-title + linebreak() + [ --- ] + states.current-slide-title + }, + ) + pad(x: 48pt, states.current-slide-note) + // clear the slide note + states.slide-note-state.update(none) + }, + ), + ) + } + // negative padding + if self.at("zero-margin-header", default: true) or self.at("zero-margin-footer", default: true) { + let negative-pad = _get-negative-pad(self) + if self.at("zero-margin-header", default: true) { + header = negative-pad(header) + } + if self.at("zero-margin-footer", default: true) { + footer = negative-pad(footer) + } + } + (header, footer) +} + +// touying-slide +#let touying-slide( + self: none, + repeat: auto, + setting: body => body, + composer: auto, + ..bodies, +) = { + assert(bodies.named().len() == 0, message: "unexpected named arguments:" + repr(bodies.named().keys())) + let composer-with-side-by-side(..args) = { + if type(composer) == function { + composer(..args) + } else { + utils.side-by-side(columns: composer, ..args) + } + } + let bodies = bodies.pos() + // update pdfpc + let update-pdfpc(curr-subslide) = ( + context [ + #metadata((t: "NewSlide")) + #metadata((t: "Idx", v: here().page() - 1)) + #metadata((t: "Overlay", v: curr-subslide - 1)) + #metadata((t: "LogicalSlide", v: states.slide-counter.get().first())) + ] + ) + let page-preamble(curr-subslide) = ( + context { + // global preamble + if here().page() == self.at("first-slide-number", default: 1) { + utils.call-or-display(self, self.preamble) + // pdfpc slide markers + if self.at("with-pdfpc-file-label", default: true) { + pdfpc.pdfpc-file(here()) + } + } + utils.call-or-display(self, self.page-preamble) + } + ) + // update states for every page + let _update-states(repetitions) = { + // 1. slide counter part + // if freeze-slide-counter is false, then update the slide-counter + if not self.at("freeze-slide-counter", default: false) { + states.slide-counter.step() + // if appendix is false, then update the last-slide-counter + if not self.at("appendix", default: false) { + states.last-slide-counter.step() + } + } + // update page counter + context counter(page).update(states.slide-counter.get()) + } + self.subslide = 1 + // for single page slide, get the repetitions + if repeat == auto { + let (_, repetitions) = _parse-content-into-results-and-repetitions( + self: self, + base: 1, + index: 1, + ..bodies, + ) + repeat = repetitions + } + self.repeat = repeat + // page header and footer + let (header, footer) = _get-header-footer(self) + let page-extra-args = _get-page-extra-args(self) + // for speed up, do not parse the content if repeat is none + if repeat == none { + return { + let conts = bodies.map(it => { + if type(it) == function { + it(self) + } else { + it + } + }) + header = _update-states(1) + update-pdfpc(1) + header + set page(..(self.page + page-extra-args + (header: header, footer: footer))) + setting(page-preamble(1) + composer-with-side-by-side(..conts)) + } + } + + if self.handout { + self.subslide = repeat + let (conts, _) = _parse-content-into-results-and-repetitions(self: self, index: repeat, ..bodies) + header = _update-states(1) + update-pdfpc(1) + header + set page(..(self.page + page-extra-args + (header: header, footer: footer))) + setting(page-preamble(1) + composer-with-side-by-side(..conts)) + } else { + // render all the subslides + let result = () + let current = 1 + for i in range(1, repeat + 1) { + self.subslide = i + let (header, footer) = _get-header-footer(self) + let new-header = header + let (conts, _) = _parse-content-into-results-and-repetitions(self: self, index: i, ..bodies) + // update the counter in the first subslide + if i == 1 { + new-header = _update-states(repeat) + update-pdfpc(i) + new-header + } else { + new-header = update-pdfpc(i) + new-header + } + result.push({ + set page(..(self.page + page-extra-args + (header: new-header, footer: footer))) + setting(page-preamble(i) + composer-with-side-by-side(..conts)) + }) + } + // return the result + result.sum() + } +} \ No newline at end of file diff --git a/src/core.typ b/src/slides.typ similarity index 86% rename from src/core.typ rename to src/slides.typ index a352eafaa..5c6668421 100644 --- a/src/core.typ +++ b/src/slides.typ @@ -1,18 +1,9 @@ #import "utils.typ" -#import "configs.typ": * - - -/// Wrapper for a slide function -/// -/// - `fn` (self => { .. }): The function that will be called to render the slide -#let touying-slide-wrapper(fn) = utils.label-it( - metadata(( - kind: "touying-slide-wrapper", - fn: fn, - )), - "touying-temporary-mark", -) +#import "configs.typ" +/// ------------------------------------------------ +/// Slides +/// ------------------------------------------------ /// Set config #let touying-set-config(config, body) = utils.label-it( @@ -49,7 +40,7 @@ /// Call touying slide function -#let call-slide-fn(self, fn, body) = { +#let _call-slide-fn(self, fn, body) = { let slide-wrapper = fn(body) assert( utils.is-kind(slide-wrapper, "touying-slide-wrapper"), @@ -60,7 +51,7 @@ /// Use headings to split a content block into slides -#let split-content-into-slides(self: none, recaller-map: (:), first-slide: false, body) = { +#let _split-content-into-slides(self: none, recaller-map: (:), first-slide: false, body) = { // Extract arguments assert(type(self) == dictionary, message: "`self` must be a dictionary") assert("slide-level" in self and type(self.slide-level) == int, message: "`self.slide-level` must be an integer") @@ -91,8 +82,12 @@ } } } - let call-slide-fn-and-reset(self, slide-fn, current-slide-cont, recaller-map) = { - let cont = call-slide-fn(self, slide-fn, current-slide-cont) + let call-slide-fn-and-reset(self, already-slide-wrapper: false, slide-fn, current-slide-cont, recaller-map) = { + let cont = if already-slide-wrapper { + slide-fn(self) + } else { + _call-slide-fn(self, slide-fn, current-slide-cont) + } let last-heading-label = get-last-heading-label(self.headings) if last-heading-label != none { recaller-map.insert(last-heading-label, cont) @@ -143,8 +138,9 @@ } (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( self + (headings: current-headings), + already-slide-wrapper: true, child.value.fn, - current-slide.sum(default: none), + none, recaller-map, ) cont @@ -245,7 +241,7 @@ cont } // Appendix content - split-content-into-slides( + _split-content-into-slides( self: self + child.value.config, recaller-map: recaller-map, first-slide: true, @@ -254,10 +250,10 @@ } else { let child = if utils.is-sequence(child) { // Split the content into slides recursively - split-content-into-slides(self: self, recaller-map: recaller-map, child) + _split-content-into-slides(self: self, recaller-map: recaller-map, child) } else if utils.is-styled(child) { // Split the content into slides recursively for styled content - utils.reconstruct-styled(child, split-content-into-slides(self: self, recaller-map: recaller-map, child.child)) + utils.reconstruct-styled(child, _split-content-into-slides(self: self, recaller-map: recaller-map, child.child)) } else { child } @@ -283,40 +279,13 @@ } } -#show: split-content-into-slides.with(self: default-config + config-common( - slide-fn: body => touying-slide-wrapper(self => [ - #set page(paper: "presentation-16-9") - - #self.headings - #self.appendix +#let touying-slides(..args, body) = { + assert(args.named().len() == 0, message: "unexpected named arguments:" + repr(args.named().keys())) + let args = (configs.default-config,) + args.pos() + let self = utils.merge-dicts(..args) - #body - ]), - new-section-slide-fn: title => touying-slide-wrapper(self => [ - #set page(paper: "presentation-16-9") - #set text(green) + show: _split-content-into-slides.with(self: self, first-slide: true) - #self.headings - - #self.appendix - - #title - ]), -)) - -= Title - -== recall1 - -sdfdf - -sdfsdfdsfsdfsdf - -== recall2 - -sdf - -= sdfsdfsdf - -sdfddd + body +} diff --git a/src/utils.typ b/src/utils.typ index e19bb3ff7..b12744e9f 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -191,17 +191,6 @@ return methods } - -// touying wrapper mark -#let touying-wrapper(fn, with-visible-subslides: false, ..args) = [ - #metadata(( - kind: "touying-wrapper", - fn: fn, - args: args, - with-visible-subslides: with-visible-subslides, - )) -] - #let slides(self) = { let m = methods(self) let res = (:) diff --git a/themes/default.typ b/themes/default.typ index 2f93e9bd1..071eab0c9 100644 --- a/themes/default.typ +++ b/themes/default.typ @@ -1,9 +1,27 @@ -#import "../slide.typ": s +#import "../src/exports.typ": * -// export default self -#let register(self: s, aspect-ratio: "16-9", ..args) = { - self.page-args += ( - paper: "presentation-" + aspect-ratio, +#let slide( + repeat: auto, + setting: body => body, + composer: auto, + ..bodies, +) = touying-slide-wrapper(self => { + touying-slide(self: self, repeat: repeat, setting: setting, composer: composer, ..bodies) +}) + + +#let default-theme( + aspect-ratio: "16-9", + ..args, + body, +) = { + show: touying-slides.with( + config-page(paper: "presentation-" + aspect-ratio), + // config-common( + // slide-fn: slide, + // ), + ..args, ) - self -} + + body +} \ No newline at end of file diff --git a/themes/themes.typ b/themes/themes.typ index f89f85410..44f65e362 100644 --- a/themes/themes.typ +++ b/themes/themes.typ @@ -1,6 +1,6 @@ #import "default.typ" -#import "simple.typ" -#import "metropolis.typ" -#import "dewdrop.typ" -#import "university.typ" -#import "aqua.typ" \ No newline at end of file +// #import "simple.typ" +// #import "metropolis.typ" +// #import "dewdrop.typ" +// #import "university.typ" +// #import "aqua.typ" \ No newline at end of file From 4cc95c15a8f2a3f626e042293c286f2f4ab3c1a6 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Thu, 29 Aug 2024 01:53:56 +0800 Subject: [PATCH 08/43] dev: clean the code and add comments --- src/exports.typ | 6 +- src/slide.typ | 136 ++++++++++++++++++++++++-- src/utils.typ | 239 +++++++++++++++++++++++++++++++++++---------- themes/default.typ | 6 +- 4 files changed, 320 insertions(+), 67 deletions(-) diff --git a/src/exports.typ b/src/exports.typ index dfcf64560..714398639 100644 --- a/src/exports.typ +++ b/src/exports.typ @@ -1,6 +1,6 @@ -#import "slide.typ": pause, meanwhile, touying-equation, touying-mitex, touying-reducer, uncover, only, alternatives-match, alternatives, alternatives-fn, alternatives-cases -#import "slides.typ": touying-set-config, appendix, touying-recall, touying-slides -#import "configs.typ": * +#import "slide.typ": pause, meanwhile, uncover, only, alternatives-match, alternatives, alternatives-fn, alternatives-cases, slide, touying-slide, touying-fn-wrapper, touying-slide-wrapper, touying-equation, touying-mitex, touying-reducer +#import "slides.typ": appendix, touying-set-config, touying-recall, touying-slides +#import "configs.typ": config-colors, config-common, config-info, config-methods, config-page, config-store, default-config #import "utils.typ" #import "states.typ" #import "magic.typ" diff --git a/src/slide.typ b/src/slide.typ index ca3b7c3a6..c4040fbc0 100644 --- a/src/slide.typ +++ b/src/slide.typ @@ -7,7 +7,7 @@ /// ------------------------------------------------ // touying function wrapper mark -#let touying-fn-wrapper(fn, with-visible-subslides: false, ..args) = label-it( +#let touying-fn-wrapper(fn, with-visible-subslides: false, ..args) = utils.label-it( metadata(( kind: "touying-fn-wrapper", fn: fn, @@ -32,13 +32,121 @@ #let pause = [#metadata((kind: "touying-pause"))] // touying meanwhile mark #let meanwhile = [#metadata((kind: "touying-meanwhile"))] -// dynamic control mark -#let uncover = touying-fn-wrapper.with(utils.uncover, with-visible-subslides: true) -#let only = touying-fn-wrapper.with(utils.only, with-visible-subslides: true) -#let alternatives-match = touying-fn-wrapper.with(utils.alternatives-match) -#let alternatives = touying-fn-wrapper.with(utils.alternatives) -#let alternatives-fn = touying-fn-wrapper.with(utils.alternatives-fn) -#let alternatives-cases = touying-fn-wrapper.with(utils.alternatives-cases) + +/// Uncover content in some subslides. Reserved space when hidden (like `#hide()`). +/// +/// Example: `uncover("2-")[abc]` will display `[abc]` if the current slide is 2 or later +/// +/// - `visible-subslides` is a single integer, an array of integers, +/// or a string that specifies the visible subslides +/// +/// Read [polylux book](https://polylux.dev/book/dynamic/complex.html) +/// +/// The simplest extension is to use an array, such as `(1, 2, 4)` indicating that +/// slides 1, 2, and 4 are visible. This is equivalent to the string `"1, 2, 4"`. +/// +/// You can also use more convenient and complex strings to specify visible slides. +/// +/// For example, "-2, 4, 6-8, 10-" means slides 1, 2, 4, 6, 7, 8, 10, and slides after 10 are visible. +/// +/// - `uncover-cont` is the content to display when the content is visible in the subslide. +#let uncover(visible-subslides, uncover-cont) = { + touying-fn-wrapper(utils.uncover, with-visible-subslides: true, visible-subslides, uncover-cont) +} + + +/// Display content in some subslides only. +/// Don't reserve space when hidden, content is completely not existing there. +/// +/// - `visible-subslides` is a single integer, an array of integers, +/// or a string that specifies the visible subslides +/// +/// Read [polylux book](https://polylux.dev/book/dynamic/complex.html) +/// +/// The simplest extension is to use an array, such as `(1, 2, 4)` indicating that +/// slides 1, 2, and 4 are visible. This is equivalent to the string `"1, 2, 4"`. +/// +/// You can also use more convenient and complex strings to specify visible slides. +/// +/// For example, "-2, 4, 6-8, 10-" means slides 1, 2, 4, 6, 7, 8, 10, and slides after 10 are visible. +/// +/// - `only-cont` is the content to display when the content is visible in the subslide. +#let only(visible-subslides, only-cont) = { + touying-fn-wrapper(utils.only, with-visible-subslides: true, visible-subslides, only-cont) +} + + +/// `#alternatives` has a couple of "cousins" that might be more convenient in some situations. The first one is `#alternatives-match` that has a name inspired by match-statements in many functional programming languages. The idea is that you give it a dictionary mapping from subslides to content: +/// +/// #example(``` +/// #alternatives-match(( +/// "1, 3-5": [this text has the majority], +/// "2, 6": [this is shown less often] +/// )) +/// ```) +/// +/// - `subslides-contents` is a dictionary mapping from subslides to content. +/// +/// - `position` is the position of the content. Default is `bottom + left`. +#let alternatives-match(self: none, subslides-contents, position: bottom + left) = { + touying-fn-wrapper(utils.alternatives-match, subslides-contents, position: position) +} + + +/// `#alternatives` is able to show contents sequentially in subslides. +/// +/// Example: `#alternatives[Ann][Bob][Christopher]` will show "Ann" in the first subslide, "Bob" in the second subslide, and "Christopher" in the third subslide. +/// +/// - `start` is the starting subslide number. Default is `1`. +/// +/// - `repeat-last` is a boolean indicating whether the last subslide should be repeated. Default is `true`. +#let alternatives( + start: 1, + repeat-last: true, + ..args, +) = { + touying-fn-wrapper(utils.alternatives, start: start, repeat-last: repeat-last, ..args) +} + + +/// You can have very fine-grained control over the content depending on the current subslide by using #alternatives-fn. It accepts a function (hence the name) that maps the current subslide index to some content. +/// +/// Example: `#alternatives-fn(start: 2, count: 7, subslide => { numbering("(i)", subslide) })` +/// +/// - `start` is the starting subslide number. Default is `1`. +/// +/// - `end` is the ending subslide number. Default is `none`. +/// +/// - `count` is the number of subslides. Default is `none`. +#let alternatives-fn( + self: none, + start: 1, + end: none, + count: none, + ..kwargs, + fn, +) = { + touying-fn-wrapper(utils.alternatives-fn, start: start, end: end, count: count, ..kwargs, fn) +} + + +/// You can use this function if you want to have one piece of content that changes only slightly depending of what "case" of subslides you are in. +/// +/// #example(``` +/// #alternatives-cases(("1, 3", "2"), case => [ +/// #set text(fill: teal) if case == 1 +/// Some text +/// ]) +/// ```) +/// +/// - `cases` is an array of strings that specify the subslides for each case. +/// +/// - `fn` is a function that maps the case to content. The argument `case` is the index of the cases array you input. +#let alternatives-cases(self: none, cases, fn, ..kwargs) = { + touying-fn-wrapper(utils.alternatives-cases, cases, fn, ..kwargs) +} + + // touying equation mark #let touying-equation(block: true, numbering: none, supplement: auto, scope: (:), body) = utils.label( metadata(( @@ -747,4 +855,14 @@ // return the result result.sum() } -} \ No newline at end of file +} + + +#let slide( + repeat: auto, + setting: body => body, + composer: auto, + ..bodies, +) = touying-slide-wrapper(self => { + touying-slide(self: self, repeat: repeat, setting: setting, composer: composer, ..bodies) +}) \ No newline at end of file diff --git a/src/utils.typ b/src/utils.typ index b12744e9f..cad4fa0b7 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -35,7 +35,7 @@ #let trim(arr, empty-contents: ([], [ ], parbreak(), linebreak())) = { let i = 0 let j = arr.len() - 1 - while i != arr.len() and arr.at(i) in empty-contents { + while i != arr.len() and arr.at(i) in empty-contents { i += 1 } while j != i - 1 and arr.at(j) in empty-contents { @@ -237,7 +237,10 @@ // Utils: bookmark #let bookmark(level: 1, numbering: none, outlined: true, body) = { if body != auto and body != none { - place(top + left, text(0pt, hide(heading(depth: level, outlined: outlined, bookmarked: true, numbering: numbering, body)))) + place( + top + left, + text(0pt, hide(heading(depth: level, outlined: outlined, bookmarked: true, numbering: numbering, body))), + ) } } @@ -254,7 +257,11 @@ } else if type(it) == content { if it.func() == raw { if it.block { - "\n" + indent * " " + "```" + it.lang + it.text.split("\n").map(l => "\n" + indent * " " + l).sum(default: "") + "\n" + indent * " " + "```" + "\n" + indent * " " + "```" + it.lang + it + .text + .split("\n") + .map(l => "\n" + indent * " " + l) + .sum(default: "") + "\n" + indent * " " + "```" } else { "`" + it.text + "`" } @@ -265,7 +272,7 @@ } else if it.func() == list.item { "\n" + indent * " " + "- " + indent-markup-text(it.body) } else if it.func() == terms.item { - "\n" + indent * " " + "/ " + markup-text(it.term) + ": " + indent-markup-text(it.description) + "\n" + indent * " " + "/ " + markup-text(it.term) + ": " + indent-markup-text(it.description) } else if it.func() == linebreak { "\n" + indent * " " } else if it.func() == parbreak { @@ -305,7 +312,11 @@ markup-text(it.text) } } else if it.func() == smartquote { - if it.double { "\"" } else { "'" } + if it.double { + "\"" + } else { + "'" + } } else { "" } @@ -320,7 +331,7 @@ #let _size-to-pt(size, container-dimension) = { let to-convert = size - if type(size) == "ratio" { + if type(size) == ratio { to-convert = container-dimension * size } measure(v(to-convert)).height @@ -337,7 +348,12 @@ } #let fit-to-height( - width: none, prescale-width: none, grow: true, shrink: true, height, body + width: none, + prescale-width: none, + grow: true, + shrink: true, + height, + body, ) = { // Place two labels with the requested vertical separation to be able to // measure their vertical distance in pt. @@ -356,10 +372,10 @@ hidden#after-label ] - locate(loc => { - let before = query(selector(before-label).before(loc), loc) + context { + let before = query(selector(before-label).before(here())) let before-pos = before.last().location().position() - let after = query(selector(after-label).before(loc), loc) + let after = query(selector(after-label).before(here())) let after-pos = after.last().location().position() let available-height = after-pos.y - before-pos.y @@ -368,7 +384,11 @@ layout(container-size => { // Helper function to more easily grab absolute units let get-pts(body, w-or-h) = { - let dim = if w-or-h == "w" {container-size.width} else {container-size.height} + let dim = if w-or-h == "w" { + container-size.width + } else { + container-size.height + } _size-to-pt(body, styles, dim) } @@ -376,7 +396,10 @@ // Note this is different from the post-scale width, which is a limiting factor // on the allowable scaling ratio let boxed-content = _limit-content-width( - width: prescale-width, body, container-size, styles + width: prescale-width, + body, + container-size, + styles, ) // post-scaling width @@ -394,10 +417,7 @@ let w-ratio = mutable-width / size.width let ratio = calc.min(h-ratio, w-ratio) * 100% - if ( - (shrink and (ratio < 100%)) - or (grow and (ratio > 100%)) - ) { + if ((shrink and (ratio < 100%)) or (grow and (ratio > 100%))) { let new-width = size.width * ratio v(-available-height) // If not boxed, the content can overflow to the next page even though it will @@ -407,14 +427,14 @@ box( width: new-width, height: available-height, - scale(x: ratio, y: ratio, origin: top + left, boxed-content) + scale(x: ratio, y: ratio, origin: top + left, boxed-content), ) } else { body } }) }) - }) + } } #let fit-to-width(grow: true, shrink: true, width, content) = { @@ -422,15 +442,14 @@ let content-size = measure(content) let content-width = content-size.width let width = _size-to-pt(width, layout-size.width) - if ( - content-width != 0pt and - ((shrink and (width < content-width)) - or (grow and (width > content-width))) - ) { + if (content-width != 0pt and ((shrink and (width < content-width)) or (grow and (width > content-width)))) { let ratio = width / content-width * 100% // The first box keeps content from prematurely wrapping let scaled = scale( - box(content, width: content-width), origin: top + left, x: ratio, y: ratio + box(content, width: content-width), + origin: top + left, + x: ratio, + y: ratio, ) // The second box lets typst know the post-scaled dimensions, since `scale` // doesn't update layout information @@ -444,12 +463,9 @@ // semitransparency cover #let cover-with-rect(..cover-args, fill: auto, inline: true, body) = { if fill == auto { - panic( - "`auto` fill value is not supported until typst provides utilities to" - + " retrieve the current page background" - ) + panic("`auto` fill value is not supported until typst provides utilities to" + " retrieve the current page background") } - if type(fill) == "string" { + if type(fill) == str { fill = rgb(fill) } @@ -478,7 +494,7 @@ stack( spacing: -wrapped-body-size.height, body, - rect(fill: fill, ..named, ..cover-args.pos()) + rect(fill: fill, ..named, ..cover-args.pos()), ) } }) @@ -506,17 +522,17 @@ if match-until != none { let parsed = int(match-until.captures.first()) // assert(parsed > 0, "parsed idx is non-positive") - ( until: parsed ) + (until: parsed) } else if match-beginning != none { let parsed = int(match-beginning.captures.first()) // assert(parsed > 0, "parsed idx is non-positive") - ( beginning: parsed ) + (beginning: parsed) } else if match-range != none { let parsed-first = int(match-range.captures.first()) let parsed-last = int(match-range.captures.last()) // assert(parsed-first > 0, "parsed idx is non-positive") // assert(parsed-last > 0, "parsed idx is non-positive") - ( beginning: parsed-first, until: parsed-last ) + (beginning: parsed-first, until: parsed-last) } else if match-single != none { let parsed = int(match-single.captures.first()) // assert(parsed > 0, "parsed idx is non-positive") @@ -528,18 +544,36 @@ parts.map(parse-part) } + +/// Check if a slide is visible +/// +/// Example: `check-visible(3, "2-")` returns `true` +/// +/// - `idx` is the index of the slide +/// +/// - `visible-subslides` is a single integer, an array of integers, +/// or a string that specifies the visible subslides +/// +/// Read [polylux book](https://polylux.dev/book/dynamic/complex.html) +/// +/// The simplest extension is to use an array, such as `(1, 2, 4)` indicating that +/// slides 1, 2, and 4 are visible. This is equivalent to the string `"1, 2, 4"`. +/// +/// You can also use more convenient and complex strings to specify visible slides. +/// +/// For example, "-2, 4, 6-8, 10-" means slides 1, 2, 4, 6, 7, 8, 10, and slides after 10 are visible. #let check-visible(idx, visible-subslides) = { - if type(visible-subslides) == "integer" { + if type(visible-subslides) == int { idx == visible-subslides - } else if type(visible-subslides) == "array" { + } else if type(visible-subslides) == array { visible-subslides.any(s => check-visible(idx, s)) - } else if type(visible-subslides) == "string" { + } else if type(visible-subslides) == str { let parts = _parse-subslide-indices(visible-subslides) check-visible(idx, parts) } else if type(visible-subslides) == content and visible-subslides.has("text") { let parts = _parse-subslide-indices(visible-subslides.text) check-visible(idx, parts) - } else if type(visible-subslides) == "dictionary" { + } else if type(visible-subslides) == dictionary { let lower-okay = if "beginning" in visible-subslides { visible-subslides.beginning <= idx } else { @@ -558,15 +592,16 @@ } } + #let last-required-subslide(visible-subslides) = { - if type(visible-subslides) == "integer" { + if type(visible-subslides) == int { visible-subslides - } else if type(visible-subslides) == "array" { + } else if type(visible-subslides) == array { calc.max(..visible-subslides.map(s => last-required-subslide(s))) - } else if type(visible-subslides) == "string" { + } else if type(visible-subslides) == str { let parts = _parse-subslide-indices(visible-subslides) last-required-subslide(parts) - } else if type(visible-subslides) == "dictionary" { + } else if type(visible-subslides) == dictionary { let last = 0 if "beginning" in visible-subslides { last = calc.max(last, visible-subslides.beginning) @@ -580,21 +615,70 @@ } } +/// Uncover content in some subslides. Reserved space when hidden (like `#hide()`). +/// +/// Example: `uncover("2-")[abc]` will display `[abc]` if the current slide is 2 or later +/// +/// - `visible-subslides` is a single integer, an array of integers, +/// or a string that specifies the visible subslides +/// +/// Read [polylux book](https://polylux.dev/book/dynamic/complex.html) +/// +/// The simplest extension is to use an array, such as `(1, 2, 4)` indicating that +/// slides 1, 2, and 4 are visible. This is equivalent to the string `"1, 2, 4"`. +/// +/// You can also use more convenient and complex strings to specify visible slides. +/// +/// For example, "-2, 4, 6-8, 10-" means slides 1, 2, 4, 6, 7, 8, 10, and slides after 10 are visible. +/// +/// - `uncover-cont` is the content to display when the content is visible in the subslide. #let uncover(self: none, visible-subslides, uncover-cont) = { let cover = self.methods.cover.with(self: self) - if check-visible(self.subslide, visible-subslides) { + if check-visible(self.subslide, visible-subslides) { uncover-cont } else { cover(uncover-cont) } } + +/// Display content in some subslides only. +/// Don't reserve space when hidden, content is completely not existing there. +/// +/// - `visible-subslides` is a single integer, an array of integers, +/// or a string that specifies the visible subslides +/// +/// Read [polylux book](https://polylux.dev/book/dynamic/complex.html) +/// +/// The simplest extension is to use an array, such as `(1, 2, 4)` indicating that +/// slides 1, 2, and 4 are visible. This is equivalent to the string `"1, 2, 4"`. +/// +/// You can also use more convenient and complex strings to specify visible slides. +/// +/// For example, "-2, 4, 6-8, 10-" means slides 1, 2, 4, 6, 7, 8, 10, and slides after 10 are visible. +/// +/// - `only-cont` is the content to display when the content is visible in the subslide. #let only(self: none, visible-subslides, only-cont) = { - if check-visible(self.subslide, visible-subslides) { only-cont } + if check-visible(self.subslide, visible-subslides) { + only-cont + } } + +/// `#alternatives` has a couple of "cousins" that might be more convenient in some situations. The first one is `#alternatives-match` that has a name inspired by match-statements in many functional programming languages. The idea is that you give it a dictionary mapping from subslides to content: +/// +/// #example(``` +/// #alternatives-match(( +/// "1, 3-5": [this text has the majority], +/// "2, 6": [this is shown less often] +/// )) +/// ```) +/// +/// - `subslides-contents` is a dictionary mapping from subslides to content. +/// +/// - `position` is the position of the content. Default is `bottom + left`. #let alternatives-match(self: none, subslides-contents, position: bottom + left) = { - let subslides-contents = if type(subslides-contents) == "dictionary" { + let subslides-contents = if type(subslides-contents) == dictionary { subslides-contents.pairs() } else { subslides-contents @@ -607,20 +691,32 @@ let max-width = calc.max(..sizes.map(sz => sz.width)) let max-height = calc.max(..sizes.map(sz => sz.height)) for (subslides, content) in subslides-contents { - only(self: self, subslides, box( - width: max-width, - height: max-height, - align(position, content) - )) + only( + self: self, + subslides, + box( + width: max-width, + height: max-height, + align(position, content), + ), + ) } } } + +/// `#alternatives` is able to show contents sequentially in subslides. +/// +/// Example: `#alternatives[Ann][Bob][Christopher]` will show "Ann" in the first subslide, "Bob" in the second subslide, and "Christopher" in the third subslide. +/// +/// - `start` is the starting subslide number. Default is `1`. +/// +/// - `repeat-last` is a boolean indicating whether the last subslide should be repeated. Default is `true`. #let alternatives( self: none, start: 1, repeat-last: true, - ..args + ..args, ) = { let contents = args.pos() let kwargs = args.named() @@ -631,13 +727,23 @@ alternatives-match(self: self, subslides.zip(contents), ..kwargs) } + +/// You can have very fine-grained control over the content depending on the current subslide by using #alternatives-fn. It accepts a function (hence the name) that maps the current subslide index to some content. +/// +/// Example: `#alternatives-fn(start: 2, count: 7, subslide => { numbering("(i)", subslide) })` +/// +/// - `start` is the starting subslide number. Default is `1`. +/// +/// - `end` is the ending subslide number. Default is `none`. +/// +/// - `count` is the number of subslides. Default is `none`. #let alternatives-fn( self: none, start: 1, end: none, count: none, ..kwargs, - fn + fn, ) = { let end = if end == none { if count == none { @@ -654,6 +760,19 @@ alternatives-match(self: self, subslides.zip(contents), ..kwargs.named()) } + +/// You can use this function if you want to have one piece of content that changes only slightly depending of what "case" of subslides you are in. +/// +/// #example(``` +/// #alternatives-cases(("1, 3", "2"), case => [ +/// #set text(fill: teal) if case == 1 +/// Some text +/// ]) +/// ```) +/// +/// - `cases` is an array of strings that specify the subslides for each case. +/// +/// - `fn` is a function that maps the case to content. The argument `case` is the index of the cases array you input. #let alternatives-cases(self: none, cases, fn, ..kwargs) = { let idcs = range(cases.len()) let contents = idcs.map(fn) @@ -662,11 +781,27 @@ // SIDE BY SIDE +/// A simple wrapper around `grid` that creates a grid with a single row. +/// It is useful for creating side-by-side slide. +/// +/// It is also the default function for composer in the slide function. +/// +/// Example: `side-by-side[a][b][c]` will display `a`, `b`, and `c` side by side. +/// +/// - `columns` is the number of columns. Default is `auto`, which means the number of columns is equal to the number of bodies. +/// +/// - `gutter` is the space between columns. Default is `1em`. +/// +/// - `..bodies` is the contents to display side by side. #let side-by-side(columns: auto, gutter: 1em, ..bodies) = { let bodies = bodies.pos() if bodies.len() == 1 { return bodies.first() } - let columns = if columns == auto { (1fr,) * bodies.len() } else { columns } + let columns = if columns == auto { + (1fr,) * bodies.len() + } else { + columns + } grid(columns: columns, gutter: gutter, ..bodies) } diff --git a/themes/default.typ b/themes/default.typ index 071eab0c9..66a4b1803 100644 --- a/themes/default.typ +++ b/themes/default.typ @@ -17,9 +17,9 @@ ) = { show: touying-slides.with( config-page(paper: "presentation-" + aspect-ratio), - // config-common( - // slide-fn: slide, - // ), + config-common( + slide-fn: slide, + ), ..args, ) From cdafbcc8b6260bb78e100c3d6adf8b65e6b89dc1 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Thu, 29 Aug 2024 02:52:53 +0800 Subject: [PATCH 09/43] dev: add more comments --- src/slide.typ | 148 ++++++++++++++++++++++++++++++++++++++++++--- themes/default.typ | 21 +++++++ 2 files changed, 160 insertions(+), 9 deletions(-) diff --git a/src/slide.typ b/src/slide.typ index c4040fbc0..e1218deea 100644 --- a/src/slide.typ +++ b/src/slide.typ @@ -6,7 +6,18 @@ /// Slide /// ------------------------------------------------ -// touying function wrapper mark +/// Wrapper for a function to make it can receive `self` as an argument. +/// It is useful when you want to use `self` to get current subslide index, like `uncover` and `only` functions. +/// +/// Example: `#let alternatives = touying-fn-wrapper.with(utils.alternatives)` +/// +/// - `fn` ((self: none, ..args) => { .. }): The function that will be called. +/// +/// - `with-visible-subslides` (bool): Whether the first argument of the function is the visible subslides. +/// +/// It is useful for functions like `uncover` and `only`. +/// +/// Touying will automatically update the max repetitions for the slide if the function is called with visible subslides. #let touying-fn-wrapper(fn, with-visible-subslides: false, ..args) = utils.label-it( metadata(( kind: "touying-fn-wrapper", @@ -17,9 +28,19 @@ "touying-temporary-mark", ) -/// Wrapper for a slide function +/// Wrapper for a slide function to make it can receive `self` as an argument. +/// +/// Notice: This function is necessary for the slide function to work in Touying. +/// +/// Example: +/// +/// ```typst +/// #let slide(..args) = touying-slide-wrapper(self => { +/// touying-slide(self: self, ..args) +/// }) +/// ``` /// -/// - `fn` (self => { .. }): The function that will be called to render the slide +/// - `fn` (self => { .. }): The function that will be called with an argument `self`. #let touying-slide-wrapper(fn) = utils.label-it( metadata(( kind: "touying-slide-wrapper", @@ -28,11 +49,15 @@ "touying-temporary-mark", ) -// touying pause mark + +/// Uncover content after the `#pause` mark in next subslide. #let pause = [#metadata((kind: "touying-pause"))] -// touying meanwhile mark + + +/// Display content after the `#meanwhile` mark meanwhile. #let meanwhile = [#metadata((kind: "touying-meanwhile"))] + /// Uncover content in some subslides. Reserved space when hidden (like `#hide()`). /// /// Example: `uncover("2-")[abc]` will display `[abc]` if the current slide is 2 or later @@ -147,7 +172,24 @@ } -// touying equation mark +/// Touying also provides a unique and highly useful feature—math equation animations, allowing you to conveniently use pause and meanwhile within math equations. +/// +/// #example(``` +/// #touying-equation(` +/// f(x) &= pause x^2 + 2x + 1 \ +/// &= pause (x + 1)^2 \ +/// `) +/// ```) +/// +/// - `block` is a boolean indicating whether the equation is a block. Default is `true`. +/// +/// - `numbering` is the numbering of the equation. Default is `none`. +/// +/// - `supplement` is the supplement of the equation. Default is `auto`. +/// +/// - `scope` is the scope when we use `eval()` function to evaluate the equation. +/// +/// - `body` is the content of the equation. It should be a string, a raw text or a function that receives `self` as an argument and returns a string. #let touying-equation(block: true, numbering: none, supplement: auto, scope: (:), body) = utils.label( metadata(( kind: "touying-equation", @@ -170,7 +212,26 @@ "touying-temporary-mark", ) -// touying mitex mark + +/// Touying can integrate with `mitex` to display math equations. +/// You can use `#touying-mitex` to display math equations with pause and meanwhile. +/// +/// #example(``` +/// #touying-mitex(mitex, ` +/// f(x) &= \pause x^2 + 2x + 1 \\ +/// &= \pause (x + 1)^2 \\ +/// `) +/// ```) +/// +/// - `mitex` is the mitex function. You can import it by code like `#import "@preview/mitex:0.2.3": mitex` +/// +/// - `block` is a boolean indicating whether the equation is a block. Default is `true`. +/// +/// - `numbering` is the numbering of the equation. Default is `none`. +/// +/// - `supplement` is the supplement of the equation. Default is `auto`. +/// +/// - `body` is the content of the equation. It should be a string, a raw text or a function that receives `self` as an argument and returns a string. #let touying-mitex(block: true, numbering: none, supplement: auto, mitex, body) = utils.label( metadata(( kind: "touying-mitex", @@ -193,7 +254,20 @@ "touying-temporary-mark", ) -// touying reducer mark + +/// Touying reducer is a powerful tool to provide more powerful animation effects for other packages or functions. +/// +/// For example, you can adds `pause` and `meanwhile` animations to cetz and fletcher packages. +/// +/// Cetz: `#let cetz-canvas = touying-reducer.with(reduce: cetz.canvas, cover: cetz.draw.hide.with(bounds: true))` +/// +/// Fletcher: `#let fletcher-diagram = touying-reducer.with(reduce: fletcher.diagram, cover: fletcher.hide)` +/// +/// - `reduce` is the reduce function that will be called. It is usually a function that receives an array of content and returns a content it painted. Just like the `cetz.canvas` or `fletcher.diagram` function. +/// +/// - `cover` is the cover function that will be called when some content is hidden. It is usually a function that receives an the argument of the content that will be hidden. Just like the `cetz.draw.hide` or `fletcher.hide` function. +/// +/// - `..args` is the arguments of the reducer function. #let touying-reducer(reduce: arr => arr.sum(), cover: arr => none, ..args) = utils.label-it( metadata(( kind: "touying-reducer", @@ -205,6 +279,7 @@ "touying-temporary-mark", ) + // parse touying equation, and get the repetitions #let _parse-touying-equation(self: none, need-cover: true, base: 1, index: 1, eqt) = { let result-arr = () @@ -742,7 +817,41 @@ (header, footer) } -// touying-slide + +/// Touying slide function, the core function of touying. It usually is used to create a slide with animation effects and works with `touying-slide-wrapper` function. +/// +/// Example: +/// +/// ``` +/// #let slide( +/// repeat: auto, +/// setting: body => body, +/// composer: auto, +/// ..bodies, +/// ) = touying-slide-wrapper(self => { +/// touying-slide(self: self, repeat: repeat, setting: setting, composer: composer, ..bodies) +/// }) +/// ``` +/// +/// - `repeat` is the number of subslides. Default is `auto`,which means touying will automatically calculate the number of subslides. +/// +/// The `repeat` argument is necessary when you use `#slide(repeat: 3, self => [ .. ])` style code to create a slide. The callback-style `uncover` and `only` cannot be detected by touying automatically. +/// +/// - `setting` is the setting of the slide. You can use it to add some set/show rules for the slide. +/// +/// - `composer` is the composer of the slide. You can use it to set the layout of the slide. +/// +/// For example, `#slide(composer: (1fr, 2fr, 1fr))[A][B][C]` to split the slide into three parts. The first and the last parts will take 1/4 of the slide, and the second part will take 1/2 of the slide. +/// +/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `utils.side-by-side` function. +/// +/// The `utils.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. +/// +/// For example, `#slide(composer: 2)[A][B][#grid.cell(colspan: 2)[Footer]] will make the `Footer` cell take 2 columns. +/// +/// If you want to customize the composer, you can pass a function to the `composer` argument. The function should receive the contents of the slide and return the content of the slide, like `#slide(composer: grid.with(columns: 2))[A][B]`. +/// +/// - `..bodies` is the contents of the slide. You can call the `slide` function with syntax like `#slide[A][B][C]` to create a slide. #let touying-slide( self: none, repeat: auto, @@ -858,6 +967,27 @@ } +/// Touying slide function. +/// +/// - `repeat` is the number of subslides. Default is `auto`,which means touying will automatically calculate the number of subslides. +/// +/// The `repeat` argument is necessary when you use `#slide(repeat: 3, self => [ .. ])` style code to create a slide. The callback-style `uncover` and `only` cannot be detected by touying automatically. +/// +/// - `setting` is the setting of the slide. You can use it to add some set/show rules for the slide. +/// +/// - `composer` is the composer of the slide. You can use it to set the layout of the slide. +/// +/// For example, `#slide(composer: (1fr, 2fr, 1fr))[A][B][C]` to split the slide into three parts. The first and the last parts will take 1/4 of the slide, and the second part will take 1/2 of the slide. +/// +/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `utils.side-by-side` function. +/// +/// The `utils.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. +/// +/// For example, `#slide(composer: 2)[A][B][#grid.cell(colspan: 2)[Footer]] will make the `Footer` cell take 2 columns. +/// +/// If you want to customize the composer, you can pass a function to the `composer` argument. The function should receive the contents of the slide and return the content of the slide, like `#slide(composer: grid.with(columns: 2))[A][B]`. +/// +/// - `..bodies` is the contents of the slide. You can call the `slide` function with syntax like `#slide[A][B][C]` to create a slide. #let slide( repeat: auto, setting: body => body, diff --git a/themes/default.typ b/themes/default.typ index 66a4b1803..705c9ab94 100644 --- a/themes/default.typ +++ b/themes/default.typ @@ -1,5 +1,26 @@ #import "../src/exports.typ": * +/// Touying slide function. +/// +/// - `repeat` is the number of subslides. Default is `auto`,which means touying will automatically calculate the number of subslides. +/// +/// The `repeat` argument is necessary when you use `#slide(repeat: 3, self => [ .. ])` style code to create a slide. The callback-style `uncover` and `only` cannot be detected by touying automatically. +/// +/// - `setting` is the setting of the slide. You can use it to add some set/show rules for the slide. +/// +/// - `composer` is the composer of the slide. You can use it to set the layout of the slide. +/// +/// For example, `#slide(composer: (1fr, 2fr, 1fr))[A][B][C]` to split the slide into three parts. The first and the last parts will take 1/4 of the slide, and the second part will take 1/2 of the slide. +/// +/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `utils.side-by-side` function. +/// +/// The `utils.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. +/// +/// For example, `#slide(composer: 2)[A][B][#grid.cell(colspan: 2)[Footer]] will make the `Footer` cell take 2 columns. +/// +/// If you want to customize the composer, you can pass a function to the `composer` argument. The function should receive the contents of the slide and return the content of the slide, like `#slide(composer: grid.with(columns: 2))[A][B]`. +/// +/// - `..bodies` is the contents of the slide. You can call the `slide` function with syntax like `#slide[A][B][C]` to create a slide. #let slide( repeat: auto, setting: body => body, From 1a59cd83510c827ab3f2b715d91d090a31eace11 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Thu, 29 Aug 2024 17:20:54 +0800 Subject: [PATCH 10/43] dev: more preambles --- src/configs.typ | 31 ++++++++++++++++++-- src/slide.typ | 77 +++++++++++++++++++++++-------------------------- src/slides.typ | 67 ++++++++++++++++++++---------------------- 3 files changed, 96 insertions(+), 79 deletions(-) diff --git a/src/configs.typ b/src/configs.typ index c3e67ce1a..22c893787 100644 --- a/src/configs.typ +++ b/src/configs.typ @@ -1,3 +1,5 @@ +#import "states.typ" +#import "pdfpc.typ" #import "utils.typ" #import "slide.typ": touying-slide-wrapper, touying-slide @@ -37,7 +39,7 @@ /// /// - auto-offset-for-heading (bool): Whether to add an offset relative to slide-level for headings. /// -/// - with-pdfpc-file-label (bool): Whether to add `` label for querying. +/// - enable-pdfpc (bool): Whether to add `` label for querying. /// /// You can export the .pdfpc file directly using: `typst query --root . ./example.typ --field value --one "" > ./example.pdfpc` /// @@ -80,8 +82,9 @@ zero-margin-header: true, zero-margin-footer: true, auto-offset-for-heading: true, - with-pdfpc-file-label: true, + enable-pdfpc: true, enable-mark-warning: true, + reset-page-counter-to-slide-counter: true, // some black magics for better slides writing, // maybe will be deprecated in the future frozen-states: (), @@ -109,12 +112,30 @@ } } } + if self.at("enable-pdfpc", default: true) { + context pdfpc.pdfpc-file(here()) + } }, + slide-preamble: none, + default-slide-preamble: none, + subslide-preamble: none, + default-subslide-preamble: none, page-preamble: none, default-page-preamble: self => { if self.at("reset-footnote-number-per-slide", default: true) { counter(footnote).update(0) } + if self.at("reset-page-counter-to-slide-counter", default: true) { + context counter(page).update(states.slide-counter.get()) + } + if self.at("enable-pdfpc", default: true) { + context [ + #metadata((t: "NewSlide")) + #metadata((t: "Idx", v: here().page() - 1)) + #metadata((t: "Overlay", v: self.subslide - 1)) + #metadata((t: "LogicalSlide", v: states.slide-counter.get().first())) + ] + } }, show-notes-on-second-screen: none, horizontal-line-to-pagebreak: true, @@ -139,7 +160,7 @@ zero-margin-header: zero-margin-header, zero-margin-footer: zero-margin-footer, auto-offset-for-heading: auto-offset-for-heading, - with-pdfpc-file-label: with-pdfpc-file-label, + enable-pdfpc: enable-pdfpc, enable-mark-warning: enable-mark-warning, frozen-states: frozen-states, frozen-counters: frozen-counters, @@ -148,6 +169,10 @@ first-slide-number: first-slide-number, preamble: preamble, default-preamble: default-preamble, + slide-preamble: slide-preamble, + default-slide-preamble: default-slide-preamble, + subslide-preamble: subslide-preamble, + default-subslide-preamble: default-subslide-preamble, page-preamble: page-preamble, default-page-preamble: default-page-preamble, show-notes-on-second-screen: show-notes-on-second-screen, diff --git a/src/slide.typ b/src/slide.typ index e1218deea..4425d1f3c 100644 --- a/src/slide.typ +++ b/src/slide.typ @@ -868,41 +868,41 @@ } } let bodies = bodies.pos() - // update pdfpc - let update-pdfpc(curr-subslide) = ( - context [ - #metadata((t: "NewSlide")) - #metadata((t: "Idx", v: here().page() - 1)) - #metadata((t: "Overlay", v: curr-subslide - 1)) - #metadata((t: "LogicalSlide", v: states.slide-counter.get().first())) - ] - ) - let page-preamble(curr-subslide) = ( - context { - // global preamble - if here().page() == self.at("first-slide-number", default: 1) { - utils.call-or-display(self, self.preamble) - // pdfpc slide markers - if self.at("with-pdfpc-file-label", default: true) { - pdfpc.pdfpc-file(here()) - } - } - utils.call-or-display(self, self.page-preamble) + let slide-preamble(self) = { + if self.at("is-first-slide", default: false) { + utils.call-or-display(self, self.at("preamble", default: none)) + utils.call-or-display(self, self.at("default-preamble", default: none)) } - ) + // add headings for the first subslide + if self.at("headings", default: ()) != () { + place(hide(self.at("headings", default: none).sum(default: none))) + } + utils.call-or-display(self, self.at("slide-preamble", default: none)) + utils.call-or-display(self, self.at("default-slide-preamble", default: none)) + } + // preamble for the subslides + let subslide-preamble(self) = { + if self.subslide == 1 { + slide-preamble(self) + } + utils.call-or-display(self, self.at("subslide-preamble", default: none)) + utils.call-or-display(self, self.at("default-subslide-preamble", default: none)) + } // update states for every page - let _update-states(repetitions) = { + let page-preamble(self) = { // 1. slide counter part // if freeze-slide-counter is false, then update the slide-counter - if not self.at("freeze-slide-counter", default: false) { - states.slide-counter.step() - // if appendix is false, then update the last-slide-counter - if not self.at("appendix", default: false) { - states.last-slide-counter.step() + if self.subslide == 1 { + if not self.at("freeze-slide-counter", default: false) { + states.slide-counter.step() + // if appendix is false, then update the last-slide-counter + if not self.at("appendix", default: false) { + states.last-slide-counter.step() + } } } - // update page counter - context counter(page).update(states.slide-counter.get()) + utils.call-or-display(self, self.at("page-preamble", default: none)) + utils.call-or-display(self, self.at("default-page-preamble", default: none)) } self.subslide = 1 // for single page slide, get the repetitions @@ -929,18 +929,18 @@ it } }) - header = _update-states(1) + update-pdfpc(1) + header + header = page-preamble(self) + header set page(..(self.page + page-extra-args + (header: header, footer: footer))) - setting(page-preamble(1) + composer-with-side-by-side(..conts)) + setting(subslide-preamble(self) + composer-with-side-by-side(..conts)) } } if self.handout { self.subslide = repeat let (conts, _) = _parse-content-into-results-and-repetitions(self: self, index: repeat, ..bodies) - header = _update-states(1) + update-pdfpc(1) + header + header = page-preamble(self) + header set page(..(self.page + page-extra-args + (header: header, footer: footer))) - setting(page-preamble(1) + composer-with-side-by-side(..conts)) + setting(subslide-preamble(self) + composer-with-side-by-side(..conts)) } else { // render all the subslides let result = () @@ -948,17 +948,12 @@ for i in range(1, repeat + 1) { self.subslide = i let (header, footer) = _get-header-footer(self) - let new-header = header let (conts, _) = _parse-content-into-results-and-repetitions(self: self, index: i, ..bodies) - // update the counter in the first subslide - if i == 1 { - new-header = _update-states(repeat) + update-pdfpc(i) + new-header - } else { - new-header = update-pdfpc(i) + new-header - } + let new-header = page-preamble(self) + header + // update the counter in the first subslide only result.push({ set page(..(self.page + page-extra-args + (header: new-header, footer: footer))) - setting(page-preamble(i) + composer-with-side-by-side(..conts)) + setting(subslide-preamble(self) + composer-with-side-by-side(..conts)) }) } // return the result diff --git a/src/slides.typ b/src/slides.typ index 5c6668421..9ad3b5261 100644 --- a/src/slides.typ +++ b/src/slides.typ @@ -51,7 +51,7 @@ /// Use headings to split a content block into slides -#let _split-content-into-slides(self: none, recaller-map: (:), first-slide: false, body) = { +#let _split-content-into-slides(self: none, recaller-map: (:), new-start: true, is-first-slide: false, body) = { // Extract arguments assert(type(self) == dictionary, message: "`self` must be a dictionary") assert("slide-level" in self and type(self.slide-level) == int, message: "`self.slide-level` must be an integer") @@ -92,7 +92,7 @@ if last-heading-label != none { recaller-map.insert(last-heading-label, cont) } - (cont, recaller-map, (), (), true) + (cont, recaller-map, (), (), true, false) } // The empty content list let empty-contents = ([], [ ], parbreak(), linebreak()) @@ -104,9 +104,6 @@ let current-slide = () // The current slide content let cont = none - // Is the first part should be a slide - let first-slide = first-slide - // Is we have a horizontal line let horizontal-line = false @@ -116,8 +113,8 @@ // split content when we have a horizontal line if horizontal-line-to-pagebreak and horizontal-line and child not in ([—], [–], [-]) { current-slide = utils.trim(current-slide) - (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings), + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), slide-fn, current-slide.sum(default: none), recaller-map, @@ -128,16 +125,16 @@ if utils.is-kind(child, "touying-slide-wrapper") { current-slide = utils.trim(current-slide) if current-slide != () { - (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings), + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), slide-fn, current-slide.sum(default: none), recaller-map, ) cont } - (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings), + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), already-slide-wrapper: true, child.value.fn, none, @@ -147,8 +144,8 @@ } else if utils.is-kind(child, "touying-slide-recaller") { current-slide = utils.trim(current-slide) if current-slide != () or current-headings != () { - (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings), + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), slide-fn, current-slide.sum(default: none), recaller-map, @@ -162,8 +159,8 @@ } else if child == pagebreak() { // split content when we have a pagebreak current-slide = utils.trim(current-slide) - (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings), + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), slide-fn, current-slide.sum(default: none), recaller-map, @@ -183,8 +180,8 @@ child.depth == 3 and new-subsubsection-slide-fn != none ) or (child.depth == 4 and new-subsubsubsection-slide-fn != none) { if current-slide != () or current-headings != () { - (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings), + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), slide-fn, current-slide.sum(default: none), recaller-map, @@ -194,35 +191,35 @@ } current-headings.push(child) - first-slide = true + new-start = true if child.depth == 1 and new-section-slide-fn != none { - (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings), + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), new-section-slide-fn, child.body, recaller-map, ) cont } else if child.depth == 2 and new-subsection-slide-fn != none { - (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings), + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), new-subsection-slide-fn, child.body, recaller-map, ) cont } else if child.depth == 3 and new-subsubsection-slide-fn != none { - (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings), + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), new-subsubsection-slide-fn, child.body, recaller-map, ) cont } else if child.depth == 4 and new-subsubsubsection-slide-fn != none { - (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings), + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), new-subsubsubsection-slide-fn, child.body, recaller-map, @@ -232,8 +229,8 @@ } else if utils.is-kind(child, "touying-set-config") { if current-slide != () or current-headings != () { - (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings), + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), slide-fn, current-slide.sum(default: none), recaller-map, @@ -244,20 +241,20 @@ _split-content-into-slides( self: self + child.value.config, recaller-map: recaller-map, - first-slide: true, + new-start: true, child.value.body, ) } else { let child = if utils.is-sequence(child) { // Split the content into slides recursively - _split-content-into-slides(self: self, recaller-map: recaller-map, child) + _split-content-into-slides(self: self, recaller-map: recaller-map, new-start: false, child) } else if utils.is-styled(child) { // Split the content into slides recursively for styled content - utils.reconstruct-styled(child, _split-content-into-slides(self: self, recaller-map: recaller-map, child.child)) + utils.reconstruct-styled(child, _split-content-into-slides(self: self, recaller-map: recaller-map, new-start: false, child.child)) } else { child } - if first-slide { + if new-start { // Add the child to the current slide current-slide.push(child) } else { @@ -269,8 +266,8 @@ // Handle the last slide current-slide = utils.trim(current-slide) if current-slide != () or current-headings != () { - (cont, recaller-map, current-headings, current-slide, first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings), + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), slide-fn, current-slide.sum(default: none), recaller-map, @@ -285,7 +282,7 @@ let args = (configs.default-config,) + args.pos() let self = utils.merge-dicts(..args) - show: _split-content-into-slides.with(self: self, first-slide: true) + show: _split-content-into-slides.with(self: self, is-first-slide: true) body } From e601edec1ffc29f7ac87f24b3555792f21266424 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Thu, 29 Aug 2024 18:02:44 +0800 Subject: [PATCH 11/43] dev: frozen-states --- src/configs.typ | 1 + src/slide.typ | 41 ++++++++++++++++++++++++++++++++++++++++- src/states.typ | 11 +++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/configs.typ b/src/configs.typ index 22c893787..efcb38d68 100644 --- a/src/configs.typ +++ b/src/configs.typ @@ -87,6 +87,7 @@ reset-page-counter-to-slide-counter: true, // some black magics for better slides writing, // maybe will be deprecated in the future + enable-frozen-states-and-counters: true, frozen-states: (), default-frozen-states: ( // ctheorems state diff --git a/src/slide.typ b/src/slide.typ index 4425d1f3c..e67b001b4 100644 --- a/src/slide.typ +++ b/src/slide.typ @@ -265,7 +265,7 @@ /// /// - `reduce` is the reduce function that will be called. It is usually a function that receives an array of content and returns a content it painted. Just like the `cetz.canvas` or `fletcher.diagram` function. /// -/// - `cover` is the cover function that will be called when some content is hidden. It is usually a function that receives an the argument of the content that will be hidden. Just like the `cetz.draw.hide` or `fletcher.hide` function. +/// - `cover` is the cover function that will be called when some content is hidden. It is usually a function that receives an the argument of the content that will be hidden. Just like the `cetz.draw.hide` or `fletcher.hide` function. /// /// - `..args` is the arguments of the reducer function. #let touying-reducer(reduce: arr => arr.sum(), cover: arr => none, ..args) = utils.label-it( @@ -868,6 +868,8 @@ } } let bodies = bodies.pos() + + // preambles let slide-preamble(self) = { if self.at("is-first-slide", default: false) { utils.call-or-display(self, self.at("preamble", default: none)) @@ -885,6 +887,41 @@ if self.subslide == 1 { slide-preamble(self) } + if self.at("enable-frozen-states-and-counters", default: true) { + if self.subslide == 1 { + // save the states and counters + context { + states.saved-frozen-states.update(self.frozen-states.map(s => s.get())) + states.saved-default-frozen-states.update(self.default-frozen-states.map(s => s.get())) + states.saved-frozen-counters.update(self.frozen-counters.map(s => s.get())) + states.saved-default-frozen-counters.update(self.default-frozen-counters.map(s => s.get())) + } + } else { + // restore the states and counters + context { + self + .frozen-states + .zip(states.saved-frozen-states.get()) + .map(pair => pair.at(0).update(pair.at(1))) + .sum(default: none) + self + .default-frozen-states + .zip(states.saved-default-frozen-states.get()) + .map(pair => pair.at(0).update(pair.at(1))) + .sum(default: none) + self + .frozen-counters + .zip(states.saved-frozen-counters.get()) + .map(pair => pair.at(0).update(pair.at(1))) + .sum(default: none) + self + .default-frozen-counters + .zip(states.saved-default-frozen-counters.get()) + .map(pair => pair.at(0).update(pair.at(1))) + .sum(default: none) + } + } + } utils.call-or-display(self, self.at("subslide-preamble", default: none)) utils.call-or-display(self, self.at("default-subslide-preamble", default: none)) } @@ -904,6 +941,8 @@ utils.call-or-display(self, self.at("page-preamble", default: none)) utils.call-or-display(self, self.at("default-page-preamble", default: none)) } + + self.subslide = 1 // for single page slide, get the repetitions if repeat == auto { diff --git a/src/states.typ b/src/states.typ index 6628a1bcc..f92b079ba 100644 --- a/src/states.typ +++ b/src/states.typ @@ -155,3 +155,14 @@ last-slide-number: last-slide-counter.final().first(), )) } + + + +// ------------------------------------- +// Saved states and counters +// ------------------------------------- + +#let saved-frozen-states = state("touying-saved-frozen-states", ()) +#let saved-default-frozen-states = state("touying-saved-default-frozen-states", ()) +#let saved-frozen-counters = state("touying-saved-frozen-counters", ()) +#let saved-default-frozen-counters = state("touying-saved-default-frozen-counters", ()) \ No newline at end of file From 3ad8d1c550cd21a94e02a5eb0aff3044a701beca Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Thu, 29 Aug 2024 20:42:10 +0800 Subject: [PATCH 12/43] dev: more smart slides --- examples/default.typ | 37 ++++ src/configs.typ | 2 +- src/{slide.typ => core.typ} | 332 +++++++++++++++++++++++++++++++++--- src/exports.typ | 4 +- src/slides.typ | 279 +----------------------------- 5 files changed, 355 insertions(+), 299 deletions(-) create mode 100644 examples/default.typ rename src/{slide.typ => core.typ} (75%) diff --git a/examples/default.typ b/examples/default.typ new file mode 100644 index 000000000..9deca6ff1 --- /dev/null +++ b/examples/default.typ @@ -0,0 +1,37 @@ +#import "../lib.typ": * +#import themes.default: * + + +#show: default-theme.with( + config-common( + slide-level: 2, + ) +) + + += Title + +== Recall + +Recall + +== Animation + +#set text(blue) + +Simple + +#pause + +$ x + y $ + +animation + + +#show: appendix + += Appendix + +appendix + +#touying-recall() \ No newline at end of file diff --git a/src/configs.typ b/src/configs.typ index efcb38d68..386276de4 100644 --- a/src/configs.typ +++ b/src/configs.typ @@ -1,7 +1,7 @@ #import "states.typ" #import "pdfpc.typ" #import "utils.typ" -#import "slide.typ": touying-slide-wrapper, touying-slide +#import "core.typ": touying-slide-wrapper, touying-slide /// The private configurations of the theme. #let config-store(..args) = { diff --git a/src/slide.typ b/src/core.typ similarity index 75% rename from src/slide.typ rename to src/core.typ index e67b001b4..347f67e30 100644 --- a/src/slide.typ +++ b/src/core.typ @@ -2,6 +2,304 @@ #import "states.typ" #import "pdfpc.typ" +/// ------------------------------------------------ +/// Slides +/// ------------------------------------------------ + +#let _delayed-wrapper(body) = utils.label-it( + metadata((kind: "touying-delayed-wrapper", body: body)), + "touying-temporary-mark", +) + +/// Set config +#let touying-set-config(config, body) = utils.label-it( + metadata(( + kind: "touying-set-config", + config: config, + body: body, + )), + "touying-temporary-mark", +) + + +/// Appendix for the presentation. The last-slide-counter will be frozen at the last slide before the appendix. +#let appendix(body) = touying-set-config( + (appendix: true), + body, +) + + +/// Recall a slide by its label +/// +/// - `lbl` (str): The label of the slide to recall +#let touying-recall(lbl) = utils.label-it( + metadata(( + kind: "touying-slide-recaller", + label: if type(lbl) == label { + str(lbl) + } else { + lbl + }, + )), + "touying-temporary-mark", +) + + +/// Call touying slide function +#let _call-slide-fn(self, fn, body) = { + let slide-wrapper = fn(body) + assert( + utils.is-kind(slide-wrapper, "touying-slide-wrapper"), + message: "you must use `touying-slide-wrapper` in your slide function", + ) + return (slide-wrapper.value.fn)(self) +} + + +/// Use headings to split a content block into slides +#let split-content-into-slides(self: none, recaller-map: (:), new-start: true, is-first-slide: false, body) = { + // Extract arguments + assert(type(self) == dictionary, message: "`self` must be a dictionary") + assert("slide-level" in self and type(self.slide-level) == int, message: "`self.slide-level` must be an integer") + assert("slide-fn" in self and type(self.slide-fn) == function, message: "`self.slide-fn` must be a function") + let slide-level = self.slide-level + let slide-fn = self.slide-fn + let new-section-slide-fn = self.at("new-section-slide-fn", default: none) + let new-subsection-slide-fn = self.at("new-subsection-slide-fn", default: none) + let new-subsubsection-slide-fn = self.at("new-subsubsection-slide-fn", default: none) + let new-subsubsubsection-slide-fn = self.at("new-subsubsubsection-slide-fn", default: none) + let horizontal-line-to-pagebreak = self.at("horizontal-line-to-pagebreak", default: true) + let children = if utils.is-sequence(body) { + body.children + } else { + (body,) + } + let get-last-heading-depth(current-headings) = { + if current-headings != () { + current-headings.at(-1).depth + } else { + 0 + } + } + let get-last-heading-label(current-headings) = { + if current-headings != () { + if current-headings.at(-1).has("label") { + str(current-headings.at(-1).label) + } + } + } + let call-slide-fn-and-reset(self, already-slide-wrapper: false, slide-fn, current-slide-cont, recaller-map) = { + let cont = if already-slide-wrapper { + slide-fn(self) + } else { + _call-slide-fn(self, slide-fn, current-slide-cont) + } + let last-heading-label = get-last-heading-label(self.headings) + if last-heading-label != none { + recaller-map.insert(last-heading-label, cont) + } + (cont, recaller-map, (), (), true, false) + } + // The empty content list + let empty-contents = ([], [ ], parbreak(), linebreak()) + // The headings that we currently have + let current-headings = () + // Recaller map + let recaller-map = recaller-map + // The current slide we are building + let current-slide = () + // The current slide content + let cont = none + // is new start + let is-new-start = new-start + // start part + let start-part = () + // result + let result = () + + // Is we have a horizontal line + let horizontal-line = false + // Iterate over the children + for child in children { + // Handle horizontal-line + // split content when we have a horizontal line + if horizontal-line-to-pagebreak and horizontal-line and child not in ([—], [–], [-]) { + current-slide = utils.trim(current-slide) + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), + slide-fn, + current-slide.sum(default: none), + recaller-map, + ) + result.push(cont) + } + // Main logic + if utils.is-kind(child, "touying-slide-wrapper") { + current-slide = utils.trim(current-slide) + if current-slide != () { + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), + slide-fn, + current-slide.sum(default: none), + recaller-map, + ) + result.push(cont) + } + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), + already-slide-wrapper: true, + child.value.fn, + none, + recaller-map, + ) + result.push(cont) + } else if utils.is-kind(child, "touying-slide-recaller") { + current-slide = utils.trim(current-slide) + if current-slide != () or current-headings != () { + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), + slide-fn, + current-slide.sum(default: none), + recaller-map, + ) + result.push(cont) + } + let lbl = child.value.label + assert(lbl in recaller-map, message: "label not found in the recaller map for slides") + // recall the slide + result.push(recaller-map.at(lbl)) + } else if child == pagebreak() { + // split content when we have a pagebreak + current-slide = utils.trim(current-slide) + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), + slide-fn, + current-slide.sum(default: none), + recaller-map, + ) + result.push(cont) + } else if horizontal-line-to-pagebreak and child == [—] { + horizontal-line = true + continue + } else if horizontal-line-to-pagebreak and horizontal-line and child in ([–], [-]) { + continue + } else if utils.is-heading(child, depth: slide-level) { + let last-heading-depth = get-last-heading-depth(current-headings) + current-slide = utils.trim(current-slide) + if child.depth <= last-heading-depth or current-slide != () or ( + child.depth == 1 and new-section-slide-fn != none + ) or (child.depth == 2 and new-subsection-slide-fn != none) or ( + child.depth == 3 and new-subsubsection-slide-fn != none + ) or (child.depth == 4 and new-subsubsubsection-slide-fn != none) { + if current-slide != () or current-headings != () { + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), + slide-fn, + current-slide.sum(default: none), + recaller-map, + ) + result.push(cont) + } + } + + current-headings.push(child) + new-start = true + + if child.depth == 1 and new-section-slide-fn != none { + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), + new-section-slide-fn, + child.body, + recaller-map, + ) + result.push(cont) + } else if child.depth == 2 and new-subsection-slide-fn != none { + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), + new-subsection-slide-fn, + child.body, + recaller-map, + ) + result.push(cont) + } else if child.depth == 3 and new-subsubsection-slide-fn != none { + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), + new-subsubsection-slide-fn, + child.body, + recaller-map, + ) + result.push(cont) + } else if child.depth == 4 and new-subsubsubsection-slide-fn != none { + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), + new-subsubsubsection-slide-fn, + child.body, + recaller-map, + ) + result.push(cont) + } + + } else if utils.is-kind(child, "touying-set-config") { + if current-slide != () or current-headings != () { + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), + slide-fn, + current-slide.sum(default: none), + recaller-map, + ) + result.push(cont) + } + // Appendix content + _delayed-wrapper(split-content-into-slides( + self: self + child.value.config, + recaller-map: recaller-map, + new-start: true, + child.value.body, + )) + } else { + let child = if utils.is-sequence(child) { + // Split the content into slides recursively + let (start-part, cont) = split-content-into-slides(self: self, recaller-map: recaller-map, new-start: false, child) + start-part + _delayed-wrapper(cont) + } else if utils.is-styled(child) { + // Split the content into slides recursively for styled content + let (start-part, cont) = split-content-into-slides(self: self, recaller-map: recaller-map, new-start: false, child.child) + if start-part != none { + utils.reconstruct-styled(child, start-part) + } + _delayed-wrapper(utils.reconstruct-styled(child, cont)) + } else { + child + } + if new-start { + // Add the child to the current slide + current-slide.push(child) + } else { + start-part.push(child) + } + } + } + + // Handle the last slide + current-slide = utils.trim(current-slide) + if current-slide != () or current-headings != () { + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), + slide-fn, + current-slide.sum(default: none), + recaller-map, + ) + result.push(cont) + } + + if is-new-start { + return result.sum(default: none) + } else { + return (start-part.sum(default: none), result.sum(default: none)) + } +} + /// ------------------------------------------------ /// Slide /// ------------------------------------------------ @@ -491,7 +789,7 @@ } // parse content into results and repetitions -#let _parse-content-into-results-and-repetitions(self: none, need-cover: true, base: 1, index: 1, ..bodies) = { +#let _parse-content-into-results-and-repetitions(self: none, need-cover: true, base: 1, index: 1, show-delayed-wrapper: false, ..bodies) = { let bodies = bodies.pos() let result-arr = () // repetitions @@ -576,7 +874,6 @@ repetitions = nextrepetitions } else if kind == "touying-fn-wrapper" { // handle touying-fn-wrapper - self.subslide = index if repetitions <= index or not need-cover { result.push((child.value.fn)(self: self, ..child.value.args)) } else { @@ -586,6 +883,14 @@ let visible-subslides = child.value.args.pos().at(0) max-repetitions = calc.max(max-repetitions, utils.last-required-subslide(visible-subslides)) } + } else if kind == "touying-delayed-wrapper" { + if show-delayed-wrapper { + if repetitions <= index or not need-cover { + result.push(child.value.body) + } else { + cover-arr.push(child.value.body) + } + } } else { if repetitions <= index or not need-cover { result.push(child) @@ -954,29 +1259,15 @@ ) repeat = repetitions } + assert(type(repeat) == int, message: "The repeat should be an integer") self.repeat = repeat // page header and footer let (header, footer) = _get-header-footer(self) let page-extra-args = _get-page-extra-args(self) - // for speed up, do not parse the content if repeat is none - if repeat == none { - return { - let conts = bodies.map(it => { - if type(it) == function { - it(self) - } else { - it - } - }) - header = page-preamble(self) + header - set page(..(self.page + page-extra-args + (header: header, footer: footer))) - setting(subslide-preamble(self) + composer-with-side-by-side(..conts)) - } - } if self.handout { self.subslide = repeat - let (conts, _) = _parse-content-into-results-and-repetitions(self: self, index: repeat, ..bodies) + let (conts, _) = _parse-content-into-results-and-repetitions(self: self, index: repeat, show-delayed-wrapper: true, ..bodies) header = page-preamble(self) + header set page(..(self.page + page-extra-args + (header: header, footer: footer))) setting(subslide-preamble(self) + composer-with-side-by-side(..conts)) @@ -987,7 +1278,10 @@ for i in range(1, repeat + 1) { self.subslide = i let (header, footer) = _get-header-footer(self) - let (conts, _) = _parse-content-into-results-and-repetitions(self: self, index: i, ..bodies) + let delayed-args = if i == repeat { + (show-delayed-wrapper: true) + } + let (conts, _) = _parse-content-into-results-and-repetitions(self: self, index: i, ..delayed-args, ..bodies) let new-header = page-preamble(self) + header // update the counter in the first subslide only result.push({ diff --git a/src/exports.typ b/src/exports.typ index 714398639..1ea5001d9 100644 --- a/src/exports.typ +++ b/src/exports.typ @@ -1,5 +1,5 @@ -#import "slide.typ": pause, meanwhile, uncover, only, alternatives-match, alternatives, alternatives-fn, alternatives-cases, slide, touying-slide, touying-fn-wrapper, touying-slide-wrapper, touying-equation, touying-mitex, touying-reducer -#import "slides.typ": appendix, touying-set-config, touying-recall, touying-slides +#import "core.typ": pause, meanwhile, uncover, only, alternatives-match, alternatives, alternatives-fn, alternatives-cases, slide, touying-slide, touying-fn-wrapper, touying-slide-wrapper, touying-equation, touying-mitex, touying-reducer, appendix, touying-set-config, touying-recall +#import "slides.typ": touying-slides #import "configs.typ": config-colors, config-common, config-info, config-methods, config-page, config-store, default-config #import "utils.typ" #import "states.typ" diff --git a/src/slides.typ b/src/slides.typ index 9ad3b5261..f066564f4 100644 --- a/src/slides.typ +++ b/src/slides.typ @@ -1,288 +1,13 @@ #import "utils.typ" #import "configs.typ" - -/// ------------------------------------------------ -/// Slides -/// ------------------------------------------------ - -/// Set config -#let touying-set-config(config, body) = utils.label-it( - metadata(( - kind: "touying-set-config", - config: config, - body: body, - )), - "touying-temporary-mark", -) - - -/// Appendix for the presentation. The last-slide-counter will be frozen at the last slide before the appendix. -#let appendix(body) = touying-set-config( - (appendix: true), - body, -) - - -/// Recall a slide by its label -/// -/// - `lbl` (str): The label of the slide to recall -#let touying-recall(lbl) = utils.label-it( - metadata(( - kind: "touying-slide-recaller", - label: if type(lbl) == label { - str(lbl) - } else { - lbl - }, - )), - "touying-temporary-mark", -) - - -/// Call touying slide function -#let _call-slide-fn(self, fn, body) = { - let slide-wrapper = fn(body) - assert( - utils.is-kind(slide-wrapper, "touying-slide-wrapper"), - message: "you must use `touying-slide-wrapper` in your slide function", - ) - return (slide-wrapper.value.fn)(self) -} - - -/// Use headings to split a content block into slides -#let _split-content-into-slides(self: none, recaller-map: (:), new-start: true, is-first-slide: false, body) = { - // Extract arguments - assert(type(self) == dictionary, message: "`self` must be a dictionary") - assert("slide-level" in self and type(self.slide-level) == int, message: "`self.slide-level` must be an integer") - assert("slide-fn" in self and type(self.slide-fn) == function, message: "`self.slide-fn` must be a function") - let slide-level = self.slide-level - let slide-fn = self.slide-fn - let new-section-slide-fn = self.at("new-section-slide-fn", default: none) - let new-subsection-slide-fn = self.at("new-subsection-slide-fn", default: none) - let new-subsubsection-slide-fn = self.at("new-subsubsection-slide-fn", default: none) - let new-subsubsubsection-slide-fn = self.at("new-subsubsubsection-slide-fn", default: none) - let horizontal-line-to-pagebreak = self.at("horizontal-line-to-pagebreak", default: true) - let children = if utils.is-sequence(body) { - body.children - } else { - (body,) - } - let get-last-heading-depth(current-headings) = { - if current-headings != () { - current-headings.at(-1).depth - } else { - 0 - } - } - let get-last-heading-label(current-headings) = { - if current-headings != () { - if current-headings.at(-1).has("label") { - str(current-headings.at(-1).label) - } - } - } - let call-slide-fn-and-reset(self, already-slide-wrapper: false, slide-fn, current-slide-cont, recaller-map) = { - let cont = if already-slide-wrapper { - slide-fn(self) - } else { - _call-slide-fn(self, slide-fn, current-slide-cont) - } - let last-heading-label = get-last-heading-label(self.headings) - if last-heading-label != none { - recaller-map.insert(last-heading-label, cont) - } - (cont, recaller-map, (), (), true, false) - } - // The empty content list - let empty-contents = ([], [ ], parbreak(), linebreak()) - // The headings that we currently have - let current-headings = () - // Recaller map - let recaller-map = recaller-map - // The current slide we are building - let current-slide = () - // The current slide content - let cont = none - - // Is we have a horizontal line - let horizontal-line = false - // Iterate over the children - for child in children { - // Handle horizontal-line - // split content when we have a horizontal line - if horizontal-line-to-pagebreak and horizontal-line and child not in ([—], [–], [-]) { - current-slide = utils.trim(current-slide) - (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings, is-first-slide: is-first-slide), - slide-fn, - current-slide.sum(default: none), - recaller-map, - ) - cont - } - // Main logic - if utils.is-kind(child, "touying-slide-wrapper") { - current-slide = utils.trim(current-slide) - if current-slide != () { - (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings, is-first-slide: is-first-slide), - slide-fn, - current-slide.sum(default: none), - recaller-map, - ) - cont - } - (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings, is-first-slide: is-first-slide), - already-slide-wrapper: true, - child.value.fn, - none, - recaller-map, - ) - cont - } else if utils.is-kind(child, "touying-slide-recaller") { - current-slide = utils.trim(current-slide) - if current-slide != () or current-headings != () { - (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings, is-first-slide: is-first-slide), - slide-fn, - current-slide.sum(default: none), - recaller-map, - ) - cont - } - let lbl = child.value.label - assert(lbl in recaller-map, message: "label not found in the recaller map for slides") - // recall the slide - recaller-map.at(lbl) - } else if child == pagebreak() { - // split content when we have a pagebreak - current-slide = utils.trim(current-slide) - (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings, is-first-slide: is-first-slide), - slide-fn, - current-slide.sum(default: none), - recaller-map, - ) - cont - } else if horizontal-line-to-pagebreak and child == [—] { - horizontal-line = true - continue - } else if horizontal-line-to-pagebreak and horizontal-line and child in ([–], [-]) { - continue - } else if utils.is-heading(child, depth: slide-level) { - let last-heading-depth = get-last-heading-depth(current-headings) - current-slide = utils.trim(current-slide) - if child.depth <= last-heading-depth or current-slide != () or ( - child.depth == 1 and new-section-slide-fn != none - ) or (child.depth == 2 and new-subsection-slide-fn != none) or ( - child.depth == 3 and new-subsubsection-slide-fn != none - ) or (child.depth == 4 and new-subsubsubsection-slide-fn != none) { - if current-slide != () or current-headings != () { - (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings, is-first-slide: is-first-slide), - slide-fn, - current-slide.sum(default: none), - recaller-map, - ) - cont - } - } - - current-headings.push(child) - new-start = true - - if child.depth == 1 and new-section-slide-fn != none { - (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings, is-first-slide: is-first-slide), - new-section-slide-fn, - child.body, - recaller-map, - ) - cont - } else if child.depth == 2 and new-subsection-slide-fn != none { - (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings, is-first-slide: is-first-slide), - new-subsection-slide-fn, - child.body, - recaller-map, - ) - cont - } else if child.depth == 3 and new-subsubsection-slide-fn != none { - (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings, is-first-slide: is-first-slide), - new-subsubsection-slide-fn, - child.body, - recaller-map, - ) - cont - } else if child.depth == 4 and new-subsubsubsection-slide-fn != none { - (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings, is-first-slide: is-first-slide), - new-subsubsubsection-slide-fn, - child.body, - recaller-map, - ) - cont - } - - } else if utils.is-kind(child, "touying-set-config") { - if current-slide != () or current-headings != () { - (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings, is-first-slide: is-first-slide), - slide-fn, - current-slide.sum(default: none), - recaller-map, - ) - cont - } - // Appendix content - _split-content-into-slides( - self: self + child.value.config, - recaller-map: recaller-map, - new-start: true, - child.value.body, - ) - } else { - let child = if utils.is-sequence(child) { - // Split the content into slides recursively - _split-content-into-slides(self: self, recaller-map: recaller-map, new-start: false, child) - } else if utils.is-styled(child) { - // Split the content into slides recursively for styled content - utils.reconstruct-styled(child, _split-content-into-slides(self: self, recaller-map: recaller-map, new-start: false, child.child)) - } else { - child - } - if new-start { - // Add the child to the current slide - current-slide.push(child) - } else { - child - } - } - } - - // Handle the last slide - current-slide = utils.trim(current-slide) - if current-slide != () or current-headings != () { - (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings, is-first-slide: is-first-slide), - slide-fn, - current-slide.sum(default: none), - recaller-map, - ) - cont - } -} - +#import "core.typ" #let touying-slides(..args, body) = { assert(args.named().len() == 0, message: "unexpected named arguments:" + repr(args.named().keys())) let args = (configs.default-config,) + args.pos() let self = utils.merge-dicts(..args) - show: _split-content-into-slides.with(self: self, is-first-slide: true) + show: core.split-content-into-slides.with(self: self, is-first-slide: true) body } From 0667a1ca73c6c69d8dac0b0889f65923ae613925 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Thu, 29 Aug 2024 23:32:50 +0800 Subject: [PATCH 13/43] fix: fix some bugs for appendix --- src/core.typ | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/core.typ b/src/core.typ index 347f67e30..abdd2f35d 100644 --- a/src/core.typ +++ b/src/core.typ @@ -152,6 +152,9 @@ none, recaller-map, ) + if child.has("label") and child.label != { + recaller-map.insert(str(child.label), cont) + } result.push(cont) } else if utils.is-kind(child, "touying-slide-recaller") { current-slide = utils.trim(current-slide) @@ -191,6 +194,7 @@ ) or (child.depth == 2 and new-subsection-slide-fn != none) or ( child.depth == 3 and new-subsubsection-slide-fn != none ) or (child.depth == 4 and new-subsubsubsection-slide-fn != none) { + current-slide = utils.trim(current-slide) if current-slide != () or current-headings != () { (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( self + (headings: current-headings, is-first-slide: is-first-slide), @@ -240,6 +244,7 @@ } } else if utils.is-kind(child, "touying-set-config") { + current-slide = utils.trim(current-slide) if current-slide != () or current-headings != () { (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( self + (headings: current-headings, is-first-slide: is-first-slide), @@ -250,7 +255,7 @@ result.push(cont) } // Appendix content - _delayed-wrapper(split-content-into-slides( + result.push(split-content-into-slides( self: self + child.value.config, recaller-map: recaller-map, new-start: true, From 9aec3156ccbf7cda2ef287149ceb52837dc1586e Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Fri, 30 Aug 2024 00:13:08 +0800 Subject: [PATCH 14/43] docs: update comments --- src/core.typ | 14 +++++- src/slides.typ | 15 +++++++ src/utils.typ | 119 +++++++++++++++++++++++++++---------------------- 3 files changed, 94 insertions(+), 54 deletions(-) diff --git a/src/core.typ b/src/core.typ index abdd2f35d..551b8d4b3 100644 --- a/src/core.typ +++ b/src/core.typ @@ -11,7 +11,13 @@ "touying-temporary-mark", ) -/// Set config +/// Update configurations for the presentation. +/// +/// Example: `#let appendix(body) = touying-set-config((appendix: true), body)` and you can use `#show: appendix` to set the appendix for the presentation. +/// +/// - `config` (dict): The new configurations for the presentation. +/// +/// - `body` (content): The content of the slide. #let touying-set-config(config, body) = utils.label-it( metadata(( kind: "touying-set-config", @@ -23,6 +29,10 @@ /// Appendix for the presentation. The last-slide-counter will be frozen at the last slide before the appendix. +/// +/// Example: `#show: appendix` +/// +/// - `body` (content): The content of the appendix. #let appendix(body) = touying-set-config( (appendix: true), body, @@ -31,6 +41,8 @@ /// Recall a slide by its label /// +/// Example: `#touying-recall()` or `#touying-recall("recall")` +/// /// - `lbl` (str): The label of the slide to recall #let touying-recall(lbl) = utils.label-it( metadata(( diff --git a/src/slides.typ b/src/slides.typ index f066564f4..07d484540 100644 --- a/src/slides.typ +++ b/src/slides.typ @@ -2,6 +2,21 @@ #import "configs.typ" #import "core.typ" +/// Touying slides function. +/// +/// #example(``` +/// #show: touying-slides.with( +/// config-page(paper: "presentation-" + aspect-ratio), +/// config-common( +/// slide-fn: slide, +/// ), +/// ..args, +/// ) +/// ```) +/// +/// `..args` is the configurations of the slides. For example, you can use `config-page(paper: "presentation-16-9")` to set the aspect ratio of the slides. +/// +/// `body` is the contents of the slides. #let touying-slides(..args, body) = { assert(args.named().len() == 0, message: "unexpected named arguments:" + repr(args.named().keys())) let args = (configs.default-config,) + args.pos() diff --git a/src/utils.typ b/src/utils.typ index cad4fa0b7..baa3211c5 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -151,7 +151,7 @@ } -// OOP: call it or display it +/// Call a `self => {..}` function and return the result, or just return the content #let call-or-display(self, it) = { if type(it) == function { it = it(self) @@ -177,8 +177,10 @@ // OOP: wrap methods #let wrap-method(fn) = (self: none, ..args) => fn(..args) -// OOP: assuming all functions in dictionary have a named `self` parameter, -// `methods` function is used to get all methods in dictionary object +/// Assuming all functions in dictionary have a named `self` parameter, +/// `methods` function is used to get all methods in dictionary object +/// +/// Example: `#let (uncover, only) = utils.methods(self)` to get `uncover` and `only` methods. #let methods(self) = { assert(type(self) == dictionary, message: "self must be a dictionary") assert("methods" in self and type(self.methods) == dictionary, message: "self.methods must be a dictionary") @@ -191,63 +193,26 @@ return methods } -#let slides(self) = { - let m = methods(self) - let res = (:) - for key in m.keys() { - res.insert(key, touying-slide-wrapper.with(key, m.at(key))) - } - return res -} - -// Utils: unify section -#let unify-section(section) = { - if section == none { - return none - } else if type(section) == dictionary { - return section + (short-title: section.at("short-title", default: auto)) - } else if type(section) == array { - return (title: section.at(0), short-title: section.at(1, default: auto)) - } else { - return (title: section, short-title: auto) - } -} -#let section-short-title(section) = { - if type(section) == dictionary { - if section.short-title == auto { - return section.title - } else { - return section.short-title - } - } else { - return section - } -} - -#let info-date(self) = { +/// Display the date of `self.info.date` with `self.datetime-format` format. +#let display-info-date(self) = { + assert("info" in self, message: "self must have an info field") if type(self.info.date) == datetime { - self.info.date.display(self.datetime-format) + self.info.date.display(self.at("datetime-format", default: auto)) } else { self.info.date } } -// Utils: bookmark -#let bookmark(level: 1, numbering: none, outlined: true, body) = { - if body != auto and body != none { - place( - top + left, - text(0pt, hide(heading(depth: level, outlined: outlined, bookmarked: true, numbering: numbering, body))), - ) - } -} - - - -// Convert content to markup text, partly from -// https://sitandr.github.io/typst-examples-book/book/typstonomicon/extract_markup_text.html +/// Convert content to markup text, partly from +/// [typst-examples-book](https://sitandr.github.io/typst-examples-book/book/typstonomicon/extract_markup_text.html). +/// +/// - `it` is the content to convert. +/// +/// - `mode` is the mode of the markup text, either `typ` or `md`. +/// +/// - `indent` is the number of spaces to indent, default is `0`. #let markup-text(it, mode: "typ", indent: 0) = { assert(mode == "typ" or mode == "md", message: "mode must be 'typ' or 'md'") let indent-markup-text = markup-text.with(mode: mode, indent: indent + 2) @@ -347,6 +312,22 @@ box(width: mutable-width, body) } + +/// Fit content to specified height. +/// +/// Example: `#utils.fit-to-height(1fr)[BIG]` +/// +/// - `width` will determine the width of the content after scaling. So, if you want the scaled content to fill half of the slide width, you can use width: 50%. +/// +/// - `prescale-width` allows you to make typst's layout assume that the given content is to be laid out in a container of a certain width before scaling. For example, you can use `prescale-width: 200%` assuming the slide's width is twice the original. +/// +/// - `grow` is a boolean indicating whether the content should be scaled up if it is smaller than the available height. Default is `true`. +/// +/// - `shrink` is a boolean indicating whether the content should be scaled down if it is larger than the available height. Default is `true`. +/// +/// - `height` is the height to fit the content to. +/// +/// - `body` is the content to fit. #let fit-to-height( width: none, prescale-width: none, @@ -437,6 +418,18 @@ } } + +/// Fit content to specified width. +/// +/// Example: `#utils.fit-to-width(1fr)[BIG]` +/// +/// - `grow` is a boolean indicating whether the content should be scaled up if it is smaller than the available width. Default is `true`. +/// +/// - `shrink` is a boolean indicating whether the content should be scaled down if it is larger than the available width. Default is `true`. +/// +/// - `width` is the width to fit the content to. +/// +/// - `body` is the content to fit. #let fit-to-width(grow: true, shrink: true, width, content) = { layout(layout-size => { let content-size = measure(content) @@ -460,7 +453,18 @@ }) } -// semitransparency cover + +/// Cover content with a rectangle of a specified color. If you set the fill to the background color of the page, you can use this to create a semi-transparent overlay. +/// +/// Example: `#utils.cover-with-rect(fill: "red")[Hidden]` +/// +/// - `cover-args` are the arguments to pass to the rectangle. +/// +/// - `fill` is the color to fill the rectangle with. +/// +/// - `inline` is a boolean indicating whether the content should be displayed inline. Default is `true`. +/// +/// - `body` is the content to cover. #let cover-with-rect(..cover-args, fill: auto, inline: true, body) = { if fill == auto { panic("`auto` fill value is not supported until typst provides utilities to" + " retrieve the current page background") @@ -505,6 +509,15 @@ } } +/// Update the alpha channel of a color. +/// +/// Example: `update-alpha(rgb("#ff0000"), 0.5)` returns `rgb(255, 0, 0, 0.5)` +/// +/// - `constructor` is the color constructor to use. Default is `rgb`. +/// +/// - `color` is the color to update. +/// +/// - `alpha` is the new alpha value. #let update-alpha(constructor: rgb, color, alpha) = constructor(..color.components(alpha: true).slice(0, -1), alpha) From ed9cf174c8d55c473943f52b05bf22ae59d16d1a Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Fri, 30 Aug 2024 01:21:57 +0800 Subject: [PATCH 15/43] feat: support pause and meanwhile in almost all layout functions --- src/configs.typ | 2 +- src/core.typ | 101 ++++++++++++++++++++++++++++++++++++++++++++++-- src/utils.typ | 12 +++--- 3 files changed, 105 insertions(+), 10 deletions(-) diff --git a/src/configs.typ b/src/configs.typ index 386276de4..7c77aab90 100644 --- a/src/configs.typ +++ b/src/configs.typ @@ -109,7 +109,7 @@ if marks.len() > 0 { let page-num = marks.at(0).location().page() let kind = marks.at(0).value.kind - panic("Unsupported mark `" + kind + "` at page " + str(page-num) + ". You can't use it inside some layout functions like `grid`. You may want to use the callback-style `uncover` function instead. Or you may want to use #slide[][] for a two-column layout. ") + panic("Unsupported mark `" + kind + "` at page " + str(page-num) + ". You can't use it inside some functions like `context`. You may want to use the callback-style `uncover` function instead.") } } } diff --git a/src/core.typ b/src/core.typ index 551b8d4b3..3e4fd7c45 100644 --- a/src/core.typ +++ b/src/core.typ @@ -815,6 +815,20 @@ // get cover function from self let cover = self.methods.cover.with(self: self) for it in bodies { + // a hack for code like #table([A], pause, [B]) + if type(it) == content and it.func() in (table.cell, grid.cell) { + if type(it.body) == content and it.body.func() == metadata and type(it.body.value) == dictionary { + let kind = it.body.value.at("kind", default: none) + if kind == "touying-pause" { + repetitions += 1 + } else if kind == "touying-meanwhile" { + // reset the repetitions + max-repetitions = calc.max(max-repetitions, repetitions) + repetitions = 1 + } + continue + } + } // if it is a function, then call it with self if type(it) == function { // subslide index @@ -954,7 +968,7 @@ cover-arr.push(utils.typst-builtin-styled(cont, child.styles)) } repetitions = nextrepetitions - } else if type(child) == content and child.func() in (list.item, enum.item, align) { + } else if type(child) == content and child.func() in (list.item, enum.item, align, link) { // handle the list item let (conts, nextrepetitions) = _parse-content-into-results-and-repetitions( self: self, @@ -970,8 +984,22 @@ cover-arr.push(utils.reconstruct(child, cont)) } repetitions = nextrepetitions - } else if type(child) == content and child.func() in (pad,) { - // handle the pad + } else if type(child) == content and child.func() in (table, grid, stack) { + // handle the table-like + let (conts, nextrepetitions) = _parse-content-into-results-and-repetitions( + self: self, + need-cover: repetitions <= index, + base: repetitions, + index: index, + ..child.children, + ) + if repetitions <= index or not need-cover { + result.push(utils.reconstruct-table-like(child, conts)) + } else { + cover-arr.push(utils.reconstruct-table-like(child, conts)) + } + repetitions = nextrepetitions + } else if type(child) == content and child.func() in (pad, figure, quote, strong, emph, footnote, highlight, overline, underline, strike, smallcaps, sub, super, box, block, hide, move, scale, circle, ellipse, rect, square, table.cell, grid.cell) { let (conts, nextrepetitions) = _parse-content-into-results-and-repetitions( self: self, need-cover: repetitions <= index, @@ -1002,6 +1030,73 @@ cover-arr.push(terms.item(child.term, cont)) } repetitions = nextrepetitions + } else if type(child) == content and child.func() == columns { + // handle columns + let (conts, nextrepetitions) = _parse-content-into-results-and-repetitions( + self: self, + need-cover: repetitions <= index, + base: repetitions, + index: index, + child.body, + ) + let cont = conts.first() + let args = if child.has("gutter") { + (gutter: child.gutter) + } + if repetitions <= index or not need-cover { + result.push(columns(child.count, ..args, cont)) + } else { + cover-arr.push(columns(child.count, ..args, cont)) + } + repetitions = nextrepetitions + } else if type(child) == content and child.func() == place { + // handle place + let (conts, nextrepetitions) = _parse-content-into-results-and-repetitions( + self: self, + need-cover: repetitions <= index, + base: repetitions, + index: index, + child.body, + ) + let cont = conts.first() + let fields = child.fields() + let _ = fields.remove("alignment", default: none) + let _ = fields.remove("body", default: none) + let alignment = if child.has("alignment") { + child.alignment + } else { + start + } + if repetitions <= index or not need-cover { + result.push(place(alignment, ..fields, cont)) + } else { + cover-arr.push(place(alignment, ..fields, cont)) + } + repetitions = nextrepetitions + } else if type(child) == content and child.func() == rotate { + // handle rotate + let (conts, nextrepetitions) = _parse-content-into-results-and-repetitions( + self: self, + need-cover: repetitions <= index, + base: repetitions, + index: index, + child.body, + ) + let cont = conts.first() + let fields = child.fields() + let _ = fields.remove("angle", default: none) + let _ = fields.remove("body", default: none) + let angle = if child.has("angle") { + child.angle + } else { + 0deg + } + if repetitions <= index or not need-cover { + result.push(rotate(angle, ..fields, cont)) + } else { + cover-arr.push(rotate(angle, ..fields, cont)) + } + repetitions = nextrepetitions } else { if repetitions <= index or not need-cover { result.push(child) diff --git a/src/utils.typ b/src/utils.typ index baa3211c5..976ecc085 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -69,21 +69,21 @@ /// - `it` is the content to reconstruct /// /// - `new-body` is the new body you want to replace the old body with -#let reconstruct(body-name: "body", named: false, it, new-body) = { +#let reconstruct(body-name: "body", named: false, it, ..new-body) = { let fields = it.fields() let label = fields.remove("label", default: none) let _ = fields.remove(body-name, default: none) if named { if label != none { - return label-it(label, (it.func())(..fields, new-body)) + return label-it(label, (it.func())(..fields, ..new-body)) } else { - return (it.func())(..fields, new-body) + return (it.func())(..fields, ..new-body) } } else { if label != none { - return label-it(label, (it.func())(..fields.values(), new-body)) + return label-it(label, (it.func())(..fields.values(), ..new-body)) } else { - return (it.func())(..fields.values(), new-body) + return (it.func())(..fields.values(), ..new-body) } } } @@ -97,7 +97,7 @@ /// /// - `new-children` is the new children you want to replace the old children with #let reconstruct-table-like(named: true, it, new-children) = { - reconstruct(body-name: "children", named: named, it, new-children) + reconstruct(body-name: "children", named: named, it, ..new-children) } From eff817c2854ff5b083ce8e62ab56aac4a4743836 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Fri, 30 Aug 2024 01:50:45 +0800 Subject: [PATCH 16/43] dev: add speaker-note --- slide.typ | 9 +++++---- src/core.typ | 26 +++++++++++++++++++++++--- src/exports.typ | 2 +- src/utils.typ | 31 +++++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+), 8 deletions(-) diff --git a/slide.typ b/slide.typ index 728d14bda..d615388f3 100644 --- a/slide.typ +++ b/slide.typ @@ -975,12 +975,11 @@ self }, show-notes-on-second-screen: (self: none, value) => { - assert(value == none or value == right, message: "value should be none or right") self.show-notes-on-second-screen = value self }, - speaker-note: (self: none, enable-pdfpc: true, mode: "typ", setting: it => it, note) => { - if enable-pdfpc { + speaker-note: (self: none, mode: "typ", setting: it => it, note) => { + if self.at("enable-pdfpc", default: true) { let raw-text = if type(note) == content and note.has("text") { note.text } else { @@ -988,7 +987,9 @@ } pdfpc.speaker-note(raw-text) } - if self.show-notes-on-second-screen != none { + let show-notes-on-second-screen = self.at("show-notes-on-second-screen", default: none) + assert(show-notes-on-second-screen == none or show-notes-on-second-screen == right, message: "`show-notes-on-second-screen` should be none or right") + if show-notes-on-second-screen != none { states.slide-note-state.update(setting(note)) } } diff --git a/src/core.typ b/src/core.typ index 3e4fd7c45..e93e349f1 100644 --- a/src/core.typ +++ b/src/core.typ @@ -428,7 +428,7 @@ /// - `subslides-contents` is a dictionary mapping from subslides to content. /// /// - `position` is the position of the content. Default is `bottom + left`. -#let alternatives-match(self: none, subslides-contents, position: bottom + left) = { +#let alternatives-match(subslides-contents, position: bottom + left) = { touying-fn-wrapper(utils.alternatives-match, subslides-contents, position: position) } @@ -459,7 +459,6 @@ /// /// - `count` is the number of subslides. Default is `none`. #let alternatives-fn( - self: none, start: 1, end: none, count: none, @@ -482,11 +481,27 @@ /// - `cases` is an array of strings that specify the subslides for each case. /// /// - `fn` is a function that maps the case to content. The argument `case` is the index of the cases array you input. -#let alternatives-cases(self: none, cases, fn, ..kwargs) = { +#let alternatives-cases(cases, fn, ..kwargs) = { touying-fn-wrapper(utils.alternatives-cases, cases, fn, ..kwargs) } +/// Speaker notes are a way to add additional information to your slides that is not visible to the audience. This can be useful for providing additional context or reminders to yourself. +/// +/// Example: `#speaker-note[This is a speaker note]` +/// +/// - `self` is the current context. +/// +/// - `mode` is the mode of the markup text, either `typ` or `md`. Default is `typ`. +/// +/// - `setting` is a function that takes the note as input and returns a processed note. +/// +/// - `note` is the content of the speaker note. +#let speaker-note(mode: "typ", setting: it => it, note) = { + touying-fn-wrapper(utils.speaker-note, mode: mode, setting: setting, note) +} + + /// Touying also provides a unique and highly useful feature—math equation animations, allowing you to conveniently use pause and meanwhile within math equations. /// /// #example(``` @@ -1155,6 +1170,11 @@ } else { 793.7pt } + let page-height = if self.page.paper == "presentation-16-9" { + 473.56pt + } else { + 595.28pt + } if type(margin) != dictionary and type(margin) != length and type(margin) != relative { return (:) } diff --git a/src/exports.typ b/src/exports.typ index 1ea5001d9..9dd5963b6 100644 --- a/src/exports.typ +++ b/src/exports.typ @@ -1,4 +1,4 @@ -#import "core.typ": pause, meanwhile, uncover, only, alternatives-match, alternatives, alternatives-fn, alternatives-cases, slide, touying-slide, touying-fn-wrapper, touying-slide-wrapper, touying-equation, touying-mitex, touying-reducer, appendix, touying-set-config, touying-recall +#import "core.typ": pause, meanwhile, uncover, only, alternatives-match, alternatives, alternatives-fn, alternatives-cases, slide, touying-slide, touying-fn-wrapper, touying-slide-wrapper, touying-equation, touying-mitex, touying-reducer, appendix, touying-set-config, touying-recall, speaker-note #import "slides.typ": touying-slides #import "configs.typ": config-colors, config-common, config-info, config-methods, config-page, config-store, default-config #import "utils.typ" diff --git a/src/utils.typ b/src/utils.typ index 976ecc085..905d4c75b 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -1,3 +1,6 @@ +#import "pdfpc.typ" +#import "states.typ" + /// Add a dictionary to another dictionary recursively /// /// Example: `add-dicts((a: (b: 1), (a: (c: 2))` returns `(a: (b: 1, c: 2)` @@ -792,6 +795,34 @@ alternatives-match(self: self, cases.zip(contents), ..kwargs.named()) } + +/// Speaker notes are a way to add additional information to your slides that is not visible to the audience. This can be useful for providing additional context or reminders to yourself. +/// +/// Example: `#speaker-note[This is a speaker note]` +/// +/// - `self` is the current context. +/// +/// - `mode` is the mode of the markup text, either `typ` or `md`. Default is `typ`. +/// +/// - `setting` is a function that takes the note as input and returns a processed note. +/// +/// - `note` is the content of the speaker note. +#let speaker-note(self: none, mode: "typ", setting: it => it, note) = { + if self.at("enable-pdfpc", default: true) { + let raw-text = if type(note) == content and note.has("text") { + note.text + } else { + markup-text(note, mode: mode).trim() + } + pdfpc.speaker-note(raw-text) + } + let show-notes-on-second-screen = self.at("show-notes-on-second-screen", default: none) + assert(show-notes-on-second-screen == none or show-notes-on-second-screen == right, message: "`show-notes-on-second-screen` should be `none` or `right`") + if show-notes-on-second-screen != none { + states.slide-note-state.update(setting(note)) + } +} + // SIDE BY SIDE /// A simple wrapper around `grid` that creates a grid with a single row. From 01477ba68f9d93e3ccf467153bcba4bc5bf5cb63 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Fri, 30 Aug 2024 01:59:48 +0800 Subject: [PATCH 17/43] feat: support show-notes-on-second-screen: bottom --- src/core.typ | 159 +++++++++++++++++++++++++++++++++++--------------- src/utils.typ | 2 +- 2 files changed, 114 insertions(+), 47 deletions(-) diff --git a/src/core.typ b/src/core.typ index e93e349f1..6dbc90740 100644 --- a/src/core.typ +++ b/src/core.typ @@ -267,21 +267,33 @@ result.push(cont) } // Appendix content - result.push(split-content-into-slides( - self: self + child.value.config, - recaller-map: recaller-map, - new-start: true, - child.value.body, - )) + result.push( + split-content-into-slides( + self: self + child.value.config, + recaller-map: recaller-map, + new-start: true, + child.value.body, + ), + ) } else { let child = if utils.is-sequence(child) { // Split the content into slides recursively - let (start-part, cont) = split-content-into-slides(self: self, recaller-map: recaller-map, new-start: false, child) + let (start-part, cont) = split-content-into-slides( + self: self, + recaller-map: recaller-map, + new-start: false, + child, + ) start-part _delayed-wrapper(cont) } else if utils.is-styled(child) { // Split the content into slides recursively for styled content - let (start-part, cont) = split-content-into-slides(self: self, recaller-map: recaller-map, new-start: false, child.child) + let (start-part, cont) = split-content-into-slides( + self: self, + recaller-map: recaller-map, + new-start: false, + child.child, + ) if start-part != none { utils.reconstruct-styled(child, start-part) } @@ -821,7 +833,14 @@ } // parse content into results and repetitions -#let _parse-content-into-results-and-repetitions(self: none, need-cover: true, base: 1, index: 1, show-delayed-wrapper: false, ..bodies) = { +#let _parse-content-into-results-and-repetitions( + self: none, + need-cover: true, + base: 1, + index: 1, + show-delayed-wrapper: false, + ..bodies, +) = { let bodies = bodies.pos() let result-arr = () // repetitions @@ -1014,7 +1033,32 @@ cover-arr.push(utils.reconstruct-table-like(child, conts)) } repetitions = nextrepetitions - } else if type(child) == content and child.func() in (pad, figure, quote, strong, emph, footnote, highlight, overline, underline, strike, smallcaps, sub, super, box, block, hide, move, scale, circle, ellipse, rect, square, table.cell, grid.cell) { + } else if type(child) == content and child.func() in ( + pad, + figure, + quote, + strong, + emph, + footnote, + highlight, + overline, + underline, + strike, + smallcaps, + sub, + super, + box, + block, + hide, + move, + scale, + circle, + ellipse, + rect, + square, + table.cell, + grid.cell, + ) { let (conts, nextrepetitions) = _parse-content-into-results-and-repetitions( self: self, need-cover: repetitions <= index, @@ -1159,7 +1203,7 @@ // get page extra args for show-notes-on-second-screen #let _get-page-extra-args(self) = { - if self.show-notes-on-second-screen == right { + if self.show-notes-on-second-screen in (bottom, right) { let margin = self.page.margin assert( self.page.paper == "presentation-16-9" or self.page.paper == "presentation-4-3", @@ -1181,12 +1225,22 @@ if type(margin) == length or type(margin) == relative { margin = (x: margin, y: margin) } - if "right" not in margin { - assert("x" in margin, message: "The margin should have right or x") - margin.right = margin.x + if self.show-notes-on-second-screen == bottom { + if "bottom" not in margin { + assert("y" in margin, message: "The margin should have bottom or y") + margin.bottom = margin.y + } + margin.bottom += page-height + return (margin: margin, height: 2 * page-height) + } else if self.show-notes-on-second-screen == right { + if "right" not in margin { + assert("x" in margin, message: "The margin should have right or x") + margin.right = margin.x + } + margin.right += page-width + return (margin: margin, width: 2 * page-width) } - margin.right += page-width - return (margin: margin, width: 2 * page-width) + return (:) } else { return (:) } @@ -1196,7 +1250,7 @@ let header = utils.call-or-display(self, self.page.at("header", default: none)) let footer = utils.call-or-display(self, self.page.at("footer", default: none)) // speaker note - if self.show-notes-on-second-screen == right { + if self.show-notes-on-second-screen in (bottom, right) { assert( self.page.paper == "presentation-16-9" or self.page.paper == "presentation-4-3", message: "The paper of page should be presentation-16-9 or presentation-4-3", @@ -1211,35 +1265,43 @@ } else { 595.28pt } - footer += place( - left + bottom, - dx: page-width, - block( - fill: rgb("#E6E6E6"), - width: page-width, - height: page-height, - { - set align(left + top) - set text(size: 24pt, fill: black, weight: "regular") - block( - width: 100%, - height: 88pt, - inset: (left: 32pt, top: 16pt), - outset: 0pt, - fill: rgb("#CCCCCC"), - { - states.current-section-title - linebreak() - [ --- ] - states.current-slide-title - }, - ) - pad(x: 48pt, states.current-slide-note) - // clear the slide note - states.slide-note-state.update(none) - }, - ), + let display-note = block( + fill: rgb("#E6E6E6"), + width: page-width, + height: page-height, + { + set align(left + top) + set text(size: 24pt, fill: black, weight: "regular") + block( + width: 100%, + height: 88pt, + inset: (left: 32pt, top: 16pt), + outset: 0pt, + fill: rgb("#CCCCCC"), + { + states.current-section-title + linebreak() + [ --- ] + states.current-slide-title + }, + ) + pad(x: 48pt, states.current-slide-note) + // clear the slide note + states.slide-note-state.update(none) + }, ) + if self.show-notes-on-second-screen == bottom { + footer += place( + left + bottom, + display-note, + ) + } else if self.show-notes-on-second-screen == right { + footer += place( + left + bottom, + dx: page-width, + display-note, + ) + } } // negative padding if self.at("zero-margin-header", default: true) or self.at("zero-margin-footer", default: true) { @@ -1399,7 +1461,12 @@ if self.handout { self.subslide = repeat - let (conts, _) = _parse-content-into-results-and-repetitions(self: self, index: repeat, show-delayed-wrapper: true, ..bodies) + let (conts, _) = _parse-content-into-results-and-repetitions( + self: self, + index: repeat, + show-delayed-wrapper: true, + ..bodies, + ) header = page-preamble(self) + header set page(..(self.page + page-extra-args + (header: header, footer: footer))) setting(subslide-preamble(self) + composer-with-side-by-side(..conts)) diff --git a/src/utils.typ b/src/utils.typ index 905d4c75b..12ada1fd7 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -817,7 +817,7 @@ pdfpc.speaker-note(raw-text) } let show-notes-on-second-screen = self.at("show-notes-on-second-screen", default: none) - assert(show-notes-on-second-screen == none or show-notes-on-second-screen == right, message: "`show-notes-on-second-screen` should be `none` or `right`") + assert(show-notes-on-second-screen in (none, bottom, right), message: "`show-notes-on-second-screen` should be `none`, `bottom` or `right`") if show-notes-on-second-screen != none { states.slide-note-state.update(setting(note)) } From 0d300bd0f87127aa5a414523df6ac26ed569543a Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Fri, 30 Aug 2024 02:49:50 +0800 Subject: [PATCH 18/43] dev: add `show-notes` method --- examples/default.typ | 3 +++ slide.typ | 6 +++--- src/configs.typ | 51 ++++++++++++++++++++++++++++++++++++++++++-- src/core.typ | 30 +++----------------------- src/utils.typ | 2 +- 5 files changed, 59 insertions(+), 33 deletions(-) diff --git a/examples/default.typ b/examples/default.typ index 9deca6ff1..f015f99b8 100644 --- a/examples/default.typ +++ b/examples/default.typ @@ -5,6 +5,7 @@ #show: default-theme.with( config-common( slide-level: 2, + show-notes-on-second-screen: bottom, ) ) @@ -15,6 +16,8 @@ Recall +#speaker-note[sdfsdf] + == Animation #set text(blue) diff --git a/slide.typ b/slide.typ index d615388f3..869fa3f8a 100644 --- a/slide.typ +++ b/slide.typ @@ -882,12 +882,12 @@ self }, // cover method - cover: utils.wrap-method(hide), + cover: utils.method-wrapper(hide), update-cover: (self: none, is-method: false, cover-fn) => { if is-method { self.methods.cover = cover-fn } else { - self.methods.cover = utils.wrap-method(cover-fn) + self.methods.cover = utils.method-wrapper(cover-fn) } self }, @@ -908,7 +908,7 @@ alternatives-fn: utils.alternatives-fn, alternatives-cases: utils.alternatives-cases, // alert interface - alert: utils.wrap-method(text.with(weight: "bold")), + alert: utils.method-wrapper(text.with(weight: "bold")), // handout mode enable-handout-mode: (self: none) => { self.handout = true diff --git a/src/configs.typ b/src/configs.typ index 7c77aab90..4437c85a6 100644 --- a/src/configs.typ +++ b/src/configs.typ @@ -187,8 +187,28 @@ /// The configuration of the methods +/// +/// - cover (function): The function to cover content. The default value is `utils.method-wrapper(hide)` function. +/// +/// - uncover (function): The function to uncover content. +/// +/// - only (function): The function to show only the content. +/// +/// - alternatives-match (function): The function to match alternatives. +/// +/// - alternatives (function): The function to show alternatives. +/// +/// - alternatives-fn (function): The function to show alternatives with a function. +/// +/// - alternatives-cases (function): The function to show alternatives with cases. +/// +/// - alert (function): The function to alert the content. +/// +/// - show-notes (function): The function to show notes on second screen. +/// +/// It should be `(self: none, width: 0pt, height: 0pt) => { .. }`. #let config-methods( - cover: utils.wrap-method(hide), + cover: utils.method-wrapper(hide), // dynamic control uncover: utils.uncover, only: utils.only, @@ -197,7 +217,33 @@ alternatives-fn: utils.alternatives-fn, alternatives-cases: utils.alternatives-cases, // alert interface - alert: utils.wrap-method(text.with(weight: "bold")), + alert: utils.method-wrapper(text.with(weight: "bold")), + // show notes + show-notes: (self: none, width: 0pt, height: 0pt) => block( + fill: rgb("#E6E6E6"), + width: width, + height: height, + { + set align(left + top) + set text(size: 24pt, fill: black, weight: "regular") + block( + width: 100%, + height: 88pt, + inset: (left: 32pt, top: 16pt), + outset: 0pt, + fill: rgb("#CCCCCC"), + { + states.current-section-title + linebreak() + [ --- ] + states.current-slide-title + }, + ) + pad(x: 48pt, states.current-slide-note) + // clear the slide note + states.slide-note-state.update(none) + }, + ), ..args, ) = { assert(args.pos().len() == 0, message: "Unexpected positional arguments.") @@ -211,6 +257,7 @@ alternatives-fn: alternatives-fn, alternatives-cases: alternatives-cases, alert: alert, + show-notes: show-notes, ) + args.named(), ) } diff --git a/src/core.typ b/src/core.typ index 6dbc90740..53422fcdc 100644 --- a/src/core.typ +++ b/src/core.typ @@ -1265,41 +1265,17 @@ } else { 595.28pt } - let display-note = block( - fill: rgb("#E6E6E6"), - width: page-width, - height: page-height, - { - set align(left + top) - set text(size: 24pt, fill: black, weight: "regular") - block( - width: 100%, - height: 88pt, - inset: (left: 32pt, top: 16pt), - outset: 0pt, - fill: rgb("#CCCCCC"), - { - states.current-section-title - linebreak() - [ --- ] - states.current-slide-title - }, - ) - pad(x: 48pt, states.current-slide-note) - // clear the slide note - states.slide-note-state.update(none) - }, - ) + let show-notes = (self.methods.show-notes)(self: self, width: page-width, height: page-height) if self.show-notes-on-second-screen == bottom { footer += place( left + bottom, - display-note, + show-notes, ) } else if self.show-notes-on-second-screen == right { footer += place( left + bottom, dx: page-width, - display-note, + show-notes, ) } } diff --git a/src/utils.typ b/src/utils.typ index 12ada1fd7..eb7677751 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -178,7 +178,7 @@ } // OOP: wrap methods -#let wrap-method(fn) = (self: none, ..args) => fn(..args) +#let method-wrapper(fn) = (self: none, ..args) => fn(..args) /// Assuming all functions in dictionary have a named `self` parameter, /// `methods` function is used to get all methods in dictionary object From 54720a31ef6e34b8710c5b96926d0933d3484ebb Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Fri, 30 Aug 2024 03:55:33 +0800 Subject: [PATCH 19/43] dev: clean code --- examples/default.typ | 16 +++- src/configs.typ | 178 +++++++++++++++++++++++++------------------ src/core.typ | 48 +++++++++++- src/slides.typ | 10 +++ src/utils.typ | 60 ++++++++++----- themes/default.typ | 7 +- 6 files changed, 220 insertions(+), 99 deletions(-) diff --git a/examples/default.typ b/examples/default.typ index f015f99b8..b1548db05 100644 --- a/examples/default.typ +++ b/examples/default.typ @@ -3,9 +3,15 @@ #show: default-theme.with( + aspect-ratio: "16-9", config-common( slide-level: 2, - show-notes-on-second-screen: bottom, + ), + config-colors( + primary: blue, + ), + config-methods( + alert: utils.alert-with-primary-color, ) ) @@ -14,13 +20,15 @@ == Recall -Recall +*Recall* #speaker-note[sdfsdf] +#show: touying-set-config.with(config-methods(cover: utils.semi-transparent-cover)) + == Animation -#set text(blue) +#set math.equation(numbering: "(1)") Simple @@ -35,6 +43,6 @@ animation = Appendix -appendix +Appendix #touying-recall() \ No newline at end of file diff --git a/src/configs.typ b/src/configs.typ index 4437c85a6..3c2e55ebe 100644 --- a/src/configs.typ +++ b/src/configs.typ @@ -1,7 +1,7 @@ #import "states.typ" #import "pdfpc.typ" #import "utils.typ" -#import "core.typ": touying-slide-wrapper, touying-slide +#import "core.typ": touying-slide-wrapper, touying-slide, slide /// The private configurations of the theme. #let config-store(..args) = { @@ -9,6 +9,57 @@ return (store: args.named()) } + +#let _default-frozen-states = ( + // ctheorems state + state("thm", + ( + "counters": ("heading": ()), + "latest": () + ) + ), +) + +#let _default-frozen-counters = ( + counter(math.equation), + counter(figure.where(kind: table)), + counter(figure.where(kind: image)), +) + +#let _default-preamble = self => { + if self.at("enable-mark-warning", default: true) { + context { + let marks = query() + if marks.len() > 0 { + let page-num = marks.at(0).location().page() + let kind = marks.at(0).value.kind + panic("Unsupported mark `" + kind + "` at page " + str(page-num) + ". You can't use it inside some functions like `context`. You may want to use the callback-style `uncover` function instead.") + } + } + } + if self.at("enable-pdfpc", default: true) { + context pdfpc.pdfpc-file(here()) + } +} + +#let _default-page-preamble = self => { + if self.at("reset-footnote-number-per-slide", default: true) { + counter(footnote).update(0) + } + if self.at("reset-page-counter-to-slide-counter", default: true) { + context counter(page).update(states.slide-counter.get()) + } + if self.at("enable-pdfpc", default: true) { + context [ + #metadata((t: "NewSlide")) + #metadata((t: "Idx", v: here().page() - 1)) + #metadata((t: "Overlay", v: self.subslide - 1)) + #metadata((t: "LogicalSlide", v: states.slide-counter.get().first())) + ] + } +} + + /// The common configurations of the slides. /// /// - handout (bool): Whether to enable the handout mode. It retains only the last subslide of each slide in handout mode. @@ -66,12 +117,7 @@ #let config-common( handout: false, slide-level: 2, - slide-fn: ( - repeat: auto, - setting: body => body, - composer: auto, - ..bodies, - ) => touying-slide-wrapper(self => touying-slide(self: self, repeat: repeat, setting: setting, composer: composer, ..bodies)), + slide-fn: slide, new-section-slide-fn: none, new-subsection-slide-fn: none, new-subsubsection-slide-fn: none, @@ -89,55 +135,18 @@ // maybe will be deprecated in the future enable-frozen-states-and-counters: true, frozen-states: (), - default-frozen-states: ( - // ctheorems state - state("thm", - ( - "counters": ("heading": ()), - "latest": () - ) - ), - ), + default-frozen-states: _default-frozen-states, frozen-counters: (), - default-frozen-counters: (counter(math.equation), counter(figure.where(kind: table)), counter(figure.where(kind: image))), + default-frozen-counters: _default-frozen-counters, first-slide-number: 1, preamble: none, - default-preamble: self => { - if self.at("enable-mark-warning", default: true) { - context { - let marks = query() - if marks.len() > 0 { - let page-num = marks.at(0).location().page() - let kind = marks.at(0).value.kind - panic("Unsupported mark `" + kind + "` at page " + str(page-num) + ". You can't use it inside some functions like `context`. You may want to use the callback-style `uncover` function instead.") - } - } - } - if self.at("enable-pdfpc", default: true) { - context pdfpc.pdfpc-file(here()) - } - }, + default-preamble: _default-preamble, slide-preamble: none, default-slide-preamble: none, subslide-preamble: none, default-subslide-preamble: none, page-preamble: none, - default-page-preamble: self => { - if self.at("reset-footnote-number-per-slide", default: true) { - counter(footnote).update(0) - } - if self.at("reset-page-counter-to-slide-counter", default: true) { - context counter(page).update(states.slide-counter.get()) - } - if self.at("enable-pdfpc", default: true) { - context [ - #metadata((t: "NewSlide")) - #metadata((t: "Idx", v: here().page() - 1)) - #metadata((t: "Overlay", v: self.subslide - 1)) - #metadata((t: "LogicalSlide", v: states.slide-counter.get().first())) - ] - } - }, + default-page-preamble: _default-page-preamble, show-notes-on-second-screen: none, horizontal-line-to-pagebreak: true, reset-footnote-number-per-slide: true, @@ -186,8 +195,48 @@ } +#let _default-init(self: none, body) = { + show strong: self.methods.alert.with(self: self) + + body +} + +#let _default-cover = utils.method-wrapper(hide) + +#let _default-show-notes(self: none, width: 0pt, height: 0pt) = block( + fill: rgb("#E6E6E6"), + width: width, + height: height, + { + set align(left + top) + set text(size: 24pt, fill: black, weight: "regular") + block( + width: 100%, + height: 88pt, + inset: (left: 32pt, top: 16pt), + outset: 0pt, + fill: rgb("#CCCCCC"), + { + states.current-section-title + linebreak() + [ --- ] + states.current-slide-title + }, + ) + pad(x: 48pt, states.current-slide-note) + // clear the slide note + states.slide-note-state.update(none) + }, +) + +#let _default-alert = utils.method-wrapper(text.with(weight: "bold")) + /// The configuration of the methods /// +/// - init (function): The function to initialize the presentation. It should be `(self: none, body) => { .. }`. +/// +/// By default, it shows the strong content with the `alert` function: `show strong: self.methods.alert.with(self: self)` +/// /// - cover (function): The function to cover content. The default value is `utils.method-wrapper(hide)` function. /// /// - uncover (function): The function to uncover content. @@ -208,7 +257,9 @@ /// /// It should be `(self: none, width: 0pt, height: 0pt) => { .. }`. #let config-methods( - cover: utils.method-wrapper(hide), + // init + init: _default-init, + cover: _default-cover, // dynamic control uncover: utils.uncover, only: utils.only, @@ -217,38 +268,15 @@ alternatives-fn: utils.alternatives-fn, alternatives-cases: utils.alternatives-cases, // alert interface - alert: utils.method-wrapper(text.with(weight: "bold")), + alert: _default-alert, // show notes - show-notes: (self: none, width: 0pt, height: 0pt) => block( - fill: rgb("#E6E6E6"), - width: width, - height: height, - { - set align(left + top) - set text(size: 24pt, fill: black, weight: "regular") - block( - width: 100%, - height: 88pt, - inset: (left: 32pt, top: 16pt), - outset: 0pt, - fill: rgb("#CCCCCC"), - { - states.current-section-title - linebreak() - [ --- ] - states.current-slide-title - }, - ) - pad(x: 48pt, states.current-slide-note) - // clear the slide note - states.slide-note-state.update(none) - }, - ), + show-notes: _default-show-notes, ..args, ) = { assert(args.pos().len() == 0, message: "Unexpected positional arguments.") return ( methods: ( + init: init, cover: cover, uncover: uncover, only: only, diff --git a/src/core.typ b/src/core.typ index 53422fcdc..72e68afc3 100644 --- a/src/core.typ +++ b/src/core.typ @@ -1299,15 +1299,18 @@ /// /// ``` /// #let slide( +/// config: (:), /// repeat: auto, /// setting: body => body, /// composer: auto, /// ..bodies, /// ) = touying-slide-wrapper(self => { -/// touying-slide(self: self, repeat: repeat, setting: setting, composer: composer, ..bodies) +/// touying-slide(self: self, config: config, repeat: repeat, setting: setting, composer: composer, ..bodies) /// }) /// ``` /// +/// - `config` is the configuration of the slide. You can use `config-xxx` to set the configuration of the slide. For more several configurations, you can use `utils.merge-dicts` to merge them. +/// /// - `repeat` is the number of subslides. Default is `auto`,which means touying will automatically calculate the number of subslides. /// /// The `repeat` argument is necessary when you use `#slide(repeat: 3, self => [ .. ])` style code to create a slide. The callback-style `uncover` and `only` cannot be detected by touying automatically. @@ -1329,11 +1332,15 @@ /// - `..bodies` is the contents of the slide. You can call the `slide` function with syntax like `#slide[A][B][C]` to create a slide. #let touying-slide( self: none, + config: (:), repeat: auto, setting: body => body, composer: auto, ..bodies, ) = { + if config != (:) { + self = utils.merge-dicts(self, config) + } assert(bodies.named().len() == 0, message: "unexpected named arguments:" + repr(bodies.named().keys())) let composer-with-side-by-side(..args) = { if type(composer) == function { @@ -1472,6 +1479,8 @@ /// Touying slide function. /// +/// - `config` is the configuration of the slide. You can use `config-xxx` to set the configuration of the slide. For more several configurations, you can use `utils.merge-dicts` to merge them. +/// /// - `repeat` is the number of subslides. Default is `auto`,which means touying will automatically calculate the number of subslides. /// /// The `repeat` argument is necessary when you use `#slide(repeat: 3, self => [ .. ])` style code to create a slide. The callback-style `uncover` and `only` cannot be detected by touying automatically. @@ -1492,10 +1501,45 @@ /// /// - `..bodies` is the contents of the slide. You can call the `slide` function with syntax like `#slide[A][B][C]` to create a slide. #let slide( + config: (:), + repeat: auto, + setting: body => body, + composer: auto, + ..bodies, +) = touying-slide-wrapper(self => { + touying-slide(self: self, config: config, repeat: repeat, setting: setting, composer: composer, ..bodies) +}) + + +/// Touying empty slide function. +/// +/// - `config` is the configuration of the slide. You can use `config-xxx` to set the configuration of the slide. For more several configurations, you can use `utils.merge-dicts` to merge them. +/// +/// - `repeat` is the number of subslides. Default is `auto`,which means touying will automatically calculate the number of subslides. +/// +/// The `repeat` argument is necessary when you use `#slide(repeat: 3, self => [ .. ])` style code to create a slide. The callback-style `uncover` and `only` cannot be detected by touying automatically. +/// +/// - `setting` is the setting of the slide. You can use it to add some set/show rules for the slide. +/// +/// - `composer` is the composer of the slide. You can use it to set the layout of the slide. +/// +/// For example, `#slide(composer: (1fr, 2fr, 1fr))[A][B][C]` to split the slide into three parts. The first and the last parts will take 1/4 of the slide, and the second part will take 1/2 of the slide. +/// +/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `utils.side-by-side` function. +/// +/// The `utils.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. +/// +/// For example, `#slide(composer: 2)[A][B][#grid.cell(colspan: 2)[Footer]] will make the `Footer` cell take 2 columns. +/// +/// If you want to customize the composer, you can pass a function to the `composer` argument. The function should receive the contents of the slide and return the content of the slide, like `#slide(composer: grid.with(columns: 2))[A][B]`. +/// +/// - `..bodies` is the contents of the slide. You can call the `slide` function with syntax like `#slide[A][B][C]` to create a slide. +#let empty-slide( + config: (:), repeat: auto, setting: body => body, composer: auto, ..bodies, ) = touying-slide-wrapper(self => { - touying-slide(self: self, repeat: repeat, setting: setting, composer: composer, ..bodies) + touying-slide(self: self, config: config, repeat: repeat, setting: setting, composer: composer, ..bodies) }) \ No newline at end of file diff --git a/src/slides.typ b/src/slides.typ index 07d484540..a67d53812 100644 --- a/src/slides.typ +++ b/src/slides.typ @@ -18,10 +18,20 @@ /// /// `body` is the contents of the slides. #let touying-slides(..args, body) = { + // get the default config assert(args.named().len() == 0, message: "unexpected named arguments:" + repr(args.named().keys())) let args = (configs.default-config,) + args.pos() let self = utils.merge-dicts(..args) + // get the init function + let init = if "init" in self.methods and type(self.methods.init) == function { + self.methods.init.with(self: self) + } else { + body => body + } + + show: init + show: core.split-content-into-slides.with(self: self, is-first-slide: true) body diff --git a/src/utils.typ b/src/utils.typ index eb7677751..efc63c05d 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -162,22 +162,24 @@ return [#it] } -// OOP: empty page -#let empty-page(self, margin: (x: 0em, y: 0em)) = { - self.page-args += ( - header: none, - footer: none, - ) - if margin != none { - self.page-args += (margin: margin) - } - if self.freeze-in-empty-page { - self.freeze-slide-counter = true - } - self -} - -// OOP: wrap methods +// // OOP: empty page +// #let empty-page(self, margin: (x: 0em, y: 0em)) = { +// self.page-args += ( +// header: none, +// footer: none, +// ) +// if margin != none { +// self.page-args += (margin: margin) +// } +// if self.freeze-in-empty-page { +// self.freeze-slide-counter = true +// } +// self +// } + +/// Wrap a function with a `self` parameter to make it a method +/// +/// Example: `#let hide = method-wrapper(hide)` to get a `hide` method #let method-wrapper(fn) = (self: none, ..args) => fn(..args) /// Assuming all functions in dictionary have a named `self` parameter, @@ -524,6 +526,27 @@ #let update-alpha(constructor: rgb, color, alpha) = constructor(..color.components(alpha: true).slice(0, -1), alpha) +/// Cover content with a transparent rectangle. +/// +/// Example: `config-methods(cover: utils.semi-transparent-cover)` +#let semi-transparent-cover(self: none, constructor: rgb, alpha: 85%, body) = { + cover-with-rect( + fill: update-alpha( + constructor: constructor, + self.page.fill, + alpha, + ), + body, + ) +} + + +/// Alert content with a primary color. +/// +/// Example: `config-methods(alert: utils.alert-with-primary-color)` +#let alert-with-primary-color(self: none, it) = text(fill: self.colors.primary, it) + + // Code: check visible subslides and dynamic control // Attribution: This file is based on the code from https://github.com/andreasKroepelin/polylux/blob/main/logic.typ // Author: Andreas Kröpelin @@ -817,7 +840,10 @@ pdfpc.speaker-note(raw-text) } let show-notes-on-second-screen = self.at("show-notes-on-second-screen", default: none) - assert(show-notes-on-second-screen in (none, bottom, right), message: "`show-notes-on-second-screen` should be `none`, `bottom` or `right`") + assert( + show-notes-on-second-screen in (none, bottom, right), + message: "`show-notes-on-second-screen` should be `none`, `bottom` or `right`", + ) if show-notes-on-second-screen != none { states.slide-note-state.update(setting(note)) } diff --git a/themes/default.typ b/themes/default.typ index 705c9ab94..bfeeb03fa 100644 --- a/themes/default.typ +++ b/themes/default.typ @@ -2,6 +2,8 @@ /// Touying slide function. /// +/// - `config` is the configuration of the slide. You can use `config-xxx` to set the configuration of the slide. For more several configurations, you can use `utils.merge-dicts` to merge them. +/// /// - `repeat` is the number of subslides. Default is `auto`,which means touying will automatically calculate the number of subslides. /// /// The `repeat` argument is necessary when you use `#slide(repeat: 3, self => [ .. ])` style code to create a slide. The callback-style `uncover` and `only` cannot be detected by touying automatically. @@ -22,12 +24,13 @@ /// /// - `..bodies` is the contents of the slide. You can call the `slide` function with syntax like `#slide[A][B][C]` to create a slide. #let slide( + config: (:), repeat: auto, setting: body => body, composer: auto, ..bodies, ) = touying-slide-wrapper(self => { - touying-slide(self: self, repeat: repeat, setting: setting, composer: composer, ..bodies) + touying-slide(self: self, config: config, repeat: repeat, setting: setting, composer: composer, ..bodies) }) @@ -36,6 +39,8 @@ ..args, body, ) = { + set text(size: 20pt) + show: touying-slides.with( config-page(paper: "presentation-" + aspect-ratio), config-common( From 377e00285d80c16771eeaaab2197716cff7c2efb Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Fri, 30 Aug 2024 15:44:49 +0800 Subject: [PATCH 20/43] dev: add some magics --- src/configs.typ | 4 ++-- src/core.typ | 8 ++++++-- src/exports.typ | 2 +- src/magic.typ | 17 ++++++++++------- src/slides.typ | 25 +++++++++++++++++++++++++ 5 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/configs.typ b/src/configs.typ index 3c2e55ebe..6c94c4e31 100644 --- a/src/configs.typ +++ b/src/configs.typ @@ -111,7 +111,7 @@ /// /// - nontight-list-enum-and-terms (bool): Whether to make `tight` argument always be `false` for list, enum, and terms. The default value is `true`. /// -/// - align-list-marker-with-baseline (bool): Whether to align the list marker with the baseline. The default value is `true`. +/// - align-list-marker-with-baseline (bool): Whether to align the list marker with the baseline. The default value is `false`. /// /// - scale-list-items (none, float): Whether to scale the list items recursively. The default value is `none`. #let config-common( @@ -151,7 +151,7 @@ horizontal-line-to-pagebreak: true, reset-footnote-number-per-slide: true, nontight-list-enum-and-terms: true, - align-list-marker-with-baseline: true, + align-list-marker-with-baseline: false, scale-list-items: none, ..args, ) = { diff --git a/src/core.typ b/src/core.typ index 72e68afc3..66d319261 100644 --- a/src/core.typ +++ b/src/core.typ @@ -1342,6 +1342,10 @@ self = utils.merge-dicts(self, config) } assert(bodies.named().len() == 0, message: "unexpected named arguments:" + repr(bodies.named().keys())) + let setting-with-auto-offset-for-heading(body) = { + set heading(offset: self.at("slide-level", default: 0)) if self.at("auto-offset-for-heading", default: true) + setting(body) + } let composer-with-side-by-side(..args) = { if type(composer) == function { composer(..args) @@ -1452,7 +1456,7 @@ ) header = page-preamble(self) + header set page(..(self.page + page-extra-args + (header: header, footer: footer))) - setting(subslide-preamble(self) + composer-with-side-by-side(..conts)) + setting-with-auto-offset-for-heading(subslide-preamble(self) + composer-with-side-by-side(..conts)) } else { // render all the subslides let result = () @@ -1468,7 +1472,7 @@ // update the counter in the first subslide only result.push({ set page(..(self.page + page-extra-args + (header: new-header, footer: footer))) - setting(subslide-preamble(self) + composer-with-side-by-side(..conts)) + setting-with-auto-offset-for-heading(subslide-preamble(self) + composer-with-side-by-side(..conts)) }) } // return the result diff --git a/src/exports.typ b/src/exports.typ index 9dd5963b6..f8154adfa 100644 --- a/src/exports.typ +++ b/src/exports.typ @@ -1,4 +1,4 @@ -#import "core.typ": pause, meanwhile, uncover, only, alternatives-match, alternatives, alternatives-fn, alternatives-cases, slide, touying-slide, touying-fn-wrapper, touying-slide-wrapper, touying-equation, touying-mitex, touying-reducer, appendix, touying-set-config, touying-recall, speaker-note +#import "core.typ": pause, meanwhile, uncover, only, alternatives-match, alternatives, alternatives-fn, alternatives-cases, slide, empty-slide, touying-slide, touying-fn-wrapper, touying-slide-wrapper, touying-equation, touying-mitex, touying-reducer, appendix, touying-set-config, touying-recall, speaker-note #import "slides.typ": touying-slides #import "configs.typ": config-colors, config-common, config-info, config-methods, config-page, config-store, default-config #import "utils.typ" diff --git a/src/magic.typ b/src/magic.typ index a7d92ab46..44efeb6af 100644 --- a/src/magic.typ +++ b/src/magic.typ @@ -8,10 +8,13 @@ /// Usage: `#show: align-list-marker-with-baseline` #let align-list-marker-with-baseline(body) = { show list.item: it => { - let current-marker = if type(list.marker) == array { - list.marker.at(0) - } else { - list.marker + let current-marker = { + set text(fill: text.fill) + if type(list.marker) == array { + list.marker.at(0) + } else { + list.marker + } } let hanging-indent = measure(current-marker).width + .6em + .3pt set terms(hanging-indent: hanging-indent) @@ -61,9 +64,9 @@ /// /// Usage: `#show: nontight-list-enum-and-terms` #let nontight-list-enum-and-terms(body) = { - show list: nontight(list) - show enum: nontight(enum) - show terms: nontight(terms) + show list.where(tight: true): nontight + show enum.where(tight: true): nontight + show terms.where(tight: true): nontight body } diff --git a/src/slides.typ b/src/slides.typ index a67d53812..dd7e3fd7b 100644 --- a/src/slides.typ +++ b/src/slides.typ @@ -1,6 +1,7 @@ #import "utils.typ" #import "configs.typ" #import "core.typ" +#import "magic.typ" /// Touying slides function. /// @@ -30,6 +31,30 @@ body => body } + show: body => { + if self.at("scale-list-items", default: none) != none { + magic.scale-list-items(scale: self.at("scale-list-items", default: none), body) + } else { + body + } + } + + show: body => { + if self.at("nontight-list-enum-and-terms", default: true) { + magic.nontight-list-enum-and-terms(body) + } else { + body + } + } + + show: body => { + if self.at("align-list-marker-with-baseline", default: false) { + magic.align-list-marker-with-baseline(body) + } else { + body + } + } + show: init show: core.split-content-into-slides.with(self: self, is-first-slide: true) From b812f0b9fee3bdfc6cfd6118458cdb57ee4229b3 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Fri, 30 Aug 2024 21:34:23 +0800 Subject: [PATCH 21/43] dev: add states.display-current-heading --- examples/default.typ | 6 +- src/core.typ | 8 +- src/states.typ | 208 ++++++++++++++---------------------------- themes/university.typ | 2 +- 4 files changed, 84 insertions(+), 140 deletions(-) diff --git a/examples/default.typ b/examples/default.typ index b1548db05..3c351ad09 100644 --- a/examples/default.typ +++ b/examples/default.typ @@ -1,5 +1,6 @@ #import "../lib.typ": * #import themes.default: * +#import "@preview/hydra:0.5.1": hydra #show: default-theme.with( @@ -12,7 +13,10 @@ ), config-methods( alert: utils.alert-with-primary-color, - ) + ), + config-page( + header: states.display-current-heading(level: 2), + ), ) diff --git a/src/core.typ b/src/core.typ index 66d319261..a3dc07c93 100644 --- a/src/core.typ +++ b/src/core.typ @@ -1361,9 +1361,13 @@ utils.call-or-display(self, self.at("preamble", default: none)) utils.call-or-display(self, self.at("default-preamble", default: none)) } + [#metadata((kind: "touying-new-slide")) ] // add headings for the first subslide if self.at("headings", default: ()) != () { - place(hide(self.at("headings", default: none).sum(default: none))) + place(hide({ + set heading(offset: 0) + self.at("headings", default: none).sum(default: none) + })) } utils.call-or-display(self, self.at("slide-preamble", default: none)) utils.call-or-display(self, self.at("default-slide-preamble", default: none)) @@ -1373,6 +1377,7 @@ if self.subslide == 1 { slide-preamble(self) } + [#metadata((kind: "touying-new-subslide")) ] if self.at("enable-frozen-states-and-counters", default: true) { if self.subslide == 1 { // save the states and counters @@ -1413,6 +1418,7 @@ } // update states for every page let page-preamble(self) = { + [#metadata((kind: "touying-new-page")) ] // 1. slide counter part // if freeze-slide-counter is false, then update the slide-counter if self.subslide == 1 { diff --git a/src/states.typ b/src/states.typ index f92b079ba..b2f0a8766 100644 --- a/src/states.typ +++ b/src/states.typ @@ -1,161 +1,95 @@ // touying slide counters #let slide-counter = counter("touying-slide-counter") #let last-slide-counter = counter("touying-last-slide-counter") -#let last-slide-number = context { last-slide-counter.final().first() } - -#let touying-progress(callback) = context { - if last-slide-counter.final().first() == 0 { - callback(1.0) - return - } - let ratio = calc.min(1.0, slide-counter.get().first() / last-slide-counter.final().first()) - callback(ratio) +#let last-slide-number = context { + last-slide-counter.final().first() } -// sections -#let sections-state = state("touying-sections-state", ((kind: "section", title: none, short-title: none, loc: none, count: 0, children: ()),)) +/// Get the progress of the current slide. +/// +/// - `callback` is the callback function `ratio => { .. }` to get the progress of the current slide. The `ratio` is a float number between 0 and 1. +#let touying-progress(callback) = ( + context { + if last-slide-counter.final().first() == 0 { + callback(1.0) + return + } + let ratio = calc.min(1.0, slide-counter.get().first() / last-slide-counter.final().first()) + callback(ratio) + } +) -// slide title state -#let slide-title-state = state("touying-slide-title-state", none) // slide note state #let slide-note-state = state("touying-slide-note-state", none) +#let current-slide-note = context slide-note-state.get() -#let _new-section(short-title: auto, duplicate: false, title) = context { - let loc = here() - sections-state.update(sections => { - if duplicate or sections.last().title != title or sections.last().short-title != short-title { - sections.push((kind: "section", title: title, short-title: short-title, loc: loc, count: 0, children: ())) - } - sections - }) -} -#let _new-subsection(short-title: auto, duplicate: false, title) = context { - let loc = here() - sections-state.update(sections => { - let last-section = sections.pop() - let last-subsection = (kind: "none") - let i = -1 - while last-subsection.kind != "subsection" { - last-subsection = last-section.children.at(i, default: (kind: "subsection", title: none, short-title: none, loc: none, count: 0, children: ())) - i += 1 - } - if duplicate or last-subsection.title != title or last-subsection.short-title != short-title { - last-section.children.push((kind: "subsection", title: title, short-title: short-title, loc: loc, count: 0, children: ())) - } - sections.push(last-section) - sections - }) -} +// ------------------------------------- +// Headings +// ------------------------------------- -#let _sections-step(repetitions) = context { - let loc = here() - sections-state.update(sections => { - let last-section = sections.pop() - if last-section.children.len() == 0 or last-section.children.last().kind == "slide" { - last-section.children.push((kind: "slide", loc: loc, count: repetitions)) - last-section.count += 1 - sections.push(last-section) - } else { - // update for subsection - let last-subsection = last-section.children.pop() - last-subsection.children.push((kind: "slide", loc: loc, count: repetitions)) - last-subsection.count += 1 - last-section.count += 1 - last-section.children.push(last-subsection) - sections.push(last-section) - } - sections +/// Get the current heading On or before the current page. +/// +/// - `level` is the level of the heading. If `level` is `auto`, it will return the last heading on or before the current page. If `level` is a number, it will return the last heading on or before the current page with the same level. +/// +/// - `hierachical` is a boolean value to indicate whether to return the heading hierachically. If `hierachical` is `true`, it will return the last heading according to the hierachical structure. If `hierachical` is `false`, it will return the last heading on or before the current page with the same level. +/// +/// - `depth` is the maximum depth of the heading to search. Usually, it should be set as slide-level. +#let current-heading(level: auto, hierachical: true, depth: 9999) = { + let current-page = here().page() + if not hierachical and level != auto { + let headings = query(heading).filter(h => ( + h.location().page() <= current-page and h.level <= depth and h.level == level + )) + return headings.at(-1, default: none) } -)} - -#let touying-final-sections(callback) = context { - callback(sections-state.final()) -} - -#let touying-outline(self: none, func: enum, enum-args: (:), list-args: (:), padding: 0pt) = touying-final-sections(sections => { - let enum-args = (full: true) + enum-args - if self != none and self.numbering != none { - enum-args = (numbering: self.numbering) + enum-args + let headings = query(heading).filter(h => h.location().page() <= current-page and h.level <= depth) + if headings == () { + return } - let args = if func == enum { enum-args } else { list-args } - pad(padding, func( - ..args, - ..sections.filter(section => section.loc != none) - .map(section => [#link(section.loc, section.title)] + if section.children.filter(it => it.kind != "slide").len() > 0 { - let subsections = section.children.filter(it => it.kind != "slide") - func( - ..args, - ..subsections.map(subsection => [#link(subsection.loc, subsection.title)]) - ) - }) - )) -}) - -#let current-section-title = context { - let sections = sections-state.get() - sections.last().title -} - -#let current-subsection-title = context { - let sections = sections-state.get() - let subsections = sections.last().children.filter(v => v.kind == "subsection") - if subsections.len() > 0 { - subsections.last().title - } else { - none + if level == auto { + return headings.last() } -} - -#let current-slide-title = context slide-title-state.get() - -#let current-slide-note = context slide-note-state.get() - -#let _typst-numbering = numbering -#let current-section-number(numbering: "01", ignore-zero: true) = context { - let sections = sections-state.get() - if not ignore-zero or sections.len() - 1 != 0 { - _typst-numbering(numbering, sections.len() - 1) + let current-level = headings.last().level + let current-heading = headings.pop() + while headings.len() > 0 and level < current-level { + current-level = headings.last().level + current-heading = headings.pop() } -} - -#let current-section-with-numbering(self, ignore-zero: true) = context { - let sections = sections-state.get() - if self.numbering != none and (not ignore-zero or sections.len() - 1 != 0) { - _typst-numbering(self.numbering, sections.len() - 1) - [ ] + if level == current-level { + return current-heading } - sections.last().title } -#let current-subsection-number(numbering: "1.1", ignore-zero: true) = context { - let sections = sections-state.get() - let subsections = sections.last().children - if (not ignore-zero or sections.len() - 1 != 0) and (not ignore-zero or subsections.len() - 1 != 0) { - _typst-numbering(numbering, sections.len() - 1, subsections.len() - 1) - } -} -#let current-subsection-with-numbering(self, ignore-zero: true) = context { - let sections = sections-state.get() - let subsections = sections.last().children - if self.numbering != none and (not ignore-zero or sections.len() - 1 != 0) and (not ignore-zero or subsections.len() - 1 != 0) { - _typst-numbering(self.numbering, sections.len() - 1, subsections.len() - 1) - [ ] +/// Display the current heading on the page. +/// +/// - `level` is the level of the heading. If `level` is `auto`, it will return the last heading on or before the current page. If `level` is a number, it will return the last heading on or before the current page with the same level. +/// +/// - `hierachical` is a boolean value to indicate whether to return the heading hierachically. If `hierachical` is `true`, it will return the last heading according to the hierachical structure. If `hierachical` is `false`, it will return the last heading on or before the current page with the same level. +/// +/// - `depth` is the maximum depth of the heading to search. Usually, it should be set as slide-level. +/// +/// - `sty` is the style of the heading. If `sty` is a function, it will use the function to style the heading. For example, `sty: current-heading => current-heading.body`. +#let display-current-heading(level: auto, hierachical: true, depth: 9999, ..sty) = ( + context { + let sty = if sty.pos().len() > 1 { + sty.pos().at(0) + } else { + current-heading => { + if current-heading.numbering != none { + numbering(current-heading.numbering, ..counter(heading).at(current-heading.location())) + h(.3em) + } + current-heading.body + } + } + let current-heading = current-heading(level: level, hierachical: hierachical, depth: depth) + if current-heading != none { + sty(current-heading) + } } - subsections.last().title -} - -#let touying-progress-with-sections(callback) = context { - callback(( - current-sections: sections-state.get(), - final-sections: sections-state.final(), - current-slide-number: slide-counter.get().first(), - last-slide-number: last-slide-counter.final().first(), - )) -} - +) // ------------------------------------- diff --git a/themes/university.typ b/themes/university.typ index 20ca52e36..5ec83ca0b 100644 --- a/themes/university.typ +++ b/themes/university.typ @@ -182,7 +182,7 @@ h(1fr) utils.info-date(self) h(1fr) - states.slide-counter.display() + " / " + states.last-slide-number + context states.slide-counter.display() + " / " + states.last-slide-number h(1fr) }, ..args, From 1bb33ca211257b4ad1f42242879eb40d8b89d7ca Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Sat, 31 Aug 2024 00:01:14 +0800 Subject: [PATCH 22/43] dev: support short heading --- examples/default.typ | 2 +- src/configs.typ | 22 +++-- src/core.typ | 22 ++--- src/exports.typ | 1 - src/states.typ | 102 ----------------------- src/utils.typ | 193 +++++++++++++++++++++++++++++++++++++++---- 6 files changed, 204 insertions(+), 138 deletions(-) delete mode 100644 src/states.typ diff --git a/examples/default.typ b/examples/default.typ index 3c351ad09..ffe88a655 100644 --- a/examples/default.typ +++ b/examples/default.typ @@ -15,7 +15,7 @@ alert: utils.alert-with-primary-color, ), config-page( - header: states.display-current-heading(level: 2), + header: utils.display-current-short-heading(level: 2), ), ) diff --git a/src/configs.typ b/src/configs.typ index 6c94c4e31..14a0c507c 100644 --- a/src/configs.typ +++ b/src/configs.typ @@ -1,4 +1,3 @@ -#import "states.typ" #import "pdfpc.typ" #import "utils.typ" #import "core.typ": touying-slide-wrapper, touying-slide, slide @@ -47,14 +46,14 @@ counter(footnote).update(0) } if self.at("reset-page-counter-to-slide-counter", default: true) { - context counter(page).update(states.slide-counter.get()) + context counter(page).update(utils.slide-counter.get()) } if self.at("enable-pdfpc", default: true) { context [ #metadata((t: "NewSlide")) #metadata((t: "Idx", v: here().page() - 1)) #metadata((t: "Overlay", v: self.subslide - 1)) - #metadata((t: "LogicalSlide", v: states.slide-counter.get().first())) + #metadata((t: "LogicalSlide", v: utils.slide-counter.get().first())) ] } } @@ -217,20 +216,24 @@ outset: 0pt, fill: rgb("#CCCCCC"), { - states.current-section-title + utils.current-section-title linebreak() [ --- ] - states.current-slide-title + utils.current-slide-title }, ) - pad(x: 48pt, states.current-slide-note) + pad(x: 48pt, utils.current-slide-note) // clear the slide note - states.slide-note-state.update(none) + utils.slide-note-state.update(none) }, ) #let _default-alert = utils.method-wrapper(text.with(weight: "bold")) +#let _default-convert-label-to-short-heading(self: none, lbl) = utils.titlecase( + lbl.replace(regex("^[^:]*:"), "").replace("_", " ").replace("-", " "), +) + /// The configuration of the methods /// /// - init (function): The function to initialize the presentation. It should be `(self: none, body) => { .. }`. @@ -255,6 +258,8 @@ /// /// - show-notes (function): The function to show notes on second screen. /// +/// - convert-label-to-short-heading (function): The function to convert label to short heading. It is useful for the short heading for heading with label. +/// /// It should be `(self: none, width: 0pt, height: 0pt) => { .. }`. #let config-methods( // init @@ -271,6 +276,8 @@ alert: _default-alert, // show notes show-notes: _default-show-notes, + // convert label to short heading + convert-label-to-short-heading: _default-convert-label-to-short-heading, ..args, ) = { assert(args.pos().len() == 0, message: "Unexpected positional arguments.") @@ -286,6 +293,7 @@ alternatives-cases: alternatives-cases, alert: alert, show-notes: show-notes, + convert-label-to-short-heading: convert-label-to-short-heading, ) + args.named(), ) } diff --git a/src/core.typ b/src/core.typ index a3dc07c93..cabcbf314 100644 --- a/src/core.typ +++ b/src/core.typ @@ -1,5 +1,5 @@ #import "utils.typ" -#import "states.typ" +#import "utils.typ" #import "pdfpc.typ" /// ------------------------------------------------ @@ -1382,32 +1382,32 @@ if self.subslide == 1 { // save the states and counters context { - states.saved-frozen-states.update(self.frozen-states.map(s => s.get())) - states.saved-default-frozen-states.update(self.default-frozen-states.map(s => s.get())) - states.saved-frozen-counters.update(self.frozen-counters.map(s => s.get())) - states.saved-default-frozen-counters.update(self.default-frozen-counters.map(s => s.get())) + utils.saved-frozen-states.update(self.frozen-states.map(s => s.get())) + utils.saved-default-frozen-states.update(self.default-frozen-states.map(s => s.get())) + utils.saved-frozen-counters.update(self.frozen-counters.map(s => s.get())) + utils.saved-default-frozen-counters.update(self.default-frozen-counters.map(s => s.get())) } } else { // restore the states and counters context { self .frozen-states - .zip(states.saved-frozen-states.get()) + .zip(utils.saved-frozen-states.get()) .map(pair => pair.at(0).update(pair.at(1))) .sum(default: none) self .default-frozen-states - .zip(states.saved-default-frozen-states.get()) + .zip(utils.saved-default-frozen-states.get()) .map(pair => pair.at(0).update(pair.at(1))) .sum(default: none) self .frozen-counters - .zip(states.saved-frozen-counters.get()) + .zip(utils.saved-frozen-counters.get()) .map(pair => pair.at(0).update(pair.at(1))) .sum(default: none) self .default-frozen-counters - .zip(states.saved-default-frozen-counters.get()) + .zip(utils.saved-default-frozen-counters.get()) .map(pair => pair.at(0).update(pair.at(1))) .sum(default: none) } @@ -1423,10 +1423,10 @@ // if freeze-slide-counter is false, then update the slide-counter if self.subslide == 1 { if not self.at("freeze-slide-counter", default: false) { - states.slide-counter.step() + utils.slide-counter.step() // if appendix is false, then update the last-slide-counter if not self.at("appendix", default: false) { - states.last-slide-counter.step() + utils.last-slide-counter.step() } } } diff --git a/src/exports.typ b/src/exports.typ index f8154adfa..9d1dfc6ea 100644 --- a/src/exports.typ +++ b/src/exports.typ @@ -2,7 +2,6 @@ #import "slides.typ": touying-slides #import "configs.typ": config-colors, config-common, config-info, config-methods, config-page, config-store, default-config #import "utils.typ" -#import "states.typ" #import "magic.typ" #import "pdfpc.typ" #import "components.typ" \ No newline at end of file diff --git a/src/states.typ b/src/states.typ deleted file mode 100644 index b2f0a8766..000000000 --- a/src/states.typ +++ /dev/null @@ -1,102 +0,0 @@ -// touying slide counters -#let slide-counter = counter("touying-slide-counter") -#let last-slide-counter = counter("touying-last-slide-counter") -#let last-slide-number = context { - last-slide-counter.final().first() -} - -/// Get the progress of the current slide. -/// -/// - `callback` is the callback function `ratio => { .. }` to get the progress of the current slide. The `ratio` is a float number between 0 and 1. -#let touying-progress(callback) = ( - context { - if last-slide-counter.final().first() == 0 { - callback(1.0) - return - } - let ratio = calc.min(1.0, slide-counter.get().first() / last-slide-counter.final().first()) - callback(ratio) - } -) - - -// slide note state -#let slide-note-state = state("touying-slide-note-state", none) -#let current-slide-note = context slide-note-state.get() - - -// ------------------------------------- -// Headings -// ------------------------------------- - -/// Get the current heading On or before the current page. -/// -/// - `level` is the level of the heading. If `level` is `auto`, it will return the last heading on or before the current page. If `level` is a number, it will return the last heading on or before the current page with the same level. -/// -/// - `hierachical` is a boolean value to indicate whether to return the heading hierachically. If `hierachical` is `true`, it will return the last heading according to the hierachical structure. If `hierachical` is `false`, it will return the last heading on or before the current page with the same level. -/// -/// - `depth` is the maximum depth of the heading to search. Usually, it should be set as slide-level. -#let current-heading(level: auto, hierachical: true, depth: 9999) = { - let current-page = here().page() - if not hierachical and level != auto { - let headings = query(heading).filter(h => ( - h.location().page() <= current-page and h.level <= depth and h.level == level - )) - return headings.at(-1, default: none) - } - let headings = query(heading).filter(h => h.location().page() <= current-page and h.level <= depth) - if headings == () { - return - } - if level == auto { - return headings.last() - } - let current-level = headings.last().level - let current-heading = headings.pop() - while headings.len() > 0 and level < current-level { - current-level = headings.last().level - current-heading = headings.pop() - } - if level == current-level { - return current-heading - } -} - - -/// Display the current heading on the page. -/// -/// - `level` is the level of the heading. If `level` is `auto`, it will return the last heading on or before the current page. If `level` is a number, it will return the last heading on or before the current page with the same level. -/// -/// - `hierachical` is a boolean value to indicate whether to return the heading hierachically. If `hierachical` is `true`, it will return the last heading according to the hierachical structure. If `hierachical` is `false`, it will return the last heading on or before the current page with the same level. -/// -/// - `depth` is the maximum depth of the heading to search. Usually, it should be set as slide-level. -/// -/// - `sty` is the style of the heading. If `sty` is a function, it will use the function to style the heading. For example, `sty: current-heading => current-heading.body`. -#let display-current-heading(level: auto, hierachical: true, depth: 9999, ..sty) = ( - context { - let sty = if sty.pos().len() > 1 { - sty.pos().at(0) - } else { - current-heading => { - if current-heading.numbering != none { - numbering(current-heading.numbering, ..counter(heading).at(current-heading.location())) + h(.3em) - } - current-heading.body - } - } - let current-heading = current-heading(level: level, hierachical: hierachical, depth: depth) - if current-heading != none { - sty(current-heading) - } - } -) - - -// ------------------------------------- -// Saved states and counters -// ------------------------------------- - -#let saved-frozen-states = state("touying-saved-frozen-states", ()) -#let saved-default-frozen-states = state("touying-saved-default-frozen-states", ()) -#let saved-frozen-counters = state("touying-saved-frozen-counters", ()) -#let saved-default-frozen-counters = state("touying-saved-default-frozen-counters", ()) \ No newline at end of file diff --git a/src/utils.typ b/src/utils.typ index efc63c05d..b7e231606 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -1,5 +1,4 @@ #import "pdfpc.typ" -#import "states.typ" /// Add a dictionary to another dictionary recursively /// @@ -29,6 +28,42 @@ return res } +// ------------------------------------- +// Slide counter +// ------------------------------------- +#let slide-counter = counter("touying-slide-counter") +#let last-slide-counter = counter("touying-last-slide-counter") +#let last-slide-number = context { + last-slide-counter.final().first() +} + +/// Get the progress of the current slide. +/// +/// - `callback` is the callback function `ratio => { .. }` to get the progress of the current slide. The `ratio` is a float number between 0 and 1. +#let touying-progress(callback) = ( + context { + if last-slide-counter.final().first() == 0 { + callback(1.0) + return + } + let ratio = calc.min(1.0, slide-counter.get().first() / last-slide-counter.final().first()) + callback(ratio) + } +) + +// slide note state +#let slide-note-state = state("touying-slide-note-state", none) +#let current-slide-note = context slide-note-state.get() + +// ------------------------------------- +// Saved states and counters +// ------------------------------------- + +#let saved-frozen-states = state("touying-saved-frozen-states", ()) +#let saved-default-frozen-states = state("touying-saved-default-frozen-states", ()) +#let saved-frozen-counters = state("touying-saved-frozen-counters", ()) +#let saved-default-frozen-counters = state("touying-saved-default-frozen-counters", ()) + /// Remove leading and trailing empty elements from an array of content /// @@ -162,26 +197,13 @@ return [#it] } -// // OOP: empty page -// #let empty-page(self, margin: (x: 0em, y: 0em)) = { -// self.page-args += ( -// header: none, -// footer: none, -// ) -// if margin != none { -// self.page-args += (margin: margin) -// } -// if self.freeze-in-empty-page { -// self.freeze-slide-counter = true -// } -// self -// } /// Wrap a function with a `self` parameter to make it a method /// /// Example: `#let hide = method-wrapper(hide)` to get a `hide` method #let method-wrapper(fn) = (self: none, ..args) => fn(..args) + /// Assuming all functions in dictionary have a named `self` parameter, /// `methods` function is used to get all methods in dictionary object /// @@ -199,6 +221,145 @@ } +// ------------------------------------- +// Headings +// ------------------------------------- + + +/// Capitalize a string +#let capitalize(s) = { + assert(type(s) == str, message: "s must be a string") + if s.len() == 0 { + return s + } + let lowercase = lower(s) + upper(lowercase.at(0)) + lowercase.slice(1) +} + + +/// Convert a string into title case +#let titlecase(s) = { + assert(type(s) == str, message: "s must be a string") + if s.len() == 0 { + return s + } + s.split(" ").map(capitalize).join(" ") +} + + +/// Convert a heading with label to short form +/// +/// - `it` is the heading +#let short-heading(self: none, it) = { + if it == none { + return + } + let convert-label-to-short-heading = if ( + type(self) == dictionary and "methods" in self and "convert-label-to-short-heading" in self.methods + ) { + self.methods.convert-label-to-short-heading + } else { + (self: none, lbl) => titlecase(lbl.replace(regex("^[^:]*:"), "").replace("_", " ").replace("-", " ")) + } + convert-label-to-short-heading = convert-label-to-short-heading.with(self: self) + assert(type(it) == content and it.func() == heading, message: "it must be a heading") + if not it.has("label") { + return it.body + } + let lbl = str(it.label) + return convert-label-to-short-heading(lbl) +} + + +/// Get the current heading On or before the current page. +/// +/// - `level` is the level of the heading. If `level` is `auto`, it will return the last heading on or before the current page. If `level` is a number, it will return the last heading on or before the current page with the same level. +/// +/// - `hierachical` is a boolean value to indicate whether to return the heading hierachically. If `hierachical` is `true`, it will return the last heading according to the hierachical structure. If `hierachical` is `false`, it will return the last heading on or before the current page with the same level. +/// +/// - `depth` is the maximum depth of the heading to search. Usually, it should be set as slide-level. +#let current-heading(level: auto, hierachical: true, depth: 9999) = { + let current-page = here().page() + if not hierachical and level != auto { + let headings = query(heading).filter(h => ( + h.location().page() <= current-page and h.level <= depth and h.level == level + )) + return headings.at(-1, default: none) + } + let headings = query(heading).filter(h => h.location().page() <= current-page and h.level <= depth) + if headings == () { + return + } + if level == auto { + return headings.last() + } + let current-level = headings.last().level + let current-heading = headings.pop() + while headings.len() > 0 and level < current-level { + current-level = headings.last().level + current-heading = headings.pop() + } + if level == current-level { + return current-heading + } +} + + +/// Display the current heading on the page. +/// +/// - `level` is the level of the heading. If `level` is `auto`, it will return the last heading on or before the current page. If `level` is a number, it will return the last heading on or before the current page with the same level. +/// +/// - `hierachical` is a boolean value to indicate whether to return the heading hierachically. If `hierachical` is `true`, it will return the last heading according to the hierachical structure. If `hierachical` is `false`, it will return the last heading on or before the current page with the same level. +/// +/// - `depth` is the maximum depth of the heading to search. Usually, it should be set as slide-level. +/// +/// - `sty` is the style of the heading. If `sty` is a function, it will use the function to style the heading. For example, `sty: current-heading => current-heading.body`. +#let display-current-heading(self: none, level: auto, hierachical: true, depth: 9999, ..sty) = ( + context { + let sty = if sty.pos().len() > 1 { + sty.pos().at(0) + } else { + current-heading => { + if current-heading.numbering != none { + numbering(current-heading.numbering, ..counter(heading).at(current-heading.location())) + h(.3em) + } + current-heading.body + } + } + let current-heading = current-heading(level: level, hierachical: hierachical, depth: depth) + if current-heading != none { + sty(current-heading) + } + } +) + + +/// Display the current short heading on the page. +/// +/// - `level` is the level of the heading. If `level` is `auto`, it will return the last heading on or before the current page. If `level` is a number, it will return the last heading on or before the current page with the same level. +/// +/// - `hierachical` is a boolean value to indicate whether to return the heading hierachically. If `hierachical` is `true`, it will return the last heading according to the hierachical structure. If `hierachical` is `false`, it will return the last heading on or before the current page with the same level. +/// +/// - `depth` is the maximum depth of the heading to search. Usually, it should be set as slide-level. +/// +/// - `sty` is the style of the heading. If `sty` is a function, it will use the function to style the heading. For example, `sty: current-heading => current-heading.body`. +#let display-current-short-heading(self: none, level: auto, hierachical: true, depth: 9999, ..sty) = ( + context { + let sty = if sty.pos().len() > 1 { + sty.pos().at(0) + } else { + current-heading => { + short-heading(self: self, current-heading) + } + } + let current-heading = current-heading(level: level, hierachical: hierachical, depth: depth) + if current-heading != none { + sty(current-heading) + } + } +) + + /// Display the date of `self.info.date` with `self.datetime-format` format. #let display-info-date(self) = { assert("info" in self, message: "self must have an info field") @@ -845,7 +1006,7 @@ message: "`show-notes-on-second-screen` should be `none`, `bottom` or `right`", ) if show-notes-on-second-screen != none { - states.slide-note-state.update(setting(note)) + slide-note-state.update(setting(note)) } } From b5687bf24d99b71a6de56f066890e02a8fc651f1 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Sat, 31 Aug 2024 00:33:14 +0800 Subject: [PATCH 23/43] feat: add and --- examples/default.typ | 9 +++++++-- src/configs.typ | 2 +- src/core.typ | 29 ++++++++++++++++++++++++++--- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/examples/default.typ b/examples/default.typ index ffe88a655..768d75e0d 100644 --- a/examples/default.typ +++ b/examples/default.typ @@ -1,12 +1,12 @@ #import "../lib.typ": * #import themes.default: * -#import "@preview/hydra:0.5.1": hydra +#import "@preview/numbly:0.1.0": numbly #show: default-theme.with( aspect-ratio: "16-9", config-common( - slide-level: 2, + slide-level: 3, ), config-colors( primary: blue, @@ -19,6 +19,11 @@ ), ) +#set heading(numbering: numbly("{1}.", default: "1.1")) + +== Outline + +#outline(title: none, indent: 1em) = Title diff --git a/src/configs.typ b/src/configs.typ index 14a0c507c..95dce4743 100644 --- a/src/configs.typ +++ b/src/configs.typ @@ -115,7 +115,7 @@ /// - scale-list-items (none, float): Whether to scale the list items recursively. The default value is `none`. #let config-common( handout: false, - slide-level: 2, + slide-level: 3, slide-fn: slide, new-section-slide-fn: none, new-subsection-slide-fn: none, diff --git a/src/core.typ b/src/core.typ index cabcbf314..dae2386ae 100644 --- a/src/core.typ +++ b/src/core.typ @@ -1102,10 +1102,15 @@ let args = if child.has("gutter") { (gutter: child.gutter) } + let count = if child.has("count") { + child.count + } else { + 2 + } if repetitions <= index or not need-cover { - result.push(columns(child.count, ..args, cont)) + result.push(columns(count, ..args, cont)) } else { - cover-arr.push(columns(child.count, ..args, cont)) + cover-arr.push(columns(count, ..args, cont)) } repetitions = nextrepetitions } else if type(child) == content and child.func() == place { @@ -1366,7 +1371,25 @@ if self.at("headings", default: ()) != () { place(hide({ set heading(offset: 0) - self.at("headings", default: none).sum(default: none) + let headings = self.at("headings", default: ()).map(it => if it.has("label") { + if str(it.label) in ("touying:unoutlined", "touying:unbookmarked") { + let fields = it.fields() + let _ = fields.remove("label", default: none) + let _ = fields.remove("body", default: none) + if str(it.label) == "touying:unoutlined" { + fields.outlined = false + } + if str(it.label) == "touying:unbookmarked" { + fields.bookmarked = false + } + heading(..fields, it.body) + } else { + it + } + } else { + it + }) + headings.sum(default: none) })) } utils.call-or-display(self, self.at("slide-preamble", default: none)) From 7985de471e3f14d573a7a9a231654abdf2a95b1f Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Sat, 31 Aug 2024 04:41:30 +0800 Subject: [PATCH 24/43] theme(metropolis): fix some bugs and complete metropolis theme --- examples/default.typ | 10 +- examples/metropolis.typ | 72 ++++--- src/components.typ | 32 ++- src/configs.typ | 318 +++++++++++++++++++---------- src/core.typ | 160 +++++++++------ src/slides.typ | 1 - src/utils.typ | 131 ++++++------ themes/default.typ | 9 + themes/metropolis copy.typ | 187 +++++++++++++++++ themes/metropolis.typ | 399 +++++++++++++++++++++++-------------- themes/themes.typ | 2 +- 11 files changed, 895 insertions(+), 426 deletions(-) create mode 100644 themes/metropolis copy.typ diff --git a/examples/default.typ b/examples/default.typ index 768d75e0d..699066c19 100644 --- a/examples/default.typ +++ b/examples/default.typ @@ -21,7 +21,7 @@ #set heading(numbering: numbly("{1}.", default: "1.1")) -== Outline += Outline #outline(title: none, indent: 1em) @@ -31,7 +31,7 @@ *Recall* -#speaker-note[sdfsdf] +#speaker-note[Recall] #show: touying-set-config.with(config-methods(cover: utils.semi-transparent-cover)) @@ -47,11 +47,11 @@ $ x + y $ animation +#touying-recall() + #show: appendix = Appendix -Appendix - -#touying-recall() \ No newline at end of file +Appendix \ No newline at end of file diff --git a/examples/metropolis.typ b/examples/metropolis.typ index d13bfd890..de9a29783 100644 --- a/examples/metropolis.typ +++ b/examples/metropolis.typ @@ -1,41 +1,40 @@ #import "../lib.typ": * - -#let s = themes.metropolis.register(aspect-ratio: "16-9", footer: self => self.info.institution) -#let s = (s.methods.info)( - self: s, - title: [Title], - subtitle: [Subtitle], - author: [Authors], - date: datetime.today(), - institution: [Institution], +#import themes.metropolis: * + +#import "@preview/numbly:0.1.0": numbly + +#show: metropolis-theme.with( + aspect-ratio: "16-9", + footer: self => self.info.institution, + config-info( + title: [Title], + subtitle: [Subtitle], + author: [Authors], + date: datetime.today(), + institution: [Institution], + logo: emoji.city, + ), ) -#let (init, slides, touying-outline, alert) = utils.methods(s) -#show: init -#set text(font: "Fira Sans", weight: "light", size: 20pt) -#show math.equation: set text(font: "Fira Math") -#set strong(delta: 100) -#set par(justify: true) -#show strong: alert +#set heading(numbering: numbly("{1}.", default: "1.1")) + +#title-slide() -#let (slide, empty-slide, title-slide, new-section-slide, focus-slide) = utils.slides(s) -#show: slides += Outline + +#outline(title: none, indent: 1em, depth: 1) = First Section -#slide[ - A slide without a title but with some *important* information. -] +A slide without a title but with some *important* information. == A long long long long long long long long long long long long long long long long long long long long long long long long Title -#slide[ - A slide with equation: +A slide with equation: - $ x_(n+1) = (x_n + a/x_n) / 2 $ +$ x_(n+1) = (x_n + a/x_n) / 2 $ - #lorem(200) -] +#lorem(200) = Second Section @@ -45,20 +44,19 @@ == Simple Animation -#slide[ - A simple #pause dynamic slide with #alert[alert] +We can use `#pause` to #pause display something later. + +#meanwhile + +Meanwhile, #pause we can also use `#meanwhile` to display other content synchronously. - #pause - - text. +#speaker-note[ + + This is a speaker note. + + You won't see it unless you use `#let s = (s.math.show-notes-on-second-screen)(self: s, right)` ] -// appendix by freezing last-slide-number -#let s = (s.methods.appendix)(self: s) -#let (slide,) = utils.slides(s) +#show: appendix = Appendix -#slide[ - Appendix. -] \ No newline at end of file +Please pay attention to the current slide number. \ No newline at end of file diff --git a/src/components.typ b/src/components.typ index 9a3dce474..6014a877b 100644 --- a/src/components.typ +++ b/src/components.typ @@ -1 +1,31 @@ -#let cell = block.with(width: 100%, height: 100%, above: 0pt, below: 0pt, breakable: false) \ No newline at end of file +#import "utils.typ" + + +#let cell = block.with(width: 100%, height: 100%, above: 0pt, below: 0pt, breakable: false) + + +/// Touying progress bar. +/// +/// - `primary` is the color of the progress bar. +/// +/// - `secondary` is the color of the background of the progress bar. +/// +/// - `height` is the height of the progress bar, optional. Default is `2pt`. +#let progress-bar(height: 2pt, primary, secondary) = utils.touying-progress(ratio => { + grid( + columns: (ratio * 100%, 1fr), + rows: height, + cell(fill: primary), cell(fill: secondary), + ) +}) + + +/// Left and right. +/// +/// - `left` is the content of the left part. +/// +/// - `right` is the content of the right part. +#let left-and-right(left, right) = grid( + columns: (auto, 1fr, auto), + left, none, right, +) \ No newline at end of file diff --git a/src/configs.typ b/src/configs.typ index 95dce4743..fae2c4437 100644 --- a/src/configs.typ +++ b/src/configs.typ @@ -2,6 +2,18 @@ #import "utils.typ" #import "core.typ": touying-slide-wrapper, touying-slide, slide +#let _default = metadata((kind: "touying-default")) + +#let _get-dict-without-default(dict) = { + let new-dict = (:) + for (key, value) in dict.pairs() { + if value != _default { + new-dict.insert(key, value) + } + } + return new-dict +} + /// The private configurations of the theme. #let config-store(..args) = { assert(args.pos().len() == 0, message: "Unexpected positional arguments.") @@ -114,48 +126,48 @@ /// /// - scale-list-items (none, float): Whether to scale the list items recursively. The default value is `none`. #let config-common( - handout: false, - slide-level: 3, - slide-fn: slide, - new-section-slide-fn: none, - new-subsection-slide-fn: none, - new-subsubsection-slide-fn: none, - new-subsubsubsection-slide-fn: none, - datetime-format: auto, - appendix: false, - freeze-slide-counter: false, - zero-margin-header: true, - zero-margin-footer: true, - auto-offset-for-heading: true, - enable-pdfpc: true, - enable-mark-warning: true, - reset-page-counter-to-slide-counter: true, + handout: _default, + slide-level: _default, + slide-fn: _default, + new-section-slide-fn: _default, + new-subsection-slide-fn: _default, + new-subsubsection-slide-fn: _default, + new-subsubsubsection-slide-fn: _default, + datetime-format: _default, + appendix: _default, + freeze-slide-counter: _default, + zero-margin-header: _default, + zero-margin-footer: _default, + auto-offset-for-heading: _default, + enable-pdfpc: _default, + enable-mark-warning: _default, + reset-page-counter-to-slide-counter: _default, // some black magics for better slides writing, // maybe will be deprecated in the future - enable-frozen-states-and-counters: true, - frozen-states: (), - default-frozen-states: _default-frozen-states, - frozen-counters: (), - default-frozen-counters: _default-frozen-counters, - first-slide-number: 1, - preamble: none, - default-preamble: _default-preamble, - slide-preamble: none, - default-slide-preamble: none, - subslide-preamble: none, - default-subslide-preamble: none, - page-preamble: none, - default-page-preamble: _default-page-preamble, - show-notes-on-second-screen: none, - horizontal-line-to-pagebreak: true, - reset-footnote-number-per-slide: true, - nontight-list-enum-and-terms: true, - align-list-marker-with-baseline: false, - scale-list-items: none, + enable-frozen-states-and-counters: _default, + frozen-states: _default, + default-frozen-states: _default, + frozen-counters: _default, + default-frozen-counters: _default, + first-slide-number: _default, + preamble: _default, + default-preamble: _default, + slide-preamble: _default, + default-slide-preamble: _default, + subslide-preamble: _default, + default-subslide-preamble: _default, + page-preamble: _default, + default-page-preamble: _default, + show-notes-on-second-screen: _default, + horizontal-line-to-pagebreak: _default, + reset-footnote-number-per-slide: _default, + nontight-list-enum-and-terms: _default, + align-list-marker-with-baseline: _default, + scale-list-items: _default, ..args, ) = { assert(args.pos().len() == 0, message: "Unexpected positional arguments.") - return ( + return _get-dict-without-default(( handout: handout, slide-level: slide-level, slide-fn: slide-fn, @@ -190,7 +202,7 @@ nontight-list-enum-and-terms: nontight-list-enum-and-terms, align-list-marker-with-baseline: align-list-marker-with-baseline, scale-list-items: scale-list-items, - ) + args.named() + )) + args.named() } @@ -263,26 +275,26 @@ /// It should be `(self: none, width: 0pt, height: 0pt) => { .. }`. #let config-methods( // init - init: _default-init, - cover: _default-cover, + init: _default, + cover: _default, // dynamic control - uncover: utils.uncover, - only: utils.only, - alternatives-match: utils.alternatives-match, - alternatives: utils.alternatives, - alternatives-fn: utils.alternatives-fn, - alternatives-cases: utils.alternatives-cases, + uncover: _default, + only: _default, + alternatives-match: _default, + alternatives: _default, + alternatives-fn: _default, + alternatives-cases: _default, // alert interface - alert: _default-alert, + alert: _default, // show notes - show-notes: _default-show-notes, + show-notes: _default, // convert label to short heading - convert-label-to-short-heading: _default-convert-label-to-short-heading, + convert-label-to-short-heading: _default, ..args, ) = { assert(args.pos().len() == 0, message: "Unexpected positional arguments.") return ( - methods: ( + methods: _get-dict-without-default(( init: init, cover: cover, uncover: uncover, @@ -294,7 +306,7 @@ alert: alert, show-notes: show-notes, convert-label-to-short-heading: convert-label-to-short-heading, - ) + args.named(), + )) + args.named(), ) } @@ -332,14 +344,14 @@ /// /// - logo (content): The logo of the institution. #let config-info( - title: none, - short-title: auto, - subtitle: none, - short-subtitle: auto, - author: none, - date: none, - institution: none, - logo: none, + title: _default, + short-title: _default, + subtitle: _default, + short-subtitle: _default, + author: _default, + date: _default, + institution: _default, + logo: _default, ..args, ) = { assert(args.pos().len() == 0, message: "Unexpected positional arguments.") @@ -350,7 +362,7 @@ short-subtitle = subtitle } return ( - info: ( + info: _get-dict-without-default(( title: title, short-title: short-title, subtitle: subtitle, @@ -359,7 +371,7 @@ date: date, institution: institution, logo: logo, - ) + args.named(), + )) + args.named(), ) } @@ -381,39 +393,39 @@ /// There are four main colors in the theme: primary, secondary, tertiary, and neutral, /// and each of them has a light, lighter, lightest, dark, darker, and darkest version. #let config-colors( - neutral: rgb("#303030"), - neutral-light: rgb("#a0a0a0"), - neutral-lighter: rgb("#d0d0d0"), - neutral-lightest: rgb("#ffffff"), - neutral-dark: rgb("#202020"), - neutral-darker: rgb("#101010"), - neutral-darkest: rgb("#000000"), - primary: rgb("#303030"), - primary-light: rgb("#a0a0a0"), - primary-lighter: rgb("#d0d0d0"), - primary-lightest: rgb("#ffffff"), - primary-dark: rgb("#202020"), - primary-darker: rgb("#101010"), - primary-darkest: rgb("#000000"), - secondary: rgb("#303030"), - secondary-light: rgb("#a0a0a0"), - secondary-lighter: rgb("#d0d0d0"), - secondary-lightest: rgb("#ffffff"), - secondary-dark: rgb("#202020"), - secondary-darker: rgb("#101010"), - secondary-darkest: rgb("#000000"), - tertiary: rgb("#303030"), - tertiary-light: rgb("#a0a0a0"), - tertiary-lighter: rgb("#d0d0d0"), - tertiary-lightest: rgb("#ffffff"), - tertiary-dark: rgb("#202020"), - tertiary-darker: rgb("#101010"), - tertiary-darkest: rgb("#000000"), + neutral: _default, + neutral-light: _default, + neutral-lighter: _default, + neutral-lightest: _default, + neutral-dark: _default, + neutral-darker: _default, + neutral-darkest: _default, + primary: _default, + primary-light: _default, + primary-lighter: _default, + primary-lightest: _default, + primary-dark: _default, + primary-darker: _default, + primary-darkest: _default, + secondary: _default, + secondary-light: _default, + secondary-lighter: _default, + secondary-lightest: _default, + secondary-dark: _default, + secondary-darker: _default, + secondary-darkest: _default, + tertiary: _default, + tertiary-light: _default, + tertiary-lighter: _default, + tertiary-lightest: _default, + tertiary-dark: _default, + tertiary-darker: _default, + tertiary-darkest: _default, ..args, ) = { assert(args.pos().len() == 0, message: "Unexpected positional arguments.") return ( - colors: ( + colors: _get-dict-without-default(( neutral: neutral, neutral-light: neutral-light, neutral-lighter: neutral-lighter, @@ -442,7 +454,7 @@ tertiary-dark: tertiary-dark, tertiary-darker: tertiary-darker, tertiary-darkest: tertiary-darkest, - ) + args.named(), + )) + args.named(), ) } @@ -485,31 +497,131 @@ /// /// The values for left and right are mutually exclusive with the values for inside and outside. #let config-page( - paper: "presentation-16-9", - header: none, - footer: none, - fill: rgb("#ffffff"), - margin: (x: 3em, y: 2.8em), + paper: _default, + header: _default, + footer: _default, + fill: _default, + margin: _default, ..args, ) = { assert(args.pos().len() == 0, message: "Unexpected positional arguments.") return ( - page: ( + page: _get-dict-without-default(( paper: paper, header: header, footer: footer, fill: fill, margin: margin, - ) + args.named(), + )) + args.named(), ) } /// The default configurations #let default-config = utils.merge-dicts( - config-common(), - config-methods(), - config-info(), - config-colors(), - config-page(), + config-common( + handout: false, + slide-level: 3, + slide-fn: slide, + new-section-slide-fn: none, + new-subsection-slide-fn: none, + new-subsubsection-slide-fn: none, + new-subsubsubsection-slide-fn: none, + datetime-format: auto, + appendix: false, + freeze-slide-counter: false, + zero-margin-header: true, + zero-margin-footer: true, + auto-offset-for-heading: true, + enable-pdfpc: true, + enable-mark-warning: true, + reset-page-counter-to-slide-counter: true, + // some black magics for better slides writing, + // maybe will be deprecated in the future + enable-frozen-states-and-counters: true, + frozen-states: (), + default-frozen-states: _default-frozen-states, + frozen-counters: (), + default-frozen-counters: _default-frozen-counters, + first-slide-number: 1, + preamble: none, + default-preamble: _default-preamble, + slide-preamble: none, + default-slide-preamble: none, + subslide-preamble: none, + default-subslide-preamble: none, + page-preamble: none, + default-page-preamble: _default-page-preamble, + show-notes-on-second-screen: none, + horizontal-line-to-pagebreak: true, + reset-footnote-number-per-slide: true, + nontight-list-enum-and-terms: true, + align-list-marker-with-baseline: false, + scale-list-items: none, + ), + config-methods( + // init + init: _default-init, + cover: _default-cover, + // dynamic control + uncover: utils.uncover, + only: utils.only, + alternatives-match: utils.alternatives-match, + alternatives: utils.alternatives, + alternatives-fn: utils.alternatives-fn, + alternatives-cases: utils.alternatives-cases, + // alert interface + alert: _default-alert, + // show notes + show-notes: _default-show-notes, + // convert label to short heading + convert-label-to-short-heading: _default-convert-label-to-short-heading, + ), + config-info( + title: none, + short-title: auto, + subtitle: none, + short-subtitle: auto, + author: none, + date: none, + institution: none, + logo: none, + ), + config-colors( + neutral: rgb("#303030"), + neutral-light: rgb("#a0a0a0"), + neutral-lighter: rgb("#d0d0d0"), + neutral-lightest: rgb("#ffffff"), + neutral-dark: rgb("#202020"), + neutral-darker: rgb("#101010"), + neutral-darkest: rgb("#000000"), + primary: rgb("#303030"), + primary-light: rgb("#a0a0a0"), + primary-lighter: rgb("#d0d0d0"), + primary-lightest: rgb("#ffffff"), + primary-dark: rgb("#202020"), + primary-darker: rgb("#101010"), + primary-darkest: rgb("#000000"), + secondary: rgb("#303030"), + secondary-light: rgb("#a0a0a0"), + secondary-lighter: rgb("#d0d0d0"), + secondary-lightest: rgb("#ffffff"), + secondary-dark: rgb("#202020"), + secondary-darker: rgb("#101010"), + secondary-darkest: rgb("#000000"), + tertiary: rgb("#303030"), + tertiary-light: rgb("#a0a0a0"), + tertiary-lighter: rgb("#d0d0d0"), + tertiary-lightest: rgb("#ffffff"), + tertiary-dark: rgb("#202020"), + tertiary-darker: rgb("#101010"), + tertiary-darkest: rgb("#000000"), + ), + config-page( + paper: "presentation-16-9", + header: none, + footer: none, + fill: rgb("#ffffff"), + margin: (x: 3em, y: 2.8em), + ), ) \ No newline at end of file diff --git a/src/core.typ b/src/core.typ index dae2386ae..d18bf96e5 100644 --- a/src/core.typ +++ b/src/core.typ @@ -86,6 +86,15 @@ } else { (body,) } + // convert all sequence to array recursively, and then flatten the array + let sequence-to-array(it) = { + if utils.is-sequence(it) { + it.children.map(sequence-to-array) + } else { + it + } + } + children = children.map(sequence-to-array).flatten() let get-last-heading-depth(current-headings) = { if current-headings != () { current-headings.at(-1).depth @@ -124,6 +133,8 @@ let cont = none // is new start let is-new-start = new-start + // is root + let is-root = is-first-slide // start part let start-part = () // result @@ -221,40 +232,41 @@ current-headings.push(child) new-start = true - if child.depth == 1 and new-section-slide-fn != none { - (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings, is-first-slide: is-first-slide), - new-section-slide-fn, - child.body, - recaller-map, - ) - result.push(cont) - } else if child.depth == 2 and new-subsection-slide-fn != none { - (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings, is-first-slide: is-first-slide), - new-subsection-slide-fn, - child.body, - recaller-map, - ) - result.push(cont) - } else if child.depth == 3 and new-subsubsection-slide-fn != none { - (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings, is-first-slide: is-first-slide), - new-subsubsection-slide-fn, - child.body, - recaller-map, - ) - result.push(cont) - } else if child.depth == 4 and new-subsubsubsection-slide-fn != none { - (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( - self + (headings: current-headings, is-first-slide: is-first-slide), - new-subsubsubsection-slide-fn, - child.body, - recaller-map, - ) - result.push(cont) + if not child.has("label") or str(child.label) != "touying:hidden" { + if child.depth == 1 and new-section-slide-fn != none { + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), + new-section-slide-fn, + child.body, + recaller-map, + ) + result.push(cont) + } else if child.depth == 2 and new-subsection-slide-fn != none { + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), + new-subsection-slide-fn, + child.body, + recaller-map, + ) + result.push(cont) + } else if child.depth == 3 and new-subsubsection-slide-fn != none { + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), + new-subsubsection-slide-fn, + child.body, + recaller-map, + ) + result.push(cont) + } else if child.depth == 4 and new-subsubsubsection-slide-fn != none { + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), + new-subsubsubsection-slide-fn, + child.body, + recaller-map, + ) + result.push(cont) + } } - } else if utils.is-kind(child, "touying-set-config") { current-slide = utils.trim(current-slide) if current-slide != () or current-headings != () { @@ -275,18 +287,30 @@ child.value.body, ), ) - } else { - let child = if utils.is-sequence(child) { - // Split the content into slides recursively - let (start-part, cont) = split-content-into-slides( - self: self, - recaller-map: recaller-map, - new-start: false, - child, + } else if is-root and utils.is-styled(child) { + current-slide = utils.trim(current-slide) + if current-slide != () or current-headings != () { + (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( + self + (headings: current-headings, is-first-slide: is-first-slide), + slide-fn, + current-slide.sum(default: none), + recaller-map, ) - start-part - _delayed-wrapper(cont) - } else if utils.is-styled(child) { + result.push(cont) + } + result.push( + utils.reconstruct-styled( + child, + split-content-into-slides( + self: self, + recaller-map: recaller-map, + new-start: true, + child.child, + ), + ), + ) + } else { + let child = if utils.is-styled(child) { // Split the content into slides recursively for styled content let (start-part, cont) = split-content-into-slides( self: self, @@ -1369,28 +1393,38 @@ [#metadata((kind: "touying-new-slide")) ] // add headings for the first subslide if self.at("headings", default: ()) != () { - place(hide({ - set heading(offset: 0) - let headings = self.at("headings", default: ()).map(it => if it.has("label") { - if str(it.label) in ("touying:unoutlined", "touying:unbookmarked") { - let fields = it.fields() - let _ = fields.remove("label", default: none) - let _ = fields.remove("body", default: none) - if str(it.label) == "touying:unoutlined" { - fields.outlined = false - } - if str(it.label) == "touying:unbookmarked" { - fields.bookmarked = false + place( + hide({ + set heading(offset: 0) + let headings = self.at("headings", default: ()).map(it => if it.has("label") { + if str(it.label) in ("touying:hidden", "touying:unnumbered", "touying:unoutlined", "touying:unbookmarked") { + let fields = it.fields() + let _ = fields.remove("label", default: none) + let _ = fields.remove("body", default: none) + if str(it.label) == "touying:hidden" { + fields.numbering = none + fields.outlined = false + fields.bookmarked = false + } + if str(it.label) == "touying:unnumbered" { + fields.numbering = none + } + if str(it.label) == "touying:unoutlined" { + fields.outlined = false + } + if str(it.label) == "touying:unbookmarked" { + fields.bookmarked = false + } + heading(..fields, it.body) + } else { + it } - heading(..fields, it.body) } else { it - } - } else { - it - }) - headings.sum(default: none) - })) + }) + headings.sum(default: none) + }), + ) } utils.call-or-display(self, self.at("slide-preamble", default: none)) utils.call-or-display(self, self.at("default-slide-preamble", default: none)) diff --git a/src/slides.typ b/src/slides.typ index dd7e3fd7b..b3e088d8e 100644 --- a/src/slides.typ +++ b/src/slides.typ @@ -20,7 +20,6 @@ /// `body` is the contents of the slides. #let touying-slides(..args, body) = { // get the default config - assert(args.named().len() == 0, message: "unexpected named arguments:" + repr(args.named().keys())) let args = (configs.default-config,) + args.pos() let self = utils.merge-dicts(..args) diff --git a/src/utils.typ b/src/utils.typ index b7e231606..6c7d1be77 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -33,9 +33,7 @@ // ------------------------------------- #let slide-counter = counter("touying-slide-counter") #let last-slide-counter = counter("touying-last-slide-counter") -#let last-slide-number = context { - last-slide-counter.final().first() -} +#let last-slide-number = context last-slide-counter.final().first() /// Get the progress of the current slide. /// @@ -309,18 +307,22 @@ /// /// - `level` is the level of the heading. If `level` is `auto`, it will return the last heading on or before the current page. If `level` is a number, it will return the last heading on or before the current page with the same level. /// +/// - `numbered` is a boolean value to indicate whether to display the numbering of the heading. Default is `true`. +/// /// - `hierachical` is a boolean value to indicate whether to return the heading hierachically. If `hierachical` is `true`, it will return the last heading according to the hierachical structure. If `hierachical` is `false`, it will return the last heading on or before the current page with the same level. /// /// - `depth` is the maximum depth of the heading to search. Usually, it should be set as slide-level. /// +/// - `setting` is the setting of the heading. Default is `body => body`. +/// /// - `sty` is the style of the heading. If `sty` is a function, it will use the function to style the heading. For example, `sty: current-heading => current-heading.body`. -#let display-current-heading(self: none, level: auto, hierachical: true, depth: 9999, ..sty) = ( +#let display-current-heading(self: none, level: auto, numbered: true, hierachical: true, depth: 9999, setting: body => body, ..sty) = ( context { let sty = if sty.pos().len() > 1 { sty.pos().at(0) } else { current-heading => { - if current-heading.numbering != none { + if numbered and current-heading.numbering != none { numbering(current-heading.numbering, ..counter(heading).at(current-heading.location())) + h(.3em) } current-heading.body @@ -328,7 +330,7 @@ } let current-heading = current-heading(level: level, hierachical: hierachical, depth: depth) if current-heading != none { - sty(current-heading) + setting(sty(current-heading)) } } ) @@ -343,7 +345,7 @@ /// - `depth` is the maximum depth of the heading to search. Usually, it should be set as slide-level. /// /// - `sty` is the style of the heading. If `sty` is a function, it will use the function to style the heading. For example, `sty: current-heading => current-heading.body`. -#let display-current-short-heading(self: none, level: auto, hierachical: true, depth: 9999, ..sty) = ( +#let display-current-short-heading(self: none, level: auto, hierachical: true, depth: 9999, setting: body => body, ..sty) = ( context { let sty = if sty.pos().len() > 1 { sty.pos().at(0) @@ -354,7 +356,7 @@ } let current-heading = current-heading(level: level, hierachical: hierachical, depth: depth) if current-heading != none { - sty(current-heading) + setting(sty(current-heading)) } } ) @@ -468,12 +470,12 @@ measure(v(to-convert)).height } -#let _limit-content-width(width: none, body, container-size, styles) = { +#let _limit-content-width(width: none, body, container-size) = { let mutable-width = width if width == none { - mutable-width = calc.min(container-size.width, measure(body, styles).width) + mutable-width = calc.min(container-size.width, measure(body).width) } else { - mutable-width = _size-to-pt(width, styles, container-size.width) + mutable-width = _size-to-pt(width, container-size.width) } box(width: mutable-width, body) } @@ -527,59 +529,56 @@ let available-height = after-pos.y - before-pos.y - style(styles => { - layout(container-size => { - // Helper function to more easily grab absolute units - let get-pts(body, w-or-h) = { - let dim = if w-or-h == "w" { - container-size.width - } else { - container-size.height - } - _size-to-pt(body, styles, dim) + layout(container-size => { + // Helper function to more easily grab absolute units + let get-pts(body, w-or-h) = { + let dim = if w-or-h == "w" { + container-size.width + } else { + container-size.height } + _size-to-pt(body, dim) + } - // Provide a sensible initial width, which will define initial scale parameters. - // Note this is different from the post-scale width, which is a limiting factor - // on the allowable scaling ratio - let boxed-content = _limit-content-width( - width: prescale-width, - body, - container-size, - styles, - ) + // Provide a sensible initial width, which will define initial scale parameters. + // Note this is different from the post-scale width, which is a limiting factor + // on the allowable scaling ratio + let boxed-content = _limit-content-width( + width: prescale-width, + body, + container-size, + ) - // post-scaling width - let mutable-width = width - if width == none { - mutable-width = container-size.width - } - mutable-width = get-pts(mutable-width, "w") + // post-scaling width + let mutable-width = width + if width == none { + mutable-width = container-size.width + } + mutable-width = get-pts(mutable-width, "w") - let size = measure(boxed-content, styles) - if size.height == 0pt or size.width == 0pt { - return body - } - let h-ratio = available-height / size.height - let w-ratio = mutable-width / size.width - let ratio = calc.min(h-ratio, w-ratio) * 100% - - if ((shrink and (ratio < 100%)) or (grow and (ratio > 100%))) { - let new-width = size.width * ratio - v(-available-height) - // If not boxed, the content can overflow to the next page even though it will - // fit. This is because scale doesn't update the layout information. - // Boxing in a container without clipping will inform typst that content - // will indeed fit in the remaining space - box( - width: new-width, - height: available-height, - scale(x: ratio, y: ratio, origin: top + left, boxed-content), - ) - } else { - body - } - }) + let size = measure(boxed-content) + if size.height == 0pt or size.width == 0pt { + return body + } + let h-ratio = available-height / size.height + let w-ratio = mutable-width / size.width + let ratio = calc.min(h-ratio, w-ratio) * 100% + + if ((shrink and (ratio < 100%)) or (grow and (ratio > 100%))) { + let new-width = size.width * ratio + v(-available-height) + // If not boxed, the content can overflow to the next page even though it will + // fit. This is because scale doesn't update the layout information. + // Boxing in a container without clipping will inform typst that content + // will indeed fit in the remaining space + box( + width: new-width, + height: available-height, + scale(x: ratio, y: ratio, origin: top + left, boxed-content), + ) + } else { + body + } }) } } @@ -708,6 +707,18 @@ #let alert-with-primary-color(self: none, it) = text(fill: self.colors.primary, it) +/// Alert content with a sencondary color. +/// +/// Example: `config-methods(alert: utils.alert-with-sencondary-color)` +#let alert-with-sencondary-color(self: none, it) = text(fill: self.colors.sencondary, it) + + +/// Alert content with a tertiary color. +/// +/// Example: `config-methods(alert: utils.alert-with-tertiary-color)` +#let alert-with-tertiary-color(self: none, it) = text(fill: self.colors.tertiary, it) + + // Code: check visible subslides and dynamic control // Attribution: This file is based on the code from https://github.com/andreasKroepelin/polylux/blob/main/logic.typ // Author: Andreas Kröpelin diff --git a/themes/default.typ b/themes/default.typ index bfeeb03fa..51ee068fa 100644 --- a/themes/default.typ +++ b/themes/default.typ @@ -34,6 +34,15 @@ }) +/// Touying metropolis theme. +/// +/// Example: +/// +/// ```typst +/// #show: default-theme.with(aspect-ratio: "16-9", config-colors(primary: blue))` +/// ``` +/// +/// - `aspect-ratio` is the aspect ratio of the slides. Default is `16-9`. #let default-theme( aspect-ratio: "16-9", ..args, diff --git a/themes/metropolis copy.typ b/themes/metropolis copy.typ new file mode 100644 index 000000000..301b3223d --- /dev/null +++ b/themes/metropolis copy.typ @@ -0,0 +1,187 @@ +// This theme is inspired by https://github.com/matze/mtheme +// The origin code was written by https://github.com/Enivex + +// Consider using: +// #set text(font: "Fira Sans", weight: "light", size: 20pt) +// #show math.equation: set text(font: "Fira Math") +// #set strong(delta: 100) +// #set par(justify: true) + +#import "../slide.typ": s +#import "../src/utils.typ" +#import "../src/states.typ" +#import "../src/components.typ" + +#let _saved-align = align + +#let slide( + self: none, + title: auto, + footer: auto, + align: horizon, + ..args, +) = { + self.page-args += ( + fill: self.colors.neutral-lightest, + ) + if title != auto { + self.m-title = title + } + if footer != auto { + self.m-footer = footer + } + (self.methods.touying-slide)( + ..args.named(), + self: self, + title: if title == auto { self.m-title = title } else { title }, + setting: body => { + show: _saved-align.with(align) + set text(fill: self.colors.neutral-dark) + show: args.named().at("setting", default: body => body) + body + }, + ..args.pos(), + ) +} + +#let title-slide( + self: none, + extra: none, + ..args, +) = { + self = utils.empty-page(self) + let info = self.info + args.named() + let content = { + set text(fill: self.colors.neutral-dark) + set align(horizon) + block(width: 100%, inset: 2em, { + text(size: 1.3em, text(weight: "medium", info.title)) + if info.subtitle != none { + linebreak() + text(size: 0.9em, info.subtitle) + } + line(length: 100%, stroke: .05em + self.colors.secondary-light) + set text(size: .8em) + if info.author != none { + block(spacing: 1em, info.author) + } + if info.date != none { + block(spacing: 1em, utils.info-date(self)) + } + set text(size: .8em) + if info.institution != none { + block(spacing: 1em, info.institution) + } + if extra != none { + block(spacing: 1em, extra) + } + }) + } + (self.methods.touying-slide)(self: self, repeat: none, content) +} + +#let new-section-slide(self: none, short-title: auto, title) = { + self = utils.empty-page(self) + let content = { + set align(horizon) + show: pad.with(20%) + set text(size: 1.5em) + states.current-section-with-numbering(self) + block(height: 2pt, width: 100%, spacing: 0pt, utils.call-or-display(self, self.m-progress-bar)) + } + (self.methods.touying-slide)(self: self, repeat: none, section: (title: title, short-title: short-title), content) +} + +#let focus-slide(self: none, body) = { + self = utils.empty-page(self) + self.page-args += ( + fill: self.colors.primary-dark, + margin: 2em, + ) + set text(fill: self.colors.neutral-lightest, size: 1.5em) + (self.methods.touying-slide)(self: self, repeat: none, align(horizon + center, body)) +} + +#let slides(self: none, title-slide: true, outline-slide: true, outline-title: [Table of contents], slide-level: 1, ..args) = { + if title-slide { + (self.methods.title-slide)(self: self) + } + if outline-slide { + (self.methods.slide)(self: self, title: outline-title, (self.methods.touying-outline)()) + } + (self.methods.touying-slides)(self: self, slide-level: slide-level, ..args) +} + +#let register( + self: s, + aspect-ratio: "16-9", + header: states.current-section-with-numbering, + footer: [], + footer-right: context states.slide-counter.display() + " / " + states.last-slide-number, + footer-progress: true, + ..args, +) = { + // color theme + self = (self.methods.colors)( + self: self, + neutral-lightest: rgb("#fafafa"), + neutral-dark: rgb("#23373b"), + primary-dark: rgb("#23373b"), + secondary-light: rgb("#eb811b"), + secondary-lighter: rgb("#d6c6b7"), + ) + // save the variables for later use + self.m-progress-bar = self => states.touying-progress(ratio => { + grid( + columns: (ratio * 100%, 1fr), + components.cell(fill: self.colors.secondary-light), + components.cell(fill: self.colors.secondary-lighter) + ) + }) + self.m-footer-progress = footer-progress + self.m-title = header + self.m-footer = footer + self.m-footer-right = footer-right + // set page + let header(self) = { + set align(top) + if self.m-title != none { + show: components.cell.with(fill: self.colors.primary-dark, inset: 1em) + set align(horizon) + set text(fill: self.colors.neutral-lightest, size: 1.2em) + utils.fit-to-width(grow: false, 100%, text(weight: "medium", utils.call-or-display(self, self.m-title))) + } else { [] } + } + let footer(self) = { + set align(bottom) + set text(size: 0.8em) + pad(.5em, { + text(fill: self.colors.neutral-dark.lighten(40%), utils.call-or-display(self, self.m-footer)) + h(1fr) + text(fill: self.colors.neutral-dark, utils.call-or-display(self, self.m-footer-right)) + }) + if self.m-footer-progress { + place(bottom, block(height: 2pt, width: 100%, spacing: 0pt, utils.call-or-display(self, self.m-progress-bar))) + } + } + self.page-args += ( + paper: "presentation-" + aspect-ratio, + header: header, + footer: footer, + header-ascent: 30%, + footer-descent: 30%, + margin: (top: 3em, bottom: 1.5em, x: 2em), + ) + // register methods + self.methods.slide = slide + self.methods.title-slide = title-slide + self.methods.new-section-slide = new-section-slide + self.methods.touying-new-section-slide = new-section-slide + self.methods.focus-slide = focus-slide + self.methods.slides = slides + self.methods.touying-outline = (self: none, enum-args: (:), ..args) => { + states.touying-outline(self: self, enum-args: (tight: false,) + enum-args, ..args) + } + self.methods.alert = (self: none, it) => text(fill: self.colors.secondary-light, it) + self +} diff --git a/themes/metropolis.typ b/themes/metropolis.typ index 68d628704..219ebbf22 100644 --- a/themes/metropolis.typ +++ b/themes/metropolis.typ @@ -1,187 +1,276 @@ // This theme is inspired by https://github.com/matze/mtheme // The origin code was written by https://github.com/Enivex -// Consider using: -// #set text(font: "Fira Sans", weight: "light", size: 20pt) -// #show math.equation: set text(font: "Fira Math") -// #set strong(delta: 100) -// #set par(justify: true) +#import "../src/exports.typ": * -#import "../slide.typ": s -#import "../src/utils.typ" -#import "../src/states.typ" -#import "../src/components.typ" - -#let _saved-align = align +#let _typst-builtin-align = align +/// Default slide function for the presentation. +/// +/// - `config` is the configuration of the slide. You can use `config-xxx` to set the configuration of the slide. For more several configurations, you can use `utils.merge-dicts` to merge them. +/// +/// - `repeat` is the number of subslides. Default is `auto`,which means touying will automatically calculate the number of subslides. +/// +/// The `repeat` argument is necessary when you use `#slide(repeat: 3, self => [ .. ])` style code to create a slide. The callback-style `uncover` and `only` cannot be detected by touying automatically. +/// +/// - `setting` is the setting of the slide. You can use it to add some set/show rules for the slide. +/// +/// - `composer` is the composer of the slide. You can use it to set the layout of the slide. +/// +/// For example, `#slide(composer: (1fr, 2fr, 1fr))[A][B][C]` to split the slide into three parts. The first and the last parts will take 1/4 of the slide, and the second part will take 1/2 of the slide. +/// +/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `utils.side-by-side` function. +/// +/// The `utils.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. +/// +/// For example, `#slide(composer: 2)[A][B][#grid.cell(colspan: 2)[Footer]] will make the `Footer` cell take 2 columns. +/// +/// If you want to customize the composer, you can pass a function to the `composer` argument. The function should receive the contents of the slide and return the content of the slide, like `#slide(composer: grid.with(columns: 2))[A][B]`. +/// +/// - `..bodies` is the contents of the slide. You can call the `slide` function with syntax like `#slide[A][B][C]` to create a slide. #let slide( - self: none, - title: auto, - footer: auto, - align: horizon, - ..args, -) = { - self.page-args += ( - fill: self.colors.neutral-lightest, - ) - if title != auto { - self.m-title = title + title: none, + align: auto, + config: (:), + repeat: auto, + setting: body => body, + composer: auto, + ..bodies, +) = touying-slide-wrapper(self => { + if align != auto { + self.store.align = align + } + // restore typst builtin align function + let align = _typst-builtin-align + let header(self) = { + set align(top) + show: components.cell.with(fill: self.colors.secondary, inset: 1em) + set align(horizon) + set text(fill: self.colors.neutral-lightest, weight: "medium", size: 1.2em) + components.left-and-right( + { + if title != none { + utils.fit-to-width.with(grow: false, 100%, title) + } else { + utils.call-or-display(self, self.store.header) + } + }, + utils.call-or-display(self, self.store.header-right), + ) } - if footer != auto { - self.m-footer = footer + let footer(self) = { + set align(bottom) + set text(size: 0.8em) + pad( + .5em, + components.left-and-right( + text(fill: self.colors.neutral-darkest.lighten(40%), utils.call-or-display(self, self.store.footer)), + text(fill: self.colors.neutral-darkest, utils.call-or-display(self, self.store.footer-right)), + ), + ) + if self.store.footer-progress { + place(bottom, components.progress-bar(height: 2pt, self.colors.primary, self.colors.primary-light)) + } } - (self.methods.touying-slide)( - ..args.named(), - self: self, - title: if title == auto { self.m-title = title } else { title }, - setting: body => { - show: _saved-align.with(align) - set text(fill: self.colors.neutral-dark) - show: args.named().at("setting", default: body => body) - body - }, - ..args.pos(), + let self = utils.merge-dicts( + self, + config-page( + fill: self.colors.neutral-lightest, + header: header, + footer: footer, + ), ) -} + let new-setting = body => { + show: align.with(self.store.align) + set text(fill: self.colors.neutral-darkest) + show: setting + body + } + touying-slide(self: self, config: config, repeat: repeat, setting: new-setting, composer: composer, ..bodies) +}) + +/// Title slide for the presentation. You should update the information in the `config-info` function. You can also pass the information directly to the `title-slide` function. +/// +/// Example: +/// +/// ```typst +/// #show: metropolis-theme.with( +/// config-info( +/// title: [Title], +/// logo: emoji.city, +/// ), +/// ) +/// +/// #title-slide(subtitle: [Subtitle], extra: [Extra information]) +/// ``` +/// +/// - `extra` is the extra information you want to display on the title slide. #let title-slide( - self: none, extra: none, ..args, -) = { - self = utils.empty-page(self) +) = touying-slide-wrapper(self => { let info = self.info + args.named() - let content = { - set text(fill: self.colors.neutral-dark) + let body = { + set text(fill: self.colors.neutral-darkest) set align(horizon) - block(width: 100%, inset: 2em, { - text(size: 1.3em, text(weight: "medium", info.title)) - if info.subtitle != none { - linebreak() - text(size: 0.9em, info.subtitle) - } - line(length: 100%, stroke: .05em + self.colors.secondary-light) - set text(size: .8em) - if info.author != none { - block(spacing: 1em, info.author) - } - if info.date != none { - block(spacing: 1em, utils.info-date(self)) - } - set text(size: .8em) - if info.institution != none { - block(spacing: 1em, info.institution) - } - if extra != none { - block(spacing: 1em, extra) - } - }) + block( + width: 100%, + inset: 2em, + { + components.left-and-right( + { + text(size: 1.3em, text(weight: "medium", info.title)) + if info.subtitle != none { + linebreak() + text(size: 0.9em, info.subtitle) + } + }, + text(2em, utils.call-or-display(self, info.logo)), + ) + line(length: 100%, stroke: .05em + self.colors.primary-light) + set text(size: .8em) + if info.author != none { + block(spacing: 1em, info.author) + } + if info.date != none { + block(spacing: 1em, utils.display-info-date(self)) + } + set text(size: .8em) + if info.institution != none { + block(spacing: 1em, info.institution) + } + if extra != none { + block(spacing: 1em, extra) + } + }, + ) } - (self.methods.touying-slide)(self: self, repeat: none, content) -} + self = utils.merge-dicts( + self, + config-common(freeze-slide-counter: true), + config-page(fill: self.colors.neutral-lightest), + ) + touying-slide(self: self, body) +}) + -#let new-section-slide(self: none, short-title: auto, title) = { - self = utils.empty-page(self) - let content = { +/// New section slide for the presentation. You can update it by updating the `new-section-slide-fn` argument for `config-common` function. +/// +/// Example: `config-common(new-section-slide-fn: new-section-slide.with(numbered: false))` +/// +/// - `level` is the level of the heading. +/// +/// - `numbered` is whether the heading is numbered. +/// +/// - `title` is the title of the section. It will be pass by touying automatically. +#let new-section-slide(level: 1, numbered: true, title) = touying-slide-wrapper(self => { + let body = { set align(horizon) show: pad.with(20%) set text(size: 1.5em) - states.current-section-with-numbering(self) - block(height: 2pt, width: 100%, spacing: 0pt, utils.call-or-display(self, self.m-progress-bar)) + utils.display-current-heading(level: level, numbered: numbered) + block( + height: 2pt, + width: 100%, + spacing: 0pt, + components.progress-bar(height: 2pt, self.colors.primary, self.colors.primary-light), + ) } - (self.methods.touying-slide)(self: self, repeat: none, section: (title: title, short-title: short-title), content) -} + self = utils.merge-dicts( + self, + config-page(fill: self.colors.neutral-lightest), + ) + touying-slide(self: self, body) +}) + -#let focus-slide(self: none, body) = { - self = utils.empty-page(self) - self.page-args += ( - fill: self.colors.primary-dark, - margin: 2em, +/// Focus on some content. +/// +/// Example: `#focus-slide[Wake up!]` +/// +/// - `align` is the alignment of the content. Default is `horizon + center`. +#let focus-slide(align: horizon + center, body) = touying-slide-wrapper(self => { + let _align = align + let align = _typst-builtin-align + self = utils.merge-dicts( + self, + config-common(freeze-slide-counter: true), + config-page(fill: self.colors.neutral-dark, margin: 2em), ) set text(fill: self.colors.neutral-lightest, size: 1.5em) - (self.methods.touying-slide)(self: self, repeat: none, align(horizon + center, body)) -} + touying-slide(self: self, align(_align, body)) +}) + + +/// Alert some content. +#let alert(body) = touying-fn-wrapper(utils.alert-with-primary-color, body) -#let slides(self: none, title-slide: true, outline-slide: true, outline-title: [Table of contents], slide-level: 1, ..args) = { - if title-slide { - (self.methods.title-slide)(self: self) - } - if outline-slide { - (self.methods.slide)(self: self, title: outline-title, (self.methods.touying-outline)()) - } - (self.methods.touying-slides)(self: self, slide-level: slide-level, ..args) -} -#let register( - self: s, +/// Touying metropolis theme. +/// +/// Example: +/// +/// ```typst +/// #show: default-theme.with(aspect-ratio: "16-9", config-colors(primary: blue))` +/// ``` +/// +/// Consider using: +/// +/// ```typst +/// #set text(font: "Fira Sans", weight: "light", size: 20pt)` +/// #show math.equation: set text(font: "Fira Math") +/// #set strong(delta: 100) +/// #set par(justify: true) +/// ``` +/// +/// - `aspect-ratio` is the aspect ratio of the slides. Default is `16-9`. +#let metropolis-theme( aspect-ratio: "16-9", - header: states.current-section-with-numbering, - footer: [], - footer-right: context { states.slide-counter.display() + " / " + states.last-slide-number }, + align: horizon, + header: utils.display-current-heading.with(setting: utils.fit-to-width.with(grow: false, 100%)), + header-right: self => self.info.logo, + footer: none, + footer-right: context utils.slide-counter.display() + " / " + utils.last-slide-number, footer-progress: true, ..args, + body, ) = { - // color theme - self = (self.methods.colors)( - self: self, - neutral-lightest: rgb("#fafafa"), - neutral-dark: rgb("#23373b"), - primary-dark: rgb("#23373b"), - secondary-light: rgb("#eb811b"), - secondary-lighter: rgb("#d6c6b7"), - ) - // save the variables for later use - self.m-progress-bar = self => states.touying-progress(ratio => { - grid( - columns: (ratio * 100%, 1fr), - components.cell(fill: self.colors.secondary-light), - components.cell(fill: self.colors.secondary-lighter) - ) - }) - self.m-footer-progress = footer-progress - self.m-title = header - self.m-footer = footer - self.m-footer-right = footer-right - // set page - let header(self) = { - set align(top) - if self.m-title != none { - show: components.cell.with(fill: self.colors.primary-dark, inset: 1em) - set align(horizon) - set text(fill: self.colors.neutral-lightest, size: 1.2em) - utils.fit-to-width(grow: false, 100%, text(weight: "medium", utils.call-or-display(self, self.m-title))) - } else { [] } - } - let footer(self) = { - set align(bottom) - set text(size: 0.8em) - pad(.5em, { - text(fill: self.colors.neutral-dark.lighten(40%), utils.call-or-display(self, self.m-footer)) - h(1fr) - text(fill: self.colors.neutral-dark, utils.call-or-display(self, self.m-footer-right)) - }) - if self.m-footer-progress { - place(bottom, block(height: 2pt, width: 100%, spacing: 0pt, utils.call-or-display(self, self.m-progress-bar))) - } - } - self.page-args += ( - paper: "presentation-" + aspect-ratio, - header: header, - footer: footer, - header-ascent: 30%, - footer-descent: 30%, - margin: (top: 3em, bottom: 1.5em, x: 2em), + set text(size: 20pt) + + show: touying-slides.with( + config-page( + paper: "presentation-" + aspect-ratio, + header-ascent: 30%, + footer-descent: 30%, + margin: (top: 3em, bottom: 1.5em, x: 2em), + ), + config-common( + slide-fn: slide, + new-section-slide-fn: new-section-slide, + ), + config-methods( + alert: utils.alert-with-primary-color, + ), + config-colors( + primary: rgb("#eb811b"), + primary-light: rgb("#d6c6b7"), + secondary: rgb("#23373b"), + neutral-lightest: rgb("#fafafa"), + neutral-dark: rgb("#23373b"), + neutral-darkest: rgb("#23373b"), + ), + // save the variables for later use + config-store( + align: align, + header: header, + header-right: header-right, + footer: footer, + footer-right: footer-right, + footer-progress: footer-progress, + ), + ..args, ) - // register methods - self.methods.slide = slide - self.methods.title-slide = title-slide - self.methods.new-section-slide = new-section-slide - self.methods.touying-new-section-slide = new-section-slide - self.methods.focus-slide = focus-slide - self.methods.slides = slides - self.methods.touying-outline = (self: none, enum-args: (:), ..args) => { - states.touying-outline(self: self, enum-args: (tight: false,) + enum-args, ..args) - } - self.methods.alert = (self: none, it) => text(fill: self.colors.secondary-light, it) - self + + body } diff --git a/themes/themes.typ b/themes/themes.typ index 44f65e362..2d3dcf548 100644 --- a/themes/themes.typ +++ b/themes/themes.typ @@ -1,6 +1,6 @@ #import "default.typ" +#import "metropolis.typ" // #import "simple.typ" -// #import "metropolis.typ" // #import "dewdrop.typ" // #import "university.typ" // #import "aqua.typ" \ No newline at end of file From dadaf1058e51a79338d2bf9b4ca82359bee5ba91 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Sat, 31 Aug 2024 04:42:53 +0800 Subject: [PATCH 25/43] dev: clean code --- themes/metropolis copy.typ | 187 ------------------------------------- 1 file changed, 187 deletions(-) delete mode 100644 themes/metropolis copy.typ diff --git a/themes/metropolis copy.typ b/themes/metropolis copy.typ deleted file mode 100644 index 301b3223d..000000000 --- a/themes/metropolis copy.typ +++ /dev/null @@ -1,187 +0,0 @@ -// This theme is inspired by https://github.com/matze/mtheme -// The origin code was written by https://github.com/Enivex - -// Consider using: -// #set text(font: "Fira Sans", weight: "light", size: 20pt) -// #show math.equation: set text(font: "Fira Math") -// #set strong(delta: 100) -// #set par(justify: true) - -#import "../slide.typ": s -#import "../src/utils.typ" -#import "../src/states.typ" -#import "../src/components.typ" - -#let _saved-align = align - -#let slide( - self: none, - title: auto, - footer: auto, - align: horizon, - ..args, -) = { - self.page-args += ( - fill: self.colors.neutral-lightest, - ) - if title != auto { - self.m-title = title - } - if footer != auto { - self.m-footer = footer - } - (self.methods.touying-slide)( - ..args.named(), - self: self, - title: if title == auto { self.m-title = title } else { title }, - setting: body => { - show: _saved-align.with(align) - set text(fill: self.colors.neutral-dark) - show: args.named().at("setting", default: body => body) - body - }, - ..args.pos(), - ) -} - -#let title-slide( - self: none, - extra: none, - ..args, -) = { - self = utils.empty-page(self) - let info = self.info + args.named() - let content = { - set text(fill: self.colors.neutral-dark) - set align(horizon) - block(width: 100%, inset: 2em, { - text(size: 1.3em, text(weight: "medium", info.title)) - if info.subtitle != none { - linebreak() - text(size: 0.9em, info.subtitle) - } - line(length: 100%, stroke: .05em + self.colors.secondary-light) - set text(size: .8em) - if info.author != none { - block(spacing: 1em, info.author) - } - if info.date != none { - block(spacing: 1em, utils.info-date(self)) - } - set text(size: .8em) - if info.institution != none { - block(spacing: 1em, info.institution) - } - if extra != none { - block(spacing: 1em, extra) - } - }) - } - (self.methods.touying-slide)(self: self, repeat: none, content) -} - -#let new-section-slide(self: none, short-title: auto, title) = { - self = utils.empty-page(self) - let content = { - set align(horizon) - show: pad.with(20%) - set text(size: 1.5em) - states.current-section-with-numbering(self) - block(height: 2pt, width: 100%, spacing: 0pt, utils.call-or-display(self, self.m-progress-bar)) - } - (self.methods.touying-slide)(self: self, repeat: none, section: (title: title, short-title: short-title), content) -} - -#let focus-slide(self: none, body) = { - self = utils.empty-page(self) - self.page-args += ( - fill: self.colors.primary-dark, - margin: 2em, - ) - set text(fill: self.colors.neutral-lightest, size: 1.5em) - (self.methods.touying-slide)(self: self, repeat: none, align(horizon + center, body)) -} - -#let slides(self: none, title-slide: true, outline-slide: true, outline-title: [Table of contents], slide-level: 1, ..args) = { - if title-slide { - (self.methods.title-slide)(self: self) - } - if outline-slide { - (self.methods.slide)(self: self, title: outline-title, (self.methods.touying-outline)()) - } - (self.methods.touying-slides)(self: self, slide-level: slide-level, ..args) -} - -#let register( - self: s, - aspect-ratio: "16-9", - header: states.current-section-with-numbering, - footer: [], - footer-right: context states.slide-counter.display() + " / " + states.last-slide-number, - footer-progress: true, - ..args, -) = { - // color theme - self = (self.methods.colors)( - self: self, - neutral-lightest: rgb("#fafafa"), - neutral-dark: rgb("#23373b"), - primary-dark: rgb("#23373b"), - secondary-light: rgb("#eb811b"), - secondary-lighter: rgb("#d6c6b7"), - ) - // save the variables for later use - self.m-progress-bar = self => states.touying-progress(ratio => { - grid( - columns: (ratio * 100%, 1fr), - components.cell(fill: self.colors.secondary-light), - components.cell(fill: self.colors.secondary-lighter) - ) - }) - self.m-footer-progress = footer-progress - self.m-title = header - self.m-footer = footer - self.m-footer-right = footer-right - // set page - let header(self) = { - set align(top) - if self.m-title != none { - show: components.cell.with(fill: self.colors.primary-dark, inset: 1em) - set align(horizon) - set text(fill: self.colors.neutral-lightest, size: 1.2em) - utils.fit-to-width(grow: false, 100%, text(weight: "medium", utils.call-or-display(self, self.m-title))) - } else { [] } - } - let footer(self) = { - set align(bottom) - set text(size: 0.8em) - pad(.5em, { - text(fill: self.colors.neutral-dark.lighten(40%), utils.call-or-display(self, self.m-footer)) - h(1fr) - text(fill: self.colors.neutral-dark, utils.call-or-display(self, self.m-footer-right)) - }) - if self.m-footer-progress { - place(bottom, block(height: 2pt, width: 100%, spacing: 0pt, utils.call-or-display(self, self.m-progress-bar))) - } - } - self.page-args += ( - paper: "presentation-" + aspect-ratio, - header: header, - footer: footer, - header-ascent: 30%, - footer-descent: 30%, - margin: (top: 3em, bottom: 1.5em, x: 2em), - ) - // register methods - self.methods.slide = slide - self.methods.title-slide = title-slide - self.methods.new-section-slide = new-section-slide - self.methods.touying-new-section-slide = new-section-slide - self.methods.focus-slide = focus-slide - self.methods.slides = slides - self.methods.touying-outline = (self: none, enum-args: (:), ..args) => { - states.touying-outline(self: self, enum-args: (tight: false,) + enum-args, ..args) - } - self.methods.alert = (self: none, it) => text(fill: self.colors.secondary-light, it) - self -} From 1465aed7178b3739718152a18910a9522c31f9d0 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Sat, 31 Aug 2024 06:55:36 +0800 Subject: [PATCH 26/43] fix: fix some bugs --- examples/default.typ | 3 ++- src/core.typ | 9 ++++++--- src/exports.typ | 2 +- src/utils.typ | 14 +++----------- themes/metropolis.typ | 4 ---- 5 files changed, 12 insertions(+), 20 deletions(-) diff --git a/examples/default.typ b/examples/default.typ index 699066c19..5567cd75c 100644 --- a/examples/default.typ +++ b/examples/default.typ @@ -7,6 +7,7 @@ aspect-ratio: "16-9", config-common( slide-level: 3, + zero-margin-header: false, ), config-colors( primary: blue, @@ -15,7 +16,7 @@ alert: utils.alert-with-primary-color, ), config-page( - header: utils.display-current-short-heading(level: 2), + header: text(gray, utils.display-current-short-heading(level: 2)), ), ) diff --git a/src/core.typ b/src/core.typ index d18bf96e5..7ffb9760f 100644 --- a/src/core.typ +++ b/src/core.typ @@ -133,8 +133,6 @@ let cont = none // is new start let is-new-start = new-start - // is root - let is-root = is-first-slide // start part let start-part = () // result @@ -287,7 +285,7 @@ child.value.body, ), ) - } else if is-root and utils.is-styled(child) { + } else if is-first-slide and utils.is-styled(child) { current-slide = utils.trim(current-slide) if current-slide != () or current-headings != () { (cont, recaller-map, current-headings, current-slide, new-start, is-first-slide) = call-slide-fn-and-reset( @@ -305,6 +303,7 @@ self: self, recaller-map: recaller-map, new-start: true, + is-first-slide: is-first-slide, child.child, ), ), @@ -538,6 +537,10 @@ } +/// Alert is a way to display a message to the audience. It can be used to draw attention to important information or to provide instructions. +#let alert(body) = touying-fn-wrapper(utils.alert, body) + + /// Touying also provides a unique and highly useful feature—math equation animations, allowing you to conveniently use pause and meanwhile within math equations. /// /// #example(``` diff --git a/src/exports.typ b/src/exports.typ index 9d1dfc6ea..e32bfd744 100644 --- a/src/exports.typ +++ b/src/exports.typ @@ -1,4 +1,4 @@ -#import "core.typ": pause, meanwhile, uncover, only, alternatives-match, alternatives, alternatives-fn, alternatives-cases, slide, empty-slide, touying-slide, touying-fn-wrapper, touying-slide-wrapper, touying-equation, touying-mitex, touying-reducer, appendix, touying-set-config, touying-recall, speaker-note +#import "core.typ": pause, meanwhile, uncover, only, alternatives-match, alternatives, alternatives-fn, alternatives-cases, slide, empty-slide, touying-slide, touying-fn-wrapper, touying-slide-wrapper, touying-equation, touying-mitex, touying-reducer, appendix, touying-set-config, touying-recall, speaker-note, alert #import "slides.typ": touying-slides #import "configs.typ": config-colors, config-common, config-info, config-methods, config-page, config-store, default-config #import "utils.typ" diff --git a/src/utils.typ b/src/utils.typ index 6c7d1be77..528381762 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -704,19 +704,11 @@ /// Alert content with a primary color. /// /// Example: `config-methods(alert: utils.alert-with-primary-color)` -#let alert-with-primary-color(self: none, it) = text(fill: self.colors.primary, it) +#let alert-with-primary-color(self: none, body) = text(fill: self.colors.primary, body) -/// Alert content with a sencondary color. -/// -/// Example: `config-methods(alert: utils.alert-with-sencondary-color)` -#let alert-with-sencondary-color(self: none, it) = text(fill: self.colors.sencondary, it) - - -/// Alert content with a tertiary color. -/// -/// Example: `config-methods(alert: utils.alert-with-tertiary-color)` -#let alert-with-tertiary-color(self: none, it) = text(fill: self.colors.tertiary, it) +/// Alert content. +#let alert(self: none, body) = (self.methods.alert)(self: self, body) // Code: check visible subslides and dynamic control diff --git a/themes/metropolis.typ b/themes/metropolis.typ index 219ebbf22..0b7af0c08 100644 --- a/themes/metropolis.typ +++ b/themes/metropolis.typ @@ -203,10 +203,6 @@ }) -/// Alert some content. -#let alert(body) = touying-fn-wrapper(utils.alert-with-primary-color, body) - - /// Touying metropolis theme. /// /// Example: From c587a3455717dc7ce03ed0acaf74151609afe3a9 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Sat, 31 Aug 2024 08:04:45 +0800 Subject: [PATCH 27/43] theme(simple): complete simple theme --- examples/simple.typ | 23 ++--- src/configs.typ | 9 +- themes/metropolis.typ | 2 +- themes/simple.typ | 221 +++++++++++++++++++++++++++--------------- themes/themes.typ | 2 +- 5 files changed, 157 insertions(+), 100 deletions(-) diff --git a/examples/simple.typ b/examples/simple.typ index cf1e80562..0b024a505 100644 --- a/examples/simple.typ +++ b/examples/simple.typ @@ -1,11 +1,10 @@ #import "../lib.typ": * +#import themes.simple: * -#let s = themes.simple.register(aspect-ratio: "16-9", footer: [Simple slides]) -#let (init, slides) = utils.methods(s) -#show: init - -#let (slide, empty-slide, title-slide, centered-slide, focus-slide) = utils.slides(s) -#show: slides +#show: simple-theme.with( + aspect-ratio: "16-9", + footer: [Simple slides], +) #title-slide[ = Keep it simple! @@ -20,9 +19,7 @@ == First slide -#slide[ - #lorem(20) -] +#lorem(20) #focus-slide[ _Focus!_ @@ -34,10 +31,8 @@ == Dynamic slide -#slide[ - Did you know that... +Did you know that... - #pause +#pause - ...you can see the current section at the top of the slide? -] \ No newline at end of file +...you can see the current section at the top of the slide? \ No newline at end of file diff --git a/src/configs.typ b/src/configs.typ index fae2c4437..519a74038 100644 --- a/src/configs.typ +++ b/src/configs.typ @@ -91,7 +91,7 @@ /// /// - datetime-format (string): The format of the datetime. /// -/// - appendix (bool): Is touying in the appendix mode. The last-slide-counter will be frozen in the appendix mode. +/// - appendix (bool): Is touying in the appendix mode. The last-slide-counter will be frozen in the appendix mode. The default value is `false`. /// /// - freeze-slide-counter (bool): Whether to freeze the slide counter. The default value is `false`. /// @@ -99,7 +99,7 @@ /// /// - zero-margin-footer (bool): Whether to show the full footer (with negative padding). The default value is `true`. /// -/// - auto-offset-for-heading (bool): Whether to add an offset relative to slide-level for headings. +/// - auto-offset-for-heading (bool): Whether to add an offset relative to slide-level for headings. The default value is `false`. /// /// - enable-pdfpc (bool): Whether to add `` label for querying. /// @@ -521,7 +521,7 @@ #let default-config = utils.merge-dicts( config-common( handout: false, - slide-level: 3, + slide-level: 2, slide-fn: slide, new-section-slide-fn: none, new-subsection-slide-fn: none, @@ -532,7 +532,7 @@ freeze-slide-counter: false, zero-margin-header: true, zero-margin-footer: true, - auto-offset-for-heading: true, + auto-offset-for-heading: false, enable-pdfpc: true, enable-mark-warning: true, reset-page-counter-to-slide-counter: true, @@ -624,4 +624,5 @@ fill: rgb("#ffffff"), margin: (x: 3em, y: 2.8em), ), + config-store(), ) \ No newline at end of file diff --git a/themes/metropolis.typ b/themes/metropolis.typ index 0b7af0c08..6421ba9a0 100644 --- a/themes/metropolis.typ +++ b/themes/metropolis.typ @@ -208,7 +208,7 @@ /// Example: /// /// ```typst -/// #show: default-theme.with(aspect-ratio: "16-9", config-colors(primary: blue))` +/// #show: metropolis-theme.with(aspect-ratio: "16-9", config-colors(primary: blue))` /// ``` /// /// Consider using: diff --git a/themes/simple.typ b/themes/simple.typ index 0d2f08905..01abcd71f 100644 --- a/themes/simple.typ +++ b/themes/simple.typ @@ -1,98 +1,159 @@ // This theme is from https://github.com/andreasKroepelin/polylux/blob/main/themes/simple.typ // Author: Andreas Kröpelin -#import "../slide.typ": s -#import "../src/utils.typ" -#import "../src/states.typ" +#import "../src/exports.typ": * -#let slide(self: none, title: none, footer: auto, ..args) = { - if footer != auto { - self.simple-footer = footer - } - (self.methods.touying-slide)(self: self, title: title, setting: body => { - if self.auto-heading == true and title != none { - heading(level: 2, title) - } - body - }, ..args) -} - -#let centered-slide(self: none, ..args) = { - self = utils.empty-page(self, margin: none) - (self.methods.touying-slide)(self: self, repeat: none, ..args.named(), - align(center + horizon, args.pos().sum(default: [])) +/// Default slide function for the presentation. +/// +/// - `config` is the configuration of the slide. You can use `config-xxx` to set the configuration of the slide. For more several configurations, you can use `utils.merge-dicts` to merge them. +/// +/// - `repeat` is the number of subslides. Default is `auto`,which means touying will automatically calculate the number of subslides. +/// +/// The `repeat` argument is necessary when you use `#slide(repeat: 3, self => [ .. ])` style code to create a slide. The callback-style `uncover` and `only` cannot be detected by touying automatically. +/// +/// - `setting` is the setting of the slide. You can use it to add some set/show rules for the slide. +/// +/// - `composer` is the composer of the slide. You can use it to set the layout of the slide. +/// +/// For example, `#slide(composer: (1fr, 2fr, 1fr))[A][B][C]` to split the slide into three parts. The first and the last parts will take 1/4 of the slide, and the second part will take 1/2 of the slide. +/// +/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `utils.side-by-side` function. +/// +/// The `utils.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. +/// +/// For example, `#slide(composer: 2)[A][B][#grid.cell(colspan: 2)[Footer]] will make the `Footer` cell take 2 columns. +/// +/// If you want to customize the composer, you can pass a function to the `composer` argument. The function should receive the contents of the slide and return the content of the slide, like `#slide(composer: grid.with(columns: 2))[A][B]`. +/// +/// - `..bodies` is the contents of the slide. You can call the `slide` function with syntax like `#slide[A][B][C]` to create a slide. +#let slide( + title: none, + config: (:), + repeat: auto, + setting: body => body, + composer: auto, + ..bodies, +) = touying-slide-wrapper(self => { + let deco-format(it) = text(size: .6em, fill: gray, it) + let header(self) = deco-format( + components.left-and-right( + utils.call-or-display(self, self.store.header), + utils.call-or-display(self, self.store.header-right), + ), ) -} + let footer(self) = deco-format( + components.left-and-right( + utils.call-or-display(self, self.store.footer), + utils.call-or-display(self, self.store.footer-right), + ), + ) + let self = utils.merge-dicts( + self, + config-page( + header: header, + footer: footer, + ), + config-common(subslide-preamble: self.store.subslide-preamble), + ) + touying-slide(self: self, config: config, repeat: repeat, setting: setting, composer: composer, ..bodies) +}) -#let title-slide(self: none, body) = { - centered-slide(self: self, body) -} -#let new-section-slide(self: none, section) = { - self = utils.empty-page(self, margin: none) - (self.methods.touying-slide)(self: self, repeat: none, section: section, align(center + horizon, heading(level: 1, states.current-section-with-numbering(self))) - ) -} +/// Centered slide for the presentation. +#let centered-slide(..args) = touying-slide-wrapper(self => { + touying-slide(self: self, ..args.named(), align(center + horizon, args.pos().sum(default: none))) +}) + + +/// Title slide for the presentation. +#let title-slide(body) = centered-slide( + config: config-common(freeze-slide-counter: true), + body, +) -#let focus-slide(self: none, background: auto, foreground: white, body) = { - self = utils.empty-page(self, margin: none) - self.page-args.fill = if background == auto { self.colors.primary } else { background } + +/// New section slide for the presentation. You can update it by updating the `new-section-slide-fn` argument for `config-common` function. +#let new-section-slide(title) = centered-slide(text(1.2em, weight: "bold", utils.display-current-heading(level: 1))) + + +/// Focus on some content. +/// +/// Example: `#focus-slide[Wake up!]` +#let focus-slide(background: auto, foreground: white, body) = touying-slide-wrapper(self => { + self = utils.merge-dicts( + self, + config-common(freeze-slide-counter: true), + config-page(fill: if background == auto { + self.colors.primary + } else { + background + }), + ) set text(fill: foreground, size: 1.5em) - centered-slide(self: self, align(center + horizon, body)) -} + touying-slide(self: self, align(center + horizon, body)) +}) -#let register( - self: s, + +/// Touying simple theme. +/// +/// Example: +/// +/// ```typst +/// #show: simple-theme.with(aspect-ratio: "16-9", config-colors(primary: blue))` +/// ``` +/// +/// - `aspect-ratio` is the aspect ratio of the slides. Default is `16-9`. +#let simple-theme( aspect-ratio: "16-9", - footer: [], - footer-right: context { states.slide-counter.display() + " / " + states.last-slide-number }, + header: utils.display-current-heading.with(setting: utils.fit-to-width.with(grow: false, 100%)), + header-right: self => self.info.logo, + footer: none, + footer-right: context utils.slide-counter.display() + " / " + utils.last-slide-number, background: rgb("#ffffff"), foreground: rgb("#000000"), primary: aqua.darken(50%), + subslide-preamble: block( + below: 1.5em, + text(1.2em, weight: "bold", utils.display-current-heading(level: 2)), + ), ..args, + body, ) = { - let deco-format(it) = text(size: .6em, fill: gray, it) - // color theme - self = (self.methods.colors)( - self: self, - neutral-light: gray, - neutral-lightest: background, - neutral-darkest: foreground, - primary: primary, - ) - // save the variables for later use - self.simple-footer = footer - self.simple-footer-right = footer-right - self.auto-heading = true - // set page - let header = self => deco-format(states.current-section-with-numbering(self)) - let footer(self) = deco-format(self.simple-footer + h(1fr) + self.simple-footer-right) - self.page-args += ( - paper: "presentation-" + aspect-ratio, - fill: self.colors.neutral-lightest, - header: header, - footer: footer, - footer-descent: 1em, - header-ascent: 1em, + set text(fill: foreground, size: 25pt) + show footnote.entry: set text(size: .6em) + + show: touying-slides.with( + config-page( + paper: "presentation-" + aspect-ratio, + fill: background, + footer-descent: 1em, + header-ascent: 1em, + ), + config-common( + slide-fn: slide, + new-section-slide-fn: new-section-slide, + zero-margin-header: false, + zero-margin-footer: false, + ), + config-methods( + alert: utils.alert-with-primary-color, + ), + config-colors( + neutral-light: gray, + neutral-lightest: background, + neutral-darkest: foreground, + primary: primary, + ), + // save the variables for later use + config-store( + header: header, + header-right: header-right, + footer: footer, + footer-right: footer-right, + subslide-preamble: subslide-preamble, + ), + ..args, ) - self.full-header = false - self.full-footer = false - // register methods - self.methods.slide = slide - self.methods.title-slide = title-slide - self.methods.centered-slide = centered-slide - self.methods.focus-slide = focus-slide - self.methods.new-section-slide = new-section-slide - self.methods.touying-new-section-slide = new-section-slide - self.methods.init = (self: none, body) => { - set heading(outlined: false) - set text(fill: foreground, size: 25pt) - show footnote.entry: set text(size: .6em) - show heading.where(level: 2): set block(below: 1.5em) - set outline(target: heading.where(level: 1), title: none, fill: none) - show outline.entry: it => it.body - show outline: it => block(inset: (x: 1em), it) - body - } - self + + body } diff --git a/themes/themes.typ b/themes/themes.typ index 2d3dcf548..7782a6fc1 100644 --- a/themes/themes.typ +++ b/themes/themes.typ @@ -1,6 +1,6 @@ #import "default.typ" +#import "simple.typ" #import "metropolis.typ" -// #import "simple.typ" // #import "dewdrop.typ" // #import "university.typ" // #import "aqua.typ" \ No newline at end of file From d215f74f8c8b73065b3bfaaf22520c060d9ebf39 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Sat, 31 Aug 2024 08:41:19 +0800 Subject: [PATCH 28/43] fix: fix auto-offset-for-heading --- src/configs.typ | 4 ++-- src/core.typ | 17 ++++++++++++++++- themes/simple.typ | 9 +++++++-- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/configs.typ b/src/configs.typ index 519a74038..0d1203b08 100644 --- a/src/configs.typ +++ b/src/configs.typ @@ -99,7 +99,7 @@ /// /// - zero-margin-footer (bool): Whether to show the full footer (with negative padding). The default value is `true`. /// -/// - auto-offset-for-heading (bool): Whether to add an offset relative to slide-level for headings. The default value is `false`. +/// - auto-offset-for-heading (bool): Whether to add an offset relative to slide-level for headings. The default value is `true`. /// /// - enable-pdfpc (bool): Whether to add `` label for querying. /// @@ -532,7 +532,7 @@ freeze-slide-counter: false, zero-margin-header: true, zero-margin-footer: true, - auto-offset-for-heading: false, + auto-offset-for-heading: true, enable-pdfpc: true, enable-mark-warning: true, reset-page-counter-to-slide-counter: true, diff --git a/src/core.typ b/src/core.typ index 7ffb9760f..1898c69f0 100644 --- a/src/core.typ +++ b/src/core.typ @@ -265,6 +265,21 @@ result.push(cont) } } + } else if self.at("auto-offset-for-heading", default: true) and utils.is-heading(child) { + let fields = child.fields() + let lbl = fields.remove("label", default: none) + let _ = fields.remove("body", default: none) + fields.offset = 0 + let new-heading = if lbl != none { + utils.label-it(heading(..fields, child.body), it.label) + } else { + heading(..fields, child.body) + } + if new-start { + current-slide.push(new-heading) + } else { + start-part.push(new-heading) + } } else if utils.is-kind(child, "touying-set-config") { current-slide = utils.trim(current-slide) if current-slide != () or current-headings != () { @@ -1418,7 +1433,7 @@ if str(it.label) == "touying:unbookmarked" { fields.bookmarked = false } - heading(..fields, it.body) + utils.label-it(heading(..fields, it.body), it.label) } else { it } diff --git a/themes/simple.typ b/themes/simple.typ index 01abcd71f..d52420581 100644 --- a/themes/simple.typ +++ b/themes/simple.typ @@ -126,8 +126,7 @@ config-page( paper: "presentation-" + aspect-ratio, fill: background, - footer-descent: 1em, - header-ascent: 1em, + margin: 2em, ), config-common( slide-fn: slide, @@ -136,6 +135,12 @@ zero-margin-footer: false, ), config-methods( + init: (self: none, body) => { + show strong: self.methods.alert.with(self: self) + show heading.where(level: self.slide-level + 1): set text(1.4em) + + body + }, alert: utils.alert-with-primary-color, ), config-colors( From 2ddd7a3dfc5ed160ba24bd4476434bd7c2523b45 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Sat, 31 Aug 2024 10:27:12 +0800 Subject: [PATCH 29/43] theme(university): complete university theme --- examples/university.typ | 38 ++- src/components.typ | 65 ++++- src/configs.typ | 6 - themes/themes.typ | 2 +- themes/university copy.typ | 288 +++++++++++++++++++++ themes/university.typ | 512 +++++++++++++++++++------------------ 6 files changed, 637 insertions(+), 274 deletions(-) create mode 100644 themes/university copy.typ diff --git a/examples/university.typ b/examples/university.typ index 02078c0d7..3d37673f3 100644 --- a/examples/university.typ +++ b/examples/university.typ @@ -1,21 +1,21 @@ #import "../lib.typ": * - -#let s = themes.university.register(aspect-ratio: "16-9") -#let s = (s.methods.info)( - self: s, - title: [Title], - subtitle: [Subtitle], - author: [Authors], - date: datetime.today(), - institution: [Institution], +#import themes.university: * + +#import "@preview/numbly:0.1.0": numbly + +#show: university-theme.with( + aspect-ratio: "16-9", + config-info( + title: [Title], + subtitle: [Subtitle], + author: [Authors], + date: datetime.today(), + institution: [Institution], + logo: emoji.school, + ), ) -#let (init, slides, touying-outline, alert) = utils.methods(s) -#show: init - -#show strong: alert -#let (slide, empty-slide, title-slide, focus-slide, matrix-slide) = utils.slides(s) -#show: slides.with(title-slide: false) +#set heading(numbering: numbly("{1}.", default: "1.1")) #title-slide(authors: ([Author A], [Author B])) @@ -23,13 +23,7 @@ == Slide Title -#slide[ - #lorem(40) -] - -#slide(subtitle: emph[What is the problem?])[ - #lorem(40) -] +#lorem(40) #focus-slide[ Another variant with primary color in background... diff --git a/src/components.typ b/src/components.typ index 6014a877b..2e48e321e 100644 --- a/src/components.typ +++ b/src/components.typ @@ -1,7 +1,7 @@ #import "utils.typ" -#let cell = block.with(width: 100%, height: 100%, above: 0pt, below: 0pt, breakable: false) +#let cell = block.with(width: 100%, height: 100%, above: 0pt, below: 0pt, outset: 0pt, breakable: false) /// Touying progress bar. @@ -28,4 +28,65 @@ #let left-and-right(left, right) = grid( columns: (auto, 1fr, auto), left, none, right, -) \ No newline at end of file +) + + +// Create a slide where the provided content blocks are displayed in a grid and coloured in a checkerboard pattern without further decoration. You can configure the grid using the rows and `columns` keyword arguments (both default to none). It is determined in the following way: +/// +/// - If `columns` is an integer, create that many columns of width `1fr`. +/// - If `columns` is `none`, create as many columns of width `1fr` as there are content blocks. +/// - Otherwise assume that `columns` is an array of widths already, use that. +/// - If `rows` is an integer, create that many rows of height `1fr`. +/// - If `rows` is `none`, create that many rows of height `1fr` as are needed given the number of co/ -ntent blocks and columns. +/// - Otherwise assume that `rows` is an array of heights already, use that. +/// - Check that there are enough rows and columns to fit in all the content blocks. +/// +/// That means that `#checkerboard[...][...]` stacks horizontally and `#checkerboard(columns: 1)[...][...]` stacks vertically. +#let checkerboard(columns: none, rows: none, ..bodies) = { + let bodies = bodies.pos() + let columns = if type(columns) == int { + (1fr,) * columns + } else if columns == none { + (1fr,) * bodies.len() + } else { + columns + } + let num-cols = columns.len() + let rows = if type(rows) == int { + (1fr,) * rows + } else if rows == none { + let quotient = calc.quo(bodies.len(), num-cols) + let correction = if calc.rem(bodies.len(), num-cols) == 0 { + 0 + } else { + 1 + } + (1fr,) * (quotient + correction) + } else { + rows + } + let num-rows = rows.len() + if num-rows * num-cols < bodies.len() { + panic("number of rows (" + str(num-rows) + ") * number of columns (" + str(num-cols) + ") must at least be number of content arguments (" + str( + bodies.len(), + ) + ")") + } + let cart-idx(i) = (calc.quo(i, num-cols), calc.rem(i, num-cols)) + let color-body(idx-body) = { + let (idx, body) = idx-body + let (row, col) = cart-idx(idx) + let color = if calc.even(row + col) { + white + } else { + silver + } + set align(center + horizon) + rect(inset: .5em, width: 100%, height: 100%, fill: color, body) + } + let body = grid( + columns: columns, rows: rows, + gutter: 0pt, + ..bodies.enumerate().map(color-body) + ) + body +} \ No newline at end of file diff --git a/src/configs.typ b/src/configs.typ index 0d1203b08..b8ad11f96 100644 --- a/src/configs.typ +++ b/src/configs.typ @@ -355,12 +355,6 @@ ..args, ) = { assert(args.pos().len() == 0, message: "Unexpected positional arguments.") - if short-title == auto { - short-title = title - } - if short-subtitle == auto { - short-subtitle = subtitle - } return ( info: _get-dict-without-default(( title: title, diff --git a/themes/themes.typ b/themes/themes.typ index 7782a6fc1..c507988a0 100644 --- a/themes/themes.typ +++ b/themes/themes.typ @@ -2,5 +2,5 @@ #import "simple.typ" #import "metropolis.typ" // #import "dewdrop.typ" -// #import "university.typ" +#import "university.typ" // #import "aqua.typ" \ No newline at end of file diff --git a/themes/university copy.typ b/themes/university copy.typ new file mode 100644 index 000000000..5ec83ca0b --- /dev/null +++ b/themes/university copy.typ @@ -0,0 +1,288 @@ +// University theme + +// Originally contributed by Pol Dellaiera - https://github.com/drupol + +#import "../slide.typ": s +#import "../src/utils.typ" +#import "../src/states.typ" +#import "../src/components.typ" + +#let slide( + self: none, + title: auto, + subtitle: auto, + header: auto, + footer: auto, + display-current-section: auto, + ..args, +) = { + if title != auto { + self.uni-title = title + } + if subtitle != auto { + self.uni-subtitle = subtitle + } + if header != auto { + self.uni-header = header + } + if footer != auto { + self.uni-footer = footer + } + if display-current-section != auto { + self.uni-display-current-section = display-current-section + } + (self.methods.touying-slide)( + ..args.named(), + self: self, + title: title, + setting: body => { + show: args.named().at("setting", default: body => body) + body + }, + ..args.pos(), + ) +} + +#let title-slide(self: none, ..args) = { + self = utils.empty-page(self) + let info = self.info + args.named() + info.authors = { + let authors = if "authors" in info { info.authors } else { info.author } + if type(authors) == array { authors } else { (authors,) } + } + let content = { + if info.logo != none { + align(right, info.logo) + } + align(center + horizon, { + block( + inset: 0em, + breakable: false, + { + text(size: 2em, fill: self.colors.primary, strong(info.title)) + if info.subtitle != none { + parbreak() + text(size: 1.2em, fill: self.colors.primary, info.subtitle) + } + } + ) + set text(size: .8em) + grid( + columns: (1fr,) * calc.min(info.authors.len(), 3), + column-gutter: 1em, + row-gutter: 1em, + ..info.authors.map(author => text(fill: black, author)) + ) + v(1em) + if info.institution != none { + parbreak() + text(size: .9em, info.institution) + } + if info.date != none { + parbreak() + text(size: .8em, utils.info-date(self)) + } + }) + } + (self.methods.touying-slide)(self: self, repeat: none, content) +} + +#let new-section-slide(self: none, short-title: auto, title) = { + self = utils.empty-page(self) + let content(self) = { + set align(horizon) + show: pad.with(20%) + set text(size: 1.5em, fill: self.colors.primary, weight: "bold") + states.current-section-with-numbering(self) + v(-.5em) + block(height: 2pt, width: 100%, spacing: 0pt, utils.call-or-display(self, self.uni-progress-bar)) + } + (self.methods.touying-slide)(self: self, repeat: none, section: (title: title, short-title: short-title), content) +} + +#let focus-slide(self: none, background-color: none, background-img: none, body) = { + let background-color = if background-img == none and background-color == none { + rgb(self.colors.primary) + } else { + background-color + } + self = utils.empty-page(self) + self.page-args += ( + fill: self.colors.primary-dark, + margin: 1em, + ..(if background-color != none { (fill: background-color) }), + ..(if background-img != none { (background: { + set image(fit: "stretch", width: 100%, height: 100%) + background-img + }) + }), + ) + set text(fill: white, weight: "bold", size: 2em) + (self.methods.touying-slide)(self: self, repeat: none, align(horizon, body)) +} + +#let matrix-slide(self: none, columns: none, rows: none, ..bodies) = { + self = utils.empty-page(self) + (self.methods.touying-slide)(self: self, composer: (..bodies) => { + let bodies = bodies.pos() + let columns = if type(columns) == int { + (1fr,) * columns + } else if columns == none { + (1fr,) * bodies.len() + } else { + columns + } + let num-cols = columns.len() + let rows = if type(rows) == int { + (1fr,) * rows + } else if rows == none { + let quotient = calc.quo(bodies.len(), num-cols) + let correction = if calc.rem(bodies.len(), num-cols) == 0 { 0 } else { 1 } + (1fr,) * (quotient + correction) + } else { + rows + } + let num-rows = rows.len() + if num-rows * num-cols < bodies.len() { + panic("number of rows (" + str(num-rows) + ") * number of columns (" + str(num-cols) + ") must at least be number of content arguments (" + str(bodies.len()) + ")") + } + let cart-idx(i) = (calc.quo(i, num-cols), calc.rem(i, num-cols)) + let color-body(idx-body) = { + let (idx, body) = idx-body + let (row, col) = cart-idx(idx) + let color = if calc.even(row + col) { white } else { silver } + set align(center + horizon) + rect(inset: .5em, width: 100%, height: 100%, fill: color, body) + } + let content = grid( + columns: columns, rows: rows, + gutter: 0pt, + ..bodies.enumerate().map(color-body) + ) + content + }, ..bodies) +} + +#let slides(self: none, title-slide: true, slide-level: 1, ..args) = { + if title-slide { + (self.methods.title-slide)(self: self) + } + (self.methods.touying-slides)(self: self, slide-level: slide-level, ..args) +} + +#let register( + self: s, + aspect-ratio: "16-9", + progress-bar: true, + display-current-section: true, + footer-columns: (25%, 1fr, 25%), + footer-a: self => self.info.author, + footer-b: self => if self.info.short-title == auto { self.info.title } else { self.info.short-title }, + footer-c: self => context { + h(1fr) + utils.info-date(self) + h(1fr) + context states.slide-counter.display() + " / " + states.last-slide-number + h(1fr) + }, + ..args, +) = { + // color theme + self = (self.methods.colors)( + self: self, + primary: rgb("#04364A"), + secondary: rgb("#176B87"), + tertiary: rgb("#448C95"), + ) + // save the variables for later use + self.uni-enable-progress-bar = progress-bar + self.uni-progress-bar = self => states.touying-progress(ratio => { + grid( + columns: (ratio * 100%, 1fr), + rows: 2pt, + components.cell(fill: self.colors.primary), + components.cell(fill: self.colors.tertiary) + ) + }) + self.uni-display-current-section = display-current-section + self.uni-title = none + self.uni-subtitle = none + self.uni-footer = self => { + let cell(fill: none, it) = rect( + width: 100%, height: 100%, inset: 1mm, outset: 0mm, fill: fill, stroke: none, + align(horizon, text(fill: white, it)) + ) + show: block.with(width: 100%, height: auto, fill: self.colors.secondary) + grid( + columns: footer-columns, + rows: (1.5em, auto), + cell(fill: self.colors.primary, utils.call-or-display(self, footer-a)), + cell(fill: self.colors.secondary, utils.call-or-display(self, footer-b)), + cell(fill: self.colors.tertiary, utils.call-or-display(self, footer-c)), + ) + } + self.uni-header = self => { + if self.uni-title != none { + block(inset: (x: .5em), + grid( + columns: 1, + gutter: .3em, + grid( + columns: (auto, 1fr, auto), + align(top + left, text(fill: self.colors.primary, weight: "bold", size: 1.2em, self.uni-title)), + [], + if self.uni-display-current-section { + align(top + right, text(fill: self.colors.primary.lighten(65%), states.current-section-with-numbering(self))) + } + ), + text(fill: self.colors.primary.lighten(65%), size: .8em, self.uni-subtitle) + ) + ) + } + } + // set page + let header(self) = { + set align(top) + grid( + rows: (auto, auto), + row-gutter: 3mm, + if self.uni-enable-progress-bar { + utils.call-or-display(self, self.uni-progress-bar) + }, + utils.call-or-display(self, self.uni-header), + ) + } + let footer(self) = { + set text(size: .4em) + set align(center + bottom) + utils.call-or-display(self, self.uni-footer) + } + + self.page-args += ( + paper: "presentation-" + aspect-ratio, + header: header, + footer: footer, + header-ascent: 0em, + footer-descent: 0em, + margin: (top: 2.5em, bottom: 1.25em, x: 2em), + ) + // register methods + self.methods.slide = slide + self.methods.title-slide = title-slide + self.methods.new-section-slide = new-section-slide + self.methods.touying-new-section-slide = new-section-slide + self.methods.focus-slide = focus-slide + self.methods.matrix-slide = matrix-slide + self.methods.slides = slides + self.methods.touying-outline = (self: none, enum-args: (:), ..args) => { + states.touying-outline(self: self, enum-args: (tight: false,) + enum-args, ..args) + } + self.methods.alert = (self: none, it) => text(fill: self.colors.primary, it) + self.methods.init = (self: none, body) => { + set text(size: 25pt) + set heading(outlined: false) + show footnote.entry: set text(size: .6em) + body + } + self +} diff --git a/themes/university.typ b/themes/university.typ index 5ec83ca0b..b0d8ceb92 100644 --- a/themes/university.typ +++ b/themes/university.typ @@ -2,287 +2,313 @@ // Originally contributed by Pol Dellaiera - https://github.com/drupol -#import "../slide.typ": s -#import "../src/utils.typ" -#import "../src/states.typ" -#import "../src/components.typ" +#import "../src/exports.typ": * +/// Default slide function for the presentation. +/// +/// - `config` is the configuration of the slide. You can use `config-xxx` to set the configuration of the slide. For more several configurations, you can use `utils.merge-dicts` to merge them. +/// +/// - `repeat` is the number of subslides. Default is `auto`,which means touying will automatically calculate the number of subslides. +/// +/// The `repeat` argument is necessary when you use `#slide(repeat: 3, self => [ .. ])` style code to create a slide. The callback-style `uncover` and `only` cannot be detected by touying automatically. +/// +/// - `setting` is the setting of the slide. You can use it to add some set/show rules for the slide. +/// +/// - `composer` is the composer of the slide. You can use it to set the layout of the slide. +/// +/// For example, `#slide(composer: (1fr, 2fr, 1fr))[A][B][C]` to split the slide into three parts. The first and the last parts will take 1/4 of the slide, and the second part will take 1/2 of the slide. +/// +/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `utils.side-by-side` function. +/// +/// The `utils.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. +/// +/// For example, `#slide(composer: 2)[A][B][#grid.cell(colspan: 2)[Footer]] will make the `Footer` cell take 2 columns. +/// +/// If you want to customize the composer, you can pass a function to the `composer` argument. The function should receive the contents of the slide and return the content of the slide, like `#slide(composer: grid.with(columns: 2))[A][B]`. +/// +/// - `..bodies` is the contents of the slide. You can call the `slide` function with syntax like `#slide[A][B][C]` to create a slide. #let slide( - self: none, - title: auto, - subtitle: auto, - header: auto, - footer: auto, - display-current-section: auto, - ..args, -) = { - if title != auto { - self.uni-title = title - } - if subtitle != auto { - self.uni-subtitle = subtitle - } - if header != auto { - self.uni-header = header - } - if footer != auto { - self.uni-footer = footer + config: (:), + repeat: auto, + setting: body => body, + composer: auto, + ..bodies, +) = touying-slide-wrapper(self => { + let header(self) = { + set align(top) + grid( + rows: (auto, auto), + row-gutter: 3mm, + if self.store.progress-bar { + components.progress-bar(height: 2pt, self.colors.primary, self.colors.tertiary) + }, + block( + inset: (x: .5em), + components.left-and-right( + text(fill: self.colors.primary, weight: "bold", size: 1.2em, self.store.header), + text(fill: self.colors.primary.lighten(65%), self.store.header-right), + ), + ), + ) } - if display-current-section != auto { - self.uni-display-current-section = display-current-section + let footer(self) = { + set align(center + bottom) + set text(size: .4em) + { + let cell(..args, it) = components.cell( + ..args, + inset: 1mm, + align(horizon, text(fill: white, it)), + ) + show: block.with(width: 100%, height: auto) + grid( + columns: self.store.footer-columns, + rows: 1.5em, + cell(fill: self.colors.primary, utils.call-or-display(self, self.store.footer-a)), + cell(fill: self.colors.secondary, utils.call-or-display(self, self.store.footer-b)), + cell(fill: self.colors.tertiary, utils.call-or-display(self, self.store.footer-c)), + ) + } } - (self.methods.touying-slide)( - ..args.named(), - self: self, - title: title, - setting: body => { - show: args.named().at("setting", default: body => body) - body - }, - ..args.pos(), + let self = utils.merge-dicts( + self, + config-page( + header: header, + footer: footer, + ), ) -} + touying-slide(self: self, config: config, repeat: repeat, setting: setting, composer: composer, ..bodies) +}) -#let title-slide(self: none, ..args) = { - self = utils.empty-page(self) + +/// Title slide for the presentation. You should update the information in the `config-info` function. You can also pass the information directly to the `title-slide` function. +/// +/// Example: +/// +/// ```typst +/// #show: university-theme.with( +/// config-info( +/// title: [Title], +/// logo: emoji.city, +/// ), +/// ) +/// +/// #title-slide(subtitle: [Subtitle]) +/// ``` +#let title-slide( + extra: none, + ..args, +) = touying-slide-wrapper(self => { let info = self.info + args.named() info.authors = { - let authors = if "authors" in info { info.authors } else { info.author } - if type(authors) == array { authors } else { (authors,) } + let authors = if "authors" in info { + info.authors + } else { + info.author + } + if type(authors) == array { + authors + } else { + (authors,) + } } - let content = { + let body = { if info.logo != none { align(right, info.logo) } - align(center + horizon, { - block( - inset: 0em, - breakable: false, - { - text(size: 2em, fill: self.colors.primary, strong(info.title)) - if info.subtitle != none { - parbreak() - text(size: 1.2em, fill: self.colors.primary, info.subtitle) - } + align( + center + horizon, + { + block( + inset: 0em, + breakable: false, + { + text(size: 2em, fill: self.colors.primary, strong(info.title)) + if info.subtitle != none { + parbreak() + text(size: 1.2em, fill: self.colors.primary, info.subtitle) + } + }, + ) + set text(size: .8em) + grid( + columns: (1fr,) * calc.min(info.authors.len(), 3), + column-gutter: 1em, + row-gutter: 1em, + ..info.authors.map(author => text(fill: self.colors.neutral-darkest, author)) + ) + v(1em) + if info.institution != none { + parbreak() + text(size: .9em, info.institution) } - ) - set text(size: .8em) - grid( - columns: (1fr,) * calc.min(info.authors.len(), 3), - column-gutter: 1em, - row-gutter: 1em, - ..info.authors.map(author => text(fill: black, author)) - ) - v(1em) - if info.institution != none { - parbreak() - text(size: .9em, info.institution) - } - if info.date != none { - parbreak() - text(size: .8em, utils.info-date(self)) - } - }) + if info.date != none { + parbreak() + text(size: .8em, utils.display-info-date(self)) + } + }, + ) } - (self.methods.touying-slide)(self: self, repeat: none, content) -} + self = utils.merge-dicts( + self, + config-common(freeze-slide-counter: true), + config-page(fill: self.colors.neutral-lightest), + ) + touying-slide(self: self, body) +}) -#let new-section-slide(self: none, short-title: auto, title) = { - self = utils.empty-page(self) - let content(self) = { + +/// New section slide for the presentation. You can update it by updating the `new-section-slide-fn` argument for `config-common` function. +/// +/// Example: `config-common(new-section-slide-fn: new-section-slide.with(numbered: false))` +/// +/// - `level` is the level of the heading. +/// +/// - `numbered` is whether the heading is numbered. +/// +/// - `title` is the title of the section. It will be pass by touying automatically. +#let new-section-slide(level: 1, numbered: true, title) = touying-slide-wrapper(self => { + let body = { set align(horizon) show: pad.with(20%) set text(size: 1.5em, fill: self.colors.primary, weight: "bold") - states.current-section-with-numbering(self) + utils.display-current-heading(level: level, numbered: numbered) v(-.5em) - block(height: 2pt, width: 100%, spacing: 0pt, utils.call-or-display(self, self.uni-progress-bar)) + block( + height: 2pt, + width: 100%, + spacing: 0pt, + components.progress-bar(height: 2pt, self.colors.primary, self.colors.tertiary), + ) } - (self.methods.touying-slide)(self: self, repeat: none, section: (title: title, short-title: short-title), content) -} + self = utils.merge-dicts( + self, + config-page(fill: self.colors.neutral-lightest), + ) + touying-slide(self: self, body) +}) -#let focus-slide(self: none, background-color: none, background-img: none, body) = { - let background-color = if background-img == none and background-color == none { + +/// Focus on some content. +/// +/// Example: `#focus-slide[Wake up!]` +/// +/// - `background-color` is the background color of the slide. Default is the primary color. +/// +/// - `background-img` is the background image of the slide. Default is none. +#let focus-slide(background-color: none, background-img: none, body) = touying-slide-wrapper(self => { + let background-color = if background-img == none and background-color == none { rgb(self.colors.primary) } else { background-color } - self = utils.empty-page(self) - self.page-args += ( - fill: self.colors.primary-dark, - margin: 1em, - ..(if background-color != none { (fill: background-color) }), - ..(if background-img != none { (background: { - set image(fit: "stretch", width: 100%, height: 100%) - background-img - }) - }), + let args = (:) + if background-color != none { + args.fill = background-color + } + if background-img != none { + args.background = { + set image(fit: "stretch", width: 100%, height: 100%) + background-img + } + } + self = utils.merge-dicts( + self, + config-common(freeze-slide-counter: true), + config-page(margin: 1em, ..args), ) - set text(fill: white, weight: "bold", size: 2em) - (self.methods.touying-slide)(self: self, repeat: none, align(horizon, body)) -} + set text(fill: self.colors.neutral-lightest, weight: "bold", size: 2em) + touying-slide(self: self, align(horizon, body)) +}) -#let matrix-slide(self: none, columns: none, rows: none, ..bodies) = { - self = utils.empty-page(self) - (self.methods.touying-slide)(self: self, composer: (..bodies) => { - let bodies = bodies.pos() - let columns = if type(columns) == int { - (1fr,) * columns - } else if columns == none { - (1fr,) * bodies.len() - } else { - columns - } - let num-cols = columns.len() - let rows = if type(rows) == int { - (1fr,) * rows - } else if rows == none { - let quotient = calc.quo(bodies.len(), num-cols) - let correction = if calc.rem(bodies.len(), num-cols) == 0 { 0 } else { 1 } - (1fr,) * (quotient + correction) - } else { - rows - } - let num-rows = rows.len() - if num-rows * num-cols < bodies.len() { - panic("number of rows (" + str(num-rows) + ") * number of columns (" + str(num-cols) + ") must at least be number of content arguments (" + str(bodies.len()) + ")") - } - let cart-idx(i) = (calc.quo(i, num-cols), calc.rem(i, num-cols)) - let color-body(idx-body) = { - let (idx, body) = idx-body - let (row, col) = cart-idx(idx) - let color = if calc.even(row + col) { white } else { silver } - set align(center + horizon) - rect(inset: .5em, width: 100%, height: 100%, fill: color, body) - } - let content = grid( - columns: columns, rows: rows, - gutter: 0pt, - ..bodies.enumerate().map(color-body) - ) - content - }, ..bodies) -} -#let slides(self: none, title-slide: true, slide-level: 1, ..args) = { - if title-slide { - (self.methods.title-slide)(self: self) - } - (self.methods.touying-slides)(self: self, slide-level: slide-level, ..args) -} +// Create a slide where the provided content blocks are displayed in a grid and coloured in a checkerboard pattern without further decoration. You can configure the grid using the rows and `columns` keyword arguments (both default to none). It is determined in the following way: +/// +/// - If `columns` is an integer, create that many columns of width `1fr`. +/// - If `columns` is `none`, create as many columns of width `1fr` as there are content blocks. +/// - Otherwise assume that `columns` is an array of widths already, use that. +/// - If `rows` is an integer, create that many rows of height `1fr`. +/// - If `rows` is `none`, create that many rows of height `1fr` as are needed given the number of co/ -ntent blocks and columns. +/// - Otherwise assume that `rows` is an array of heights already, use that. +/// - Check that there are enough rows and columns to fit in all the content blocks. +/// +/// That means that `#matrix-slide[...][...]` stacks horizontally and `#matrix-slide(columns: 1)[...][...]` stacks vertically. +#let matrix-slide(columns: none, rows: none, ..bodies) = touying-slide-wrapper(self => { + self = utils.merge-dicts( + self, + config-common(freeze-slide-counter: true), + config-page(margin: 0em), + ) + touying-slide(self: self, composer: components.checkerboard.with(columns: columns, rows: rows), ..bodies) +}) -#let register( - self: s, + +/// Touying university theme. +/// +/// Example: +/// +/// ```typst +/// #show: university-theme.with(aspect-ratio: "16-9", config-colors(primary: blue))` +/// ``` +/// +/// - `aspect-ratio` is the aspect ratio of the slides. Default is `16-9`. +#let university-theme( aspect-ratio: "16-9", progress-bar: true, - display-current-section: true, + header: utils.display-current-heading(level: 2), + header-right: utils.display-current-heading(level: 1), footer-columns: (25%, 1fr, 25%), footer-a: self => self.info.author, - footer-b: self => if self.info.short-title == auto { self.info.title } else { self.info.short-title }, - footer-c: self => context { + footer-b: self => if self.info.short-title == auto { + self.info.title + } else { + self.info.short-title + }, + footer-c: self => { h(1fr) - utils.info-date(self) + utils.display-info-date(self) h(1fr) - context states.slide-counter.display() + " / " + states.last-slide-number + context utils.slide-counter.display() + " / " + utils.last-slide-number h(1fr) }, ..args, + body, ) = { - // color theme - self = (self.methods.colors)( - self: self, - primary: rgb("#04364A"), - secondary: rgb("#176B87"), - tertiary: rgb("#448C95"), - ) - // save the variables for later use - self.uni-enable-progress-bar = progress-bar - self.uni-progress-bar = self => states.touying-progress(ratio => { - grid( - columns: (ratio * 100%, 1fr), - rows: 2pt, - components.cell(fill: self.colors.primary), - components.cell(fill: self.colors.tertiary) - ) - }) - self.uni-display-current-section = display-current-section - self.uni-title = none - self.uni-subtitle = none - self.uni-footer = self => { - let cell(fill: none, it) = rect( - width: 100%, height: 100%, inset: 1mm, outset: 0mm, fill: fill, stroke: none, - align(horizon, text(fill: white, it)) - ) - show: block.with(width: 100%, height: auto, fill: self.colors.secondary) - grid( - columns: footer-columns, - rows: (1.5em, auto), - cell(fill: self.colors.primary, utils.call-or-display(self, footer-a)), - cell(fill: self.colors.secondary, utils.call-or-display(self, footer-b)), - cell(fill: self.colors.tertiary, utils.call-or-display(self, footer-c)), - ) - } - self.uni-header = self => { - if self.uni-title != none { - block(inset: (x: .5em), - grid( - columns: 1, - gutter: .3em, - grid( - columns: (auto, 1fr, auto), - align(top + left, text(fill: self.colors.primary, weight: "bold", size: 1.2em, self.uni-title)), - [], - if self.uni-display-current-section { - align(top + right, text(fill: self.colors.primary.lighten(65%), states.current-section-with-numbering(self))) - } - ), - text(fill: self.colors.primary.lighten(65%), size: .8em, self.uni-subtitle) - ) - ) - } - } - // set page - let header(self) = { - set align(top) - grid( - rows: (auto, auto), - row-gutter: 3mm, - if self.uni-enable-progress-bar { - utils.call-or-display(self, self.uni-progress-bar) - }, - utils.call-or-display(self, self.uni-header), - ) - } - let footer(self) = { - set text(size: .4em) - set align(center + bottom) - utils.call-or-display(self, self.uni-footer) - } + set text(size: 25pt) - self.page-args += ( - paper: "presentation-" + aspect-ratio, - header: header, - footer: footer, - header-ascent: 0em, - footer-descent: 0em, - margin: (top: 2.5em, bottom: 1.25em, x: 2em), + show: touying-slides.with( + config-page( + paper: "presentation-" + aspect-ratio, + header-ascent: 0em, + footer-descent: 0em, + margin: (top: 2em, bottom: 1.25em, x: 2em), + ), + config-common( + slide-fn: slide, + new-section-slide-fn: new-section-slide, + ), + config-methods( + alert: utils.alert-with-primary-color, + ), + config-colors( + primary: rgb("#04364A"), + secondary: rgb("#176B87"), + tertiary: rgb("#448C95"), + neutral-lightest: rgb("#ffffff"), + neutral-darkest: rgb("#000000"), + ), + // save the variables for later use + config-store( + progress-bar: progress-bar, + header: header, + header-right: header-right, + footer-columns: footer-columns, + footer-a: footer-a, + footer-b: footer-b, + footer-c: footer-c, + ), + ..args, ) - // register methods - self.methods.slide = slide - self.methods.title-slide = title-slide - self.methods.new-section-slide = new-section-slide - self.methods.touying-new-section-slide = new-section-slide - self.methods.focus-slide = focus-slide - self.methods.matrix-slide = matrix-slide - self.methods.slides = slides - self.methods.touying-outline = (self: none, enum-args: (:), ..args) => { - states.touying-outline(self: self, enum-args: (tight: false,) + enum-args, ..args) - } - self.methods.alert = (self: none, it) => text(fill: self.colors.primary, it) - self.methods.init = (self: none, body) => { - set text(size: 25pt) - set heading(outlined: false) - show footnote.entry: set text(size: .6em) - body - } - self -} + + body +} \ No newline at end of file From d841e979e35ba0e0fec60824d4fc37d0653fb48e Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Sat, 31 Aug 2024 10:30:57 +0800 Subject: [PATCH 30/43] fix: fix _default-show-notes --- src/configs.typ | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/configs.typ b/src/configs.typ index b8ad11f96..fa03f7c6e 100644 --- a/src/configs.typ +++ b/src/configs.typ @@ -228,10 +228,10 @@ outset: 0pt, fill: rgb("#CCCCCC"), { - utils.current-section-title + utils.display-current-heading(level: 1, depth: self.slide-level) linebreak() [ --- ] - utils.current-slide-title + utils.display-current-heading(level: 2, depth: self.slide-level) }, ) pad(x: 48pt, utils.current-slide-note) From d6e4737476c910a43616a1ee54a7b01234dafaf5 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Sat, 31 Aug 2024 22:00:13 +0800 Subject: [PATCH 31/43] dev: add progressive-outline and custom-progressive-outline --- examples/dewdrop.typ | 38 ++- src/components.typ | 178 ++++++++++++- themes/dewdrop copy.typ | 318 +++++++++++++++++++++++ themes/dewdrop.typ | 520 +++++++++++++++++-------------------- themes/themes.typ | 2 +- themes/university copy.typ | 288 -------------------- 6 files changed, 745 insertions(+), 599 deletions(-) create mode 100644 themes/dewdrop copy.typ delete mode 100644 themes/university copy.typ diff --git a/examples/dewdrop.typ b/examples/dewdrop.typ index b58e7265c..3a0e6febf 100644 --- a/examples/dewdrop.typ +++ b/examples/dewdrop.typ @@ -1,26 +1,22 @@ #import "../lib.typ": * +#import themes.dewdrop: * -#let s = themes.dewdrop.register( +#import "@preview/numbly:0.1.0": numbly + +#show: dewdrop-theme.with( aspect-ratio: "16-9", - footer: [Dewdrop], - navigation: "mini-slides", - // navigation: none, -) -#let s = (s.methods.info)( - self: s, - title: [Title], - subtitle: [Subtitle], - author: [Authors], - date: datetime.today(), - institution: [Institution], + footer: self => self.info.institution, + config-info( + title: [Title], + subtitle: [Subtitle], + author: [Authors], + date: datetime.today(), + institution: [Institution], + logo: emoji.city, + ), ) -#let (init, slides, touying-outline, alert) = utils.methods(s) -#show: init - -#show strong: alert -#let (slide, empty-slide, title-slide, outline-slide, new-section-slide, focus-slide) = utils.slides(s) -#show: slides +#set heading(numbering: numbly("{1}.", default: "1.1")) = Section A @@ -64,13 +60,11 @@ Meanwhile, #pause we can also use `#meanwhile` to #pause display other content synchronously. ] -// appendix by freezing last-slide-number -#let s = (s.methods.appendix)(self: s) -#let (slide,) = utils.slides(s) +#show: appendix = Appendix -=== Appendix +== Appendix #slide[ Please pay attention to the current slide number. diff --git a/src/components.typ b/src/components.typ index 2e48e321e..56f552a83 100644 --- a/src/components.typ +++ b/src/components.typ @@ -89,4 +89,180 @@ ..bodies.enumerate().map(color-body) ) body -} \ No newline at end of file +} + + +/// Show progressive outline. It will make other sections except the current section to be semi-transparent. +/// +/// - `alpha` is the transparency of the other sections. Default is `60%`. +/// +/// - `level` is the level of the outline. Default is `1`. +/// +/// - `transform` is the transformation applied to the text of the outline. It should take the following arguments: +/// +/// - `cover` is a boolean indicating whether the current entry should be covered. +/// +/// - `..args` are the other arguments passed to the `progressive-outline`. +#let progressive-outline( + alpha: 60%, + level: 1, + transform: (cover: false, alpha: 60%, ..args, it) => if cover { + text(utils.update-alpha(text.fill, alpha), it) + } else { + it + }, + ..args, +) = ( + context { + // start page and end page + let start-page = 1 + let end-page = calc.inf + let current-heading = utils.current-heading(level: level) + if current-heading != none { + start-page = current-heading.location().page() + if level != auto { + let next-headings = query( + selector(heading.where(level: level)).after(inclusive: false, current-heading.location()), + ) + if next-headings != () { + end-page = next-headings.at(0).location().page() + } + } else { + end-page = start-page + 1 + } + } + show outline.entry: it => transform( + cover: it.element.location().page() < start-page or it.element.location().page() >= end-page, + level: level, + alpha: alpha, + ..args, + it, + ) + + outline(..args) + } +) + + +/// Show a custom progressive outline. +/// +/// - `self` is the self context. +/// +/// - `alpha` is the transparency of the other headings. Default is `60%`. +/// +/// - `level` is the level of the outline. Default is `auto`. +/// +/// - `numbered` is a boolean array indicating whether the headings should be numbered. Default is `false`. +/// +/// - `filled` is a boolean array indicating whether the headings should be filled. Default is `false`. +/// +/// - `paged` is a boolean array indicating whether the headings should be paged. Default is `false`. +/// +/// - `text-fill` is an array of colors for the text fill of the headings. Default is `none`. +/// +/// - `text-size` is an array of sizes for the text of the headings. Default is `none`. +/// +/// - `text-weight` is an array of weights for the text of the headings. Default is `none`. +/// +/// - `vspace` is an array of vertical spaces above the headings. Default is `none`. +/// +/// - `title` is the title of the outline. Default is `none`. +/// +/// - `indent` is an array of indentations for the headings. Default is `(0em, )`. +/// +/// - `fill` is an array of fills for the headings. Default is `repeat[.]`. +/// +/// - `short-heading` is a boolean indicating whether the headings should be shortened. Default is `true`. +/// +/// - `uncover-fn` is a function that takes the body of the heading and returns the body of the heading when it is uncovered. Default is the identity function. +/// +/// - `..args` are the other arguments passed to the `progressive-outline` and `transform`. +#let custom-progressive-outline( + self: none, + alpha: 60%, + level: auto, + numbered: (false,), + filled: (false,), + paged: (false,), + text-fill: none, + text-size: none, + text-weight: none, + vspace: none, + title: none, + indent: (0em,), + fill: (repeat[.],), + short-heading: true, + uncover-fn: body => body, + ..args, +) = progressive-outline( + alpha: alpha, + level: level, + transform: (cover: false, alpha: alpha, ..args, it) => { + let array-at(arr, idx) = arr.at(idx, default: arr.last()) + let set-text(level, body) = { + set text(fill: ( + if cover { + utils.update-alpha(array-at(text-fill, level - 1), alpha) + } else { + array-at(text-fill, level - 1) + } + )) if type(text-fill) == array and text-fill.len() > 0 + set text( + size: array-at(text-size, level - 1), + ) if type(text-size) == array and text-size.len() > 0 + set text( + weight: array-at(text-weight, level - 1), + ) if type(text-weight) == array and text-weight.len() > 0 + body + } + let body = { + if type(vspace) == array and vspace.len() > it.level - 1 { + v(vspace.at(it.level - 1)) + } + h(range(1, it.level + 1).map(level => array-at(indent, level - 1)).sum()) + set-text( + it.level, + { + if array-at(numbered, it.level - 1) { + numbering(it.element.numbering, ..counter(heading).at(it.element.location())) + h(.3em) + } + link( + it.element.location(), + { + if short-heading { + utils.short-heading(self: self, it.element) + } else { + it.element.body + } + box( + width: 1fr, + inset: (x: .2em), + if array-at(filled, it.level - 1) { + array-at(fill, level - 1) + }, + ) + if array-at(paged, it.level - 1) { + numbering( + if page.numbering != none { + page.numbering + } else { + "1" + }, + ..counter(page).at(it.element.location()), + ) + } + }, + ) + }, + ) + } + if cover { + body + } else { + uncover-fn(body) + } + }, + title: title, + ..args, +) \ No newline at end of file diff --git a/themes/dewdrop copy.typ b/themes/dewdrop copy.typ new file mode 100644 index 000000000..29df2f415 --- /dev/null +++ b/themes/dewdrop copy.typ @@ -0,0 +1,318 @@ +// This theme is inspired by https://github.com/zbowang/BeamerTheme +// The typst version was written by https://github.com/OrangeX4 + +#import "../slide.typ": s +#import "../src/utils.typ" +#import "../src/states.typ" + +#let slide( + self: none, + subsection: none, + title: none, + footer: auto, + ..args, +) = { + self.page-args += ( + fill: self.colors.neutral-lightest, + ) + if footer != auto { + self.m-footer = footer + } + (self.methods.touying-slide)( + ..args.named(), + self: self, + subsection: subsection, + title: title, + setting: body => { + set text(fill: self.colors.neutral-darkest) + show heading: set text(fill: self.colors.primary) + show: args.named().at("setting", default: body => body) + if self.auto-heading-for-subsection and subsection != none { + heading(level: 1, states.current-subsection-with-numbering(self)) + } + if self.auto-heading and title != none { + heading(level: 2, title) + } + body + }, + ..args.pos(), + ) +} + +#let title-slide( + self: none, + extra: none, + ..args, +) = { + self = utils.empty-page(self) + let info = self.info + args.named() + let content = { + set text(fill: self.colors.neutral-darkest) + set align(center + horizon) + block(width: 100%, inset: 3em, { + block( + fill: self.colors.neutral-light, + inset: 1em, + width: 100%, + radius: 0.2em, + text(size: 1.3em, fill: self.colors.primary, text(weight: "medium", info.title)) + + (if info.subtitle != none { + linebreak() + text(size: 0.9em, fill: self.colors.primary, info.subtitle) + }) + ) + set text(size: .8em) + if info.author != none { + block(spacing: 1em, info.author) + } + v(1em) + if info.date != none { + block(spacing: 1em, utils.info-date(self)) + } + set text(size: .8em) + if info.institution != none { + block(spacing: 1em, info.institution) + } + if extra != none { + block(spacing: 1em, extra) + } + }) + } + (self.methods.touying-slide)(self: self, repeat: none, content) +} + +#let outline-slide(self: none, ..args) = { + (self.methods.slide)(self: self, heading(level: 2, self.outline-title) + parbreak() + (self.methods.touying-outline)(self: self, cover: false)) +} + +#let focus-slide(self: none, body) = { + self = utils.empty-page(self) + self.page-args += ( + fill: self.colors.primary, + margin: 2em, + ) + set text(fill: self.colors.neutral-lightest, size: 1.5em) + (self.methods.touying-slide)(self: self, repeat: none, align(horizon + center, body)) +} + +#let new-section-slide(self: none, section) = { + (self.methods.slide)(self: self, section: section, heading(level: 2, self.outline-title) + parbreak() + (self.methods.touying-outline)(self: self)) +} + +#let d-outline(self: none, enum-args: (:), list-args: (:), cover: true) = states.touying-progress-with-sections(dict => { + let (current-sections, final-sections) = dict + current-sections = current-sections.filter(section => section.loc != none) + final-sections = final-sections.filter(section => section.loc != none) + let current-index = current-sections.len() - 1 + let d-cover(i, body) = if i != current-index and cover { + (self.methods.d-cover)(self: self, body) + } else { + body + } + set enum(..enum-args) + set list(..enum-args) + set text(fill: self.colors.primary) + for (i, section) in final-sections.enumerate() { + d-cover(i, { + enum.item(i + 1, [#link(section.loc, section.title)] + if section.children.filter(it => it.kind != "slide").len() > 0 { + let subsections = section.children.filter(it => it.kind != "slide") + set text(fill: self.colors.neutral-dark, size: 0.9em) + list( + ..subsections.map(subsection => [#link(subsection.loc, subsection.title)]) + ) + }) + }) + parbreak() + } +}) + +#let d-sidebar(self: none) = states.touying-progress-with-sections(dict => { + let (current-sections, final-sections) = dict + current-sections = current-sections + .filter(section => section.loc != none) + .map(section => (section, section.children)) + .flatten() + .filter(item => item.kind != "slide") + final-sections = final-sections + .filter(section => section.loc != none) + .map(section => (section, section.children)) + .flatten() + .filter(item => item.kind != "slide") + let current-index = current-sections.len() - 1 + show: block.with(width: self.d-sidebar.width, inset: (top: 4em, x: 1em)) + set align(left) + set par(justify: false) + set text(size: 0.9em) + for (i, section) in final-sections.enumerate() { + if section.kind == "section" { + set text(fill: if i != current-index { self.colors.primary.lighten(self.d-alpha) } else { self.colors.primary }) + [#link(section.loc, utils.section-short-title(section.title))] + } else { + set text(fill: if i != current-index { self.colors.neutral-dark.lighten(self.d-alpha) } else { self.colors.neutral-dark }, size: 0.9em) + [#link(section.loc, utils.section-short-title(h(.3em) + section.title))] + } + parbreak() + } +}) + +#let d-mini-slides(self: none) = states.touying-progress-with-sections(dict => { + let (current-sections, final-sections) = dict + current-sections = current-sections.filter(section => section.loc != none) + final-sections = final-sections.filter(section => section.loc != none) + let current-i = current-sections.len() - 1 + let cols = () + let current-count = 0 + for (i, section) in current-sections.enumerate() { + if self.d-mini-slides.section { + for slide in section.children.filter(it => it.kind == "slide") { + current-count += 1 + } + } + for subsection in section.children.filter(it => it.kind != "slide") { + for slide in subsection.children { + current-count += 1 + } + } + } + let final-count = 0 + for (i, section) in final-sections.enumerate() { + let primary-color = if i != current-i { self.colors.primary.lighten(self.d-alpha) } else { self.colors.primary } + cols.push({ + set align(left) + set text(fill: primary-color) + [#link(section.loc, utils.section-short-title(section.title))] + linebreak() + if self.d-mini-slides.section { + for slide in section.children.filter(it => it.kind == "slide") { + final-count += 1 + if i == current-i and final-count == current-count { + [#link(slide.loc, sym.circle.filled)] + } else { + [#link(slide.loc, sym.circle)] + } + } + } + if self.d-mini-slides.section and self.d-mini-slides.subsection { + linebreak() + } + for subsection in section.children.filter(it => it.kind != "slide") { + for slide in subsection.children { + final-count += 1 + if i == current-i and final-count == current-count { + [#link(slide.loc, sym.circle.filled)] + } else { + [#link(slide.loc, sym.circle)] + } + } + if self.d-mini-slides.subsection { + linebreak() + } + } + }) + } + set align(top) + show: block.with(inset: (top: .5em, x: 2em)) + show linebreak: it => it + v(-1em) + set text(size: .7em) + grid(columns: cols.map(_ => auto).intersperse(1fr), ..cols.intersperse([])) +}) + +#let slides(self: none, title-slide: true, outline-slide: true, slide-level: 2, ..args) = { + if title-slide { + (self.methods.title-slide)(self: self) + } + if outline-slide { + (self.methods.outline-slide)(self: self) + } + (self.methods.touying-slides)(self: self, slide-level: slide-level, ..args) +} + +#let register( + self: s, + aspect-ratio: "16-9", + navigation: "sidebar", + sidebar: (width: 10em), + mini-slides: (height: 4em, x: 2em, section: false, subsection: true), + footer: [], + footer-right: context { states.slide-counter.display() + " / " + states.last-slide-number }, + primary: rgb("#0c4842"), + alpha: 70%, + ..args, +) = { + assert(navigation in ("sidebar", "mini-slides", none), message: "navigation must be one of sidebar, mini-slides, none") + // color theme + self = (self.methods.colors)( + self: self, + neutral-darkest: rgb("#000000"), + neutral-dark: rgb("#202020"), + neutral-light: rgb("#f3f3f3"), + neutral-lightest: rgb("#ffffff"), + primary: primary, + ) + // save the variables for later use + self.d-navigation = navigation + self.d-sidebar = sidebar + self.d-mini-slides = mini-slides + self.d-footer = footer + self.d-footer-right = footer-right + self.d-alpha = alpha + self.auto-heading = true + self.auto-heading-for-subsection = true + self.outline-title = [Outline] + // set page + let header(self) = { + if self.d-navigation == "sidebar" { + place(right + top, (self.methods.d-sidebar)(self: self)) + } else if self.d-navigation == "mini-slides" { + (self.methods.d-mini-slides)(self: self) + } + } + let footer(self) = { + set text(size: 0.8em) + set align(bottom) + show: pad.with(.5em) + text(fill: self.colors.neutral-darkest.lighten(40%), utils.call-or-display(self, self.d-footer)) + h(1fr) + text(fill: self.colors.neutral-darkest.lighten(20%), utils.call-or-display(self, self.d-footer-right)) + } + self.page-args += ( + paper: "presentation-" + aspect-ratio, + fill: self.colors.neutral-lightest, + header: header, + footer: footer, + header-ascent: 0em, + footer-descent: 0em, + ) + if navigation == "sidebar" {( + margin: (top: 2em, bottom: 1em, x: sidebar.width), + )} else if navigation == "mini-slides" {( + margin: (top: mini-slides.height, bottom: 2em, x: mini-slides.x), + )} else {( + margin: (top: 2em, bottom: 2em, x: mini-slides.x), + )} + self = (self.methods.numbering)(self: self, section: "1.", "1.1") + // register methods + self.methods.slide = slide + self.methods.title-slide = title-slide + self.methods.outline-slide = outline-slide + self.methods.focus-slide = focus-slide + self.methods.new-section-slide = new-section-slide + self.methods.touying-new-section-slide = new-section-slide + self.methods.slides = slides + self.methods.d-cover = (self: none, body) => { + utils.cover-with-rect(fill: utils.update-alpha( + constructor: rgb, self.page-args.fill, self.d-alpha), body) + } + self.methods.touying-outline = d-outline + self.methods.d-outline = d-outline + self.methods.d-sidebar = d-sidebar + self.methods.d-mini-slides = d-mini-slides + self.methods.alert = (self: none, it) => text(fill: self.colors.primary, it) + self.methods.init = (self: none, body) => { + set heading(outlined: false) + set text(size: 20pt) + set par(justify: true) + show heading: set block(below: 1em) + body + } + self +} diff --git a/themes/dewdrop.typ b/themes/dewdrop.typ index 91635111a..6163e1ec5 100644 --- a/themes/dewdrop.typ +++ b/themes/dewdrop.typ @@ -1,318 +1,264 @@ // This theme is inspired by https://github.com/zbowang/BeamerTheme // The typst version was written by https://github.com/OrangeX4 -#import "../slide.typ": s -#import "../src/utils.typ" -#import "../src/states.typ" +#import "../src/exports.typ": * +#let _typst-builtin-repeat = repeat + +/// Default slide function for the presentation. +/// +/// - `config` is the configuration of the slide. You can use `config-xxx` to set the configuration of the slide. For more several configurations, you can use `utils.merge-dicts` to merge them. +/// +/// - `repeat` is the number of subslides. Default is `auto`,which means touying will automatically calculate the number of subslides. +/// +/// The `repeat` argument is necessary when you use `#slide(repeat: 3, self => [ .. ])` style code to create a slide. The callback-style `uncover` and `only` cannot be detected by touying automatically. +/// +/// - `setting` is the setting of the slide. You can use it to add some set/show rules for the slide. +/// +/// - `composer` is the composer of the slide. You can use it to set the layout of the slide. +/// +/// For example, `#slide(composer: (1fr, 2fr, 1fr))[A][B][C]` to split the slide into three parts. The first and the last parts will take 1/4 of the slide, and the second part will take 1/2 of the slide. +/// +/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `utils.side-by-side` function. +/// +/// The `utils.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. +/// +/// For example, `#slide(composer: 2)[A][B][#grid.cell(colspan: 2)[Footer]] will make the `Footer` cell take 2 columns. +/// +/// If you want to customize the composer, you can pass a function to the `composer` argument. The function should receive the contents of the slide and return the content of the slide, like `#slide(composer: grid.with(columns: 2))[A][B]`. +/// +/// - `..bodies` is the contents of the slide. You can call the `slide` function with syntax like `#slide[A][B][C]` to create a slide. #let slide( - self: none, - subsection: none, - title: none, - footer: auto, - ..args, -) = { - self.page-args += ( - fill: self.colors.neutral-lightest, - ) - if footer != auto { - self.m-footer = footer + config: (:), + repeat: auto, + setting: body => body, + composer: auto, + ..bodies, +) = touying-slide-wrapper(self => { + let header(self) = { + if self.store.navigation == "sidebar" { + place( + right + top, + { + v(4em) + show: block.with(width: self.store.sidebar.width, inset: (x: 1em)) + set align(left) + set par(justify: false) + set text(size: .9em) + components.custom-progressive-outline( + self: self, + level: auto, + alpha: self.store.alpha, + text-fill: (self.colors.primary, self.colors.neutral-darkest), + text-size: (1em, .9em), + vspace: (-.2em,), + indent: (0em, self.store.sidebar.at("indent", default: .5em)), + fill: (self.store.sidebar.at("fill", default: _typst-builtin-repeat[.]),), + filled: (self.store.sidebar.at("filled", default: false),), + paged: (self.store.sidebar.at("paged", default: false),), + short-heading: self.store.sidebar.at("short-heading", default: true), + ) + }, + ) + } else if self.store.navigation == "mini-slides" { + // (self.methods.d-mini-slides)(self: self) + } } - (self.methods.touying-slide)( - ..args.named(), - self: self, - subsection: subsection, - title: title, - setting: body => { - set text(fill: self.colors.neutral-darkest) - show heading: set text(fill: self.colors.primary) - show: args.named().at("setting", default: body => body) - if self.auto-heading-for-subsection and subsection != none { - heading(level: 1, states.current-subsection-with-numbering(self)) - } - if self.auto-heading and title != none { - heading(level: 2, title) - } - body - }, - ..args.pos(), + let footer(self) = { + set align(bottom) + set text(size: 0.8em) + show: pad.with(.5em) + components.left-and-right( + text(fill: self.colors.neutral-darkest.lighten(40%), utils.call-or-display(self, self.store.footer)), + text(fill: self.colors.neutral-darkest.lighten(20%), utils.call-or-display(self, self.store.footer-right)), + ) + } + let self = utils.merge-dicts( + self, + config-page( + fill: self.colors.neutral-lightest, + header: header, + footer: footer, + ), ) -} + let new-setting(body) = { + set text(fill: self.colors.neutral-darkest) + setting(body) + } + touying-slide(self: self, config: config, repeat: repeat, setting: setting, composer: composer, ..bodies) +}) + +/// Title slide for the presentation. You should update the information in the `config-info` function. You can also pass the information directly to the `title-slide` function. +/// +/// Example: +/// +/// ```typst +/// #show: dewdrop-theme.with( +/// config-info( +/// title: [Title], +/// logo: emoji.city, +/// ), +/// ) +/// +/// #title-slide(subtitle: [Subtitle], extra: [Extra information]) +/// ``` +/// +/// - `extra` is the extra information you want to display on the title slide. #let title-slide( - self: none, extra: none, ..args, -) = { - self = utils.empty-page(self) +) = touying-slide-wrapper(self => { let info = self.info + args.named() - let content = { + let body = { set text(fill: self.colors.neutral-darkest) set align(center + horizon) - block(width: 100%, inset: 3em, { - block( - fill: self.colors.neutral-light, - inset: 1em, - width: 100%, - radius: 0.2em, - text(size: 1.3em, fill: self.colors.primary, text(weight: "medium", info.title)) - + (if info.subtitle != none { - linebreak() - text(size: 0.9em, fill: self.colors.primary, info.subtitle) - }) - ) - set text(size: .8em) - if info.author != none { - block(spacing: 1em, info.author) - } - v(1em) - if info.date != none { - block(spacing: 1em, utils.info-date(self)) - } - set text(size: .8em) - if info.institution != none { - block(spacing: 1em, info.institution) - } - if extra != none { - block(spacing: 1em, extra) - } - }) + block( + width: 100%, + inset: 3em, + { + block( + fill: self.colors.neutral-light, + inset: 1em, + width: 100%, + radius: 0.2em, + text(size: 1.3em, fill: self.colors.primary, text(weight: "medium", info.title)) + ( + if info.subtitle != none { + linebreak() + text(size: 0.9em, fill: self.colors.primary, info.subtitle) + } + ), + ) + set text(size: .8em) + if info.author != none { + block(spacing: 1em, info.author) + } + v(1em) + if info.date != none { + block(spacing: 1em, utils.info-date(self)) + } + set text(size: .8em) + if info.institution != none { + block(spacing: 1em, info.institution) + } + if extra != none { + block(spacing: 1em, extra) + } + }, + ) } - (self.methods.touying-slide)(self: self, repeat: none, content) -} - -#let outline-slide(self: none, ..args) = { - (self.methods.slide)(self: self, heading(level: 2, self.outline-title) + parbreak() + (self.methods.touying-outline)(self: self, cover: false)) -} - -#let focus-slide(self: none, body) = { - self = utils.empty-page(self) - self.page-args += ( - fill: self.colors.primary, - margin: 2em, + self = utils.merge-dicts( + self, + config-common(freeze-slide-counter: true), + config-page(fill: self.colors.neutral-lightest), ) - set text(fill: self.colors.neutral-lightest, size: 1.5em) - (self.methods.touying-slide)(self: self, repeat: none, align(horizon + center, body)) -} + touying-slide(self: self, body) +}) -#let new-section-slide(self: none, section) = { - (self.methods.slide)(self: self, section: section, heading(level: 2, self.outline-title) + parbreak() + (self.methods.touying-outline)(self: self)) -} -#let d-outline(self: none, enum-args: (:), list-args: (:), cover: true) = states.touying-progress-with-sections(dict => { - let (current-sections, final-sections) = dict - current-sections = current-sections.filter(section => section.loc != none) - final-sections = final-sections.filter(section => section.loc != none) - let current-index = current-sections.len() - 1 - let d-cover(i, body) = if i != current-index and cover { - (self.methods.d-cover)(self: self, body) - } else { - body - } - set enum(..enum-args) - set list(..enum-args) - set text(fill: self.colors.primary) - for (i, section) in final-sections.enumerate() { - d-cover(i, { - enum.item(i + 1, [#link(section.loc, section.title)] + if section.children.filter(it => it.kind != "slide").len() > 0 { - let subsections = section.children.filter(it => it.kind != "slide") - set text(fill: self.colors.neutral-dark, size: 0.9em) - list( - ..subsections.map(subsection => [#link(subsection.loc, subsection.title)]) - ) - }) - }) - parbreak() - } +/// New section slide for the presentation. You can update it by updating the `new-section-slide-fn` argument for `config-common` function. +/// +/// Example: `config-common(new-section-slide-fn: new-section-slide.with(numbered: false))` +/// +/// - `title` is the title of the section. It will be pass by touying automatically. +#let new-section-slide(..args, title) = touying-slide-wrapper(self => { + self = utils.merge-dicts( + self, + config-page(fill: self.colors.neutral-lightest), + ) + touying-slide( + self: self, + { + text(1.2em, fill: self.colors.primary, weight: "bold", utils.call-or-display(self, self.store.outline-title)) + text( + fill: self.colors.neutral-darkest, + components.progressive-outline(alpha: self.store.alpha, title: none, indent: 1em, ..args), + ) + }, + ) }) -#let d-sidebar(self: none) = states.touying-progress-with-sections(dict => { - let (current-sections, final-sections) = dict - current-sections = current-sections - .filter(section => section.loc != none) - .map(section => (section, section.children)) - .flatten() - .filter(item => item.kind != "slide") - final-sections = final-sections - .filter(section => section.loc != none) - .map(section => (section, section.children)) - .flatten() - .filter(item => item.kind != "slide") - let current-index = current-sections.len() - 1 - show: block.with(width: self.d-sidebar.width, inset: (top: 4em, x: 1em)) - set align(left) - set par(justify: false) - set text(size: 0.9em) - for (i, section) in final-sections.enumerate() { - if section.kind == "section" { - set text(fill: if i != current-index { self.colors.primary.lighten(self.d-alpha) } else { self.colors.primary }) - [#link(section.loc, utils.section-short-title(section.title))] - } else { - set text(fill: if i != current-index { self.colors.neutral-dark.lighten(self.d-alpha) } else { self.colors.neutral-dark }, size: 0.9em) - [#link(section.loc, utils.section-short-title(h(.3em) + section.title))] - } - parbreak() - } -}) -#let d-mini-slides(self: none) = states.touying-progress-with-sections(dict => { - let (current-sections, final-sections) = dict - current-sections = current-sections.filter(section => section.loc != none) - final-sections = final-sections.filter(section => section.loc != none) - let current-i = current-sections.len() - 1 - let cols = () - let current-count = 0 - for (i, section) in current-sections.enumerate() { - if self.d-mini-slides.section { - for slide in section.children.filter(it => it.kind == "slide") { - current-count += 1 - } - } - for subsection in section.children.filter(it => it.kind != "slide") { - for slide in subsection.children { - current-count += 1 - } - } - } - let final-count = 0 - for (i, section) in final-sections.enumerate() { - let primary-color = if i != current-i { self.colors.primary.lighten(self.d-alpha) } else { self.colors.primary } - cols.push({ - set align(left) - set text(fill: primary-color) - [#link(section.loc, utils.section-short-title(section.title))] - linebreak() - if self.d-mini-slides.section { - for slide in section.children.filter(it => it.kind == "slide") { - final-count += 1 - if i == current-i and final-count == current-count { - [#link(slide.loc, sym.circle.filled)] - } else { - [#link(slide.loc, sym.circle)] - } - } - } - if self.d-mini-slides.section and self.d-mini-slides.subsection { - linebreak() - } - for subsection in section.children.filter(it => it.kind != "slide") { - for slide in subsection.children { - final-count += 1 - if i == current-i and final-count == current-count { - [#link(slide.loc, sym.circle.filled)] - } else { - [#link(slide.loc, sym.circle)] - } - } - if self.d-mini-slides.subsection { - linebreak() - } - } - }) - } - set align(top) - show: block.with(inset: (top: .5em, x: 2em)) - show linebreak: it => it + v(-1em) - set text(size: .7em) - grid(columns: cols.map(_ => auto).intersperse(1fr), ..cols.intersperse([])) +/// Focus on some content. +/// +/// Example: `#focus-slide[Wake up!]` +/// +/// - `align` is the alignment of the content. Default is `horizon + center`. +#let focus-slide(body) = touying-slide-wrapper(self => { + self = utils.merge-dicts( + self, + config-common(freeze-slide-counter: true), + config-page(fill: self.colors.primary, margin: 2em), + ) + set text(fill: self.colors.neutral-lightest, size: 1.5em) + touying-slide(self: self, align(horizon + center, body)) }) -#let slides(self: none, title-slide: true, outline-slide: true, slide-level: 2, ..args) = { - if title-slide { - (self.methods.title-slide)(self: self) - } - if outline-slide { - (self.methods.outline-slide)(self: self) - } - (self.methods.touying-slides)(self: self, slide-level: slide-level, ..args) -} -#let register( - self: s, +/// Touying dewdrop theme. +/// +/// Example: +/// +/// ```typst +/// #show: dewdrop-theme.with(aspect-ratio: "16-9", config-colors(primary: blue))` +/// ``` +/// +/// - `aspect-ratio` is the aspect ratio of the slides. Default is `16-9`. +#let dewdrop-theme( aspect-ratio: "16-9", navigation: "sidebar", - sidebar: (width: 10em), + sidebar: (width: 10em, filled: false, numbered: false, indent: .5em), mini-slides: (height: 4em, x: 2em, section: false, subsection: true), - footer: [], - footer-right: context { states.slide-counter.display() + " / " + states.last-slide-number }, + footer: none, + footer-right: context utils.slide-counter.display() + " / " + utils.last-slide-number, primary: rgb("#0c4842"), - alpha: 70%, + alpha: 60%, + outline-title: [Outline], ..args, + body, ) = { - assert(navigation in ("sidebar", "mini-slides", none), message: "navigation must be one of sidebar, mini-slides, none") - // color theme - self = (self.methods.colors)( - self: self, - neutral-darkest: rgb("#000000"), - neutral-dark: rgb("#202020"), - neutral-light: rgb("#f3f3f3"), - neutral-lightest: rgb("#ffffff"), - primary: primary, + set text(size: 20pt) + set par(justify: true) + + show: touying-slides.with( + config-page( + paper: "presentation-" + aspect-ratio, + header-ascent: 0em, + footer-descent: 0em, + margin: if navigation == "sidebar" { + (top: 2em, bottom: 1em, x: sidebar.width) + } else if navigation == "mini-slides" { + (top: mini-slides.height, bottom: 2em, x: mini-slides.x) + } else { + (top: 2em, bottom: 2em, x: mini-slides.x) + }, + ), + config-common( + slide-fn: slide, + new-section-slide-fn: new-section-slide, + ), + config-methods( + alert: utils.alert-with-primary-color, + ), + config-colors( + neutral-darkest: rgb("#000000"), + neutral-dark: rgb("#202020"), + neutral-light: rgb("#f3f3f3"), + neutral-lightest: rgb("#ffffff"), + primary: primary, + ), + // save the variables for later use + config-store( + navigation: navigation, + sidebar: sidebar, + mini-slides: mini-slides, + footer: footer, + footer-right: footer-right, + alpha: alpha, + outline-title: outline-title, + ), + ..args, ) - // save the variables for later use - self.d-navigation = navigation - self.d-sidebar = sidebar - self.d-mini-slides = mini-slides - self.d-footer = footer - self.d-footer-right = footer-right - self.d-alpha = alpha - self.auto-heading = true - self.auto-heading-for-subsection = true - self.outline-title = [Outline] - // set page - let header(self) = { - if self.d-navigation == "sidebar" { - place(right + top, (self.methods.d-sidebar)(self: self)) - } else if self.d-navigation == "mini-slides" { - (self.methods.d-mini-slides)(self: self) - } - } - let footer(self) = { - set text(size: 0.8em) - set align(bottom) - show: pad.with(.5em) - text(fill: self.colors.neutral-darkest.lighten(40%), utils.call-or-display(self, self.d-footer)) - h(1fr) - text(fill: self.colors.neutral-darkest.lighten(20%), utils.call-or-display(self, self.d-footer-right)) - } - self.page-args += ( - paper: "presentation-" + aspect-ratio, - fill: self.colors.neutral-lightest, - header: header, - footer: footer, - header-ascent: 0em, - footer-descent: 0em, - ) + if navigation == "sidebar" {( - margin: (top: 2em, bottom: 1em, x: sidebar.width), - )} else if navigation == "mini-slides" {( - margin: (top: mini-slides.height, bottom: 2em, x: mini-slides.x), - )} else {( - margin: (top: 2em, bottom: 2em, x: mini-slides.x), - )} - self = (self.methods.numbering)(self: self, section: "1.", "1.1") - // register methods - self.methods.slide = slide - self.methods.title-slide = title-slide - self.methods.outline-slide = outline-slide - self.methods.focus-slide = focus-slide - self.methods.new-section-slide = new-section-slide - self.methods.touying-new-section-slide = new-section-slide - self.methods.slides = slides - self.methods.d-cover = (self: none, body) => { - utils.cover-with-rect(fill: utils.update-alpha( - constructor: rgb, self.page-args.fill, self.d-alpha), body) - } - self.methods.touying-outline = d-outline - self.methods.d-outline = d-outline - self.methods.d-sidebar = d-sidebar - self.methods.d-mini-slides = d-mini-slides - self.methods.alert = (self: none, it) => text(fill: self.colors.primary, it) - self.methods.init = (self: none, body) => { - set heading(outlined: false) - set text(size: 20pt) - set par(justify: true) - show heading: set block(below: 1em) - body - } - self -} + + body +} \ No newline at end of file diff --git a/themes/themes.typ b/themes/themes.typ index c507988a0..ee185a2ca 100644 --- a/themes/themes.typ +++ b/themes/themes.typ @@ -1,6 +1,6 @@ #import "default.typ" #import "simple.typ" #import "metropolis.typ" -// #import "dewdrop.typ" +#import "dewdrop.typ" #import "university.typ" // #import "aqua.typ" \ No newline at end of file diff --git a/themes/university copy.typ b/themes/university copy.typ deleted file mode 100644 index 5ec83ca0b..000000000 --- a/themes/university copy.typ +++ /dev/null @@ -1,288 +0,0 @@ -// University theme - -// Originally contributed by Pol Dellaiera - https://github.com/drupol - -#import "../slide.typ": s -#import "../src/utils.typ" -#import "../src/states.typ" -#import "../src/components.typ" - -#let slide( - self: none, - title: auto, - subtitle: auto, - header: auto, - footer: auto, - display-current-section: auto, - ..args, -) = { - if title != auto { - self.uni-title = title - } - if subtitle != auto { - self.uni-subtitle = subtitle - } - if header != auto { - self.uni-header = header - } - if footer != auto { - self.uni-footer = footer - } - if display-current-section != auto { - self.uni-display-current-section = display-current-section - } - (self.methods.touying-slide)( - ..args.named(), - self: self, - title: title, - setting: body => { - show: args.named().at("setting", default: body => body) - body - }, - ..args.pos(), - ) -} - -#let title-slide(self: none, ..args) = { - self = utils.empty-page(self) - let info = self.info + args.named() - info.authors = { - let authors = if "authors" in info { info.authors } else { info.author } - if type(authors) == array { authors } else { (authors,) } - } - let content = { - if info.logo != none { - align(right, info.logo) - } - align(center + horizon, { - block( - inset: 0em, - breakable: false, - { - text(size: 2em, fill: self.colors.primary, strong(info.title)) - if info.subtitle != none { - parbreak() - text(size: 1.2em, fill: self.colors.primary, info.subtitle) - } - } - ) - set text(size: .8em) - grid( - columns: (1fr,) * calc.min(info.authors.len(), 3), - column-gutter: 1em, - row-gutter: 1em, - ..info.authors.map(author => text(fill: black, author)) - ) - v(1em) - if info.institution != none { - parbreak() - text(size: .9em, info.institution) - } - if info.date != none { - parbreak() - text(size: .8em, utils.info-date(self)) - } - }) - } - (self.methods.touying-slide)(self: self, repeat: none, content) -} - -#let new-section-slide(self: none, short-title: auto, title) = { - self = utils.empty-page(self) - let content(self) = { - set align(horizon) - show: pad.with(20%) - set text(size: 1.5em, fill: self.colors.primary, weight: "bold") - states.current-section-with-numbering(self) - v(-.5em) - block(height: 2pt, width: 100%, spacing: 0pt, utils.call-or-display(self, self.uni-progress-bar)) - } - (self.methods.touying-slide)(self: self, repeat: none, section: (title: title, short-title: short-title), content) -} - -#let focus-slide(self: none, background-color: none, background-img: none, body) = { - let background-color = if background-img == none and background-color == none { - rgb(self.colors.primary) - } else { - background-color - } - self = utils.empty-page(self) - self.page-args += ( - fill: self.colors.primary-dark, - margin: 1em, - ..(if background-color != none { (fill: background-color) }), - ..(if background-img != none { (background: { - set image(fit: "stretch", width: 100%, height: 100%) - background-img - }) - }), - ) - set text(fill: white, weight: "bold", size: 2em) - (self.methods.touying-slide)(self: self, repeat: none, align(horizon, body)) -} - -#let matrix-slide(self: none, columns: none, rows: none, ..bodies) = { - self = utils.empty-page(self) - (self.methods.touying-slide)(self: self, composer: (..bodies) => { - let bodies = bodies.pos() - let columns = if type(columns) == int { - (1fr,) * columns - } else if columns == none { - (1fr,) * bodies.len() - } else { - columns - } - let num-cols = columns.len() - let rows = if type(rows) == int { - (1fr,) * rows - } else if rows == none { - let quotient = calc.quo(bodies.len(), num-cols) - let correction = if calc.rem(bodies.len(), num-cols) == 0 { 0 } else { 1 } - (1fr,) * (quotient + correction) - } else { - rows - } - let num-rows = rows.len() - if num-rows * num-cols < bodies.len() { - panic("number of rows (" + str(num-rows) + ") * number of columns (" + str(num-cols) + ") must at least be number of content arguments (" + str(bodies.len()) + ")") - } - let cart-idx(i) = (calc.quo(i, num-cols), calc.rem(i, num-cols)) - let color-body(idx-body) = { - let (idx, body) = idx-body - let (row, col) = cart-idx(idx) - let color = if calc.even(row + col) { white } else { silver } - set align(center + horizon) - rect(inset: .5em, width: 100%, height: 100%, fill: color, body) - } - let content = grid( - columns: columns, rows: rows, - gutter: 0pt, - ..bodies.enumerate().map(color-body) - ) - content - }, ..bodies) -} - -#let slides(self: none, title-slide: true, slide-level: 1, ..args) = { - if title-slide { - (self.methods.title-slide)(self: self) - } - (self.methods.touying-slides)(self: self, slide-level: slide-level, ..args) -} - -#let register( - self: s, - aspect-ratio: "16-9", - progress-bar: true, - display-current-section: true, - footer-columns: (25%, 1fr, 25%), - footer-a: self => self.info.author, - footer-b: self => if self.info.short-title == auto { self.info.title } else { self.info.short-title }, - footer-c: self => context { - h(1fr) - utils.info-date(self) - h(1fr) - context states.slide-counter.display() + " / " + states.last-slide-number - h(1fr) - }, - ..args, -) = { - // color theme - self = (self.methods.colors)( - self: self, - primary: rgb("#04364A"), - secondary: rgb("#176B87"), - tertiary: rgb("#448C95"), - ) - // save the variables for later use - self.uni-enable-progress-bar = progress-bar - self.uni-progress-bar = self => states.touying-progress(ratio => { - grid( - columns: (ratio * 100%, 1fr), - rows: 2pt, - components.cell(fill: self.colors.primary), - components.cell(fill: self.colors.tertiary) - ) - }) - self.uni-display-current-section = display-current-section - self.uni-title = none - self.uni-subtitle = none - self.uni-footer = self => { - let cell(fill: none, it) = rect( - width: 100%, height: 100%, inset: 1mm, outset: 0mm, fill: fill, stroke: none, - align(horizon, text(fill: white, it)) - ) - show: block.with(width: 100%, height: auto, fill: self.colors.secondary) - grid( - columns: footer-columns, - rows: (1.5em, auto), - cell(fill: self.colors.primary, utils.call-or-display(self, footer-a)), - cell(fill: self.colors.secondary, utils.call-or-display(self, footer-b)), - cell(fill: self.colors.tertiary, utils.call-or-display(self, footer-c)), - ) - } - self.uni-header = self => { - if self.uni-title != none { - block(inset: (x: .5em), - grid( - columns: 1, - gutter: .3em, - grid( - columns: (auto, 1fr, auto), - align(top + left, text(fill: self.colors.primary, weight: "bold", size: 1.2em, self.uni-title)), - [], - if self.uni-display-current-section { - align(top + right, text(fill: self.colors.primary.lighten(65%), states.current-section-with-numbering(self))) - } - ), - text(fill: self.colors.primary.lighten(65%), size: .8em, self.uni-subtitle) - ) - ) - } - } - // set page - let header(self) = { - set align(top) - grid( - rows: (auto, auto), - row-gutter: 3mm, - if self.uni-enable-progress-bar { - utils.call-or-display(self, self.uni-progress-bar) - }, - utils.call-or-display(self, self.uni-header), - ) - } - let footer(self) = { - set text(size: .4em) - set align(center + bottom) - utils.call-or-display(self, self.uni-footer) - } - - self.page-args += ( - paper: "presentation-" + aspect-ratio, - header: header, - footer: footer, - header-ascent: 0em, - footer-descent: 0em, - margin: (top: 2.5em, bottom: 1.25em, x: 2em), - ) - // register methods - self.methods.slide = slide - self.methods.title-slide = title-slide - self.methods.new-section-slide = new-section-slide - self.methods.touying-new-section-slide = new-section-slide - self.methods.focus-slide = focus-slide - self.methods.matrix-slide = matrix-slide - self.methods.slides = slides - self.methods.touying-outline = (self: none, enum-args: (:), ..args) => { - states.touying-outline(self: self, enum-args: (tight: false,) + enum-args, ..args) - } - self.methods.alert = (self: none, it) => text(fill: self.colors.primary, it) - self.methods.init = (self: none, body) => { - set text(size: 25pt) - set heading(outlined: false) - show footnote.entry: set text(size: .6em) - body - } - self -} From 343c5de2ce37f715c6db47902797a566f028017f Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Sat, 31 Aug 2024 22:33:39 +0800 Subject: [PATCH 32/43] fix: fix dewdrop theme --- examples/dewdrop.typ | 5 +- themes/dewdrop.typ | 118 +++++++++++++++++++++++++++---------------- 2 files changed, 79 insertions(+), 44 deletions(-) diff --git a/examples/dewdrop.typ b/examples/dewdrop.typ index 3a0e6febf..d49831147 100644 --- a/examples/dewdrop.typ +++ b/examples/dewdrop.typ @@ -12,12 +12,15 @@ author: [Authors], date: datetime.today(), institution: [Institution], - logo: emoji.city, ), ) #set heading(numbering: numbly("{1}.", default: "1.1")) +#title-slide() + +#outline-slide() + = Section A == Subsection A.1 diff --git a/themes/dewdrop.typ b/themes/dewdrop.typ index 6163e1ec5..c54cb8246 100644 --- a/themes/dewdrop.typ +++ b/themes/dewdrop.typ @@ -5,6 +5,46 @@ #let _typst-builtin-repeat = repeat +#let dewdrop-header(self) = { + if self.store.navigation == "sidebar" { + place( + right + top, + { + v(4em) + show: block.with(width: self.store.sidebar.width, inset: (x: 1em)) + set align(left) + set par(justify: false) + set text(size: .9em) + components.custom-progressive-outline( + self: self, + level: auto, + alpha: self.store.alpha, + text-fill: (self.colors.primary, self.colors.neutral-darkest), + text-size: (1em, .9em), + vspace: (-.2em,), + indent: (0em, self.store.sidebar.at("indent", default: .5em)), + fill: (self.store.sidebar.at("fill", default: _typst-builtin-repeat[.]),), + filled: (self.store.sidebar.at("filled", default: false),), + paged: (self.store.sidebar.at("paged", default: false),), + short-heading: self.store.sidebar.at("short-heading", default: true), + ) + }, + ) + } else if self.store.navigation == "mini-slides" { + // (self.methods.d-mini-slides)(self: self) + } +} + +#let dewdrop-footer(self) = { + set align(bottom) + set text(size: 0.8em) + show: pad.with(.5em) + components.left-and-right( + text(fill: self.colors.neutral-darkest.lighten(40%), utils.call-or-display(self, self.store.footer)), + text(fill: self.colors.neutral-darkest.lighten(20%), utils.call-or-display(self, self.store.footer-right)), + ) +} + /// Default slide function for the presentation. /// /// - `config` is the configuration of the slide. You can use `config-xxx` to set the configuration of the slide. For more several configurations, you can use `utils.merge-dicts` to merge them. @@ -35,51 +75,14 @@ composer: auto, ..bodies, ) = touying-slide-wrapper(self => { - let header(self) = { - if self.store.navigation == "sidebar" { - place( - right + top, - { - v(4em) - show: block.with(width: self.store.sidebar.width, inset: (x: 1em)) - set align(left) - set par(justify: false) - set text(size: .9em) - components.custom-progressive-outline( - self: self, - level: auto, - alpha: self.store.alpha, - text-fill: (self.colors.primary, self.colors.neutral-darkest), - text-size: (1em, .9em), - vspace: (-.2em,), - indent: (0em, self.store.sidebar.at("indent", default: .5em)), - fill: (self.store.sidebar.at("fill", default: _typst-builtin-repeat[.]),), - filled: (self.store.sidebar.at("filled", default: false),), - paged: (self.store.sidebar.at("paged", default: false),), - short-heading: self.store.sidebar.at("short-heading", default: true), - ) - }, - ) - } else if self.store.navigation == "mini-slides" { - // (self.methods.d-mini-slides)(self: self) - } - } - let footer(self) = { - set align(bottom) - set text(size: 0.8em) - show: pad.with(.5em) - components.left-and-right( - text(fill: self.colors.neutral-darkest.lighten(40%), utils.call-or-display(self, self.store.footer)), - text(fill: self.colors.neutral-darkest.lighten(20%), utils.call-or-display(self, self.store.footer-right)), - ) - } let self = utils.merge-dicts( self, config-page( fill: self.colors.neutral-lightest, - header: header, - footer: footer, + header: dewdrop-header, + footer: dewdrop-footer, ), + config-common(subslide-preamble: self.store.subslide-preamble), ) let new-setting(body) = { set text(fill: self.colors.neutral-darkest) @@ -135,7 +138,7 @@ } v(1em) if info.date != none { - block(spacing: 1em, utils.info-date(self)) + block(spacing: 1em, utils.display-info-date(self)) } set text(size: .8em) if info.institution != none { @@ -150,12 +153,34 @@ self = utils.merge-dicts( self, config-common(freeze-slide-counter: true), - config-page(fill: self.colors.neutral-lightest), + config-page(fill: self.colors.neutral-lightest, margin: 0em), ) touying-slide(self: self, body) }) +/// Outline slide for the presentation. +#let outline-slide(..args) = touying-slide-wrapper(self => { + self = utils.merge-dicts( + self, + config-page( + fill: self.colors.neutral-lightest, + footer: dewdrop-footer, + ), + ) + touying-slide( + self: self, + { + text(1.2em, fill: self.colors.primary, weight: "bold", utils.call-or-display(self, self.store.outline-title)) + text( + fill: self.colors.neutral-darkest, + outline(title: none, indent: 1em, ..args), + ) + }, + ) +}) + + /// New section slide for the presentation. You can update it by updating the `new-section-slide-fn` argument for `config-common` function. /// /// Example: `config-common(new-section-slide-fn: new-section-slide.with(numbered: false))` @@ -164,7 +189,10 @@ #let new-section-slide(..args, title) = touying-slide-wrapper(self => { self = utils.merge-dicts( self, - config-page(fill: self.colors.neutral-lightest), + config-page( + fill: self.colors.neutral-lightest, + footer: dewdrop-footer, + ), ) touying-slide( self: self, @@ -214,6 +242,9 @@ primary: rgb("#0c4842"), alpha: 60%, outline-title: [Outline], + subslide-preamble: self => block( + text(1.2em, weight: "bold", fill: self.colors.primary, utils.display-current-heading(depth: self.slide-level)), + ), ..args, body, ) = { @@ -256,6 +287,7 @@ footer-right: footer-right, alpha: alpha, outline-title: outline-title, + subslide-preamble: subslide-preamble, ), ..args, ) From eae43885ea33de6508e108484cb712f5a099f587 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Sat, 31 Aug 2024 23:58:05 +0800 Subject: [PATCH 33/43] feat: add utils.adaptive-columns --- examples/dewdrop.typ | 35 +++++++++++--------------------- src/utils.typ | 48 ++++++++++++++++++++++++++++++++++++++++++-- themes/dewdrop.typ | 28 +++++++++++++++++--------- themes/simple.typ | 1 - 4 files changed, 76 insertions(+), 36 deletions(-) diff --git a/examples/dewdrop.typ b/examples/dewdrop.typ index d49831147..488f12c19 100644 --- a/examples/dewdrop.typ +++ b/examples/dewdrop.typ @@ -6,6 +6,7 @@ #show: dewdrop-theme.with( aspect-ratio: "16-9", footer: self => self.info.institution, + navigation: "mini-slides", config-info( title: [Title], subtitle: [Subtitle], @@ -25,25 +26,17 @@ == Subsection A.1 -#slide[ - A slide with equation: - - $ x_(n+1) = (x_n + a/x_n) / 2 $ -] +$ x_(n+1) = (x_n + a/x_n) / 2 $ == Subsection A.2 -#slide[ - A slide without a title but with *important* infos -] +A slide without a title but with *important* infos = Section B == Subsection B.1 -#slide[ - #lorem(80) -] +#lorem(80) #focus-slide[ Wake up! @@ -51,17 +44,15 @@ == Subsection B.2 -#slide[ - We can use `#pause` to #pause display something later. +We can use `#pause` to #pause display something later. - #pause - - Just like this. +#pause - #meanwhile - - Meanwhile, #pause we can also use `#meanwhile` to #pause display other content synchronously. -] +Just like this. + +#meanwhile + +Meanwhile, #pause we can also use `#meanwhile` to #pause display other content synchronously. #show: appendix @@ -69,6 +60,4 @@ == Appendix -#slide[ - Please pay attention to the current slide number. -] \ No newline at end of file +Please pay attention to the current slide number. \ No newline at end of file diff --git a/src/utils.typ b/src/utils.typ index 528381762..9d8ab3b0f 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -316,7 +316,15 @@ /// - `setting` is the setting of the heading. Default is `body => body`. /// /// - `sty` is the style of the heading. If `sty` is a function, it will use the function to style the heading. For example, `sty: current-heading => current-heading.body`. -#let display-current-heading(self: none, level: auto, numbered: true, hierachical: true, depth: 9999, setting: body => body, ..sty) = ( +#let display-current-heading( + self: none, + level: auto, + numbered: true, + hierachical: true, + depth: 9999, + setting: body => body, + ..sty, +) = ( context { let sty = if sty.pos().len() > 1 { sty.pos().at(0) @@ -345,7 +353,14 @@ /// - `depth` is the maximum depth of the heading to search. Usually, it should be set as slide-level. /// /// - `sty` is the style of the heading. If `sty` is a function, it will use the function to style the heading. For example, `sty: current-heading => current-heading.body`. -#let display-current-short-heading(self: none, level: auto, hierachical: true, depth: 9999, setting: body => body, ..sty) = ( +#let display-current-short-heading( + self: none, + level: auto, + hierachical: true, + depth: 9999, + setting: body => body, + ..sty, +) = ( context { let sty = if sty.pos().len() > 1 { sty.pos().at(0) @@ -481,6 +496,35 @@ } +/// Adaptive columns layout +/// +/// - `gutter` is the space between columns. +/// +/// - `max-count` is the maximum number of columns. +/// +/// - `start` is the content to place before the columns. +/// +/// - `end` is the content to place after the columns. +/// +/// - `body` is the content to place in the columns. +#let adaptive-columns(gutter: 4%, max-count: 3, start: none, end: none, body) = layout(size => { + let n = calc.min( + calc.ceil(measure(body).height / (size.height - measure(start).height - measure(end).height)), + max-count, + ) + if n < 1 { + n = 1 + } + start + if n == 1 { + body + } else { + columns(n, body) + } + end +}) + + /// Fit content to specified height. /// /// Example: `#utils.fit-to-height(1fr)[BIG]` diff --git a/themes/dewdrop.typ b/themes/dewdrop.typ index c54cb8246..5cd821e80 100644 --- a/themes/dewdrop.typ +++ b/themes/dewdrop.typ @@ -170,13 +170,18 @@ ) touying-slide( self: self, - { - text(1.2em, fill: self.colors.primary, weight: "bold", utils.call-or-display(self, self.store.outline-title)) + utils.adaptive-columns( + start: text( + 1.2em, + fill: self.colors.primary, + weight: "bold", + utils.call-or-display(self, self.store.outline-title), + ), text( fill: self.colors.neutral-darkest, outline(title: none, indent: 1em, ..args), - ) - }, + ), + ), ) }) @@ -196,13 +201,18 @@ ) touying-slide( self: self, - { - text(1.2em, fill: self.colors.primary, weight: "bold", utils.call-or-display(self, self.store.outline-title)) + utils.adaptive-columns( + start: text( + 1.2em, + fill: self.colors.primary, + weight: "bold", + utils.call-or-display(self, self.store.outline-title), + ), text( fill: self.colors.neutral-darkest, components.progressive-outline(alpha: self.store.alpha, title: none, indent: 1em, ..args), - ) - }, + ), + ), ) }) @@ -210,8 +220,6 @@ /// Focus on some content. /// /// Example: `#focus-slide[Wake up!]` -/// -/// - `align` is the alignment of the content. Default is `horizon + center`. #let focus-slide(body) = touying-slide-wrapper(self => { self = utils.merge-dicts( self, diff --git a/themes/simple.typ b/themes/simple.typ index d52420581..41682cb66 100644 --- a/themes/simple.typ +++ b/themes/simple.typ @@ -27,7 +27,6 @@ /// /// - `..bodies` is the contents of the slide. You can call the `slide` function with syntax like `#slide[A][B][C]` to create a slide. #let slide( - title: none, config: (:), repeat: auto, setting: body => body, From cf003034d0f06cb24fd2f07c503d48fe5a61f7fb Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Sun, 1 Sep 2024 02:10:12 +0800 Subject: [PATCH 34/43] theme(dewdrop): complete dewdrop theme --- src/components.typ | 110 ++++++++++++++ src/utils.typ | 2 + themes/dewdrop copy.typ | 318 ---------------------------------------- themes/dewdrop.typ | 23 ++- 4 files changed, 130 insertions(+), 323 deletions(-) delete mode 100644 themes/dewdrop copy.typ diff --git a/src/components.typ b/src/components.typ index 56f552a83..5df76155a 100644 --- a/src/components.typ +++ b/src/components.typ @@ -265,4 +265,114 @@ }, title: title, ..args, +) + + + +#let mini-slides( + self: none, + fill: rgb("000000"), + alpha: 60%, + display-section: false, + display-subsection: true, + short-heading: true, +) = ( + context { + let headings = query(heading.where(level: 1).or(heading.where(level: 2))) + let sections = headings.filter(it => it.level == 1) + if sections == () { + return + } + let first-page = sections.at(0).location().page() + headings = headings.filter(it => it.location().page() >= first-page) + let slides = query().filter(it => ( + utils.is-kind(it, "touying-new-slide") and it.location().page() >= first-page + )) + let current-page = here().page() + let current-index = sections.filter(it => it.location().page() <= current-page).len() - 1 + let cols = () + let col = () + for (hd, next-hd) in headings.zip(headings.slice(1) + (none,)) { + let next-page = if next-hd != none { + next-hd.location().page() + } else { + calc.inf + } + if hd.level == 1 { + if col != () { + cols.push(align(left, col.sum())) + col = () + } + col.push({ + let body = if short-heading { + utils.short-heading(self: self, hd) + } else { + hd.body + } + [#link(hd.location(), body)] + linebreak() + while slides.len() > 0 and slides.at(0).location().page() < next-page { + let slide = slides.remove(0) + if display-section { + let next-slide-page = if slides.len() > 0 { + slides.at(0).location().page() + } else { + calc.inf + } + if slide.location().page() <= current-page and current-page < next-slide-page { + [#link(slide.location(), sym.circle.filled)] + } else { + [#link(slide.location(), sym.circle)] + } + } + } + if display-section and display-subsection { + linebreak() + } + }) + } else { + col.push({ + while slides.len() > 0 and slides.at(0).location().page() < next-page { + let slide = slides.remove(0) + if display-subsection { + let next-slide-page = if slides.len() > 0 { + slides.at(0).location().page() + } else { + calc.inf + } + if slide.location().page() <= current-page and current-page < next-slide-page { + [#link(slide.location(), sym.circle.filled)] + } else { + [#link(slide.location(), sym.circle)] + } + } + } + if display-subsection { + linebreak() + } + }) + } + } + if col != () { + cols.push(align(left, col.sum())) + col = () + } + if current-index < 0 or current-index >= cols.len() { + cols = cols.map(body => text(fill: fill, body)) + } else { + cols = cols.enumerate().map(pair => { + let (idx, body) = pair + if idx == current-index { + text(fill: fill, body) + } else { + text(fill: utils.update-alpha(fill, alpha), body) + } + }) + } + set align(top) + show: block.with(inset: (top: .5em, x: 2em)) + show linebreak: it => it + v(-1em) + set text(size: .7em) + grid(columns: cols.map(_ => auto).intersperse(1fr), ..cols.intersperse([])) + } ) \ No newline at end of file diff --git a/src/utils.typ b/src/utils.typ index 9d8ab3b0f..4ff311103 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -498,6 +498,8 @@ /// Adaptive columns layout /// +/// Example: `utils.adaptive-columns(outline())` +/// /// - `gutter` is the space between columns. /// /// - `max-count` is the maximum number of columns. diff --git a/themes/dewdrop copy.typ b/themes/dewdrop copy.typ deleted file mode 100644 index 29df2f415..000000000 --- a/themes/dewdrop copy.typ +++ /dev/null @@ -1,318 +0,0 @@ -// This theme is inspired by https://github.com/zbowang/BeamerTheme -// The typst version was written by https://github.com/OrangeX4 - -#import "../slide.typ": s -#import "../src/utils.typ" -#import "../src/states.typ" - -#let slide( - self: none, - subsection: none, - title: none, - footer: auto, - ..args, -) = { - self.page-args += ( - fill: self.colors.neutral-lightest, - ) - if footer != auto { - self.m-footer = footer - } - (self.methods.touying-slide)( - ..args.named(), - self: self, - subsection: subsection, - title: title, - setting: body => { - set text(fill: self.colors.neutral-darkest) - show heading: set text(fill: self.colors.primary) - show: args.named().at("setting", default: body => body) - if self.auto-heading-for-subsection and subsection != none { - heading(level: 1, states.current-subsection-with-numbering(self)) - } - if self.auto-heading and title != none { - heading(level: 2, title) - } - body - }, - ..args.pos(), - ) -} - -#let title-slide( - self: none, - extra: none, - ..args, -) = { - self = utils.empty-page(self) - let info = self.info + args.named() - let content = { - set text(fill: self.colors.neutral-darkest) - set align(center + horizon) - block(width: 100%, inset: 3em, { - block( - fill: self.colors.neutral-light, - inset: 1em, - width: 100%, - radius: 0.2em, - text(size: 1.3em, fill: self.colors.primary, text(weight: "medium", info.title)) - + (if info.subtitle != none { - linebreak() - text(size: 0.9em, fill: self.colors.primary, info.subtitle) - }) - ) - set text(size: .8em) - if info.author != none { - block(spacing: 1em, info.author) - } - v(1em) - if info.date != none { - block(spacing: 1em, utils.info-date(self)) - } - set text(size: .8em) - if info.institution != none { - block(spacing: 1em, info.institution) - } - if extra != none { - block(spacing: 1em, extra) - } - }) - } - (self.methods.touying-slide)(self: self, repeat: none, content) -} - -#let outline-slide(self: none, ..args) = { - (self.methods.slide)(self: self, heading(level: 2, self.outline-title) + parbreak() + (self.methods.touying-outline)(self: self, cover: false)) -} - -#let focus-slide(self: none, body) = { - self = utils.empty-page(self) - self.page-args += ( - fill: self.colors.primary, - margin: 2em, - ) - set text(fill: self.colors.neutral-lightest, size: 1.5em) - (self.methods.touying-slide)(self: self, repeat: none, align(horizon + center, body)) -} - -#let new-section-slide(self: none, section) = { - (self.methods.slide)(self: self, section: section, heading(level: 2, self.outline-title) + parbreak() + (self.methods.touying-outline)(self: self)) -} - -#let d-outline(self: none, enum-args: (:), list-args: (:), cover: true) = states.touying-progress-with-sections(dict => { - let (current-sections, final-sections) = dict - current-sections = current-sections.filter(section => section.loc != none) - final-sections = final-sections.filter(section => section.loc != none) - let current-index = current-sections.len() - 1 - let d-cover(i, body) = if i != current-index and cover { - (self.methods.d-cover)(self: self, body) - } else { - body - } - set enum(..enum-args) - set list(..enum-args) - set text(fill: self.colors.primary) - for (i, section) in final-sections.enumerate() { - d-cover(i, { - enum.item(i + 1, [#link(section.loc, section.title)] + if section.children.filter(it => it.kind != "slide").len() > 0 { - let subsections = section.children.filter(it => it.kind != "slide") - set text(fill: self.colors.neutral-dark, size: 0.9em) - list( - ..subsections.map(subsection => [#link(subsection.loc, subsection.title)]) - ) - }) - }) - parbreak() - } -}) - -#let d-sidebar(self: none) = states.touying-progress-with-sections(dict => { - let (current-sections, final-sections) = dict - current-sections = current-sections - .filter(section => section.loc != none) - .map(section => (section, section.children)) - .flatten() - .filter(item => item.kind != "slide") - final-sections = final-sections - .filter(section => section.loc != none) - .map(section => (section, section.children)) - .flatten() - .filter(item => item.kind != "slide") - let current-index = current-sections.len() - 1 - show: block.with(width: self.d-sidebar.width, inset: (top: 4em, x: 1em)) - set align(left) - set par(justify: false) - set text(size: 0.9em) - for (i, section) in final-sections.enumerate() { - if section.kind == "section" { - set text(fill: if i != current-index { self.colors.primary.lighten(self.d-alpha) } else { self.colors.primary }) - [#link(section.loc, utils.section-short-title(section.title))] - } else { - set text(fill: if i != current-index { self.colors.neutral-dark.lighten(self.d-alpha) } else { self.colors.neutral-dark }, size: 0.9em) - [#link(section.loc, utils.section-short-title(h(.3em) + section.title))] - } - parbreak() - } -}) - -#let d-mini-slides(self: none) = states.touying-progress-with-sections(dict => { - let (current-sections, final-sections) = dict - current-sections = current-sections.filter(section => section.loc != none) - final-sections = final-sections.filter(section => section.loc != none) - let current-i = current-sections.len() - 1 - let cols = () - let current-count = 0 - for (i, section) in current-sections.enumerate() { - if self.d-mini-slides.section { - for slide in section.children.filter(it => it.kind == "slide") { - current-count += 1 - } - } - for subsection in section.children.filter(it => it.kind != "slide") { - for slide in subsection.children { - current-count += 1 - } - } - } - let final-count = 0 - for (i, section) in final-sections.enumerate() { - let primary-color = if i != current-i { self.colors.primary.lighten(self.d-alpha) } else { self.colors.primary } - cols.push({ - set align(left) - set text(fill: primary-color) - [#link(section.loc, utils.section-short-title(section.title))] - linebreak() - if self.d-mini-slides.section { - for slide in section.children.filter(it => it.kind == "slide") { - final-count += 1 - if i == current-i and final-count == current-count { - [#link(slide.loc, sym.circle.filled)] - } else { - [#link(slide.loc, sym.circle)] - } - } - } - if self.d-mini-slides.section and self.d-mini-slides.subsection { - linebreak() - } - for subsection in section.children.filter(it => it.kind != "slide") { - for slide in subsection.children { - final-count += 1 - if i == current-i and final-count == current-count { - [#link(slide.loc, sym.circle.filled)] - } else { - [#link(slide.loc, sym.circle)] - } - } - if self.d-mini-slides.subsection { - linebreak() - } - } - }) - } - set align(top) - show: block.with(inset: (top: .5em, x: 2em)) - show linebreak: it => it + v(-1em) - set text(size: .7em) - grid(columns: cols.map(_ => auto).intersperse(1fr), ..cols.intersperse([])) -}) - -#let slides(self: none, title-slide: true, outline-slide: true, slide-level: 2, ..args) = { - if title-slide { - (self.methods.title-slide)(self: self) - } - if outline-slide { - (self.methods.outline-slide)(self: self) - } - (self.methods.touying-slides)(self: self, slide-level: slide-level, ..args) -} - -#let register( - self: s, - aspect-ratio: "16-9", - navigation: "sidebar", - sidebar: (width: 10em), - mini-slides: (height: 4em, x: 2em, section: false, subsection: true), - footer: [], - footer-right: context { states.slide-counter.display() + " / " + states.last-slide-number }, - primary: rgb("#0c4842"), - alpha: 70%, - ..args, -) = { - assert(navigation in ("sidebar", "mini-slides", none), message: "navigation must be one of sidebar, mini-slides, none") - // color theme - self = (self.methods.colors)( - self: self, - neutral-darkest: rgb("#000000"), - neutral-dark: rgb("#202020"), - neutral-light: rgb("#f3f3f3"), - neutral-lightest: rgb("#ffffff"), - primary: primary, - ) - // save the variables for later use - self.d-navigation = navigation - self.d-sidebar = sidebar - self.d-mini-slides = mini-slides - self.d-footer = footer - self.d-footer-right = footer-right - self.d-alpha = alpha - self.auto-heading = true - self.auto-heading-for-subsection = true - self.outline-title = [Outline] - // set page - let header(self) = { - if self.d-navigation == "sidebar" { - place(right + top, (self.methods.d-sidebar)(self: self)) - } else if self.d-navigation == "mini-slides" { - (self.methods.d-mini-slides)(self: self) - } - } - let footer(self) = { - set text(size: 0.8em) - set align(bottom) - show: pad.with(.5em) - text(fill: self.colors.neutral-darkest.lighten(40%), utils.call-or-display(self, self.d-footer)) - h(1fr) - text(fill: self.colors.neutral-darkest.lighten(20%), utils.call-or-display(self, self.d-footer-right)) - } - self.page-args += ( - paper: "presentation-" + aspect-ratio, - fill: self.colors.neutral-lightest, - header: header, - footer: footer, - header-ascent: 0em, - footer-descent: 0em, - ) + if navigation == "sidebar" {( - margin: (top: 2em, bottom: 1em, x: sidebar.width), - )} else if navigation == "mini-slides" {( - margin: (top: mini-slides.height, bottom: 2em, x: mini-slides.x), - )} else {( - margin: (top: 2em, bottom: 2em, x: mini-slides.x), - )} - self = (self.methods.numbering)(self: self, section: "1.", "1.1") - // register methods - self.methods.slide = slide - self.methods.title-slide = title-slide - self.methods.outline-slide = outline-slide - self.methods.focus-slide = focus-slide - self.methods.new-section-slide = new-section-slide - self.methods.touying-new-section-slide = new-section-slide - self.methods.slides = slides - self.methods.d-cover = (self: none, body) => { - utils.cover-with-rect(fill: utils.update-alpha( - constructor: rgb, self.page-args.fill, self.d-alpha), body) - } - self.methods.touying-outline = d-outline - self.methods.d-outline = d-outline - self.methods.d-sidebar = d-sidebar - self.methods.d-mini-slides = d-mini-slides - self.methods.alert = (self: none, it) => text(fill: self.colors.primary, it) - self.methods.init = (self: none, body) => { - set heading(outlined: false) - set text(size: 20pt) - set par(justify: true) - show heading: set block(below: 1em) - body - } - self -} diff --git a/themes/dewdrop.typ b/themes/dewdrop.typ index 5cd821e80..0830f9bbb 100644 --- a/themes/dewdrop.typ +++ b/themes/dewdrop.typ @@ -31,7 +31,14 @@ }, ) } else if self.store.navigation == "mini-slides" { - // (self.methods.d-mini-slides)(self: self) + components.mini-slides( + self: self, + fill: self.colors.primary, + alpha: self.store.alpha, + display-section: self.store.mini-slides.at("display-section", default: false), + display-subsection: self.store.mini-slides.at("display-subsection", default: true), + short-heading: self.store.mini-slides.at("short-heading", default: true), + ) } } @@ -179,7 +186,7 @@ ), text( fill: self.colors.neutral-darkest, - outline(title: none, indent: 1em, ..args), + outline(title: none, indent: 1em, depth: self.slide-level, ..args), ), ), ) @@ -210,7 +217,7 @@ ), text( fill: self.colors.neutral-darkest, - components.progressive-outline(alpha: self.store.alpha, title: none, indent: 1em, ..args), + components.progressive-outline(alpha: self.store.alpha, title: none, indent: 1em, depth: self.slide-level, ..args), ), ), ) @@ -243,8 +250,8 @@ #let dewdrop-theme( aspect-ratio: "16-9", navigation: "sidebar", - sidebar: (width: 10em, filled: false, numbered: false, indent: .5em), - mini-slides: (height: 4em, x: 2em, section: false, subsection: true), + sidebar: (width: 10em, filled: false, numbered: false, indent: .5em, short-heading: true), + mini-slides: (height: 4em, x: 2em, display-section: false, display-subsection: true, short-heading: true), footer: none, footer-right: context utils.slide-counter.display() + " / " + utils.last-slide-number, primary: rgb("#0c4842"), @@ -277,6 +284,12 @@ new-section-slide-fn: new-section-slide, ), config-methods( + init: (self: none, body) => { + show strong: self.methods.alert.with(self: self) + show heading: set text(self.colors.primary) + + body + }, alert: utils.alert-with-primary-color, ), config-colors( From 531b149cebb547ae94e18ffcf560e65cb0287b33 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Sun, 1 Sep 2024 15:41:49 +0800 Subject: [PATCH 35/43] fix: fix university theme --- examples/default.typ | 2 +- examples/example.typ | 71 +++++++++++++++++++------------------------ src/core.typ | 2 +- src/utils.typ | 5 +-- themes/university.typ | 10 +++--- 5 files changed, 41 insertions(+), 49 deletions(-) diff --git a/examples/default.typ b/examples/default.typ index 5567cd75c..91fc587b6 100644 --- a/examples/default.typ +++ b/examples/default.typ @@ -24,7 +24,7 @@ = Outline -#outline(title: none, indent: 1em) +#utils.adaptive-columns(outline(title: none, indent: 1em)) = Title diff --git a/examples/example.typ b/examples/example.typ index d83a8cbee..179d57749 100644 --- a/examples/example.typ +++ b/examples/example.typ @@ -1,35 +1,17 @@ -#import "../lib.typ": * #import "@preview/cetz:0.2.2" #import "@preview/fletcher:0.4.4" as fletcher: node, edge #import "@preview/ctheorems:1.1.2": * +#import "@preview/numbly:0.1.0": numbly +#import "../lib.typ": * +#import themes.university: * // cetz and fletcher bindings for touying #let cetz-canvas = touying-reducer.with(reduce: cetz.canvas, cover: cetz.draw.hide.with(bounds: true)) #let fletcher-diagram = touying-reducer.with(reduce: fletcher.diagram, cover: fletcher.hide) -// Register university theme -// You can replace it with other themes and it can still work normally -#let s = themes.university.register(aspect-ratio: "16-9") - -// Set the numbering of section and subsection -#let s = (s.methods.numbering)(self: s, section: "1.", "1.1") - -// Set the speaker notes configuration, you can show it by pympress -// #let s = (s.methods.show-notes-on-second-screen)(self: s, right) - -// Global information configuration -#let s = (s.methods.info)( - self: s, - title: [Title], - subtitle: [Subtitle], - author: [Authors], - date: datetime.today(), - institution: [Institution], -) - // Pdfpc configuration // typst query --root . ./example.typ --field value --one "" > ./example.pdfpc -#let s = (s.methods.append-preamble)(self: s, pdfpc.config( +#pdfpc.config( duration-minutes: 30, start-time: datetime(hour: 14, minute: 10, second: 0), end-time: datetime(hour: 14, minute: 40, second: 0), @@ -43,7 +25,7 @@ alignment: "vertical", direction: "inward", ), -)) +) // Theorems configuration by ctheorems #show: thmrules.with(qed-symbol: $square$) @@ -58,15 +40,28 @@ #let example = thmplain("example", "Example").with(numbering: none) #let proof = thmproof("proof", "Proof") -// Extract methods -#let (init, slides, touying-outline, alert, speaker-note) = utils.methods(s) -#show: init +#show: university-theme.with( + aspect-ratio: "16-9", + // Set the speaker notes configuration, you can show it by pympress + // config-common(align-list-marker-with-baseline: right), + + config-info( + title: [Title], + subtitle: [Subtitle], + author: [Authors], + date: datetime.today(), + institution: [Institution], + logo: emoji.school, + ), +) + +#set heading(numbering: numbly("{1}.", default: "1.1")) + +#title-slide() -#show strong: alert +== Outline -// Extract slide functions -#let (slide, empty-slide) = utils.slides(s) -#show: slides +#utils.adaptive-columns(outline(title: none, indent: 1em)) = Animation @@ -88,9 +83,9 @@ Meanwhile, #pause we can also use `#meanwhile` to #pause display other content s ] -== Complex Animation - Mark-Style +== Complex Animation -At subslide #utils.touying-wrapper((self: none) => str(self.subslide)), we can +At subslide #touying-fn-wrapper((self: none) => str(self.subslide)), we can use #uncover("2-")[`#uncover` function] for reserving space, @@ -99,7 +94,7 @@ use #only("2-")[`#only` function] for not reserving space, #alternatives[call `#only` multiple times \u{2717}][use `#alternatives` function #sym.checkmark] for choosing one of the alternatives. -== Complex Animation - Callback-Style +== Callback Style Animation #slide(repeat: 3, self => [ #let (uncover, only, alternatives) = utils.methods(self) @@ -232,12 +227,8 @@ Fletcher Animation in Touying: #lorem(200) -// appendix by freezing last-slide-number -#let s = (s.methods.appendix)(self: s) -#let (slide, empty-slide) = utils.slides(s) +#show: appendix -== Appendix += Appendix -#slide[ - Please pay attention to the current slide number. -] +Please pay attention to the current slide number. diff --git a/src/core.typ b/src/core.typ index 1898c69f0..ea1720144 100644 --- a/src/core.typ +++ b/src/core.typ @@ -574,7 +574,7 @@ /// - `scope` is the scope when we use `eval()` function to evaluate the equation. /// /// - `body` is the content of the equation. It should be a string, a raw text or a function that receives `self` as an argument and returns a string. -#let touying-equation(block: true, numbering: none, supplement: auto, scope: (:), body) = utils.label( +#let touying-equation(block: true, numbering: none, supplement: auto, scope: (:), body) = utils.label-it( metadata(( kind: "touying-equation", block: block, diff --git a/src/utils.typ b/src/utils.typ index 4ff311103..6224ef5da 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -92,6 +92,7 @@ if type(label-name) == label { [#it#label-name] } else { + assert(type(label-name) == str, message: repr(label-name)) [#it#label(label-name)] } } @@ -111,13 +112,13 @@ let _ = fields.remove(body-name, default: none) if named { if label != none { - return label-it(label, (it.func())(..fields, ..new-body)) + return label-it((it.func())(..fields, ..new-body), label) } else { return (it.func())(..fields, ..new-body) } } else { if label != none { - return label-it(label, (it.func())(..fields.values(), ..new-body)) + return label-it((it.func())(..fields.values(), ..new-body), label) } else { return (it.func())(..fields.values(), ..new-body) } diff --git a/themes/university.typ b/themes/university.typ index b0d8ceb92..db62b5547 100644 --- a/themes/university.typ +++ b/themes/university.typ @@ -45,8 +45,8 @@ block( inset: (x: .5em), components.left-and-right( - text(fill: self.colors.primary, weight: "bold", size: 1.2em, self.store.header), - text(fill: self.colors.primary.lighten(65%), self.store.header-right), + text(fill: self.colors.primary, weight: "bold", size: 1.2em, utils.call-or-display(self, self.store.header)), + text(fill: self.colors.primary.lighten(65%), utils.call-or-display(self, self.store.header-right)), ), ), ) @@ -89,7 +89,7 @@ /// #show: university-theme.with( /// config-info( /// title: [Title], -/// logo: emoji.city, +/// logo: emoji.school, /// ), /// ) /// @@ -114,7 +114,7 @@ } let body = { if info.logo != none { - align(right, info.logo) + align(right, text(fill: self.colors.primary, info.logo)) } align( center + horizon, @@ -256,7 +256,7 @@ aspect-ratio: "16-9", progress-bar: true, header: utils.display-current-heading(level: 2), - header-right: utils.display-current-heading(level: 1), + header-right: self => utils.display-current-heading(level: 1) + h(.3em) + self.info.logo, footer-columns: (25%, 1fr, 25%), footer-a: self => self.info.author, footer-b: self => if self.info.short-title == auto { From fcac7119b2156ee8b6d3e3374207b1773fa292f8 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Sun, 1 Sep 2024 16:07:09 +0800 Subject: [PATCH 36/43] fix: fix some bugs --- examples/default.typ | 2 +- examples/example.typ | 6 ++++-- src/components.typ | 31 +++++++++++++++++++++++++++++++ src/core.typ | 1 + src/utils.typ | 31 ------------------------------- themes/dewdrop.typ | 4 ++-- 6 files changed, 39 insertions(+), 36 deletions(-) diff --git a/examples/default.typ b/examples/default.typ index 91fc587b6..a5bd84e4d 100644 --- a/examples/default.typ +++ b/examples/default.typ @@ -24,7 +24,7 @@ = Outline -#utils.adaptive-columns(outline(title: none, indent: 1em)) +#components.adaptive-columns(outline(title: none, indent: 1em)) = Title diff --git a/examples/example.typ b/examples/example.typ index 179d57749..b7b99e3d2 100644 --- a/examples/example.typ +++ b/examples/example.typ @@ -59,9 +59,9 @@ #title-slide() -== Outline +== Outline -#utils.adaptive-columns(outline(title: none, indent: 1em)) +#components.adaptive-columns(outline(title: none, indent: 1em)) = Animation @@ -231,4 +231,6 @@ Fletcher Animation in Touying: = Appendix +== Appendix + Please pay attention to the current slide number. diff --git a/src/components.typ b/src/components.typ index 5df76155a..844497e97 100644 --- a/src/components.typ +++ b/src/components.typ @@ -4,6 +4,37 @@ #let cell = block.with(width: 100%, height: 100%, above: 0pt, below: 0pt, outset: 0pt, breakable: false) +/// Adaptive columns layout +/// +/// Example: `components.adaptive-columns(outline())` +/// +/// - `gutter` is the space between columns. +/// +/// - `max-count` is the maximum number of columns. +/// +/// - `start` is the content to place before the columns. +/// +/// - `end` is the content to place after the columns. +/// +/// - `body` is the content to place in the columns. +#let adaptive-columns(gutter: 4%, max-count: 3, start: none, end: none, body) = layout(size => { + let n = calc.min( + calc.ceil(measure(body).height / (size.height - measure(start).height - measure(end).height)), + max-count, + ) + if n < 1 { + n = 1 + } + start + if n == 1 { + body + } else { + columns(n, body) + } + end +}) + + /// Touying progress bar. /// /// - `primary` is the color of the progress bar. diff --git a/src/core.typ b/src/core.typ index ea1720144..23cc6c916 100644 --- a/src/core.typ +++ b/src/core.typ @@ -153,6 +153,7 @@ recaller-map, ) result.push(cont) + horizontal-line = false } // Main logic if utils.is-kind(child, "touying-slide-wrapper") { diff --git a/src/utils.typ b/src/utils.typ index 6224ef5da..b4137d615 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -497,37 +497,6 @@ } -/// Adaptive columns layout -/// -/// Example: `utils.adaptive-columns(outline())` -/// -/// - `gutter` is the space between columns. -/// -/// - `max-count` is the maximum number of columns. -/// -/// - `start` is the content to place before the columns. -/// -/// - `end` is the content to place after the columns. -/// -/// - `body` is the content to place in the columns. -#let adaptive-columns(gutter: 4%, max-count: 3, start: none, end: none, body) = layout(size => { - let n = calc.min( - calc.ceil(measure(body).height / (size.height - measure(start).height - measure(end).height)), - max-count, - ) - if n < 1 { - n = 1 - } - start - if n == 1 { - body - } else { - columns(n, body) - } - end -}) - - /// Fit content to specified height. /// /// Example: `#utils.fit-to-height(1fr)[BIG]` diff --git a/themes/dewdrop.typ b/themes/dewdrop.typ index 0830f9bbb..408bd83fa 100644 --- a/themes/dewdrop.typ +++ b/themes/dewdrop.typ @@ -177,7 +177,7 @@ ) touying-slide( self: self, - utils.adaptive-columns( + components.adaptive-columns( start: text( 1.2em, fill: self.colors.primary, @@ -208,7 +208,7 @@ ) touying-slide( self: self, - utils.adaptive-columns( + components.adaptive-columns( start: text( 1.2em, fill: self.colors.primary, From 09875f3f2cc5da3c0e3870c378fe8c49e14cee28 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Mon, 2 Sep 2024 00:32:52 +0800 Subject: [PATCH 37/43] feat: add label-only-on-last-subslide --- examples/example.typ | 12 +++---- src/configs.typ | 3 ++ src/core.typ | 76 +++++++++++++++++++++++++------------------- src/utils.typ | 12 ++++--- 4 files changed, 59 insertions(+), 44 deletions(-) diff --git a/examples/example.typ b/examples/example.typ index b7b99e3d2..2650bd12a 100644 --- a/examples/example.typ +++ b/examples/example.typ @@ -42,9 +42,7 @@ #show: university-theme.with( aspect-ratio: "16-9", - // Set the speaker notes configuration, you can show it by pympress - // config-common(align-list-marker-with-baseline: right), - + // config-common(handout: true), config-info( title: [Title], subtitle: [Subtitle], @@ -111,12 +109,12 @@ use #only("2-")[`#only` function] for not reserving space, == Math Equation Animation -Touying equation with `pause`: +Equation with `pause`: -#touying-equation(` +$ f(x) &= pause x^2 + 2x + 1 \ - &= pause (x + 1)^2 \ -`) + &= pause (x + 1)^2 \ +$ #meanwhile diff --git a/src/configs.typ b/src/configs.typ index fa03f7c6e..9284fc8c1 100644 --- a/src/configs.typ +++ b/src/configs.typ @@ -149,6 +149,7 @@ default-frozen-states: _default, frozen-counters: _default, default-frozen-counters: _default, + label-only-on-last-subslide: _default, first-slide-number: _default, preamble: _default, default-preamble: _default, @@ -187,6 +188,7 @@ frozen-counters: frozen-counters, default-frozen-states: default-frozen-states, default-frozen-counters: default-frozen-counters, + label-only-on-last-subslide: label-only-on-last-subslide, first-slide-number: first-slide-number, preamble: preamble, default-preamble: default-preamble, @@ -537,6 +539,7 @@ default-frozen-states: _default-frozen-states, frozen-counters: (), default-frozen-counters: _default-frozen-counters, + label-only-on-last-subslide: (figure, math.equation), first-slide-number: 1, preamble: none, default-preamble: _default-preamble, diff --git a/src/core.typ b/src/core.typ index 23cc6c916..b0a025090 100644 --- a/src/core.typ +++ b/src/core.typ @@ -666,7 +666,8 @@ // parse touying equation, and get the repetitions -#let _parse-touying-equation(self: none, need-cover: true, base: 1, index: 1, eqt) = { +#let _parse-touying-equation(self: none, need-cover: true, base: 1, index: 1, eqt-metadata) = { + let eqt = eqt-metadata.value let result-arr = () // repetitions let repetitions = base @@ -724,30 +725,33 @@ result.push("cover(" + cover-arr.sum() + ")") cover-arr = () } - result-arr.push( - math.equation( - block: eqt.block, - numbering: eqt.numbering, - supplement: eqt.supplement, - eval( - "$" + result.sum(default: "") + "$", - scope: eqt.scope + ( - cover: (..args) => { - let cover = eqt.scope.at("cover", default: cover) - if args.pos().len() != 0 { - cover(args.pos().first()) - } - }, - ), + let equation = math.equation( + block: eqt.block, + numbering: eqt.numbering, + supplement: eqt.supplement, + eval( + "$" + result.sum(default: "") + "$", + scope: eqt.scope + ( + cover: (..args) => { + let cover = eqt.scope.at("cover", default: cover) + if args.pos().len() != 0 { + cover(args.pos().first()) + } + }, ), ), ) + if eqt-metadata.has("label") and eqt-metadata.label != { + equation = utils.label-it(equation, eqt-metadata.label) + } + result-arr.push(equation) max-repetitions = calc.max(max-repetitions, repetitions) return (result-arr, max-repetitions) } // parse touying mitex, and get the repetitions -#let _parse-touying-mitex(self: none, need-cover: true, base: 1, index: 1, eqt) = { +#let _parse-touying-mitex(self: none, need-cover: true, base: 1, index: 1, eqt-metadata) = { + let eqt = eqt-metadata.value let result-arr = () // repetitions let repetitions = base @@ -803,14 +807,16 @@ result.push("\\phantom{" + cover-arr.sum() + "}") cover-arr = () } - result-arr.push( - (eqt.mitex)( - block: eqt.block, - numbering: eqt.numbering, - supplement: eqt.supplement, - result.sum(default: ""), - ), + let equation = (eqt.mitex)( + block: eqt.block, + numbering: eqt.numbering, + supplement: eqt.supplement, + result.sum(default: ""), ) + if eqt-metadata.has("label") and eqt-metadata.label != { + equation = utils.label-it(equation, eqt-metadata.label) + } + result-arr.push(equation) max-repetitions = calc.max(max-repetitions, repetitions) return (result-arr, max-repetitions) } @@ -884,6 +890,11 @@ show-delayed-wrapper: false, ..bodies, ) = { + let labeled(func) = { + return not ( + "repeat" in self and "subslide" in self and "label-only-on-last-subslide" in self and func in self.label-only-on-last-subslide and self.subslide != self.repeat + ) + } let bodies = bodies.pos() let result-arr = () // repetitions @@ -940,7 +951,7 @@ need-cover: repetitions <= index, base: repetitions, index: index, - child.value, + child, ) let cont = conts.first() if repetitions <= index or not need-cover { @@ -956,7 +967,7 @@ need-cover: repetitions <= index, base: repetitions, index: index, - child.value, + child, ) let cont = conts.first() if repetitions <= index or not need-cover { @@ -1056,9 +1067,9 @@ ) let cont = conts.first() if repetitions <= index or not need-cover { - result.push(utils.reconstruct(child, cont)) + result.push(utils.reconstruct(child, labeled: labeled(child.func()), cont)) } else { - cover-arr.push(utils.reconstruct(child, cont)) + cover-arr.push(utils.reconstruct(child, labeled: labeled(child.func()), cont)) } repetitions = nextrepetitions } else if type(child) == content and child.func() in (table, grid, stack) { @@ -1071,9 +1082,9 @@ ..child.children, ) if repetitions <= index or not need-cover { - result.push(utils.reconstruct-table-like(child, conts)) + result.push(utils.reconstruct-table-like(child, labeled: labeled(child.func()), conts)) } else { - cover-arr.push(utils.reconstruct-table-like(child, conts)) + cover-arr.push(utils.reconstruct-table-like(child, labeled: labeled(child.func()), conts)) } repetitions = nextrepetitions } else if type(child) == content and child.func() in ( @@ -1101,6 +1112,7 @@ square, table.cell, grid.cell, + math.equation, ) { let (conts, nextrepetitions) = _parse-content-into-results-and-repetitions( self: self, @@ -1111,9 +1123,9 @@ ) let cont = conts.first() if repetitions <= index or not need-cover { - result.push(utils.reconstruct(named: true, child, cont)) + result.push(utils.reconstruct(named: true, labeled: labeled(child.func()), child, cont)) } else { - cover-arr.push(utils.reconstruct(named: true, child, cont)) + cover-arr.push(utils.reconstruct(named: true, labeled: labeled(child.func()), child, cont)) } repetitions = nextrepetitions } else if type(child) == content and child.func() == terms.item { diff --git a/src/utils.typ b/src/utils.typ index b4137d615..3644162ac 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -101,23 +101,25 @@ /// /// - `body-name` is the property name of the body field /// +/// - `labeled` is a boolean indicating whether the fields should be labeled +/// /// - `named` is a boolean indicating whether the fields should be named /// /// - `it` is the content to reconstruct /// /// - `new-body` is the new body you want to replace the old body with -#let reconstruct(body-name: "body", named: false, it, ..new-body) = { +#let reconstruct(body-name: "body", labeled: true, named: false, it, ..new-body) = { let fields = it.fields() let label = fields.remove("label", default: none) let _ = fields.remove(body-name, default: none) if named { - if label != none { + if label != none and labeled { return label-it((it.func())(..fields, ..new-body), label) } else { return (it.func())(..fields, ..new-body) } } else { - if label != none { + if label != none and labeled { return label-it((it.func())(..fields.values(), ..new-body), label) } else { return (it.func())(..fields.values(), ..new-body) @@ -133,8 +135,8 @@ /// - `it` is the content to reconstruct /// /// - `new-children` is the new children you want to replace the old children with -#let reconstruct-table-like(named: true, it, new-children) = { - reconstruct(body-name: "children", named: named, it, ..new-children) +#let reconstruct-table-like(named: true, labeled: true, it, new-children) = { + reconstruct(body-name: "children", named: named, labeled: labeled, it, ..new-children) } From 2e4ce7388f634b1178062f7f345fc1499c76a3c3 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Mon, 2 Sep 2024 01:32:54 +0800 Subject: [PATCH 38/43] theme(aqua): complete aqua theme --- examples/aqua-zh.typ | 41 +- examples/aqua.typ | 42 +- examples/example.typ | 4 +- examples/slides.typ | 38 -- slide.typ | 997 ------------------------------------------- src/components.typ | 40 +- src/utils.typ | 28 +- themes/aqua.typ | 423 +++++++++++------- themes/themes.typ | 2 +- 9 files changed, 359 insertions(+), 1256 deletions(-) delete mode 100644 examples/slides.typ delete mode 100644 slide.typ diff --git a/examples/aqua-zh.typ b/examples/aqua-zh.typ index c69fe03d5..0e6f954ca 100644 --- a/examples/aqua-zh.typ +++ b/examples/aqua-zh.typ @@ -1,21 +1,22 @@ #import "../lib.typ": * - -#let s = themes.aqua.register(aspect-ratio: "16-9", lang: "zh") -#let s = (s.methods.info)( - self: s, - title: [标题], - subtitle: [副标题], - author: [作者], - date: datetime.today(), - institution: [机构], +#import themes.aqua: * + +#show: aqua-theme.with( + aspect-ratio: "16-9", + config-info( + title: [标题], + subtitle: [副标题], + author: [作者], + date: datetime.today(), + institution: [机构], + ), ) -#let (init, slides, touying-outline, alert) = utils.methods(s) -#show: init -#show strong: alert +#set text(lang: "zh") + +#title-slide() -#let (slide, empty-slide, title-slide, outline-slide, focus-slide) = utils.slides(s) -#show: slides +#outline-slide() = 第一节 @@ -31,10 +32,12 @@ == 总结 -#align(center + horizon)[ - #set text(size: 3em, weight: "bold", s.colors.primary) +#slide(self => [ + #align(center + horizon)[ + #set text(size: 3em, weight: "bold", self.colors.primary) - THANKS FOR ALL + THANKS FOR ALL - 敬请指正! -] \ No newline at end of file + 敬请指正! + ] +]) \ No newline at end of file diff --git a/examples/aqua.typ b/examples/aqua.typ index fdb2ea6d9..b36aa04e0 100644 --- a/examples/aqua.typ +++ b/examples/aqua.typ @@ -1,29 +1,26 @@ #import "../lib.typ": * - -#let s = themes.aqua.register(aspect-ratio: "16-9", lang: "en") -#let s = (s.methods.info)( - self: s, - title: [Title], - subtitle: [Subtitle], - author: [Authors], - date: datetime.today(), - institution: [Institution], +#import themes.aqua: * + +#show: aqua-theme.with( + aspect-ratio: "16-9", + config-info( + title: [Title], + subtitle: [Subtitle], + author: [Authors], + date: datetime.today(), + institution: [Institution], + ), ) -#let (init, slides, touying-outline, alert) = utils.methods(s) -#show: init -#show strong: alert +#title-slide() -#let (slide, empty-slide, title-slide, outline-slide, focus-slide) = utils.slides(s) -#show: slides +#outline-slide() = The Section == Slide Title -#slide[ - #lorem(40) -] +#lorem(40) #focus-slide[ Another variant with primary color in background... @@ -31,8 +28,11 @@ == Summary -#align(center + horizon)[ - #set text(size: 3em, weight: "bold", s.colors.primary) - THANKS FOR ALL -] +#slide(self => [ + #align(center + horizon)[ + #set text(size: 3em, weight: "bold", fill: self.colors.primary) + THANKS FOR ALL + ] +]) + diff --git a/examples/example.typ b/examples/example.typ index 2650bd12a..929f4b195 100644 --- a/examples/example.typ +++ b/examples/example.typ @@ -1,9 +1,9 @@ +#import "../lib.typ": * +#import themes.university: * #import "@preview/cetz:0.2.2" #import "@preview/fletcher:0.4.4" as fletcher: node, edge #import "@preview/ctheorems:1.1.2": * #import "@preview/numbly:0.1.0": numbly -#import "../lib.typ": * -#import themes.university: * // cetz and fletcher bindings for touying #let cetz-canvas = touying-reducer.with(reduce: cetz.canvas, cover: cetz.draw.hide.with(bounds: true)) diff --git a/examples/slides.typ b/examples/slides.typ deleted file mode 100644 index 8de0b7bef..000000000 --- a/examples/slides.typ +++ /dev/null @@ -1,38 +0,0 @@ -#import "../lib.typ": * - -#let s = themes.default.register(aspect-ratio: "16-9") -// #let s = themes.simple.register(aspect-ratio: "16-9", footer: [Simple slides]) -// #let s = themes.metropolis.register(aspect-ratio: "16-9", footer: [Custom footer]) -// #let s = themes.dewdrop.register(aspect-ratio: "16-9", footer: [Dewdrop]) -// #let s = themes.university.register(aspect-ratio: "16-9") -// #let s = themes.aqua.register(aspect-ratio: "16-9") -#let s = (s.methods.info)( - self: s, - title: [Title], - subtitle: [Subtitle], - author: [Authors], - date: datetime.today(), - institution: [Institution], -) -#let s = (s.methods.enable-transparent-cover)(self: s) -#let (init, slides, touying-outline, alert) = utils.methods(s) -#show: init - -#show strong: alert - -#let (slide, empty-slide) = utils.slides(s) -#show: slides - -= Let's start a new section - -== First Title - -First content - -#pause - -with a pause. - -== Second Title - -Second content. \ No newline at end of file diff --git a/slide.typ b/slide.typ deleted file mode 100644 index 869fa3f8a..000000000 --- a/slide.typ +++ /dev/null @@ -1,997 +0,0 @@ -#import "src/utils.typ" -#import "src/states.typ" -#import "src/pdfpc.typ" - -// touying pause mark -#let pause = [#metadata((kind: "touying-pause"))] -// touying meanwhile mark -#let meanwhile = [#metadata((kind: "touying-meanwhile"))] -// touying slides-end mark -#let slides-end = [#metadata((kind: "touying-slides-end"))] -// dynamic control mark -#let uncover = utils.touying-wrapper.with(utils.uncover, with-visible-subslides: true) -#let only = utils.touying-wrapper.with(utils.only, with-visible-subslides: true) -#let alternatives-match = utils.touying-wrapper.with(utils.alternatives-match) -#let alternatives = utils.touying-wrapper.with(utils.alternatives) -#let alternatives-fn = utils.touying-wrapper.with(utils.alternatives-fn) -#let alternatives-cases = utils.touying-wrapper.with(utils.alternatives-cases) -// touying equation mark -#let touying-equation(block: true, numbering: none, supplement: auto, scope: (:), body) = [ - #metadata(( - kind: "touying-equation", - block: block, - numbering: numbering, - supplement: supplement, - scope: scope, - body: { - if type(body) == function { - body - } else if type(body) == str { - body - } else if type(body) == content and body.has("text") { - body.text - } else { - panic("Unsupported type: " + str(type(body))) - } - }, - )) -] -// touying mitex mark -#let touying-mitex(block: true, numbering: none, supplement: auto, mitex, body) = [ - #metadata(( - kind: "touying-mitex", - block: block, - numbering: numbering, - supplement: supplement, - mitex: mitex, - body: { - if type(body) == function { - body - } else if type(body) == str { - body - } else if type(body) == content and body.has("text") { - body.text - } else { - panic("Unsupported type: " + str(type(body))) - } - }, - )) -] -// touying reducer mark -#let touying-reducer(reduce: arr => arr.sum(), cover: arr => none, ..args) = [ - #metadata(( - kind: "touying-reducer", - reduce: reduce, - cover: cover, - kwargs: args.named(), - args: args.pos(), - )) -] - -// parse touying equation, and get the repetitions -#let _parse-touying-equation(self: none, need-cover: true, base: 1, index: 1, eqt) = { - let result-arr = () - // repetitions - let repetitions = base - let max-repetitions = repetitions - // get cover function from self - let cover = self.methods.cover.with(self: self) - // get eqt body - let it = eqt.body - // if it is a function, then call it with self - if type(it) == function { - it = it(self) - } - assert(type(it) == str, message: "Unsupported type: " + str(type(it))) - // parse the content - let result = () - let cover-arr = () - let children = it.split(regex("(#meanwhile;?)|(meanwhile)")).intersperse("touying-meanwhile") - .map(s => s.split(regex("(#pause;?)|(pause)")).intersperse("touying-pause")).flatten() - .map(s => s.split(regex("(\\\\\\s)|(\\\\\\n)")).intersperse("\\\n")).flatten() - .map(s => s.split(regex("&")).intersperse("&")).flatten() - for child in children { - if child == "touying-pause" { - repetitions += 1 - } else if child == "touying-meanwhile" { - // clear the cover-arr when encounter #meanwhile - if cover-arr.len() != 0 { - result.push("cover(" + cover-arr.sum() + ")") - cover-arr = () - } - // then reset the repetitions - max-repetitions = calc.max(max-repetitions, repetitions) - repetitions = 1 - } else if child == "\\\n" or child == "&" { - // clear the cover-arr when encounter linebreak or parbreak - if cover-arr.len() != 0 { - result.push("cover(" + cover-arr.sum() + ")") - cover-arr = () - } - result.push(child) - } else { - if repetitions <= index or not need-cover { - result.push(child) - } else { - cover-arr.push(child) - } - } - } - // clear the cover-arr when end - if cover-arr.len() != 0 { - result.push("cover(" + cover-arr.sum() + ")") - cover-arr = () - } - result-arr.push( - math.equation( - block: eqt.block, - numbering: eqt.numbering, - supplement: eqt.supplement, - eval("$" + result.sum(default: "") + "$", scope: eqt.scope + (cover: (..args) => { - let cover = eqt.scope.at("cover", default: cover) - if args.pos().len() != 0 { - cover(args.pos().first()) - } - })), - ) - ) - max-repetitions = calc.max(max-repetitions, repetitions) - return (result-arr, max-repetitions) -} - -// parse touying mitex, and get the repetitions -#let _parse-touying-mitex(self: none, need-cover: true, base: 1, index: 1, eqt) = { - let result-arr = () - // repetitions - let repetitions = base - let max-repetitions = repetitions - // get eqt body - let it = eqt.body - // if it is a function, then call it with self - if type(it) == function { - it = it(self) - } - assert(type(it) == str, message: "Unsupported type: " + str(type(it))) - // parse the content - let result = () - let cover-arr = () - let children = it.split(regex("\\\\meanwhile")).intersperse("touying-meanwhile") - .map(s => s.split(regex("\\\\pause")).intersperse("touying-pause")).flatten() - .map(s => s.split(regex("(\\\\\\\\\s)|(\\\\\\\\\n)")).intersperse("\\\\\n")).flatten() - .map(s => s.split(regex("&")).intersperse("&")).flatten() - for child in children { - if child == "touying-pause" { - repetitions += 1 - } else if child == "touying-meanwhile" { - // clear the cover-arr when encounter #meanwhile - if cover-arr.len() != 0 { - result.push("\\phantom{" + cover-arr.sum() + "}") - cover-arr = () - } - // then reset the repetitions - max-repetitions = calc.max(max-repetitions, repetitions) - repetitions = 1 - } else if child == "\\\n" or child == "&" { - // clear the cover-arr when encounter linebreak or parbreak - if cover-arr.len() != 0 { - result.push("\\phantom{" + cover-arr.sum() + "}") - cover-arr = () - } - result.push(child) - } else { - if repetitions <= index or not need-cover { - result.push(child) - } else { - cover-arr.push(child) - } - } - } - // clear the cover-arr when end - if cover-arr.len() != 0 { - result.push("\\phantom{" + cover-arr.sum() + "}") - cover-arr = () - } - result-arr.push( - (eqt.mitex)( - block: eqt.block, - numbering: eqt.numbering, - supplement: eqt.supplement, - result.sum(default: ""), - ) - ) - max-repetitions = calc.max(max-repetitions, repetitions) - return (result-arr, max-repetitions) -} - -// parse touying reducer, and get the repetitions -#let _parse-touying-reducer(self: none, base: 1, index: 1, reducer) = { - let result-arr = () - // repetitions - let repetitions = base - let max-repetitions = repetitions - // get cover function from self - let cover = reducer.cover - // parse the content - let result = () - let cover-arr = () - for child in reducer.args.flatten() { - if type(child) == content and child.func() == metadata and type(child.value) == dictionary { - let kind = child.value.at("kind", default: none) - if kind == "touying-pause" { - repetitions += 1 - } else if kind == "touying-meanwhile" { - // clear the cover-arr when encounter #meanwhile - if cover-arr.len() != 0 { - result.push(cover(cover-arr.sum())) - cover-arr = () - } - // then reset the repetitions - max-repetitions = calc.max(max-repetitions, repetitions) - repetitions = 1 - } else { - if repetitions <= index { - result.push(child) - } else { - cover-arr.push(child) - } - } - } else { - if repetitions <= index { - result.push(child) - } else { - cover-arr.push(child) - } - } - } - // clear the cover-arr when end - if cover-arr.len() != 0 { - let r = cover(cover-arr) - if type(r) == array { - result += r - } else { - result.push(r) - } - cover-arr = () - } - result-arr.push( - (reducer.reduce)( - ..reducer.kwargs, - result, - ) - ) - max-repetitions = calc.max(max-repetitions, repetitions) - return (result-arr, max-repetitions) -} - -// parse a sequence into content, and get the repetitions -#let _parse-content(self: none, need-cover: true, base: 1, index: 1, ..bodies) = { - let bodies = bodies.pos() - let result-arr = () - // repetitions - let repetitions = base - let max-repetitions = repetitions - // get cover function from self - let cover = self.methods.cover.with(self: self) - for it in bodies { - // if it is a function, then call it with self - if type(it) == function { - // subslide index - it = it(self) - } - // parse the content - let result = () - let cover-arr = () - let children = if utils.is-sequence(it) { it.children } else { (it,) } - for child in children { - if type(child) == content and child.func() == metadata and type(child.value) == dictionary { - let kind = child.value.at("kind", default: none) - if kind == "touying-pause" { - repetitions += 1 - } else if kind == "touying-meanwhile" { - // clear the cover-arr when encounter #meanwhile - if cover-arr.len() != 0 { - result.push(cover(cover-arr.sum())) - cover-arr = () - } - // then reset the repetitions - max-repetitions = calc.max(max-repetitions, repetitions) - repetitions = 1 - } else if kind == "touying-equation" { - // handle touying-equation - let (conts, nextrepetitions) = _parse-touying-equation( - self: self, need-cover: repetitions <= index, base: repetitions, index: index, child.value - ) - let cont = conts.first() - if repetitions <= index or not need-cover { - result.push(cont) - } else { - cover-arr.push(cont) - } - repetitions = nextrepetitions - } else if kind == "touying-mitex" { - // handle touying-mitex - let (conts, nextrepetitions) = _parse-touying-mitex( - self: self, need-cover: repetitions <= index, base: repetitions, index: index, child.value - ) - let cont = conts.first() - if repetitions <= index or not need-cover { - result.push(cont) - } else { - cover-arr.push(cont) - } - repetitions = nextrepetitions - } else if kind == "touying-reducer" { - // handle touying-reducer - let (conts, nextrepetitions) = _parse-touying-reducer( - self: self, base: repetitions, index: index, child.value - ) - let cont = conts.first() - if repetitions <= index or not need-cover { - result.push(cont) - } else { - cover-arr.push(cont) - } - repetitions = nextrepetitions - } else if kind == "touying-wrapper" { - // handle touying-wrapper - self.subslide = index - if repetitions <= index or not need-cover { - result.push((child.value.fn)(self: self, ..child.value.args)) - } else { - cover-arr.push((child.value.fn)(self: self, ..child.value.args)) - } - if child.value.with-visible-subslides { - max-repetitions = calc.max(max-repetitions, utils.last-required-subslide(child.value.args.pos().at(0))) - } - } else { - if repetitions <= index or not need-cover { - result.push(child) - } else { - cover-arr.push(child) - } - } - } else if child == linebreak() or child == parbreak() { - // clear the cover-arr when encounter linebreak or parbreak - if cover-arr.len() != 0 { - result.push(cover(cover-arr.sum())) - cover-arr = () - } - result.push(child) - } else if utils.is-sequence(child) { - // handle the sequence - let (conts, nextrepetitions) = _parse-content( - self: self, need-cover: repetitions <= index, base: repetitions, index: index, child - ) - let cont = conts.first() - if repetitions <= index or not need-cover { - result.push(cont) - } else { - cover-arr.push(cont) - } - repetitions = nextrepetitions - } else if utils.is-styled(child) { - // handle styled - let (conts, nextrepetitions) = _parse-content( - self: self, need-cover: repetitions <= index, base: repetitions, index: index, child.child - ) - let cont = conts.first() - if repetitions <= index or not need-cover { - result.push(utils.typst-builtin-styled(cont, child.styles)) - } else { - cover-arr.push(utils.typst-builtin-styled(cont, child.styles)) - } - repetitions = nextrepetitions - } else if type(child) == content and child.func() in (list.item, enum.item, align) { - // handle the list item - let (conts, nextrepetitions) = _parse-content( - self: self, need-cover: repetitions <= index, base: repetitions, index: index, child.body - ) - let cont = conts.first() - if repetitions <= index or not need-cover { - result.push(utils.reconstruct(child, cont)) - } else { - cover-arr.push(utils.reconstruct(child, cont)) - } - repetitions = nextrepetitions - } else if type(child) == content and child.func() in (pad,) { - // handle the pad - let (conts, nextrepetitions) = _parse-content( - self: self, need-cover: repetitions <= index, base: repetitions, index: index, child.body - ) - let cont = conts.first() - if repetitions <= index or not need-cover { - result.push(utils.reconstruct(named: true, child, cont)) - } else { - cover-arr.push(utils.reconstruct(named: true, child, cont)) - } - repetitions = nextrepetitions - } else if type(child) == content and child.func() == terms.item { - // handle the terms item - let (conts, nextrepetitions) = _parse-content( - self: self, need-cover: repetitions <= index, base: repetitions, index: index, child.description - ) - let cont = conts.first() - if repetitions <= index or not need-cover { - result.push(terms.item(child.term, cont)) - } else { - cover-arr.push(terms.item(child.term, cont)) - } - repetitions = nextrepetitions - } else { - if repetitions <= index or not need-cover { - result.push(child) - } else { - cover-arr.push(child) - } - } - } - // clear the cover-arr when end - if cover-arr.len() != 0 { - result.push(cover(cover-arr.sum())) - cover-arr = () - } - result-arr.push(result.sum(default: [])) - } - max-repetitions = calc.max(max-repetitions, repetitions) - return (result-arr, max-repetitions) -} - -#let _get-negative-pad(self) = { - let margin = self.page-args.margin - if type(margin) != dictionary and type(margin) != length and type(margin) != relative { - return it => it - } - let cell = block.with(width: 100%, height: 100%, above: 0pt, below: 0pt, breakable: false) - if type(margin) == length or type(margin) == relative { - return it => pad(x: -margin, cell(it)) - } - let pad-args = (:) - if "x" in margin { - pad-args.x = -margin.x - } - if "left" in margin { - pad-args.left = -margin.left - } - if "right" in margin { - pad-args.right = -margin.right - } - if "rest" in margin { - pad-args.rest = -margin.rest - } - it => pad(..pad-args, cell(it)) -} - -#let _get-page-extra-args(self) = { - if self.show-notes-on-second-screen == right { - let margin = self.page-args.margin - assert( - self.page-args.paper == "presentation-16-9" or self.page-args.paper == "presentation-4-3", - message: "The paper of page should be presentation-16-9 or presentation-4-3" - ) - let page-width = if self.page-args.paper == "presentation-16-9" { 841.89pt } else { 793.7pt } - if type(margin) != dictionary and type(margin) != length and type(margin) != relative { - return (:) - } - if type(margin) == length or type(margin) == relative { - margin = (x: margin, y: margin) - } - if "right" not in margin { - assert("x" in margin, message: "The margin should have right or x") - margin.right = margin.x - } - margin.right += page-width - return (margin: margin, width: 2 * page-width) - } else { - return (:) - } -} - -#let _get-header-footer(self) = { - let header = utils.call-or-display(self, self.page-args.at("header", default: none)) - let footer = utils.call-or-display(self, self.page-args.at("footer", default: none)) - // speaker note - if self.show-notes-on-second-screen == right { - assert( - self.page-args.paper == "presentation-16-9" or self.page-args.paper == "presentation-4-3", - message: "The paper of page should be presentation-16-9 or presentation-4-3" - ) - let page-width = if self.page-args.paper == "presentation-16-9" { 841.89pt } else { 793.7pt } - let page-height = if self.page-args.paper == "presentation-16-9" { 473.56pt } else { 595.28pt } - footer += place( - left + bottom, - dx: page-width, - block( - fill: rgb("#E6E6E6"), - width: page-width, - height: page-height, - { - set align(left + top) - set text(size: 24pt, fill: black, weight: "regular") - block( - width: 100%, height: 88pt, inset: (left: 32pt, top: 16pt), outset: 0pt, fill: rgb("#CCCCCC"), - { - states.current-section-title - linebreak() - [ --- ] - states.current-slide-title - }, - ) - pad(x: 48pt, states.current-slide-note) - // clear the slide note - states.slide-note-state.update(none) - } - ) - ) - } - // negative padding - if self.full-header or self.full-footer { - let negative-pad = _get-negative-pad(self) - if self.full-header { - header = negative-pad(header) - } - if self.full-footer { - footer = negative-pad(footer) - } - } - (header, footer) -} - -// touying-slide -#let touying-slide( - self: none, - repeat: auto, - setting: body => body, - composer: auto, - section: none, - subsection: none, - title: none, - ..bodies, -) = { - assert(bodies.named().len() == 0, message: "unexpected named arguments:" + repr(bodies.named().keys())) - let composer-with-side-by-side(..args) = { - if type(composer) == function { - composer(..args) - } else { - utils.side-by-side(columns: composer, ..args) - } - } - let bodies = bodies.pos() - // update pdfpc - let update-pdfpc(curr-subslide) = context [ - #metadata((t: "NewSlide")) - #metadata((t: "Idx", v: here().page() - 1)) - #metadata((t: "Overlay", v: curr-subslide - 1)) - #metadata((t: "LogicalSlide", v: states.slide-counter.get().first())) - ] - let page-preamble(curr-subslide) = context { - if self.reset-footnote { - counter(footnote).update(0) - } - if here().page() == self.first-slide-number { - // preamble - utils.call-or-display(self, self.preamble) - // pdfpc slide markers - if self.pdfpc-file { - pdfpc.pdfpc-file(here()) - } - } - utils.call-or-display(self, self.page-preamble) - if curr-subslide == 1 and title != none { - utils.bookmark(level: 3, title) - } - } - // update states - let _update-states(repetitions) = { - // 1. slide counter part - // if freeze-slide-counter is false, then update the slide-counter - if not self.freeze-slide-counter { - states.slide-counter.step() - // if appendix is false, then update the last-slide-counter - if not self.appendix { - states.last-slide-counter.step() - } - } - // update page counter - context counter(page).update(states.slide-counter.get()) - // 2. section and subsection part - if not self.appendix or self.appendix-in-outline { - // if section is not none, then create a new section - let section = utils.unify-section(section) - if section != none { - states._new-section(duplicate: self.duplicate, short-title: section.short-title, section.title) - utils.bookmark(level: 1, numbering: self.numbering, section.title) - } - // if subsection is not none, then create a new subsection - let subsection = utils.unify-section(subsection) - if subsection != none { - states._new-subsection(duplicate: self.duplicate, short-title: subsection.short-title, subsection.title) - utils.bookmark(level: 2, numbering: self.numbering, subsection.title) - } - states._sections-step(repetitions) - } - // 3. slide title part - states.slide-title-state.update(title) - } - self.subslide = 1 - // for single page slide, get the repetitions - if repeat == auto { - let (_, repetitions) = _parse-content( - self: self, - base: 1, - index: 1, - ..bodies, - ) - repeat = repetitions - } - self.repeat = repeat - // page header and footer - let (header, footer) = _get-header-footer(self) - let page-extra-args = _get-page-extra-args(self) - // for speed up, do not parse the content if repeat is none - if repeat == none { - return { - let conts = bodies.map(it => { - if type(it) == function { - it(self) - } else { - it - } - }) - header = _update-states(1) + update-pdfpc(1) + header - set page(..(self.page-args + page-extra-args + (header: header, footer: footer))) - setting( - page-preamble(1) + composer-with-side-by-side(..conts) - ) - } - } - - if self.handout { - self.subslide = repeat - let (conts, _) = _parse-content(self: self, index: repeat, ..bodies) - header = _update-states(1) + update-pdfpc(1) + header - set page(..(self.page-args + page-extra-args + (header: header, footer: footer))) - setting( - page-preamble(1) + composer-with-side-by-side(..conts) - ) - } else { - // render all the subslides - let result = () - let current = 1 - for i in range(1, repeat + 1) { - self.subslide = i - let (header, footer) = _get-header-footer(self) - let new-header = header - let (conts, _) = _parse-content(self: self, index: i, ..bodies) - // update the counter in the first subslide - if i == 1 { - new-header = _update-states(repeat) + update-pdfpc(i) + new-header - } else { - new-header = update-pdfpc(i) + new-header - } - result.push({ - set page(..(self.page-args + page-extra-args + (header: new-header, footer: footer))) - setting(page-preamble(i) + composer-with-side-by-side(..conts)) - }) - } - // return the result - result.sum() - } -} - -// touying-slides -#let touying-slides(self: none, slide-level: 1, body) = { - // make sure 0 <= slide-level <= 2 - assert(type(slide-level) == int and 0 <= slide-level and slide-level <= 2, message: "slide-level should be 0, 1 or 2") - // init - let (section, subsection, title, slide) = (none, none, none, ()) - let last-title = none - let children = if utils.is-sequence(body) { body.children } else { (body,) } - // convert all sequence to array recursively, and then flatten the array - let sequence-to-array(it) = { - if utils.is-sequence(it) { - it.children.map(sequence-to-array) - } else { - it - } - } - children = children.map(sequence-to-array).flatten() - // trim space of children - children = utils.trim(children) - if children.len() == 0 { return none } - // begin - let i = 0 - let is-end = false - for child in children { - i += 1 - if self.enable-styled-warning and utils.is-styled(child) and type(child.child) == content and not child.child.has("text") { - panic("You should not use set/show rule here, please put it before `#show: slides` or inside `#slide[]`, or use `#slides-end` to terminate `#show: slides` first. Use `#(s.enable-styled-warning = false)` if you think it's a false warning.") - } else if type(child) == content and child.func() == metadata and type(child.value) == dictionary and child.value.at("kind", default: none) == "touying-slides-end" { - is-end = true - break - } else if type(child) == content and child.func() == metadata and type(child.value) == dictionary and child.value.at("kind", default: none) == "touying-slide-wrapper" { - slide = utils.trim(slide) - if slide != () { - (self.methods.slide)(self: self, section: section, subsection: subsection, ..(if last-title != none { (title: last-title) }), slide.sum()) - (section, subsection, title, slide) = (none, none, none, ()) - } - if child.value.name in self.slides { - (child.value.fn)(section: section, subsection: subsection, ..(if last-title != none { (title: last-title) }), ..child.value.args) - } else { - (child.value.fn)(..child.value.args) - } - (section, subsection, title, slide) = (none, none, none, ()) - } else if type(child) == content and child.func() == heading and child.depth <= slide-level + 1 { - slide = utils.trim(slide) - if (child.depth == 1 and section != none) or (child.depth == 2 and subsection != none) or (child.depth > slide-level and title != none) or slide != () { - (self.methods.slide)(self: self, section: section, subsection: subsection, ..(if last-title != none { (title: last-title) }), slide.sum(default: "")) - (section, subsection, title, slide) = (none, none, none, ()) - if child.depth <= slide-level { - last-title = none - } - } - let child-body = if child.body != [] { child.body } else { none } - if child.depth == 1 { - if slide-level >= 1 { - if type(self.methods.at("touying-new-section-slide", default: none)) == function { - (self.methods.touying-new-section-slide)(self: self, child-body) - } else { - section = child-body - } - last-title = none - } else { - title = child.body - last-title = child-body - } - } else if child.depth == 2 { - if slide-level >= 2 { - if type(self.methods.at("touying-new-subsection-slide", default: none)) == function { - (self.methods.touying-new-subsection-slide)(self: self, child-body) - } else { - subsection = child-body - } - last-title = none - } else { - title = child.body - last-title = child-body - } - } else { - title = child.body - last-title = child-body - } - } else { - slide.push(child) - } - } - slide = utils.trim(slide) - if section != none or subsection != none or title != none or slide != () { - (self.methods.slide)(self: self, section: section, subsection: subsection, ..(if last-title != none { (title: last-title) }), slide.sum(default: "")) - } - if is-end { - children.slice(i).sum(default: "") - } -} - -// build the touying singleton -#let s = ( - // info interface - info: ( - title: none, - short-title: auto, - subtitle: none, - short-subtitle: auto, - author: none, - date: none, - institution: none, - logo: none, - ), - // colors interface - colors: ( - neutral: rgb("#303030"), - neutral-light: rgb("#a0a0a0"), - neutral-lighter: rgb("#d0d0d0"), - neutral-lightest: rgb("#ffffff"), - neutral-dark: rgb("#202020"), - neutral-darker: rgb("#101010"), - neutral-darkest: rgb("#000000"), - primary: rgb("#303030"), - primary-light: rgb("#a0a0a0"), - primary-lighter: rgb("#d0d0d0"), - primary-lightest: rgb("#ffffff"), - primary-dark: rgb("#202020"), - primary-darker: rgb("#101010"), - primary-darkest: rgb("#000000"), - secondary: rgb("#303030"), - secondary-light: rgb("#a0a0a0"), - secondary-lighter: rgb("#d0d0d0"), - secondary-lightest: rgb("#ffffff"), - secondary-dark: rgb("#202020"), - secondary-darker: rgb("#101010"), - secondary-darkest: rgb("#000000"), - tertiary: rgb("#303030"), - tertiary-light: rgb("#a0a0a0"), - tertiary-lighter: rgb("#d0d0d0"), - tertiary-lightest: rgb("#ffffff"), - tertiary-dark: rgb("#202020"), - tertiary-darker: rgb("#101010"), - tertiary-darkest: rgb("#000000"), - ), - // slides mode - slides: ("slide",), - // handle mode - handout: false, - // appendix mode - appendix: false, - appendix-in-outline: true, - // freeze slide counter - freeze-slide-counter: false, - freeze-in-empty-page: true, - // enable pdfpc-file - pdfpc-file: true, - // first-slide page number, which will affect preamble, - // default is 1 - first-slide-number: 1, - // warnings - enable-mark-warning: true, - enable-styled-warning: true, - // global preamble - preamble: self => { - if self.enable-mark-warning { - context { - let marks = query() - if marks.len() > 0 { - let page-num = marks.at(0).location().page() - let kind = marks.at(0).value.kind - panic("Unsupported mark `" + kind + "` at page " + str(page-num) + ". You can't use it inside some layout functions like `grid`. You may want to use the callback-style `uncover` function instead. Or you may want to use #slide[][] for a two-column layout. ") - } - } - } - }, - // page preamble - page-preamble: none, - // reset footnote - reset-footnote: true, - // page args - page-args: ( - paper: "presentation-16-9", - header: none, - footer: none, - fill: rgb("#ffffff"), - margin: (x: 3em, y: 2.8em), - ), - // full header / footer - full-header: true, - full-footer: true, - // speaker notes - show-notes-on-second-screen: none, - // numbering - numbering: none, - // duplicate for section and subsection - duplicate: false, - // datetime format - datetime-format: auto, - // register the methods - methods: ( - // info - info: (self: none, ..args) => { - self.info += args.named() - self - }, - // colors - colors: (self: none, ..args) => { - self.colors += args.named() - self - }, - // cover method - cover: utils.method-wrapper(hide), - update-cover: (self: none, is-method: false, cover-fn) => { - if is-method { - self.methods.cover = cover-fn - } else { - self.methods.cover = utils.method-wrapper(cover-fn) - } - self - }, - enable-transparent-cover: ( - self: none, constructor: rgb, alpha: 85%) => { - // it is based on the default cover method - self.methods.cover = (self: none, body) => { - utils.cover-with-rect(fill: utils.update-alpha( - constructor: constructor, self.page-args.fill, alpha), body) - } - self - }, - // dynamic control - uncover: utils.uncover, - only: utils.only, - alternatives-match: utils.alternatives-match, - alternatives: utils.alternatives, - alternatives-fn: utils.alternatives-fn, - alternatives-cases: utils.alternatives-cases, - // alert interface - alert: utils.method-wrapper(text.with(weight: "bold")), - // handout mode - enable-handout-mode: (self: none) => { - self.handout = true - self - }, - // disable pdfpc-file mode - disable-pdfpc-file: (self: none) => { - self.pdfpc-file = false - self - }, - // default slide - touying-slide: touying-slide, - slide: touying-slide, - empty-slide: (self: none, ..args) => { - self = utils.empty-page(self) - (self.methods.slide)(self: self, ..args) - }, - touying-slides: touying-slides, - slides: touying-slides, - // append the preamble - append-preamble: (self: none, preamble) => { - let origin-preamble = self.preamble - self.preamble = self => utils.call-or-display(self, origin-preamble) + utils.call-or-display(self, preamble) - self - }, - // datetime format - datetime-format: (self: none, format) => { - self.datetime-format = format - self - }, - // numbering - numbering: (self: none, section: auto, subsection: auto, numbering) => { - if section == auto and subsection == auto { - self.numbering = numbering - return self - } - let section-numbering = if section == auto { numbering } else { section } - let subsection-numbering = if subsection == auto { numbering } else { subsection } - self.numbering = (..args) => if args.pos().len() == 1 { - states._typst-numbering(section-numbering, ..args) - } else { - states._typst-numbering(subsection-numbering, ..args) - } - self - }, - // default init - init: (self: none, body) => { - // default text size - set text(size: 20pt) - set heading(outlined: false) - show heading.where(level: 2): set block(below: 1em) - body - }, - // default outline - touying-outline: (self: none, ..args) => { - states.touying-outline(self: self, ..args) - }, - appendix: (self: none) => { - self.appendix = true - self - }, - appendix-in-outline: (self: none, value) => { - self.appendix-in-outline = value - self - }, - show-notes-on-second-screen: (self: none, value) => { - self.show-notes-on-second-screen = value - self - }, - speaker-note: (self: none, mode: "typ", setting: it => it, note) => { - if self.at("enable-pdfpc", default: true) { - let raw-text = if type(note) == content and note.has("text") { - note.text - } else { - utils.markup-text(note, mode: mode).trim() - } - pdfpc.speaker-note(raw-text) - } - let show-notes-on-second-screen = self.at("show-notes-on-second-screen", default: none) - assert(show-notes-on-second-screen == none or show-notes-on-second-screen == right, message: "`show-notes-on-second-screen` should be none or right") - if show-notes-on-second-screen != none { - states.slide-note-state.update(setting(note)) - } - } - ), -) diff --git a/src/components.typ b/src/components.typ index 844497e97..4ace6f8be 100644 --- a/src/components.typ +++ b/src/components.typ @@ -1,5 +1,6 @@ #import "utils.typ" +#let _typst-builtin-numbering = numbering #let cell = block.with(width: 100%, height: 100%, above: 0pt, below: 0pt, outset: 0pt, breakable: false) @@ -148,18 +149,20 @@ // start page and end page let start-page = 1 let end-page = calc.inf - let current-heading = utils.current-heading(level: level) - if current-heading != none { - start-page = current-heading.location().page() - if level != auto { - let next-headings = query( - selector(heading.where(level: level)).after(inclusive: false, current-heading.location()), - ) - if next-headings != () { - end-page = next-headings.at(0).location().page() + if level != none { + let current-heading = utils.current-heading(level: level) + if current-heading != none { + start-page = current-heading.location().page() + if level != auto { + let next-headings = query( + selector(heading.where(level: level)).after(inclusive: false, current-heading.location()), + ) + if next-headings != () { + end-page = next-headings.at(0).location().page() + } + } else { + end-page = start-page + 1 } - } else { - end-page = start-page + 1 } } show outline.entry: it => transform( @@ -189,6 +192,8 @@ /// /// - `paged` is a boolean array indicating whether the headings should be paged. Default is `false`. /// +/// - `numbering` is an array of numbering strings for the headings. Default is `()`. +/// /// - `text-fill` is an array of colors for the text fill of the headings. Default is `none`. /// /// - `text-size` is an array of sizes for the text of the headings. Default is `none`. @@ -215,6 +220,7 @@ numbered: (false,), filled: (false,), paged: (false,), + numbering: (), text-fill: none, text-size: none, text-weight: none, @@ -255,8 +261,14 @@ it.level, { if array-at(numbered, it.level - 1) { - numbering(it.element.numbering, ..counter(heading).at(it.element.location())) - h(.3em) + let current-numbering = numbering.at(it.level - 1, default: it.element.numbering) + if current-numbering != none { + _typst-builtin-numbering( + current-numbering, + ..counter(heading).at(it.element.location()), + ) + h(.3em) + } } link( it.element.location(), @@ -274,7 +286,7 @@ }, ) if array-at(paged, it.level - 1) { - numbering( + _typst-builtin-numbering( if page.numbering != none { page.numbering } else { diff --git a/src/utils.typ b/src/utils.typ index 3644162ac..945d1c596 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -1,5 +1,7 @@ #import "pdfpc.typ" +#let _typst-builtin-numbering = numbering + /// Add a dictionary to another dictionary recursively /// /// Example: `add-dicts((a: (b: 1), (a: (c: 2))` returns `(a: (b: 1, c: 2)` @@ -334,7 +336,10 @@ } else { current-heading => { if numbered and current-heading.numbering != none { - numbering(current-heading.numbering, ..counter(heading).at(current-heading.location())) + h(.3em) + _typst-builtin-numbering( + current-heading.numbering, + ..counter(heading).at(current-heading.location()), + ) + h(.3em) } current-heading.body } @@ -347,6 +352,27 @@ ) +/// Display the current heading number on the page. +/// +/// - `level` is the level of the heading. If `level` is `auto`, it will return the last heading on or before the current page. If `level` is a number, it will return the last heading on or before the current page with the same level. +/// +/// - `numbering` is the numbering of the heading. If `numbering` is `auto`, it will use the numbering of the heading. If `numbering` is a string, it will use the string as the numbering. +/// +/// - `hierachical` is a boolean value to indicate whether to return the heading hierachically. If `hierachical` is `true`, it will return the last heading according to the hierachical structure. If `hierachical` is `false`, it will return the last heading on or before the current page with the same level. +/// +/// - `depth` is the maximum depth of the heading to search. Usually, it should be set as slide-level. +#let display-current-heading-number(level: auto, numbering: auto, hierachical: true, depth: 9999) = ( + context { + let current-heading = current-heading(level: level, hierachical: hierachical, depth: depth) + if current-heading != none and numbering == auto and current-heading.numbering != none { + _typst-builtin-numbering(current-heading.numbering, ..counter(heading).at(current-heading.location())) + } else if current-heading != none and numbering != auto { + _typst-builtin-numbering(numbering, ..counter(heading).at(current-heading.location())) + } + } +) + + /// Display the current short heading on the page. /// /// - `level` is the level of the heading. If `level` is `auto`, it will return the last heading on or before the current page. If `level` is a number, it will return the last heading on or before the current page with the same level. diff --git a/themes/aqua.typ b/themes/aqua.typ index bba297d18..9f4778ef0 100644 --- a/themes/aqua.typ +++ b/themes/aqua.typ @@ -1,15 +1,89 @@ -#import "../slide.typ": s -#import "../src/utils.typ" -#import "../src/states.typ" -#import "../src/components.typ" - -#let title-slide(self: none, ..args) = { - self = utils.empty-page(self) - self.page-args.margin += (top: 30%, bottom: 0%) - self.page-args.background = utils.call-or-display(self, self.aqua-background) - let info = self.info + args.named() +#import "../src/exports.typ": * + +#let _typst-builtin-align = align - let content = { +/// Default slide function for the presentation. +/// +/// - `config` is the configuration of the slide. You can use `config-xxx` to set the configuration of the slide. For more several configurations, you can use `utils.merge-dicts` to merge them. +/// +/// - `repeat` is the number of subslides. Default is `auto`,which means touying will automatically calculate the number of subslides. +/// +/// The `repeat` argument is necessary when you use `#slide(repeat: 3, self => [ .. ])` style code to create a slide. The callback-style `uncover` and `only` cannot be detected by touying automatically. +/// +/// - `setting` is the setting of the slide. You can use it to add some set/show rules for the slide. +/// +/// - `composer` is the composer of the slide. You can use it to set the layout of the slide. +/// +/// For example, `#slide(composer: (1fr, 2fr, 1fr))[A][B][C]` to split the slide into three parts. The first and the last parts will take 1/4 of the slide, and the second part will take 1/2 of the slide. +/// +/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `utils.side-by-side` function. +/// +/// The `utils.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. +/// +/// For example, `#slide(composer: 2)[A][B][#grid.cell(colspan: 2)[Footer]] will make the `Footer` cell take 2 columns. +/// +/// If you want to customize the composer, you can pass a function to the `composer` argument. The function should receive the contents of the slide and return the content of the slide, like `#slide(composer: grid.with(columns: 2))[A][B]`. +/// +/// - `..bodies` is the contents of the slide. You can call the `slide` function with syntax like `#slide[A][B][C]` to create a slide. +#let slide( + title: none, + align: auto, + config: (:), + repeat: auto, + setting: body => body, + composer: auto, + ..bodies, +) = touying-slide-wrapper(self => { + if align != auto { + self.store.align = align + } + // restore typst builtin align function + let align = _typst-builtin-align + let header(self) = { + place( + center + top, + dy: .5em, + rect( + width: 100%, + height: 1.8em, + fill: self.colors.primary, + align(left + horizon, h(1.5em) + text(fill: white, utils.call-or-display(self, self.store.header))), + ), + ) + place(left + top, line(start: (30%, 0%), end: (27%, 100%), stroke: .5em + white)) + } + let footer(self) = { + set text(size: 0.8em) + place(right, dx: -5%, utils.call-or-display(self, utils.call-or-display(self, self.store.footer))) + } + let self = utils.merge-dicts( + self, + config-page( + fill: self.colors.neutral-lightest, + header: header, + footer: footer, + ), + ) + touying-slide(self: self, config: config, repeat: repeat, setting: setting, composer: composer, ..bodies) +}) + + +/// Title slide for the presentation. You should update the information in the `config-info` function. You can also pass the information directly to the `title-slide` function. +/// +/// Example: +/// +/// ```typst +/// #show: aqua-theme.with( +/// config-info( +/// title: [Title], +/// ), +/// ) +/// +/// #title-slide(subtitle: [Subtitle], extra: [Extra information]) +/// ``` +#let title-slide(..args) = touying-slide-wrapper(self => { + let info = self.info + args.named() + let body = { set align(center) stack( spacing: 3em, @@ -22,21 +96,26 @@ if info.date != none { text( fill: self.colors.primary-light, - size: 20pt, + size: 20pt, weight: "regular", - utils.info-date(self), + utils.display-info-date(self), ) - } + }, ) } - (self.methods.touying-slide)(self: self, repeat: none, content) -} - -#let outline-slide(self: none, enum-args: (:), leading: 50pt) = { - self = utils.empty-page(self) - self.page-args += ( - background: utils.call-or-display(self, self.aqua-background), + self = utils.merge-dicts( + self, + config-common(freeze-slide-counter: true), + config-page( + background: utils.call-or-display(self, self.store.background), + margin: (x: 0em, top: 30%, bottom: 0%), + ), ) + touying-slide(self: self, body) +}) + + +#let outline-slide(leading: 50pt) = touying-slide-wrapper(self => { set text(size: 30pt, fill: self.colors.primary) set par(leading: leading) @@ -48,40 +127,59 @@ center + horizon, { set par(leading: 20pt) - if self.aqua-lang == "zh" { - text( - size: 80pt, - weight: "bold", - [#text(size:36pt)[CONTENTS]\ 目录] - ) - } else if self.aqua-lang == "en" { - text( - size: 48pt, - weight: "bold", - [CONTENTS] - ) + context { + if text.lang == "zh" { + text( + size: 80pt, + weight: "bold", + [#text(size:36pt)[CONTENTS]\ 目录], + ) + } else { + text( + size: 48pt, + weight: "bold", + [CONTENTS], + ) + } } - } + }, ), align( left + horizon, { set par(leading: leading) set text(weight: "bold") - (self.methods.touying-outline)(self: self, enum-args: (numbering: self.numbering, ..enum-args)) - } - ) + components.custom-progressive-outline( + level: none, + depth: 1, + numbered: (true,), + ) + }, + ), ) } - (self.methods.touying-slide)(self: self, repeat: none, body) -} - -#let new-section-slide(self: none, section) = { - self = utils.empty-page(self) - self.page-args += ( - margin: (left:0%, right:0%, top: 20%, bottom:0%), - background: utils.call-or-display(self, self.aqua-background), + self = utils.merge-dicts( + self, + config-common(freeze-slide-counter: true), + config-page( + background: utils.call-or-display(self, self.store.background), + margin: 0em, + ), ) + touying-slide(self: self, body) +}) + + +/// New section slide for the presentation. You can update it by updating the `new-section-slide-fn` argument for `config-common` function. +/// +/// Example: `config-common(new-section-slide-fn: new-section-slide.with(numbered: false))` +/// +/// - `level` is the level of the heading. +/// +/// - `numbered` is whether the heading is numbered. +/// +/// - `title` is the title of the section. It will be pass by touying automatically. +#let new-section-slide(level: 1, numbered: true, title) = touying-slide-wrapper(self => { let body = { stack( dir: ttb, @@ -91,8 +189,8 @@ text( fill: self.colors.primary, size: 166pt, - states.current-section-number(numbering: self.numbering) - ) + utils.display-current-heading-number(level: level), + ), ), align( center, @@ -100,132 +198,131 @@ fill: self.colors.primary, size: 60pt, weight: "bold", - section - ) - ) - ) + utils.display-current-heading(level: level, numbered: false), + ), + ), + ) } - (self.methods.touying-slide)(self: self, repeat: none, section: section, body) -} + self = utils.merge-dicts( + self, + config-page( + margin: (left: 0%, right: 0%, top: 20%, bottom: 0%), + background: utils.call-or-display(self, self.store.background), + ), + ) + touying-slide(self: self, body) +}) -#let focus-slide(self: none, body) = { - self = utils.empty-page(self) - self.page-args += (fill: self.colors.primary, margin: 2em) - set text(fill: white, size: 2em, weight: "bold") - (self.methods.touying-slide)(self: self, repeat: none, align(horizon + center, body)) -} -#let slide(self: none, title: auto, ..args) = { - if title != auto { - self.aqua-title = title - } - (self.methods.touying-slide)( - self: self, - title: title, - setting: body => { - show heading.where(level:1): body => text(fill: self.colors.primary-light)[#body#v(3%)] - body - }, - ..args, +/// Focus on some content. +/// +/// Example: `#focus-slide[Wake up!]` +#let focus-slide(body) = touying-slide-wrapper(self => { + self = utils.merge-dicts( + self, + config-common(freeze-slide-counter: true), + config-page(fill: self.colors.primary, margin: 2em), ) -} + set text(fill: self.colors.neutral-lightest, size: 2em, weight: "bold") + touying-slide(self: self, align(horizon + center, body)) +}) -#let slides(self: none, title-slide: true, outline-slide: true, slide-level: 1, ..args) = { - if title-slide { - (self.methods.title-slide)(self: self) - } - if outline-slide { - (self.methods.outline-slide)(self: self) - } - (self.methods.touying-slides)(self: self, slide-level: slide-level, ..args) -} -#let register( - self: s, +/// Touying aqua theme. +/// +/// Example: +/// +/// ```typst +/// #show: aqua-theme.with(aspect-ratio: "16-9", config-colors(primary: blue))` +/// ``` +/// +/// Consider using: +/// +/// ```typst +/// #set text(font: "Fira Sans", weight: "light", size: 20pt)` +/// #show math.equation: set text(font: "Fira Math") +/// #set strong(delta: 100) +/// #set par(justify: true) +/// ``` +/// +/// - `aspect-ratio` is the aspect ratio of the slides. Default is `16-9`. +#let aqua-theme( aspect-ratio: "16-9", - footer: context { states.slide-counter.display() }, - lang: "en", + header: utils.display-current-heading, + footer: context utils.slide-counter.display(), ..args, + body, ) = { - assert(lang in ("zh", "en"), message: "lang must be 'zh' or 'en'") + set text(size: 20pt) + set heading(numbering: "1.1") + show heading.where(level: 1): set heading(numbering: "01") - self = (self.methods.colors)( - self: self, - primary: rgb("#003F88"), - primary-light: rgb("#2159A5"), - primary-lightest: rgb("#F2F4F8"), - ) + show: touying-slides.with( + config-page( + paper: "presentation-" + aspect-ratio, + margin: (x: 2em, top: 3.5em, bottom: 2em), + ), + config-common( + slide-fn: slide, + new-section-slide-fn: new-section-slide, + ), + config-methods( + init: (self: none, body) => { + show strong: self.methods.alert.with(self: self) + show heading.where(level: self.slide-level + 1): body => text(fill: self.colors.primary-light)[#body#v(3%)] - self.aqua-title = states.current-section-with-numbering - self.aqua-footer = footer - self.aqua-lang = lang - self.aqua-background = (self) => { - let page-width = if self.page-args.paper == "presentation-16-9" { 841.89pt } else { 793.7pt } - let r = if self.show-notes-on-second-screen == none { 1.0 } else { 0.5 } - let bias1 = - page-width * (1-r) - let bias2 = - page-width * 2 * (1-r) - place(left + top, dx: -15pt, dy: -26pt, - circle(radius: 40pt, fill: self.colors.primary)) - place(left + top, dx: 65pt, dy: 12pt, - circle(radius: 21pt, fill: self.colors.primary)) - place(left + top, dx: r * 3%, dy: 15%, - circle(radius: 13pt, fill: self.colors.primary)) - place(left + top, dx: r * 2.5%, dy: 27%, - circle(radius: 8pt, fill: self.colors.primary)) - place(right + bottom, dx: 15pt + bias2, dy: 26pt, - circle(radius: 40pt, fill: self.colors.primary)) - place(right + bottom, dx: -65pt + bias2, dy: -12pt, - circle(radius: 21pt, fill: self.colors.primary)) - place(right + bottom, dx: r * -3% + bias2, dy: -15%, - circle(radius: 13pt, fill: self.colors.primary)) - place(right + bottom, dx: r * -2.5% + bias2, dy: -27%, - circle(radius: 8pt, fill: self.colors.primary)) - place(center + horizon, dx: bias1, polygon(fill: self.colors.primary-lightest, - (35% * page-width, -17%), (70% * page-width, 10%), (35% * page-width, 30%), (0% * page-width, 10%))) - place(center + horizon, dy: 7%, dx: bias1, - ellipse(fill: white, width: r * 45%, height: 120pt)) - place(center + horizon, dy: 5%, dx: bias1, - ellipse(fill: self.colors.primary-lightest, width: r * 40%, height: 80pt)) - place(center + horizon, dy: 12%, dx: bias1, - rect(fill: self.colors.primary-lightest, width: r * 40%, height: 60pt)) - place(center + horizon, dy: 20%, dx: bias1, - ellipse(fill: white, width: r * 40%, height: 70pt)) - place(center + horizon, dx: r * 28% + bias1, dy: -6%, - circle(radius: 13pt, fill: white)) - } - let header(self) = { - place(center + top, dy: .5em, - rect( - width: 100%, - height: 1.8em, - fill: self.colors.primary, - align(left + horizon, h(1.5em) + text(fill:white, utils.call-or-display(self, self.aqua-title))) - ) - ) - place(left + top, line(start: (30%, 0%), end: (27%, 100%), stroke: .5em + white)) - } - let footer(self) = { - set text(size: 0.8em) - place(right, dx: -5%, utils.call-or-display(self, utils.call-or-display(self, self.aqua-footer))) - } - self.page-args += ( - paper: "presentation-" + aspect-ratio, - margin: (x: 2em, top: 3.5em, bottom: 2em), - header: header, - footer: footer, + body + }, + alert: utils.alert-with-primary-color, + ), + config-colors( + primary: rgb("#003F88"), + primary-light: rgb("#2159A5"), + primary-lightest: rgb("#F2F4F8"), + neutral-lightest: rgb("#FFFFFF") + ), + // save the variables for later use + config-store( + align: align, + header: header, + footer: footer, + background: self => { + let page-width = if self.page.paper == "presentation-16-9" { 841.89pt } else { 793.7pt } + let r = if self.at("show-notes-on-second-screen", default: none) == none { 1.0 } else { 0.5 } + let bias1 = - page-width * (1-r) + let bias2 = - page-width * 2 * (1-r) + place(left + top, dx: -15pt, dy: -26pt, + circle(radius: 40pt, fill: self.colors.primary)) + place(left + top, dx: 65pt, dy: 12pt, + circle(radius: 21pt, fill: self.colors.primary)) + place(left + top, dx: r * 3%, dy: 15%, + circle(radius: 13pt, fill: self.colors.primary)) + place(left + top, dx: r * 2.5%, dy: 27%, + circle(radius: 8pt, fill: self.colors.primary)) + place(right + bottom, dx: 15pt + bias2, dy: 26pt, + circle(radius: 40pt, fill: self.colors.primary)) + place(right + bottom, dx: -65pt + bias2, dy: -12pt, + circle(radius: 21pt, fill: self.colors.primary)) + place(right + bottom, dx: r * -3% + bias2, dy: -15%, + circle(radius: 13pt, fill: self.colors.primary)) + place(right + bottom, dx: r * -2.5% + bias2, dy: -27%, + circle(radius: 8pt, fill: self.colors.primary)) + place(center + horizon, dx: bias1, polygon(fill: self.colors.primary-lightest, + (35% * page-width, -17%), (70% * page-width, 10%), (35% * page-width, 30%), (0% * page-width, 10%))) + place(center + horizon, dy: 7%, dx: bias1, + ellipse(fill: white, width: r * 45%, height: 120pt)) + place(center + horizon, dy: 5%, dx: bias1, + ellipse(fill: self.colors.primary-lightest, width: r * 40%, height: 80pt)) + place(center + horizon, dy: 12%, dx: bias1, + rect(fill: self.colors.primary-lightest, width: r * 40%, height: 60pt)) + place(center + horizon, dy: 20%, dx: bias1, + ellipse(fill: white, width: r * 40%, height: 70pt)) + place(center + horizon, dx: r * 28% + bias1, dy: -6%, + circle(radius: 13pt, fill: white)) + } + ), + ..args, ) - self.methods.init = (self: none, body) => { - set heading(outlined: false) - set text(size: 20pt) - body - } - self.numbering = "01" - self.methods.title-slide = title-slide - self.methods.outline-slide = outline-slide - self.methods.focus-slide = focus-slide - self.methods.new-section-slide = new-section-slide - self.methods.touying-new-section-slide = new-section-slide - self.methods.slide = slide - self.methods.slides = slides - self + + body } diff --git a/themes/themes.typ b/themes/themes.typ index ee185a2ca..f89f85410 100644 --- a/themes/themes.typ +++ b/themes/themes.typ @@ -3,4 +3,4 @@ #import "metropolis.typ" #import "dewdrop.typ" #import "university.typ" -// #import "aqua.typ" \ No newline at end of file +#import "aqua.typ" \ No newline at end of file From 9ec14ef8033b7fd1830ed6cd1343eca31f982958 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Mon, 2 Sep 2024 02:22:24 +0800 Subject: [PATCH 39/43] dev: update comments --- src/components.typ | 42 ++++++++++++++- src/configs.typ | 117 ++++++++++++++++++++++++++---------------- src/core.typ | 14 ++--- src/pdfpc.typ | 37 +++++++++++++ src/utils.typ | 29 +---------- themes/aqua.typ | 37 ++++++++----- themes/default.typ | 4 +- themes/dewdrop.typ | 46 ++++++++++++++++- themes/metropolis.typ | 39 ++++++++++++-- themes/simple.typ | 39 +++++++++++--- themes/university.typ | 34 +++++++++++- 11 files changed, 326 insertions(+), 112 deletions(-) diff --git a/src/components.typ b/src/components.typ index 4ace6f8be..1e0f2bc7d 100644 --- a/src/components.typ +++ b/src/components.typ @@ -5,6 +5,34 @@ #let cell = block.with(width: 100%, height: 100%, above: 0pt, below: 0pt, outset: 0pt, breakable: false) +/// SIDE BY SIDE +/// +/// A simple wrapper around `grid` that creates a grid with a single row. +/// It is useful for creating side-by-side slide. +/// +/// It is also the default function for composer in the slide function. +/// +/// Example: `side-by-side[a][b][c]` will display `a`, `b`, and `c` side by side. +/// +/// - `columns` is the number of columns. Default is `auto`, which means the number of columns is equal to the number of bodies. +/// +/// - `gutter` is the space between columns. Default is `1em`. +/// +/// - `..bodies` is the contents to display side by side. +#let side-by-side(columns: auto, gutter: 1em, ..bodies) = { + let bodies = bodies.pos() + if bodies.len() == 1 { + return bodies.first() + } + let columns = if columns == auto { + (1fr,) * bodies.len() + } else { + columns + } + grid(columns: columns, gutter: gutter, ..bodies) +} + + /// Adaptive columns layout /// /// Example: `components.adaptive-columns(outline())` @@ -311,7 +339,19 @@ ) - +/// Show mini slides. It is usually used to show the navigation of the presentation in header. +/// +/// - `self` is the self context, which is used to get the short heading of the headings. +/// +/// - `fill` is the fill color of the headings. Default is `rgb("000000")`. +/// +/// - `alpha` is the transparency of the headings. Default is `60%`. +/// +/// - `display-section` is a boolean indicating whether the sections should be displayed. Default is `false`. +/// +/// - `display-subsection` is a boolean indicating whether the subsections should be displayed. Default is `true`. +/// +/// - `short-heading` is a boolean indicating whether the headings should be shortened. Default is `true`. #let mini-slides( self: none, fill: rgb("000000"), diff --git a/src/configs.typ b/src/configs.typ index 9284fc8c1..24196de68 100644 --- a/src/configs.typ +++ b/src/configs.typ @@ -73,58 +73,88 @@ /// The common configurations of the slides. /// -/// - handout (bool): Whether to enable the handout mode. It retains only the last subslide of each slide in handout mode. +/// - `handout` (bool): Whether to enable the handout mode. It retains only the last subslide of each slide in handout mode. The default value is `false`. /// -/// - cover (function): The function to cover content. The default value is `hide` function. +/// - `slide-level` (int): The level of the slides. The default value is `2`, which means the level 1 and 2 headings will be treated as slides. /// -/// - slide-level (int): The level of the slides. The default value is `2`, which means the level 1 and 2 headings will be treated as slides. +/// - `slide-fn` (function): The function to create a new slide. /// -/// - slide-fn (function): The function to create a new slide. +/// - `new-section-slide` (function): The function to create a new slide for a new section. The default value is `none`. /// -/// - new-section-slide (function): The function to create a new slide for a new section. The default value is `none`. +/// - `new-subsection-slide` (function): The function to create a new slide for a new subsection. The default value is `none`. /// -/// - new-subsection-slide (function): The function to create a new slide for a new subsection. The default value is `none`. +/// - `new-subsubsection-slide` (function): The function to create a new slide for a new subsubsection. The default value is `none`. /// -/// - new-subsubsection-slide (function): The function to create a new slide for a new subsubsection. The default value is `none`. +/// - `new-subsubsubsection-slide` (function): The function to create a new slide for a new subsubsubsection. The default value is `none`. /// -/// - new-subsubsubsection-slide (function): The function to create a new slide for a new subsubsubsection. The default value is `none`. +/// - `datetime-format` (auto, string): The format of the datetime. The default value is `auto`. /// -/// - datetime-format (string): The format of the datetime. +/// - `appendix` (bool): Is touying in the appendix mode. The last-slide-counter will be frozen in the appendix mode. The default value is `false`. /// -/// - appendix (bool): Is touying in the appendix mode. The last-slide-counter will be frozen in the appendix mode. The default value is `false`. +/// - `freeze-slide-counter` (bool): Whether to freeze the slide counter. The default value is `false`. /// -/// - freeze-slide-counter (bool): Whether to freeze the slide counter. The default value is `false`. +/// - `zero-margin-header` (bool): Whether to show the full header (with negative padding). The default value is `true`. /// -/// - zero-margin-header (bool): Whether to show the full header (with negative padding). The default value is `true`. +/// - `zero-margin-footer` (bool): Whether to show the full footer (with negative padding). The default value is `true`. /// -/// - zero-margin-footer (bool): Whether to show the full footer (with negative padding). The default value is `true`. +/// - `auto-offset-for-heading` (bool): Whether to add an offset relative to slide-level for headings. The default value is `true`. /// -/// - auto-offset-for-heading (bool): Whether to add an offset relative to slide-level for headings. The default value is `true`. -/// -/// - enable-pdfpc (bool): Whether to add `` label for querying. +/// - `enable-pdfpc` (bool): Whether to add `` label for querying. The default value is `true`. /// /// You can export the .pdfpc file directly using: `typst query --root . ./example.typ --field value --one "" > ./example.pdfpc` /// +/// - `enable-mark-warning` (bool): Whether to enable the mark warning. The default value is `true`. +/// +/// - `reset-page-counter-to-slide-counter` (bool): Whether to reset the page counter to the slide counter. The default value is `true`. +/// /// ------------------------------------------------------------ /// The following configurations are some black magics for better slides writing, /// maybe will be deprecated in the future. /// ------------------------------------------------------------ /// -/// - show-notes-on-second-screen (none, alignment): Whether to show the speaker notes on the second screen. +/// - `show-notes-on-second-screen` (none, alignment): Whether to show the speaker notes on the second screen. The default value is `none`. /// -/// Currently, the alignment can be `none` and `right`. +/// Currently, the alignment can be `none`, `bottom` and `right`. /// -/// - horizontal-line-to-pagebreak (bool): Whether to convert horizontal lines to page breaks. +/// - `horizontal-line-to-pagebreak` (bool): Whether to convert horizontal lines to page breaks. The default value is `true`. /// /// You can use markdown-like syntax `---` to divide slides. /// -/// - reset-footnote-number-per-slide (bool): Whether to reset the footnote number per slide. +/// - `reset-footnote-number-per-slide` (bool): Whether to reset the footnote number per slide. The default value is `true`. +/// +/// - `nontight-list-enum-and-terms` (bool): Whether to make `tight` argument always be `false` for list, enum, and terms. The default value is `true`. +/// +/// - `align-list-marker-with-baseline` (bool): Whether to align the list marker with the baseline. The default value is `false`. +/// +/// - `scale-list-items` (none, float): Whether to scale the list items recursively. For example, `scale-list-items: 0.8` will scale the list items by 0.8. The default value is `none`. +/// +/// - `enable-frozen-states-and-counters` (bool): Whether to enable the frozen states and counters. It is useful for equations, figures and theorems. The default value is `true`. +/// +/// - `frozen-states` (array): The frozen states for the frozen states and counters. The default value is `()`. +/// +/// - `default-frozen-states` (function): The default frozen states for the frozen states and counters. The default value is state for `ctheorems` package. +/// +/// - `frozen-counters` (array): The frozen counters for the frozen states and counters. You can pass some counters like `(counter(math.equation),)`. The default value is `()`. +/// +/// - `default-frozen-counters` (array): The default frozen counters for the frozen states and counters. The default value is `(counter(math.equation), counter(figure.where(kind: table)), counter(figure.where(kind: image))`. +/// +/// - `label-only-on-last-subslide` (array): We only label some contents in the last subslide, which is useful for ref equations, figures, and theorems with multiple subslides. The default value is `(figure, math.equation)`. +/// +/// - `preamble` (function): The function to run before each slide. The default value is `none`. +/// +/// - `default-preamble` (function): The default preamble for each slide. The default value is a function to check the mark warning and add pdfpc file. +/// +/// - `slide-preamble` (function): The function to run before each slide. The default value is `none`. /// -/// - nontight-list-enum-and-terms (bool): Whether to make `tight` argument always be `false` for list, enum, and terms. The default value is `true`. +/// - `default-slide-preamble` (function): The default preamble for each slide. The default value is `none`. /// -/// - align-list-marker-with-baseline (bool): Whether to align the list marker with the baseline. The default value is `false`. +/// - `subslide-preamble` (function): The function to run before each subslide. The default value is `none`. /// -/// - scale-list-items (none, float): Whether to scale the list items recursively. The default value is `none`. +/// - `default-subslide-preamble` (function): The default preamble for each subslide. The default value is `none`. +/// +/// - `page-preamble` (function): The function to run before each page. The default value is `none`. +/// +/// - `default-page-preamble` (function): The default preamble for each page. The default value is a function to reset the footnote number per slide and reset the page counter to the slide counter. #let config-common( handout: _default, slide-level: _default, @@ -150,7 +180,6 @@ frozen-counters: _default, default-frozen-counters: _default, label-only-on-last-subslide: _default, - first-slide-number: _default, preamble: _default, default-preamble: _default, slide-preamble: _default, @@ -189,7 +218,6 @@ default-frozen-states: default-frozen-states, default-frozen-counters: default-frozen-counters, label-only-on-last-subslide: label-only-on-last-subslide, - first-slide-number: first-slide-number, preamble: preamble, default-preamble: default-preamble, slide-preamble: slide-preamble, @@ -250,31 +278,33 @@ /// The configuration of the methods /// -/// - init (function): The function to initialize the presentation. It should be `(self: none, body) => { .. }`. +/// - `init` (function): The function to initialize the presentation. It should be `(self: none, body) => { .. }`. /// /// By default, it shows the strong content with the `alert` function: `show strong: self.methods.alert.with(self: self)` /// -/// - cover (function): The function to cover content. The default value is `utils.method-wrapper(hide)` function. +/// - `cover` (function): The function to cover content. The default value is `utils.method-wrapper(hide)` function. /// -/// - uncover (function): The function to uncover content. +/// - `uncover` (function): The function to uncover content. The default value is `utils.uncover` function. /// -/// - only (function): The function to show only the content. +/// - `only` (function): The function to show only the content. The default value is `utils.only` function. /// -/// - alternatives-match (function): The function to match alternatives. +/// - `alternatives-match` (function): The function to match alternatives. The default value is `utils.alternatives-match` function. /// -/// - alternatives (function): The function to show alternatives. +/// - `alternatives` (function): The function to show alternatives. The default value is `utils.alternatives` function. /// -/// - alternatives-fn (function): The function to show alternatives with a function. +/// - `alternatives-fn` (function): The function to show alternatives with a function. The default value is `utils.alternatives-fn` function. /// -/// - alternatives-cases (function): The function to show alternatives with cases. +/// - `alternatives-cases` (function): The function to show alternatives with cases. The default value is `utils.alternatives-cases` function. /// -/// - alert (function): The function to alert the content. +/// - `alert` (function): The function to alert the content. The default value is `utils.method-wrapper(text.with(weight: "bold"))` function. /// -/// - show-notes (function): The function to show notes on second screen. +/// - `show-notes` (function): The function to show notes on second screen. It should be `(self: none, width: 0pt, height: 0pt) => { .. }` with core code `utils.current-slide-note` and `utils.slide-note-state.update(none)`. /// -/// - convert-label-to-short-heading (function): The function to convert label to short heading. It is useful for the short heading for heading with label. +/// - `convert-label-to-short-heading` (function): The function to convert label to short heading. It is useful for the short heading for heading with label. It will be used in function with `short-heading`. /// -/// It should be `(self: none, width: 0pt, height: 0pt) => { .. }`. +/// The default value is `utils.titlecase(lbl.replace(regex("^[^:]*:"), "").replace("_", " ").replace("-", " "))`. +/// +/// It means that some headings with labels like `section:my-section` will be converted to `My Section`. #let config-methods( // init init: _default, @@ -534,13 +564,18 @@ reset-page-counter-to-slide-counter: true, // some black magics for better slides writing, // maybe will be deprecated in the future + show-notes-on-second-screen: none, + horizontal-line-to-pagebreak: true, + reset-footnote-number-per-slide: true, + nontight-list-enum-and-terms: true, + align-list-marker-with-baseline: false, + scale-list-items: none, enable-frozen-states-and-counters: true, frozen-states: (), default-frozen-states: _default-frozen-states, frozen-counters: (), default-frozen-counters: _default-frozen-counters, label-only-on-last-subslide: (figure, math.equation), - first-slide-number: 1, preamble: none, default-preamble: _default-preamble, slide-preamble: none, @@ -549,12 +584,6 @@ default-subslide-preamble: none, page-preamble: none, default-page-preamble: _default-page-preamble, - show-notes-on-second-screen: none, - horizontal-line-to-pagebreak: true, - reset-footnote-number-per-slide: true, - nontight-list-enum-and-terms: true, - align-list-marker-with-baseline: false, - scale-list-items: none, ), config-methods( // init diff --git a/src/core.typ b/src/core.typ index b0a025090..08131b25a 100644 --- a/src/core.typ +++ b/src/core.typ @@ -1381,9 +1381,9 @@ /// /// For example, `#slide(composer: (1fr, 2fr, 1fr))[A][B][C]` to split the slide into three parts. The first and the last parts will take 1/4 of the slide, and the second part will take 1/2 of the slide. /// -/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `utils.side-by-side` function. +/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `components.side-by-side` function. /// -/// The `utils.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. +/// The `components.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. /// /// For example, `#slide(composer: 2)[A][B][#grid.cell(colspan: 2)[Footer]] will make the `Footer` cell take 2 columns. /// @@ -1410,7 +1410,7 @@ if type(composer) == function { composer(..args) } else { - utils.side-by-side(columns: composer, ..args) + components.side-by-side(columns: composer, ..args) } } let bodies = bodies.pos() @@ -1589,9 +1589,9 @@ /// /// For example, `#slide(composer: (1fr, 2fr, 1fr))[A][B][C]` to split the slide into three parts. The first and the last parts will take 1/4 of the slide, and the second part will take 1/2 of the slide. /// -/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `utils.side-by-side` function. +/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `components.side-by-side` function. /// -/// The `utils.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. +/// The `components.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. /// /// For example, `#slide(composer: 2)[A][B][#grid.cell(colspan: 2)[Footer]] will make the `Footer` cell take 2 columns. /// @@ -1623,9 +1623,9 @@ /// /// For example, `#slide(composer: (1fr, 2fr, 1fr))[A][B][C]` to split the slide into three parts. The first and the last parts will take 1/4 of the slide, and the second part will take 1/2 of the slide. /// -/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `utils.side-by-side` function. +/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `components.side-by-side` function. /// -/// The `utils.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. +/// The `components.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. /// /// For example, `#slide(composer: 2)[A][B][#grid.cell(colspan: 2)[Footer]] will make the `Footer` cell take 2 columns. /// diff --git a/src/pdfpc.typ b/src/pdfpc.typ index 7c9e3a21c..1ef47571d 100644 --- a/src/pdfpc.typ +++ b/src/pdfpc.typ @@ -50,6 +50,7 @@ [#metadata(pdfpc)] } +/// Add some speaker notes to the slide for exporting to pdfpc file. #let speaker-note(text) = { let text = if type(text) == str { text @@ -73,6 +74,42 @@ #metadata((t: "HiddenSlide")) ] + +/// Configuration for the pdfpc export. You can export the pdfpc file by shell command `typst query --root . ./example.typ --field value --one "" > ./example.pdfpc`. +/// +/// Example: +/// +/// ```typ +/// #pdfpc.config( +/// duration-minutes: 30, +/// start-time: datetime(hour: 14, minute: 10, second: 0), +/// end-time: datetime(hour: 14, minute: 40, second: 0), +/// last-minutes: 5, +/// note-font-size: 12, +/// disable-markdown: false, +/// default-transition: ( +/// type: "push", +/// duration-seconds: 2, +/// angle: ltr, +/// alignment: "vertical", +/// direction: "inward", +/// ), +/// ) +/// ``` +/// +/// - `duration-minutes` is the duration of the presentation in minutes. +/// +/// - `start-time` is the start time of the presentation. +/// +/// - `end-time` is the end time of the presentation. +/// +/// - `last-minutes` is the number of minutes to show the last slide. +/// +/// - `note-font-size` is the font size of the speaker notes. +/// +/// - `disable-markdown` is a flag to disable markdown in the speaker notes. +/// +/// - `default-transition` is the default transition for the slides. #let config( duration-minutes: none, start-time: none, diff --git a/src/utils.typ b/src/utils.typ index 945d1c596..bea4ecc4d 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -1055,31 +1055,4 @@ if show-notes-on-second-screen != none { slide-note-state.update(setting(note)) } -} - -// SIDE BY SIDE - -/// A simple wrapper around `grid` that creates a grid with a single row. -/// It is useful for creating side-by-side slide. -/// -/// It is also the default function for composer in the slide function. -/// -/// Example: `side-by-side[a][b][c]` will display `a`, `b`, and `c` side by side. -/// -/// - `columns` is the number of columns. Default is `auto`, which means the number of columns is equal to the number of bodies. -/// -/// - `gutter` is the space between columns. Default is `1em`. -/// -/// - `..bodies` is the contents to display side by side. -#let side-by-side(columns: auto, gutter: 1em, ..bodies) = { - let bodies = bodies.pos() - if bodies.len() == 1 { - return bodies.first() - } - let columns = if columns == auto { - (1fr,) * bodies.len() - } else { - columns - } - grid(columns: columns, gutter: gutter, ..bodies) -} +} \ No newline at end of file diff --git a/themes/aqua.typ b/themes/aqua.typ index 9f4778ef0..0080a3bb9 100644 --- a/themes/aqua.typ +++ b/themes/aqua.typ @@ -1,7 +1,5 @@ #import "../src/exports.typ": * -#let _typst-builtin-align = align - /// Default slide function for the presentation. /// /// - `config` is the configuration of the slide. You can use `config-xxx` to set the configuration of the slide. For more several configurations, you can use `utils.merge-dicts` to merge them. @@ -16,9 +14,9 @@ /// /// For example, `#slide(composer: (1fr, 2fr, 1fr))[A][B][C]` to split the slide into three parts. The first and the last parts will take 1/4 of the slide, and the second part will take 1/2 of the slide. /// -/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `utils.side-by-side` function. +/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `components.side-by-side` function. /// -/// The `utils.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. +/// The `components.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. /// /// For example, `#slide(composer: 2)[A][B][#grid.cell(colspan: 2)[Footer]] will make the `Footer` cell take 2 columns. /// @@ -26,19 +24,12 @@ /// /// - `..bodies` is the contents of the slide. You can call the `slide` function with syntax like `#slide[A][B][C]` to create a slide. #let slide( - title: none, - align: auto, config: (:), repeat: auto, setting: body => body, composer: auto, ..bodies, ) = touying-slide-wrapper(self => { - if align != auto { - self.store.align = align - } - // restore typst builtin align function - let align = _typst-builtin-align let header(self) = { place( center + top, @@ -115,6 +106,9 @@ }) +/// Outline slide for the presentation. +/// +/// - `leading` is the leading of paragraphs in the outline. Default is `50pt`. #let outline-slide(leading: 50pt) = touying-slide-wrapper(self => { set text(size: 30pt, fill: self.colors.primary) set par(leading: leading) @@ -176,10 +170,8 @@ /// /// - `level` is the level of the heading. /// -/// - `numbered` is whether the heading is numbered. -/// /// - `title` is the title of the section. It will be pass by touying automatically. -#let new-section-slide(level: 1, numbered: true, title) = touying-slide-wrapper(self => { +#let new-section-slide(level: 1, title) = touying-slide-wrapper(self => { let body = { stack( dir: ttb, @@ -246,6 +238,23 @@ /// ``` /// /// - `aspect-ratio` is the aspect ratio of the slides. Default is `16-9`. +/// +/// - `header` is the header of the slides. Default is `utils.display-current-heading`. +/// +/// - `footer` is the footer of the slides. Default is `context utils.slide-counter.display()`. +/// +/// ---------------------------------------- +/// +/// The default colors: +/// +/// ```typ +/// config-colors( +/// primary: rgb("#003F88"), +/// primary-light: rgb("#2159A5"), +/// primary-lightest: rgb("#F2F4F8"), +/// neutral-lightest: rgb("#FFFFFF") +/// ) +/// ``` #let aqua-theme( aspect-ratio: "16-9", header: utils.display-current-heading, diff --git a/themes/default.typ b/themes/default.typ index 51ee068fa..5ce1cdc73 100644 --- a/themes/default.typ +++ b/themes/default.typ @@ -14,9 +14,9 @@ /// /// For example, `#slide(composer: (1fr, 2fr, 1fr))[A][B][C]` to split the slide into three parts. The first and the last parts will take 1/4 of the slide, and the second part will take 1/2 of the slide. /// -/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `utils.side-by-side` function. +/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `components.side-by-side` function. /// -/// The `utils.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. +/// The `components.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. /// /// For example, `#slide(composer: 2)[A][B][#grid.cell(colspan: 2)[Footer]] will make the `Footer` cell take 2 columns. /// diff --git a/themes/dewdrop.typ b/themes/dewdrop.typ index 408bd83fa..a78c11dd8 100644 --- a/themes/dewdrop.typ +++ b/themes/dewdrop.typ @@ -66,9 +66,9 @@ /// /// For example, `#slide(composer: (1fr, 2fr, 1fr))[A][B][C]` to split the slide into three parts. The first and the last parts will take 1/4 of the slide, and the second part will take 1/2 of the slide. /// -/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `utils.side-by-side` function. +/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `components.side-by-side` function. /// -/// The `utils.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. +/// The `components.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. /// /// For example, `#slide(composer: 2)[A][B][#grid.cell(colspan: 2)[Footer]] will make the `Footer` cell take 2 columns. /// @@ -247,6 +247,48 @@ /// ``` /// /// - `aspect-ratio` is the aspect ratio of the slides. Default is `16-9`. +/// +/// - `navigation` is the navigation of the slides. You can choose from `"sidebar"`, `"mini-slides"`, and `none`. Default is `"sidebar"`. +/// +/// - `sidebar` is the configuration of the sidebar. You can set the width, filled, numbered, indent, and short-heading of the sidebar. Default is `(width: 10em, filled: false, numbered: false, indent: .5em, short-heading: true)`. +/// - `width` is the width of the sidebar. +/// - `filled` is whether the outline in the sidebar is filled. +/// - `numbered` is whether the outline in the sidebar is numbered. +/// - `indent` is the indent of the outline in the sidebar. +/// - `short-heading` is whether the outline in the sidebar is short. +/// +/// - `mini-slides` is the configuration of the mini-slides. You can set the height, x, display-section, display-subsection, and short-heading of the mini-slides. Default is `(height: 4em, x: 2em, display-section: false, display-subsection: true, short-heading: true)`. +/// - `height` is the height of the mini-slides. +/// - `x` is the x of the mini-slides. +/// - `display-section` is whether the slides of section is displayed in the mini-slides. +/// - `display-subsection` is whether we add linebreak between subsections. +/// - `short-heading` is whether the mini-slides is short. Default is `true`. +/// +/// - `footer` is the footer of the slides. Default is `none`. +/// +/// - `footer-right` is the right part of the footer. Default is `context utils.slide-counter.display() + " / " + utils.last-slide-number`. +/// +/// - `primary` is the primary color of the slides. Default is `rgb("#0c4842")`. +/// +/// - `alpha` is the alpha of transparency. Default is `60%`. +/// +/// - `outline-title` is the title of the outline. Default is `[Outline]`. +/// +/// - `subslide-preamble` is the preamble of the subslide. Default is `self => block(text(1.2em, weight: "bold", fill: self.colors.primary, utils.display-current-heading(depth: self.slide-level)))`. +/// +/// ---------------------------------------- +/// +/// The default colors: +/// +/// ```typ +/// config-colors( +/// neutral-darkest: rgb("#000000"), +/// neutral-dark: rgb("#202020"), +/// neutral-light: rgb("#f3f3f3"), +/// neutral-lightest: rgb("#ffffff"), +/// primary: rgb("#0c4842"), +/// ) +/// ``` #let dewdrop-theme( aspect-ratio: "16-9", navigation: "sidebar", diff --git a/themes/metropolis.typ b/themes/metropolis.typ index 6421ba9a0..73a042e4e 100644 --- a/themes/metropolis.typ +++ b/themes/metropolis.typ @@ -7,6 +7,8 @@ /// Default slide function for the presentation. /// +/// - `title` is the title of the slide. Default is `auto`. +/// /// - `config` is the configuration of the slide. You can use `config-xxx` to set the configuration of the slide. For more several configurations, you can use `utils.merge-dicts` to merge them. /// /// - `repeat` is the number of subslides. Default is `auto`,which means touying will automatically calculate the number of subslides. @@ -19,9 +21,9 @@ /// /// For example, `#slide(composer: (1fr, 2fr, 1fr))[A][B][C]` to split the slide into three parts. The first and the last parts will take 1/4 of the slide, and the second part will take 1/2 of the slide. /// -/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `utils.side-by-side` function. +/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `components.side-by-side` function. /// -/// The `utils.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. +/// The `components.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. /// /// For example, `#slide(composer: 2)[A][B][#grid.cell(colspan: 2)[Footer]] will make the `Footer` cell take 2 columns. /// @@ -29,7 +31,7 @@ /// /// - `..bodies` is the contents of the slide. You can call the `slide` function with syntax like `#slide[A][B][C]` to create a slide. #let slide( - title: none, + title: auto, align: auto, config: (:), repeat: auto, @@ -49,7 +51,7 @@ set text(fill: self.colors.neutral-lightest, weight: "medium", size: 1.2em) components.left-and-right( { - if title != none { + if title != auto { utils.fit-to-width.with(grow: false, 100%, title) } else { utils.call-or-display(self, self.store.header) @@ -221,10 +223,37 @@ /// ``` /// /// - `aspect-ratio` is the aspect ratio of the slides. Default is `16-9`. +/// +/// - `align` is the alignment of the content. Default is `horizon`. +/// +/// - `header` is the header of the slide. Default is `utils.display-current-heading(setting: utils.fit-to-width.with(grow: false, 100%))`. +/// +/// - `header-right` is the right part of the header. Default is `self => self.info.logo`. +/// +/// - `footer` is the footer of the slide. Default is `none`. +/// +/// - `footer-right` is the right part of the footer. Default is `context utils.slide-counter.display() + " / " + utils.last-slide-number`. +/// +/// - `footer-progress` is whether to show the progress bar in the footer. Default is `true`. +/// +/// ---------------------------------------- +/// +/// The default colors: +/// +/// ```typ +/// config-colors( +/// primary: rgb("#eb811b"), +/// primary-light: rgb("#d6c6b7"), +/// secondary: rgb("#23373b"), +/// neutral-lightest: rgb("#fafafa"), +/// neutral-dark: rgb("#23373b"), +/// neutral-darkest: rgb("#23373b"), +/// ) +/// ``` #let metropolis-theme( aspect-ratio: "16-9", align: horizon, - header: utils.display-current-heading.with(setting: utils.fit-to-width.with(grow: false, 100%)), + header: utils.display-current-heading(setting: utils.fit-to-width.with(grow: false, 100%)), header-right: self => self.info.logo, footer: none, footer-right: context utils.slide-counter.display() + " / " + utils.last-slide-number, diff --git a/themes/simple.typ b/themes/simple.typ index 41682cb66..ceba0b921 100644 --- a/themes/simple.typ +++ b/themes/simple.typ @@ -17,9 +17,9 @@ /// /// For example, `#slide(composer: (1fr, 2fr, 1fr))[A][B][C]` to split the slide into three parts. The first and the last parts will take 1/4 of the slide, and the second part will take 1/2 of the slide. /// -/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `utils.side-by-side` function. +/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `components.side-by-side` function. /// -/// The `utils.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. +/// The `components.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. /// /// For example, `#slide(composer: 2)[A][B][#grid.cell(colspan: 2)[Footer]] will make the `Footer` cell take 2 columns. /// @@ -65,6 +65,8 @@ /// Title slide for the presentation. +/// +/// Example: `#title-slide[Hello, World!]` #let title-slide(body) = centered-slide( config: config-common(freeze-slide-counter: true), body, @@ -102,14 +104,37 @@ /// ``` /// /// - `aspect-ratio` is the aspect ratio of the slides. Default is `16-9`. +/// +/// - `header` is the header of the slides. Default is `utils.display-current-heading(setting: utils.fit-to-width.with(grow: false, 100%))`. +/// +/// - `header-right` is the right part of the header. Default is `self.info.logo`. +/// +/// - `footer` is the footer of the slides. Default is `none`. +/// +/// - `footer-right` is the right part of the footer. Default is `context utils.slide-counter.display() + " / " + utils.last-slide-number`. +/// +/// - `primary` is the primary color of the slides. Default is `aqua.darken(50%)`. +/// +/// - `subslide-preamble` is the preamble of the subslides. Default is `block(below: 1.5em, text(1.2em, weight: "bold", utils.display-current-heading(level: 2)))`. +/// +/// ---------------------------------------- +/// +/// The default colors: +/// +/// ```typ +/// config-colors( +/// neutral-light: gray, +/// neutral-lightest: rgb("#ffffff"), +/// neutral-darkest: rgb("#000000"), +/// primary: aqua.darken(50%), +/// ) +/// ``` #let simple-theme( aspect-ratio: "16-9", - header: utils.display-current-heading.with(setting: utils.fit-to-width.with(grow: false, 100%)), + header: utils.display-current-heading(setting: utils.fit-to-width.with(grow: false, 100%)), header-right: self => self.info.logo, footer: none, footer-right: context utils.slide-counter.display() + " / " + utils.last-slide-number, - background: rgb("#ffffff"), - foreground: rgb("#000000"), primary: aqua.darken(50%), subslide-preamble: block( below: 1.5em, @@ -144,8 +169,8 @@ ), config-colors( neutral-light: gray, - neutral-lightest: background, - neutral-darkest: foreground, + neutral-lightest: rgb("#ffffff"), + neutral-darkest: rgb("#000000"), primary: primary, ), // save the variables for later use diff --git a/themes/university.typ b/themes/university.typ index db62b5547..e3b31a027 100644 --- a/themes/university.typ +++ b/themes/university.typ @@ -18,9 +18,9 @@ /// /// For example, `#slide(composer: (1fr, 2fr, 1fr))[A][B][C]` to split the slide into three parts. The first and the last parts will take 1/4 of the slide, and the second part will take 1/2 of the slide. /// -/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `utils.side-by-side` function. +/// If you pass a non-function value like `(1fr, 2fr, 1fr)`, it will be assumed to be the first argument of the `components.side-by-side` function. /// -/// The `utils.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. +/// The `components.side-by-side` function is a simple wrapper of the `grid` function. It means you can use the `grid.cell(colspan: 2, ..)` to make the cell take 2 columns. /// /// For example, `#slide(composer: 2)[A][B][#grid.cell(colspan: 2)[Footer]] will make the `Footer` cell take 2 columns. /// @@ -95,6 +95,8 @@ /// /// #title-slide(subtitle: [Subtitle]) /// ``` +/// +/// - `extra` is the extra information of the slide. You can pass the extra information to the `title-slide` function. #let title-slide( extra: none, ..args, @@ -252,6 +254,34 @@ /// ``` /// /// - `aspect-ratio` is the aspect ratio of the slides. Default is `16-9`. +/// +/// - `progress-bar` is whether to show the progress bar. Default is `true`. +/// +/// - `header` is the header of the slides. Default is `utils.display-current-heading(level: 2)`. +/// +/// - `header-right` is the right part of the header. Default is `self.info.logo`. +/// +/// - `footer-columns` is the columns of the footer. Default is `(25%, 1fr, 25%)`. +/// +/// - `footer-a` is the left part of the footer. Default is `self.info.author`. +/// +/// - `footer-b` is the middle part of the footer. Default is `self.info.short-title` or `self.info.title`. +/// +/// - `footer-c` is the right part of the footer. Default is `self => h(1fr) + utils.display-info-date(self) + h(1fr) + context utils.slide-counter.display() + " / " + utils.last-slide-number + h(1fr)`. +/// +/// ---------------------------------------- +/// +/// The default colors: +/// +/// ```typ +/// config-colors( +/// primary: rgb("#04364A"), +/// secondary: rgb("#176B87"), +/// tertiary: rgb("#448C95"), +/// neutral-lightest: rgb("#ffffff"), +/// neutral-darkest: rgb("#000000"), +/// ) +/// ``` #let university-theme( aspect-ratio: "16-9", progress-bar: true, From 4b02e7fa5f3fdb8ebed847b610d01942eb350f39 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Mon, 2 Sep 2024 02:28:18 +0800 Subject: [PATCH 40/43] fix: fix some bugs --- src/configs.typ | 2 ++ src/core.typ | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/configs.typ b/src/configs.typ index 24196de68..d8e73d2cb 100644 --- a/src/configs.typ +++ b/src/configs.typ @@ -284,6 +284,8 @@ /// /// - `cover` (function): The function to cover content. The default value is `utils.method-wrapper(hide)` function. /// +/// You can configure it with `cover: utils.semi-transparent-cover` to use the semi-transparent cover. +/// /// - `uncover` (function): The function to uncover content. The default value is `utils.uncover` function. /// /// - `only` (function): The function to show only the content. The default value is `utils.only` function. diff --git a/src/core.typ b/src/core.typ index 08131b25a..fc4205abe 100644 --- a/src/core.typ +++ b/src/core.typ @@ -1,6 +1,7 @@ #import "utils.typ" #import "utils.typ" #import "pdfpc.typ" +#import "components.typ" /// ------------------------------------------------ /// Slides @@ -28,7 +29,7 @@ ) -/// Appendix for the presentation. The last-slide-counter will be frozen at the last slide before the appendix. +/// Appendix for the presentation. The last-slide-counter will be frozen at the last slide before the appendix. It is simple wrapper for `touying-set-config`, just like `#show: touying-set-config.with((appendix: true))`. /// /// Example: `#show: appendix` /// From fc910538144c8759863f76c97ac71775b9eb276a Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Mon, 2 Sep 2024 02:33:11 +0800 Subject: [PATCH 41/43] fix: fix some bugs --- themes/simple.typ | 7 +++---- themes/university.typ | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/themes/simple.typ b/themes/simple.typ index ceba0b921..c8d31c5c4 100644 --- a/themes/simple.typ +++ b/themes/simple.typ @@ -51,6 +51,7 @@ config-page( header: header, footer: footer, + fill: self.colors.neutral-lightest, ), config-common(subslide-preamble: self.store.subslide-preamble), ) @@ -143,13 +144,9 @@ ..args, body, ) = { - set text(fill: foreground, size: 25pt) - show footnote.entry: set text(size: .6em) - show: touying-slides.with( config-page( paper: "presentation-" + aspect-ratio, - fill: background, margin: 2em, ), config-common( @@ -160,6 +157,8 @@ ), config-methods( init: (self: none, body) => { + set text(fill: self.colors.neutral-darkest, size: 25pt) + show footnote.entry: set text(size: .6em) show strong: self.methods.alert.with(self: self) show heading.where(level: self.slide-level + 1): set text(1.4em) diff --git a/themes/university.typ b/themes/university.typ index e3b31a027..571945a0b 100644 --- a/themes/university.typ +++ b/themes/university.typ @@ -116,7 +116,7 @@ } let body = { if info.logo != none { - align(right, text(fill: self.colors.primary, info.logo)) + place(right, text(fill: self.colors.primary, info.logo)) } align( center + horizon, From 38c409241be0a9e5935c68f04029754eca78c580 Mon Sep 17 00:00:00 2001 From: OrangeX4 <318483724@qq.com> Date: Mon, 2 Sep 2024 04:01:42 +0800 Subject: [PATCH 42/43] docs: update changelog --- changelog.md | 92 ++++++++++++++++++++++++++++++++++++++++++++ examples/default.typ | 2 +- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 129117d34..03a510c3c 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,97 @@ # Changelog +## v0.5.0 + +这一个重大的破坏性版本更新。Touying 去除了许多错误决策下的历史包袱,重新设计了许多功能。这个版本的目标是让 Touying 更加易用,更加灵活,更加强大。 + +**重大的变化包括:** + +- 避免闭包和 OOP 语法,这样做的优势在于让 Touying 的配置更加简单,且可以使用 document comments 为 slide 函数提供更多的自动补全信息。 + - 原有的 `#let slide(self: none, ..args) = { .. }` 现在变为了 `#let slide(..args) = touying-slide-wrapper(self => { .. })`,其中 `self` 会被自动注入。 + - 我们可以使用 `config-xxx` 语法来配置 Touying,例如 `#show: university-theme.with(aspect-ratio: "16-9", config-colors(primary: blue))`。 +- `touying-slide` 函数不再包含 `section`、`subsection` 和 `title` 参数,这些参数会被作为 `self.headings` 以不可见的 1,2 或 3 级等 heading(可以通过 slide-level 配置)自动插入到 slide 页面中。 + - 我们可以利用 Typst 提供的强大的 heading 来支持 numbering、outline 和 bookmark 等功能。 + - 在 `#slide[= XXX]` 这样 slide 函数内部的 heading 会通过 offset 参数将其变为 `slide-level + 1` 级的 heading。 + - 我们可以利用 heading 的 label 来控制很多东西,例如支持 `touying:hidden` 等特殊 label,或者实现 short heading,亦或者实现 `#touying-recall()` 再现某个 slide。 +- Touying 现在支持在任意位置正常地使用 set 和 show 规则,而不再需要在特定位置使用。 + +一个简单的使用例子如下,更多的示例可以在 `examples` 目录下找到: + +```typst +#import "@preview/touying:0.5.0": * +#import themes.university: * + +#show: university-theme.with( + aspect-ratio: "16-9", + config-info( + title: [Title], + subtitle: [Subtitle], + author: [Authors], + date: datetime.today(), + institution: [Institution], + logo: emoji.school, + ), +) + +#set heading(numbering: "1.1") + +#title-slide() + += The Section + +== Slide Title + +#lorem(40) +``` + +**主题迁移指南:** + +可以到 `themes` 目录下查看具体主题的变化。大体来说,如果你想迁移已有的主题,你应该: + +1. 将 `register` 函数重命名为 `xxx-theme`,并去除 `self` 参数。 +2. 添加一个 `show: touying-slides.with(..)` 的配置。 + 1. 将原有的 `self.methods.colors` 更改为 `config-colors(primary: rgb("#xxxxxx"))`。 + 2. 将原有的 `self.page-args` 更改为 `config-page()`。 + 3. 将原有的 `self.methods.slide = slide` 更改为 `config-methods(slide: slide)` + 4. 将原有的 `self.methods.new-section-slide = new-section-slide` 更改为 `config-methods(new-section-slide: new-section-slide)` + 5. 将原有的 `self.xxx-footer` 这样的主题私有变量更改为 `config-store(footer: [..])`,后续你可以通过 `self.store.footer` 来获取。 + 6. 将原有的 `header` 和 `footer` 移到 `slide` 函数中配置,而不是在 `xxx-theme` 函数中配置。 + 7. 你可以在 `xxx-theme` 中直接使用 set 或 show 规则,或者也可以通过 `config-methods(init: (self: none, body) => { .. })` 来配置,这样你能充分利用 `self` 参数。 +3. 对于 `states.current-section-with-numbering`,你可以使用 `utils.display-current-heading(level: 1)` 来代替。 + 1. 如果你仅要获取上一个标题,无所谓是 section 还是 subsection,你可以使用 `utils.display-previous-heading()` 来代替。 +4. `alert` 函数可以通过 `config-methods(alert: utils.alert-with-primary-color)` 来代替。 +5. 我们不再需要 `touying-outline()` 函数,你可以直接使用 `components.adaptive-columns(outline())` 来代替。或者考虑使用 `components.progressive-outline()` 或 `components.custom-progressive-outline()`。 +6. 将 `context states.slide-counter.display() + " / " + states.last-slide-number` 替换为 `context utils.slide-counter.display() + " / " + utils.last-slide-number`。即我们不再使用 `states`,而是使用 `utils`。 +7. 删除 `slides` 函数,我们不再需要这个函数。因为我们不应该隐式注入 `title-slide()`,而是应该显式地使用 `#title-slide()`。如果你实在需要,你可以考虑在 `xxx-theme` 函数中加入。 +8. 将原来的 `#let slide(self: none, ..args) = { .. }` 变为了 `#let slide(..args) = touying-slide-wrapper(self => { .. })`。其中 `self` 会被自动注入。 + 1. 将具体的参数配置更改为 `self = utils.merge-dicts(self, config-page(fill: self.colors.neutral-lightest))` 方式。 + 2. 去除 `self = utils.empty-page(self)`,你可以使用 `config-common(freeze-slide-counter: true)` 和 `config-page(margin: 0em)` 来代替。 + 3. 将 `(self.methods.touying-slide)()` 更改为 `touying-slide()`。 +9. 你可以通过配置 `config-common(subslide-preamble: self => text(1.2em, weight: "bold", utils.display-current-heading(depth: self.slide-level)))` 来实现给 slide 插入可视的 heading。 +10. 最后,别忘了给你的函数们加上 document comments,这样你的用户就能获得更好的自动补全提示,尤其是在使用 Tinymist 插件时。 + + +**其他变化:** + +- feat: 实现了 fake frozen states support,你现在可以正常地使用 `numbering` 和 `#pause`。这个行为可以通过 `config-common()` 里的 `enable-frozen-states-and-counters`,`frozen-states` 和 `frozen-counters` 控制。 +- feat: 实现了 `label-only-on-last-subslide` 功能,可以让 `@equation` 和 `@figure` 在有 `#pause` 动画的情况下正在工作,避免非 unique label 警告。 +- feat: 添加了 `touying-recall(