1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
//! **One disabled state**, shared by every control that can be unavailable.
//!
//! Milestone 320 gave `Chip` and `SegmentedButton` an `enabled` flag and ended by
//! noting that each control was carrying its own copy of the rule. That undersold it:
//! five widgets had the flag with the same two colours written out by hand in each, and
//! **ten more had no way to be disabled at all**. A form could not grey out a checkbox,
//! a switch, a radio, a slider or a dropdown — the five controls a form is mostly made
//! of. That is a gap in the framework, not in any one widget.
//!
//! ## The two colours
//!
//! A disabled control **flattens**; it does not fade. Every variant collapses to
//! [`disabled_container`] under [`disabled_content`], so that unavailable reads as
//! unavailable rather than as a quieter version of whatever the control was. The
//! difference shows most on a *selected* control: fading the accent gives a pale accent,
//! which reads as *quietly selected*, and a disabled filter is not offering a dimmer
//! answer — it is not offering.
//!
//! The split is **container against content**, not widget against widget, and the switch
//! is what proves it: the reference disables its *track* at 12 % and its *thumb* at 38 %,
//! one control taking both halves of the same rule. So a checkbox's tick box and a radio's
//! dot take [`disabled_content`] even though they look like containers — they are the mark
//! itself, with nothing behind them — while a button's surface, a chip's fill and that
//! switch track take [`disabled_container`].
//!
//! The one thing that is neither is a mark drawn **on** a disabled fill: a disabled
//! selected checkbox's tick, a disabled selected switch's thumb. Another wash of
//! `on_surface` there would sink into the 38 % it sits on, so it punches through in
//! [`disabled_mark`] instead.
//!
//! All three are **opaque**, resolved in sRGB by [`over_surface`] rather than handed to
//! the GPU as an alpha. Milestone 329, and the reason is the sentence above: an opaque
//! colour is what *flattens rather than fades* actually means. It also happens to be the
//! only way these numbers mean what the reference means by them — see [`over_surface`].
//!
//! ## Greying out is the easy half
//!
//! The colours are the visible quarter of the contract. A control that is disabled must
//! also be **inert**, and the three parts that are not visible are the ones that get
//! forgotten — milestone 320 found a `Chip` whose delete cross would have stayed live on
//! an inert chip, and it would have looked perfect in a screenshot. So the whole contract
//! is written down here and [checked against the source](self#the-guard):
//!
//! 1. **the press goes nowhere** — `on_click` (or `positional_click`) yields `None`;
//! 2. **no ink** — a splash promises that something is happening, and nothing is;
//! 3. **out of the tab order** — `focusable` is false, so Tab does not stop at a control
//! that cannot be operated;
//! 4. **announced as disabled** rather than falling silent. A reader that simply stopped
//! hearing about a filter would be told the filter had gone away, which is a different
//! and worse fact.
//!
//! What a disabled control keeps is its **answer**: `toggled`, `checked`, the chosen
//! segment. Read-only is not invisible, and the current state is still owed to a reader
//! who cannot change it.
//!
//! ## The guard
//!
//! `every_control_with_an_enabled_flag_honours_all_four` reads the crate's own sources,
//! finds every widget carrying `enabled: bool`, and insists that **every** hook it
//! implements which could carry one of those four out mentions that flag — the tap hooks,
//! and equally the drag and key hooks, since a slider is dragged and a field is typed
//! into. It is the same instrument as `transparent`'s — that module is crate-private, so
//! this cannot be a link — written for the same reason: a rule kept by convention is kept
//! by whoever remembered it.
//!
//! It has caught three omissions so far, two of them in the milestones that introduced
//! the rule it checks.
use Color;
use crateTheme;
/// A disabled container's opacity over the surface — the reference's 12 %.
pub const DISABLED_CONTAINER_OPACITY: f32 = 0.12;
/// A disabled label's, glyph's or outline's opacity — the reference's 38 %.
pub const DISABLED_CONTENT_OPACITY: f32 = 0.38;
/// The surface a disabled control sits on: `on_surface` at
/// [`DISABLED_CONTAINER_OPACITY`] over the surface, whatever the control's variant
/// would have been.
///
/// **Resolved, not translucent** — see [`over_surface`].
/// `on_surface` at `opacity` **over `surface`**, resolved to an opaque colour here
/// rather than handed to the GPU as an alpha.
///
/// The reference's 12 % and 38 % are a design language, and a design language assumes
/// the blend happens in the space the colours are written in: sRGB. Ours does not.
/// The render target is `Rgba8UnormSrgb`, so the hardware decodes the destination to
/// linear, blends there and re-encodes — correct physics, ordinary wgpu, and a whisper
/// that paints at roughly what 33 % would give. Milestone 328 measured it: a 12 % wash
/// meant to read near tone 24 in the dark scheme painted at tone 38.
///
/// Resolving the blend here, in sRGB, is not a workaround for that. It is what this
/// module has said all along — *a disabled control flattens; it does not fade* — and an
/// opaque colour is the literal form of it. The alpha was the part that disagreed.
///
/// The cost is an assumption: that the control sits on `surface`. One on the darker
/// `background` lands about 11/255 light of a true blend, which is the difference
/// between the two backdrops and is why the reference specifies these tokens *over the
/// surface* too. What this does **not** touch is everything else translucent — scrims,
/// ink, state layers — which still blend in linear light. That is the open question, on
/// the roadmap, pinned by `frus-test/tests/blending.rs`.
/// What is drawn on a disabled control — its label, its glyph, and a selection control's
/// own mark: `on_surface` at [`DISABLED_CONTENT_OPACITY`].
///
/// **Outlines go both ways**, and the same test decides it as everywhere else. The outline
/// *of a container* — a chip, an outlined button, a field, a dropdown row — is part of that
/// container and takes [`disabled_container`]. A checkbox's box or a radio's ring is not a
/// container with a mark inside it; it **is** the mark, so it takes this.
///
/// **Resolved, not translucent** — see [`over_surface`].
/// A mark drawn **on** a disabled fill — a disabled selected checkbox's tick, a disabled
/// selected switch's thumb.
///
/// Opaque `surface`, not another translucent `on_surface`: stacking 38 % on 38 % leaves
/// the two within a few percent of each other, and the tick disappears into the box it is
/// supposed to be inside.