touying_theme/firatheme.typ
2025-05-23 19:49:03 +08:00

401 lines
14 KiB
Typst

// 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.44em, fill: self.colors.primary, text(weight: "bold", info.title, hyphenate: false))
+ (
if info.subtitle != none {
linebreak() * 2
text(size: 1.1em, 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)
}
linebreak()*2
},
)
}
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: 2, ..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)
show heading.where(level: 2): set text(size: 1.1em)
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
}