Skip to main content

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}