forge
ExploreLog in

Tahinli/forge

public
0 watching
★ 0
4.1 KiB
1//! The colour theme for a request. A signed-in user's stored preference wins. Anonymous
2//! visitors use the `forge_theme` cookie, which the footer form sets. The default is light: a
3//! visitor with no explicit choice gets light whatever their operating system uses. Only an
4//! explicit `auto` renders no `data-theme`, so the stylesheet follows the operating system.
5
6use std::time::Duration;
7
8use forge_core::User;
9use topcoat::context::Cx;
10use topcoat::cookie::{Cookie, Cookies, SameSite, cookies, time};
11
12/// Name of the anonymous theme cookie.
13pub const THEME_COOKIE: &str = "forge_theme";
14
15/// Lifetime of the anonymous theme cookie.
16pub const THEME_COOKIE_TTL: Duration = Duration::from_secs(365 * 24 * 60 * 60);
17
18/// The stored colour preference for this request: `"auto"`, `"light"` or `"dark"`. A signed-in
19/// user's stored preference wins. Anonymous visitors use the cookie. A missing, empty or
20/// unknown value reads as `"light"`, the default. Only an explicit `"auto"` follows the system.
21pub fn preference(cx: &Cx, viewer: Option<&User>) -> &'static str {
22 let stored = match viewer {
23 Some(user) => user.theme.clone(),
24 None => cookies(cx)
25 .get(THEME_COOKIE)
26 .map(|cookie| cookie.value().to_owned())
27 .unwrap_or_default(),
28 };
29 resolve_preference(&stored)
30}
31
32/// Maps a stored value to a preference. Only `"auto"` and `"dark"` are kept as they are. Every
33/// other value, including the empty string, means the default, `"light"`.
34pub fn resolve_preference(stored: &str) -> &'static str {
35 match stored {
36 "auto" => "auto",
37 "dark" => "dark",
38 _ => "light",
39 }
40}
41
42/// The `data-theme` value for this request: `Some("light")` or `Some("dark")`, or `None` when the
43/// page should follow the operating system.
44pub fn data_theme(cx: &Cx, viewer: Option<&User>) -> Option<&'static str> {
45 match preference(cx, viewer) {
46 "auto" => None,
47 theme => Some(theme),
48 }
49}
50
51/// The preference the top-bar button switches to from `current`: light, then dark, then auto,
52/// then back to light. Anything that is not `"light"` or `"dark"` (including `"auto"`) moves on
53/// to light, the first step of the cycle.
54pub fn next_preference(current: &str) -> &'static str {
55 match current {
56 "light" => "dark",
57 "dark" => "auto",
58 _ => "light",
59 }
60}
61
62/// Sets the anonymous theme cookie. The caller has already checked that `theme` is valid.
63pub fn set_theme_cookie(cx: &Cx, theme: &str) {
64 let max_age =
65 time::Duration::seconds(i64::try_from(THEME_COOKIE_TTL.as_secs()).unwrap_or(i64::MAX));
66 cookies(cx).add(
67 Cookie::build((THEME_COOKIE, theme.to_owned()))
68 .path("/")
69 .secure(true)
70 .http_only(true)
71 .same_site(SameSite::Lax)
72 .max_age(max_age)
73 .build(),
74 );
75}
76
77#[cfg(test)]
78mod tests {
79 use super::{next_preference, resolve_preference};
80
81 #[test]
82 fn the_toggle_cycles_light_dark_auto_light() {
83 assert_eq!(next_preference("light"), "dark");
84 assert_eq!(next_preference("dark"), "auto");
85 assert_eq!(next_preference("auto"), "light");
86 }
87
88 #[test]
89 fn unknown_preferences_move_on_to_light() {
90 assert_eq!(next_preference(""), "light");
91 assert_eq!(next_preference("neon"), "light");
92 }
93
94 #[test]
95 fn no_choice_means_light() {
96 assert_eq!(resolve_preference(""), "light");
97 assert_eq!(resolve_preference("neon"), "light");
98 assert_eq!(resolve_preference("light"), "light");
99 }
100
101 #[test]
102 fn explicit_choices_are_kept() {
103 assert_eq!(resolve_preference("dark"), "dark");
104 assert_eq!(resolve_preference("auto"), "auto");
105 }
106
107 #[test]
108 fn the_default_cycle_starts_on_light_and_reaches_every_choice() {
109 let start = resolve_preference("");
110 assert_eq!(start, "light");
111 assert_eq!(next_preference(start), "dark");
112 assert_eq!(next_preference(next_preference(start)), "auto");
113 assert_eq!(
114 next_preference(next_preference(next_preference(start))),
115 "light"
116 );
117 }
118}