From 46f48184c59fc0aa5660ec13d6effc9f18ce5f8c Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Tue, 29 Aug 2023 00:45:25 -0500 Subject: [PATCH 1/8] Use a semitransparent rect instead of gray text for `"transparent"` uncover --- logic.typ | 92 ++++++++++++++++++++++++++++++++++++++++++++--------- polylux.typ | 2 +- 2 files changed, 78 insertions(+), 16 deletions(-) diff --git a/logic.typ b/logic.typ index aeba0a8..7e2dd3b 100644 --- a/logic.typ +++ b/logic.typ @@ -6,14 +6,76 @@ #let enable-handout-mode(flag) = handout-mode.update(flag) -#let _slides-cover(mode, body) = { - if mode == "invisible" { - hide(body) - } else if mode == "transparent" { - text(gray.lighten(50%), body) - } else { - panic("Illegal cover mode: " + mode) +#let cover-with-rect(outset: 0em, fill: auto, body) = { + if fill == auto { + panic( + "`auto` fill value is not supported until typst provides utilities to" + + " retrieve the current page background" + ) } + if type(fill) == "string" { + fill = rgb(fill) + } + + layout(layout-size => { + set text(top-edge: "bounds", bottom-edge: "bounds") + style(styles => { + let body-size = measure(body, styles) + let bounding-width = calc.min(body-size.width, layout-size.width) + let wrapped-body-size = measure(box(body, width: bounding-width), styles) + stack( + spacing: -wrapped-body-size.height, + box(body), + rect( + fill: fill, + width: wrapped-body-size.width, + height: wrapped-body-size.height, + outset: outset, + ) + ) + }) + }) +} + +// 50% alpha +#let cover-with-white-rect = cover-with-rect.with(fill: "fff8") +#let cover-with-black-rect = cover-with-rect.with(fill: "0008") + +// States are normally defined at the top of the file by convention, but functions aren't +// hoisted. So wait to populate the state until here, when functions are accessible +#let content-hider-modes = state("content-hider-modes", + ( + "invisible": hide, + // Backwards compatible. When `get` rules are established, the default "transparent" + // behavior could change to use the page background for a more robust alternative, + // considering prior "transparent" behavior is broken in those cases anyway + "transparent": cover-with-white-rect, + "transparent-black": cover-with-black-rect, + "default": hide + ) +) + +#let add-hider-mode(name, function) = { + content-hider-modes.update(old => { + old.insert(name, function) + old + }) +} + +#let _slides-cover(mode: auto, body) = { + let mode-key = mode + if mode == auto { + mode-key = "default" + } + locate(loc => { + let hider-options = content-hider-modes.at(loc) + if mode-key not in hider-options { + panic( + "Illegal cover mode: `" + mode + "`. Must be one of: " + hider-options.keys().join(", ") + ) + } + hider-options.at(mode-key)(body) + }) } #let _parse-subslide-indices(s) = { @@ -108,12 +170,12 @@ if _check-visible(subslide.at(loc).first(), vs) { body } else if reserve-space { - _slides-cover(mode, body) + _slides-cover(mode: mode, body) } }) } -#let uncover(visible-subslides, mode: "invisible", body) = { +#let uncover(visible-subslides, mode: auto, body) = { _conditional-display(visible-subslides, true, mode, body) } @@ -121,7 +183,7 @@ _conditional-display(visible-subslides, false, "doesn't even matter", body) } -#let one-by-one(start: 1, mode: "invisible", ..children) = { +#let one-by-one(start: 1, mode: auto, ..children) = { for (idx, child) in children.pos().enumerate() { uncover((beginning: start + idx), mode: mode, child) } @@ -192,7 +254,7 @@ alternatives-match(cases.zip(contents), ..kwargs.named()) } -#let line-by-line(start: 1, mode: "invisible", body) = { +#let line-by-line(start: 1, mode: auto, body) = { let items = if repr(body.func()) == "sequence" { body.children } else { @@ -211,7 +273,7 @@ } -#let _items-one-by-one(fn, start: 1, mode: "invisible", ..args) = { +#let _items-one-by-one(fn, start: 1, mode: auto, ..args) = { let kwargs = args.named() let items = args.pos() let covered-items = items.enumerate().map( @@ -223,15 +285,15 @@ ) } -#let list-one-by-one(start: 1, mode: "invisible", ..args) = { +#let list-one-by-one(start: 1, mode: auto, ..args) = { _items-one-by-one(list, start: start, mode: mode, ..args) } -#let enum-one-by-one(start: 1, mode: "invisible", ..args) = { +#let enum-one-by-one(start: 1, mode: auto, ..args) = { _items-one-by-one(enum, start: start, mode: mode, ..args) } -#let terms-one-by-one(start: 1, mode: "invisible", ..args) = { +#let terms-one-by-one(start: 1, mode: auto, ..args) = { let kwargs = args.named() let items = args.pos() let covered-items = items.enumerate().map( diff --git a/polylux.typ b/polylux.typ index 25cfea1..b7a46ea 100644 --- a/polylux.typ +++ b/polylux.typ @@ -1,5 +1,5 @@ #import "themes/themes.typ" #import "logic.typ" -#import "logic.typ": polylux-slide, uncover, only, alternatives, alternatives-match, alternatives-fn, alternatives-cases, one-by-one, line-by-line, list-one-by-one, enum-one-by-one, terms-one-by-one, pause, enable-handout-mode +#import "logic.typ": polylux-slide, uncover, cover-with-rect, add-hider-mode, only, alternatives, alternatives-match, alternatives-fn, alternatives-cases, one-by-one, line-by-line, list-one-by-one, enum-one-by-one, terms-one-by-one, pause, enable-handout-mode #import "utils/utils.typ" #import "utils/utils.typ": polylux-outline, fit-to-height, side-by-side, pdfpc From e7c4a66fc8ea453688d8cc3e976c8b4c0eef3c3a Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Tue, 29 Aug 2023 11:48:53 -0500 Subject: [PATCH 2/8] Allow deeper `cover-with-rect` customization For instance, if an object to be hidden spans two background colors, you can specify `(fill: "000d", stroke: (bottom: rgb("fffd) + 0.2in))` to have the cover work even in that case "width" and "height" can also be directly overridden in cases where their defaults don't work properly, e.g. with heavy strokes of slanted lines --- logic.typ | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/logic.typ b/logic.typ index 7e2dd3b..fc5bd16 100644 --- a/logic.typ +++ b/logic.typ @@ -6,7 +6,7 @@ #let enable-handout-mode(flag) = handout-mode.update(flag) -#let cover-with-rect(outset: 0em, fill: auto, body) = { +#let cover-with-rect(..cover-args, fill: auto, body) = { if fill == auto { panic( "`auto` fill value is not supported until typst provides utilities to" @@ -23,15 +23,17 @@ let body-size = measure(body, styles) let bounding-width = calc.min(body-size.width, layout-size.width) let wrapped-body-size = measure(box(body, width: bounding-width), styles) + let named = cover-args.named() + if "width" not in named { + named.insert("width", wrapped-body-size.width) + } + if "height" not in named { + named.insert("height", wrapped-body-size.height) + } stack( spacing: -wrapped-body-size.height, box(body), - rect( - fill: fill, - width: wrapped-body-size.width, - height: wrapped-body-size.height, - outset: outset, - ) + rect(fill: fill, ..named, ..cover-args.pos()) ) }) }) From 7328cd197b03def8a988c7ae730260b38c782c90 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Tue, 29 Aug 2023 12:01:29 -0500 Subject: [PATCH 3/8] Raise default transparency for white and black backgrounds. This will more closely match the current default of `gray.lighten(50%)` --- logic.typ | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/logic.typ b/logic.typ index fc5bd16..6fb37aa 100644 --- a/logic.typ +++ b/logic.typ @@ -39,9 +39,9 @@ }) } -// 50% alpha -#let cover-with-white-rect = cover-with-rect.with(fill: "fff8") -#let cover-with-black-rect = cover-with-rect.with(fill: "0008") +// matches equivalent transparency of "gray.lighten(50%)" +#let cover-with-white-rect = cover-with-rect.with(fill: rgb(255, 255, 255, 213)) +#let cover-with-black-rect = cover-with-rect.with(fill: rgb(0, 0, 0, 213)) // States are normally defined at the top of the file by convention, but functions aren't // hoisted. So wait to populate the state until here, when functions are accessible From 1e7e5370ce095c9df2adec6bfb100a4d5b7bb232 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Tue, 29 Aug 2023 12:02:30 -0500 Subject: [PATCH 4/8] Allow arbitrary custom hiding modes. Convenient for one-time background colors that shouldn't incur a cumbersome registration to global state --- logic.typ | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/logic.typ b/logic.typ index 6fb37aa..18e9c26 100644 --- a/logic.typ +++ b/logic.typ @@ -53,7 +53,7 @@ // considering prior "transparent" behavior is broken in those cases anyway "transparent": cover-with-white-rect, "transparent-black": cover-with-black-rect, - "default": hide + "default": hide, ) ) @@ -69,6 +69,10 @@ if mode == auto { mode-key = "default" } + if type(mode) == "function" { + // skip mode lookup, user directly provided hider function + return mode(body) + } locate(loc => { let hider-options = content-hider-modes.at(loc) if mode-key not in hider-options { From 3de393be3fe2cbb110e5e06c0ebb82b39efe63ed Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Tue, 29 Aug 2023 12:17:48 -0500 Subject: [PATCH 5/8] Place `box` around output of `cover-with-rect` instead of body. This allows inline content to remain inline after being covered --- logic.typ | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/logic.typ b/logic.typ index 18e9c26..ff07c04 100644 --- a/logic.typ +++ b/logic.typ @@ -17,7 +17,8 @@ fill = rgb(fill) } - layout(layout-size => { + // The extra `box` allows inline content to remain inline after being covered + box(layout(layout-size => { set text(top-edge: "bounds", bottom-edge: "bounds") style(styles => { let body-size = measure(body, styles) @@ -32,11 +33,11 @@ } stack( spacing: -wrapped-body-size.height, - box(body), + body, rect(fill: fill, ..named, ..cover-args.pos()) ) }) - }) + })) } // matches equivalent transparency of "gray.lighten(50%)" From 53db9d042edf2f3aa76c4fcdfbe620dc396a8d78 Mon Sep 17 00:00:00 2001 From: Nathan Jessurun Date: Tue, 29 Aug 2023 12:52:49 -0500 Subject: [PATCH 6/8] Use `outset` instead of `top-edge` and `bottom-edge` to determine rect size --- logic.typ | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/logic.typ b/logic.typ index ff07c04..30bec3a 100644 --- a/logic.typ +++ b/logic.typ @@ -19,7 +19,6 @@ // The extra `box` allows inline content to remain inline after being covered box(layout(layout-size => { - set text(top-edge: "bounds", bottom-edge: "bounds") style(styles => { let body-size = measure(body, styles) let bounding-width = calc.min(body-size.width, layout-size.width) @@ -31,6 +30,16 @@ if "height" not in named { named.insert("height", wrapped-body-size.height) } + if "outset" not in named { + // This outset covers the tops of tall letters and the bottoms of letters with + // descenders. Alternatively, we could use + // `set text(top-edge: "bounds", bottom-edge: "bounds")` to get the same effect, + // but this changes text alignment and also misaligns bullets in enums/lists. + // In contrast, `outset` preserves spacing and alignment at the cost of adding + // a slight, visible border when the covered object is right next to the edge + // of a color change. + named.insert("outset", (top: 0.15em, bottom: 0.25em)) + } stack( spacing: -wrapped-body-size.height, body, From 959d8263209e7b8831f779d726fa687fcfed202a Mon Sep 17 00:00:00 2001 From: ntjess Date: Fri, 1 Sep 2023 11:55:58 -0500 Subject: [PATCH 7/8] Adjust default transparency of `black` rect cover --- logic.typ | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/logic.typ b/logic.typ index 30bec3a..34d3356 100644 --- a/logic.typ +++ b/logic.typ @@ -51,7 +51,11 @@ // matches equivalent transparency of "gray.lighten(50%)" #let cover-with-white-rect = cover-with-rect.with(fill: rgb(255, 255, 255, 213)) -#let cover-with-black-rect = cover-with-rect.with(fill: rgb(0, 0, 0, 213)) +// White cover was determined using a color picker over `gray.lighten(50%)`. Black cover +// could theoretically be the *inverse* of this value (i.e., if white uses 213, +// black can use 255 - 213 = 42), but in practice this leaves text too visible. +// Using 127 more closely matches the visual contrast from the `white` variant. +#let cover-with-black-rect = cover-with-rect.with(fill: rgb(0, 0, 0, 127)) // States are normally defined at the top of the file by convention, but functions aren't // hoisted. So wait to populate the state until here, when functions are accessible From 6733e5ed0e390fda75b17bfe770ea2cd8745b1b0 Mon Sep 17 00:00:00 2001 From: ntjess Date: Fri, 1 Sep 2023 12:26:18 -0500 Subject: [PATCH 8/8] Allow covered content to optionally *not* convert to inline Consider the case of `cover-with-rect(block(...))`. This should explicitly be in its own block, but adding a box deterministically inside `cover` will promote this to inline content. Providing an optional argument allows block-level content to stay that way if desired. "inline" is still a sensible default, since paragraph breaks around a box will allow it to be its own block anyway. --- logic.typ | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/logic.typ b/logic.typ index 34d3356..dec9be8 100644 --- a/logic.typ +++ b/logic.typ @@ -6,7 +6,7 @@ #let enable-handout-mode(flag) = handout-mode.update(flag) -#let cover-with-rect(..cover-args, fill: auto, body) = { +#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" @@ -17,8 +17,7 @@ fill = rgb(fill) } - // The extra `box` allows inline content to remain inline after being covered - box(layout(layout-size => { + let to-display = layout(layout-size => { style(styles => { let body-size = measure(body, styles) let bounding-width = calc.min(body-size.width, layout-size.width) @@ -46,7 +45,12 @@ rect(fill: fill, ..named, ..cover-args.pos()) ) }) - })) + }) + if inline { + box(to-display) + } else { + to-display + } } // matches equivalent transparency of "gray.lighten(50%)"