// This theme is inspired by https://github.com/zbowang/BeamerTheme // The typst version was written by https://github.com/OrangeX4 #import "@preview/touying:0.6.1": * #let _typst-builtin-repeat = repeat #let fira-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" { 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), linebreaks: self.store.mini-slides.at("linebreaks", default: true), short-heading: self.store.mini-slides.at("short-heading", default: true), ) } else if self.store.navigation == "line" { } } #let fira-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 (dictionary): 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 (int, auto): 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 (function): The setting of the slide. You can use it to add some set/show rules for the slide. /// /// - composer (function, array): 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 `components.side-by-side` function. /// /// 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. /// /// 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 (array): 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 => { let self = utils.merge-dicts( self, config-page( header: fira-header, footer: none, ), config-common(subslide-preamble: self.store.subslide-preamble), ) 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: fira-theme.with( /// config-info( /// title: [Title], /// logo: emoji.city, /// ), /// ) /// /// #title-slide(subtitle: [Subtitle], extra: [Extra information]) /// ``` /// /// - config (dictionary): 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. /// /// - extra (string, none): The extra information you want to display on the title slide. #let title-slide( config: (:), extra: none, ..args, ) = touying-slide-wrapper(self => { self = utils.merge-dicts( self, config, config-common(freeze-slide-counter: true), config-page(margin: 0em), ) let info = self.info + args.named() let body = { set par(justify: false) 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.6em, fill: self.colors.primary, text(weight: "bold", info.title, hyphenate: false)) + ( if info.subtitle != none { linebreak()*2 text(size: 1.2em, fill: self.colors.primary, info.subtitle) } ), ) set text(size: 1em) if info.author != none { block(spacing: 1em, info.author) } v(1em) if info.date != none { block(spacing: 1em, utils.display-info-date(self)) } set text(size: 1em) if info.institution != none { block(spacing: 1em, info.institution) } if extra != none { block(spacing: 1em, extra) } }, ) } touying-slide(self: self, body) }) /// Outline slide for the presentation. /// /// - config (dictionary): 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. /// /// - title (string): The title of the slide. Default is `utils.i18n-outline-title`. #let outline-slide(config: (:), title: utils.i18n-outline-title, ..args) = touying-slide-wrapper(self => { self = utils.merge-dicts( self, config-page( footer: none, ), ) touying-slide( self: self, config: config, components.adaptive-columns( start: text( 1.4em, fill: self.colors.primary, weight: "bold", utils.call-or-display(self, title), )+linebreak()*2, text( fill: self.colors.neutral-darkest, outline(title: none, indent: 1em, depth: 1, ..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))` /// /// - config (dictionary): 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. /// /// - title (string): The title of the slide. Default is `utils.i18n-outline-title`. /// /// - body (array): The contents of the slide. #let new-section-slide(config: (:), title: utils.i18n-outline-title, ..args, body) = touying-slide-wrapper(self => { self = utils.merge-dicts( self, config-page( footer: none, ), ) touying-slide( self: self, config: config, components.adaptive-columns( start: text( 1.2em, fill: self.colors.primary, weight: "bold", utils.call-or-display(self, title), ), text( fill: self.colors.neutral-darkest, components.progressive-outline( alpha: self.store.alpha, title: none, indent: 1em, depth: self.slide-level, ..args, ), ), ), ) }) /// Focus on some content. /// /// Example: `#focus-slide[Wake up!]` /// /// - config (dictionary): 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. #let focus-slide(config: (:), 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, config: config, align(horizon + center, body)) }) /// Touying fira theme. /// /// Example: /// /// ```typst /// #show: fira-theme.with(aspect-ratio: "16-9", config-colors(primary: blue))` /// ``` /// /// 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"), /// ) /// ``` /// /// - aspect-ratio (string): The aspect ratio of the slides. Default is `16-9`. /// /// - navigation (string): The navigation of the slides. You can choose from `"sidebar"`, `"mini-slides"`, and `none`. Default is `"sidebar"`. /// /// - sidebar (dictionary): 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 (string): The width of the sidebar. /// - filled (boolean): Whether the outline in the sidebar is filled. /// - numbered (boolean): Whether the outline in the sidebar is numbered. /// - indent (length): The indent of the outline in the sidebar. /// - short-heading (boolean): Whether the outline in the sidebar is short. /// /// - mini-slides (dictionary): 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, linebreaks: true, short-heading: true)`. /// - height (length): The height of the mini-slides. /// - x (length): The x position of the mini-slides. /// - display-section (boolean): Whether the slides of sections are displayed in the mini-slides. /// - display-subsection (boolean): Whether the slides of subsections are displayed in the mini-slides. /// - linebreaks (boolean): Whether line breaks are in between links for sections and subsections in the mini-slides. /// - short-heading (boolean): Whether the mini-slides are short. Default is `true`. /// /// - footer (content, function): The footer of the slides. Default is `none`. /// /// - footer-right (content, function): The right part of the footer. Default is `context utils.slide-counter.display() + " / " + utils.last-slide-number`. /// /// - primary (color): The primary color of the slides. Default is `rgb("#0c4842")`. /// /// - alpha (fraction, float): The alpha of transparency. Default is `60%`. /// /// - outline-title (content, function): The title of the outline. Default is `utils.i18n-outline-title`. /// /// - subslide-preamble (content, function): 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)))`. #let fira-theme( aspect-ratio: "4-3", navigation: "sidebar", sidebar: ( width: 10em, filled: false, numbered: false, indent: .5em, short-heading: true, ), mini-slides: ( height: 4em, x: 2em, display-section: false, display-subsection: true, linebreaks: true, short-heading: true, ), footer: none, footer-right: none, primary: rgb("#0c4842"), alpha: 60%, subslide-preamble: self => block( text(1.2em, weight: "bold", fill: self.colors.primary, utils.display-current-heading(depth: self.slide-level, style: auto)), ), ..args, body, ) = { sidebar = utils.merge-dicts( (width: 10em, filled: false, numbered: false, indent: .5em, short-heading: true), sidebar, ) mini-slides = utils.merge-dicts( (height: 4em, x: 2em, display-section: false, display-subsection: true, linebreaks: true, short-heading: true), mini-slides, ) set text(size: 24pt, font: ("FiraGO","Source Han Sans SC"), weight: 400) show math.equation: set text(font: "Fira Math") set strong(delta: 200) set par(justify: true) set outline.entry(fill: none) show heading: it => [ #set text(size: 1.1em) #it.body // #underline(it.body,stroke: 4pt, offset: 4pt) ] 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: none //new-section-slide, ), config-methods( init: (self: none, body) => { show heading: set text(self.colors.primary) body }, 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, subslide-preamble: subslide-preamble, ), ..args, ) body }