Skip to main content

kui_core/
metrics.rs

1//! The sizes the stock widgets are built from, as one struct beside the
2//! palette (backlog T2, the axis ADR 0019 scoped itself out of).
3//!
4//! `BUTTON_TEXT` 15, `MENU_WIDTH` 200, every `pad_xy(14.0, 8.0)` and
5//! `radius(6.0)` in `widgets.rs` were hard-coded the way the colours were
6//! before the theme, and for the same reason: nobody had asked. The
7//! argument for a [`Metrics`] is ADR 0019's one axis over — the stock
8//! widgets and an app's own controls should agree on a radius and a
9//! padding without either copying a number out of the other — and the
10//! two questions the backlog entry said to settle first are settled here:
11//!
12//! - **A metric does not scale by itself.** Every field is logical px (or
13//!   a text size in logical px), applied *before* `env.scale`, which is
14//!   the renderer's and multiplies everything the frame draws. Density is
15//!   the app's choice, the way the palette is: [`Metrics::compact`] is a
16//!   tighter set, [`Metrics::scaled`] is every length multiplied for a
17//!   density slider, and `Core::set_metrics` is the door. The day an OS
18//!   text-size setting is plumbed into `env.system`, `scaled` is the
19//!   arithmetic and a `MetricsSource` beside [`crate::theme::ThemeSource`]
20//!   is the shape; nothing here pre-empts it.
21//! - **The default is the contract; a set metric is the app's.** The
22//!   corpus runs with [`Metrics::default`], which is byte for byte the
23//!   constants the widgets had, so no scene moved and the report pins the
24//!   stock geometry as it always did. An app that sets its own changes
25//!   what *its* frames draw, exactly as `set_theme` does, and a
26//!   conformance scene never sets one — which is what keeps "the stock
27//!   button is 15-px text in 14×8 padding" a sentence about kui and not
28//!   about an app.
29//!
30//! Roles, like the palette's: `control_pad_x` is *a button's horizontal
31//! padding*, not "spacing unit 3", and a view that wants a number between
32//! two has arithmetic.
33
34/// The titlebar's height on Windows and everywhere else: the caption
35/// height the OS draws, so the one metric that is the platform's rather
36/// than a density's. Named here so [`Metrics::comfortable`] picks the
37/// running one and the schema row (`MetricRole::platform`) carries both,
38/// which is what keeps a generated table from saying which machine wrote
39/// it (backlog W13).
40pub const TITLEBAR_H_WINDOWS: f32 = 32.0;
41pub const TITLEBAR_H_ELSEWHERE: f32 = 34.0;
42
43/// The sizes the stock widgets are built from. Plain data and [`Copy`]: a
44/// view reads it off `ui.metrics()` and may keep or change its own copy,
45/// and `Core::set_metrics` makes one the frame's.
46#[derive(Clone, Copy, Debug, PartialEq)]
47pub struct Metrics {
48    // -- text ----------------------------------------------------------
49    /// A stock control's label: the button's text size.
50    pub control_text: f32,
51    /// The chrome's text: a menu row, a menu-bar title, the titlebar's
52    /// title.
53    pub chrome_text: f32,
54    /// A tooltip's text.
55    pub hint_text: f32,
56
57    // -- corners -------------------------------------------------------
58    /// The corner of every stock surface: a button, a field, a menu, a
59    /// tooltip.
60    pub radius: f32,
61    /// The corner of a row inside one: a menu row, a menu-bar title.
62    pub radius_inner: f32,
63
64    // -- padding, x then y -----------------------------------------------
65    /// A button's.
66    pub control_pad_x: f32,
67    pub control_pad_y: f32,
68    /// A text field's.
69    pub field_pad_x: f32,
70    pub field_pad_y: f32,
71    /// A tooltip's.
72    pub hint_pad_x: f32,
73    pub hint_pad_y: f32,
74    /// A menu row's; a menu-bar title's is two px shorter, so the bar's
75    /// height and not the title's padding decides the strip.
76    pub menu_pad_x: f32,
77    pub menu_pad_y: f32,
78
79    // -- extents -------------------------------------------------------
80    /// A menu panel's width.
81    pub menu_width: f32,
82    /// The drawn menu bar's height.
83    pub menu_bar_h: f32,
84    /// The titlebar's height where the strip is the app's alone: the
85    /// platform's caption height (32 on Windows, 34 elsewhere), which
86    /// [`Metrics::compact`] leaves alone. Where the OS keeps controls of
87    /// its own over the strip — the macOS traffic lights under custom
88    /// chrome — the strip is as tall as the OS's titlebar, which the
89    /// driver measures into `env.window.native_controls`, and this row
90    /// is not read (`widgets::titlebar_height`).
91    pub titlebar_h: f32,
92}
93
94impl Default for Metrics {
95    /// What the widgets have always drawn: the constants `widgets.rs`
96    /// carried, restated once.
97    fn default() -> Self {
98        Self::comfortable()
99    }
100}
101
102impl Metrics {
103    /// The stock set — [`Default`], named.
104    pub const fn comfortable() -> Self {
105        Metrics {
106            control_text: 15.0,
107            chrome_text: 13.0,
108            hint_text: 12.0,
109            radius: 6.0,
110            radius_inner: 4.0,
111            control_pad_x: 14.0,
112            control_pad_y: 8.0,
113            field_pad_x: 10.0,
114            field_pad_y: 8.0,
115            hint_pad_x: 10.0,
116            hint_pad_y: 6.0,
117            menu_pad_x: 8.0,
118            menu_pad_y: 5.0,
119            menu_width: 200.0,
120            menu_bar_h: 26.0,
121            titlebar_h: if cfg!(target_os = "windows") {
122                TITLEBAR_H_WINDOWS
123            } else {
124                TITLEBAR_H_ELSEWHERE
125            },
126        }
127    }
128
129    /// A tighter set for a dense tool — a mux, an inspector, a table of
130    /// controls: smaller text, shallower padding, sharper corners. The
131    /// titlebar keeps the platform's height, since that is the OS's
132    /// number and not a density.
133    pub const fn compact() -> Self {
134        Metrics {
135            control_text: 13.0,
136            chrome_text: 12.0,
137            hint_text: 11.0,
138            radius: 4.0,
139            radius_inner: 3.0,
140            control_pad_x: 10.0,
141            control_pad_y: 5.0,
142            field_pad_x: 8.0,
143            field_pad_y: 5.0,
144            hint_pad_x: 8.0,
145            hint_pad_y: 4.0,
146            menu_pad_x: 8.0,
147            menu_pad_y: 3.0,
148            menu_width: 180.0,
149            menu_bar_h: 22.0,
150            ..Self::comfortable()
151        }
152    }
153
154    /// Every density multiplied by `factor` — a density slider, or an OS
155    /// text-size setting the day one is plumbed. Logical px in, logical
156    /// px out: `env.scale` is applied after this by the renderer and is
157    /// never folded in here. A row the schema marks the platform's
158    /// (`titlebar_h`: the OS's caption height, which the traffic lights
159    /// are drawn against) is left alone, as [`Metrics::compact`] leaves
160    /// it — a 1.5 slider drew a 51 px strip beside 34 px buttons
161    /// (backlog AR35).
162    pub fn scaled(self, factor: f32) -> Self {
163        let mut m = self;
164        for row in crate::schema::METRIC_ROLES {
165            if row.platform.is_some() {
166                continue;
167            }
168            (row.set)(&mut m, (row.get)(&self) * factor);
169        }
170        m
171    }
172}