telar_theme_core/density.rs
1//! One ambient answer to "how big are the controls here", which every catalogue component interprets for
2//! itself.
3//!
4//! The alternative is a size matrix: a `size` prop on every component, and a table of what each of its parts
5//! measures at each of them. That is N × M numbers to keep in step, and every one of them is a decision the
6//! component has already made once — a button's padding is 1.75 spacing units *whatever* size it is.
7//!
8//! So this scales the bases instead. A control size does not say "a small button is 24px tall"; it says the
9//! unit everything is derived from is smaller here, and each component's own proportions carry that through
10//! unchanged. One value to thread, N interpretations, and none of them written down twice.
11//!
12//! It is a signal like the theme, so a change re-runs the paint closures that read it — and, through
13//! `StyledContainer::styled_by`, re-resolves the layout styles derived from it too.
14
15use std::mem::ManuallyDrop;
16
17use reactive_core::{RwSignal, signal};
18
19/// How large the controls in this part of the tree are, in the sense SwiftUI's `controlSize` means: a
20/// preference the *container* expresses and each control interprets, not a size any one of them is given.
21#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
22pub enum ControlSize {
23 /// Dense chrome — a toolbar, an inspector, a status bar.
24 Mini,
25 Small,
26 #[default]
27 Regular,
28 /// A touch target, or a control that is the point of the screen it is on.
29 Large,
30}
31
32impl ControlSize {
33 /// What the theme's metric bases are multiplied by here. Radius is deliberately not among them: a smaller
34 /// control is smaller, not flatter — the corner is the design language's, and it does not change with the
35 /// size of the thing wearing it.
36 pub fn scale(self) -> f32 {
37 match self {
38 ControlSize::Mini => 0.75,
39 ControlSize::Small => 0.875,
40 ControlSize::Regular => 1.0,
41 ControlSize::Large => 1.25,
42 }
43 }
44}
45
46thread_local! {
47 // `ManuallyDrop` for the same reason the theme signals are: no TLS destructor, cleanup goes through the
48 // runtime being dropped.
49 static CONTROL_SIZE: ManuallyDrop<RwSignal<ControlSize>> =
50 ManuallyDrop::new(signal(ControlSize::Regular));
51}
52
53/// Sets the ambient control size. Reactive: everything that read it re-runs, so a switch re-spaces the
54/// controls already on screen rather than waiting for whatever rebuilds them.
55pub fn set_control_size(size: ControlSize) {
56 CONTROL_SIZE.with(|s| s.set(size));
57}
58
59/// The ambient control size, subscribing the caller.
60pub fn use_control_size() -> ControlSize {
61 CONTROL_SIZE.with(|s| s.get())
62}
63
64/// The factor the catalogue's metric bases carry here — [`use_control_size`] resolved to a number, which is
65/// the only form a component ever needs it in.
66pub fn control_scale() -> f32 {
67 use_control_size().scale()
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 #[test]
75 fn the_ambient_size_scales_the_bases_and_regular_leaves_them_alone() {
76 assert_eq!(ControlSize::Regular.scale(), 1.0);
77 assert!(ControlSize::Mini.scale() < 1.0);
78 assert!(ControlSize::Large.scale() > 1.0);
79
80 assert_eq!(use_control_size(), ControlSize::Regular, "the default");
81 set_control_size(ControlSize::Mini);
82 assert_eq!(control_scale(), ControlSize::Mini.scale());
83 set_control_size(ControlSize::Regular);
84 }
85}