max / makeover-layout
| 1 | use crateIntent; |
| 2 | |
| 3 | /// Which way the light falls across a two-tone edge. |
| 4 | /// |
| 5 | /// The whole content of a bevel, once colour and thickness are deferred. The |
| 6 | /// light is always assumed to come from the top left: every consumer measured |
| 7 | /// agreed on that and none of them ever varied it, so it is an invariant here |
| 8 | /// rather than a parameter. |
| 9 | /// |
| 10 | /// # The two corners that belong to both edges |
| 11 | /// |
| 12 | /// Top-right and bottom-left are where the lit run meets the shaded one, and |
| 13 | /// the description's claim is that they belong to *both*. How a renderer says |
| 14 | /// that is its own business, because the answer is bounded by resolution and |
| 15 | /// not by taste: |
| 16 | /// |
| 17 | /// - A terminal cell is roughly 8x17 device pixels, so giving the whole corner |
| 18 | /// to one tone thickens that edge by a cell and reads as one run overrunning |
| 19 | /// the other. A half-cell glyph divides the cell already, so `makeover-tui` |
| 20 | /// splits it and recovers real information. Its box-drawing fallback cannot: |
| 21 | /// a single stroke has no half to give, so there both corners go to dark. |
| 22 | /// - A pixel bevel is a one-point stroke by default, which makes the corner a |
| 23 | /// one-point square. There is nothing to divide — a diagonal seam across one |
| 24 | /// point is sub-pixel, and antialiasing renders it as the blend a mitred join |
| 25 | /// already produces. So `makeover-immediate` mitres and is *not* diverging; |
| 26 | /// it is the same rule at a resolution where the split degenerates. |
| 27 | /// |
| 28 | /// Stated here so the difference reads as a decision rather than as drift. A |
| 29 | /// renderer with room to divide the corner should; one without should mitre or |
| 30 | /// pick the shaded tone, and neither is a bug. |
| 31 | |
| 32 | |
| 33 | /// Lit from the top left: light on top and left, dark on bottom and right. |
| 34 | Raised, |
| 35 | /// The same edge inverted, which is also the pressed state of anything |
| 36 | /// that draws itself [`Bevel::Raised`]. |
| 37 | Inset, |
| 38 | |
| 39 | |
| 40 | |
| 41 | /// The edge intents, as `(top_left, bottom_right)`. |
| 42 | /// |
| 43 | /// Split out from any painting because the inversion *is* the idea, and |
| 44 | /// it is the one part every renderer implements identically. |
| 45 | |
| 46 | pub const |
| 47 | match self |
| 48 | SelfRaised => , |
| 49 | SelfInset => , |
| 50 | |
| 51 | |
| 52 | |
| 53 | /// Pressing inverts. A raised control reads as inset while held. |
| 54 | /// |
| 55 | /// Stated here rather than left to each consumer because a cascade can |
| 56 | /// carry a pressed state and an immediate-mode renderer cannot: audiofiles |
| 57 | /// resolves this per call site, eighteen times. |
| 58 | |
| 59 | pub const |
| 60 | match self |
| 61 | SelfRaised => SelfInset, |
| 62 | SelfInset => SelfRaised, |
| 63 | |
| 64 | |
| 65 | |
| 66 | |
| 67 | /// One side of a bevel, named by the intent it takes. |
| 68 | |
| 69 | |
| 70 | /// The lit side. |
| 71 | Light, |
| 72 | /// The shadowed side. |
| 73 | Dark, |
| 74 | |
| 75 | |
| 76 | |
| 77 | |
| 78 | match self |
| 79 | SelfLight => "bevel-light", |
| 80 | SelfDark => "bevel-dark", |
| 81 | |
| 82 | |
| 83 | |
| 84 | |
| 85 | /// A surface intent a region is filled with. |
| 86 | /// |
| 87 | /// `#[non_exhaustive]`, so a renderer must carry a wildcard arm and a new |
| 88 | /// member is additive rather than breaking. The vocabulary exists to grow and |
| 89 | /// the renderers exist to disagree about how much of it they answer, so growth |
| 90 | /// must not be a lockstep event. The renderer's wildcard is not a hole: |
| 91 | /// [`Fill`] is resolved through a fallible lookup, and a missing intent is |
| 92 | /// answered with structure rather than with a substituted colour. |
| 93 | /// |
| 94 | /// [`Sunken`]: Fill::Sunken |
| 95 | |
| 96 | |
| 97 | |
| 98 | /// The page behind everything. |
| 99 | Page, |
| 100 | /// A surface lifted off the page: cards, controls, menus, toasts. |
| 101 | Raised, |
| 102 | /// A surface floating above the page rather than resting on it. |
| 103 | Overlay, |
| 104 | /// The inside of a well. |
| 105 | Well, |
| 106 | /// A surface set back from the one it sits on, by colour and nothing else. |
| 107 | /// |
| 108 | /// Not a well. A well is a hole with an edge, and the two are authored in |
| 109 | /// opposite directions: `makeover` derives `surface-well` by inverting |
| 110 | /// against the theme's own content colour, while `surface-sunken` is |
| 111 | /// authored and free to sit darker than raised (goingson's does). Naming |
| 112 | /// only the well left the recessed-with-no-edge surface unsayable, which is |
| 113 | /// what an unchosen tab is: it recedes so the chosen one can come forward, |
| 114 | /// and it carries no bevel of its own. |
| 115 | Sunken, |
| 116 | |
| 117 | |
| 118 | // No `fallback` here, deliberately. An earlier cut had `Fill::Well` fall back |
| 119 | // to `Fill::Page` so a consumer on makeover 2.2.0, which has no `surface-well`, |
| 120 | // had something to paint. makeover-tui found that wrong within a day: page is |
| 121 | // the surface a well is usually cut into, so on a terminal that substitution |
| 122 | // produces exactly the invisibility it was meant to prevent, and the right |
| 123 | // answer there is a drawn edge rather than a different colour. |
| 124 | // |
| 125 | // Substituting one intent for another is renderer policy. The description says |
| 126 | // what the region is and stops. |
| 127 | |
| 128 | |
| 129 | |
| 130 | match self |
| 131 | SelfPage => "surface-page", |
| 132 | SelfRaised => "surface-raised", |
| 133 | SelfOverlay => "surface-overlay", |
| 134 | SelfWell => "surface-well", |
| 135 | SelfSunken => "surface-sunken", |
| 136 | |
| 137 | |
| 138 | |
| 139 | |
| 140 | /// How a region sits relative to the surface behind it. |
| 141 | /// |
| 142 | /// Fill and bevel are named together because naming them apart is what let |
| 143 | /// them disagree. Every consumer measured had at least one region carrying a |
| 144 | /// raised bevel over a recessed fill: audiofiles fixed it in `raised_frame` |
| 145 | /// and recorded the bug in its doc comment, and Balanced Breakfast still had |
| 146 | /// twelve of them a year later. A single name for the pair makes that |
| 147 | /// unrepresentable. |
| 148 | /// `#[non_exhaustive]` for the same reason as [`Fill`], and in the same |
| 149 | /// release: a depth this renderer has no drawing for should cost it a |
| 150 | /// wildcard arm, not a compile error and a wait on someone else's publish. |
| 151 | |
| 152 | |
| 153 | |
| 154 | /// Level with its surroundings. No edge. |
| 155 | Flat, |
| 156 | /// A card laid on the panel it sits in. |
| 157 | Raised, |
| 158 | /// A hole in the panel, with content down inside it. For anything the |
| 159 | /// user looks *into*: a table body, a tag tree, a text field. |
| 160 | Well, |
| 161 | /// Set back from what it sits on, by colour alone. No edge. |
| 162 | /// |
| 163 | /// The one member carrying a fill without a bevel, so a renderer cannot |
| 164 | /// assume the two arrive together. That is deliberate and it is still the |
| 165 | /// pairing rule: both halves come off the same `Depth`, so they cannot |
| 166 | /// disagree, and here one half is legitimately absent. |
| 167 | /// |
| 168 | /// Distinct from [`Depth::Flat`], which has no fill either and inherits. |
| 169 | /// Recessed and level-with are different claims, and only one of them |
| 170 | /// needs a colour. |
| 171 | Sunken, |
| 172 | /// A surface sitting *over* the page rather than in it. A modal, a popover, |
| 173 | /// a menu. |
| 174 | /// |
| 175 | /// Takes elevation and no bevel: a surface overlaying the page is lifted |
| 176 | /// off it, and a surface in the page is cut into it. That is the same |
| 177 | /// pairing rule the rest of the enum holds, applied to the one case where |
| 178 | /// the separation is not an edge at all — the lift and the scrim behind it |
| 179 | /// are already saying where the surface is. |
| 180 | /// |
| 181 | /// Every renderer already has the surface: `makeover-tui` carries |
| 182 | /// `Palette::overlay`, `makeover-immediate` `Palette::elevation`, and |
| 183 | /// `makeover-webview` emits `--elevation-overlay`. This variant is the |
| 184 | /// route from a description to any of them, which is why it is one variant |
| 185 | /// rather than a feature. |
| 186 | Overlay, |
| 187 | |
| 188 | |
| 189 | |
| 190 | /// The edge this depth is drawn with, if it has one. |
| 191 | |
| 192 | pub const |
| 193 | match self |
| 194 | // Sunken joins Flat here, for the opposite reason: Flat has no edge |
| 195 | // because nothing separates it from its surroundings, and Sunken has |
| 196 | // none because its colour is already doing the separating. |
| 197 | SelfFlat | SelfSunken => None, |
| 198 | // A third reason to have no edge, which is why it gets its own arm |
| 199 | // rather than joining the two above: an overlay is separated by the |
| 200 | // lift and by the scrim behind it, so an edge would be a second |
| 201 | // answer to a question already answered. |
| 202 | SelfOverlay => None, |
| 203 | SelfRaised => Some, |
| 204 | SelfWell => Some, |
| 205 | |
| 206 | |
| 207 | |
| 208 | /// The surface this depth is filled with. |
| 209 | /// |
| 210 | /// [`Depth::Flat`] has no fill of its own: it inherits whatever it sits on, |
| 211 | /// which is the difference between level-with and painted-the-same-colour. |
| 212 | |
| 213 | pub const |
| 214 | match self |
| 215 | SelfFlat => None, |
| 216 | SelfRaised => Some, |
| 217 | SelfWell => Some, |
| 218 | SelfSunken => Some, |
| 219 | SelfOverlay => Some, |
| 220 | |
| 221 | |
| 222 | |
| 223 | /// Pressing a raised region reads as a well, and nothing else moves. |
| 224 | /// |
| 225 | /// [`Depth::Overlay`] is untouched along with the rest: an overlay is a |
| 226 | /// surface, not a control, so there is nothing there to press. |
| 227 | |
| 228 | pub const |
| 229 | match self |
| 230 | SelfRaised => SelfWell, |
| 231 | other => other, |
| 232 | |
| 233 | |
| 234 | |
| 235 | |
| 236 | /// An interaction state a region can be in, beside whatever [`Depth`] it is. |
| 237 | /// |
| 238 | /// Orthogonal to depth on purpose. A disabled button is still [`Depth::Raised`] |
| 239 | /// and a disabled field is still a [`Depth::Well`], so folding either member |
| 240 | /// into `Depth` would make [`Depth::bevel`] and [`Depth::fill`] answer for |
| 241 | /// something that is not a depth, and would leave disabled-button and |
| 242 | /// disabled-field sharing one variant that cannot tell them apart. |
| 243 | /// |
| 244 | /// # Why hover and pressed are not members |
| 245 | /// |
| 246 | /// The line is whether every renderer has the state to express, not whether CSS |
| 247 | /// does. Hover is renderer policy and `makeover-webview` says so in its own |
| 248 | /// header: a terminal and an immediate-mode painter have no pointer hovering |
| 249 | /// over anything, and pressed already arrives through [`Bevel::pressed`] and |
| 250 | /// [`Depth::pressed`], where it belongs, because pressing is a depth inversion |
| 251 | /// rather than a separate condition. |
| 252 | /// |
| 253 | /// Focus and disabled are different in kind. A TUI has a focused widget and a |
| 254 | /// greyed-out one; so does egui. Both were unsayable here, so all three webview |
| 255 | /// consumers supplied them from outside the primitive by out-specifying rules |
| 256 | /// they did not own: goingson alone carries 19 of them, and the MNW server |
| 257 | /// another 21. That is the divergence this crate exists to end, arriving one |
| 258 | /// layer down. |
| 259 | /// |
| 260 | /// # The principle this encodes |
| 261 | /// |
| 262 | /// A primitive owns every state it implies. A renderer that emits a hover rule |
| 263 | /// for a thing owes disabled and the capability answer for that same thing, |
| 264 | /// because anything less exports the completion work to N consumers who will |
| 265 | /// each do it differently. |
| 266 | /// |
| 267 | /// Focus is not on that list and is not on this axis. It is the renderer's, |
| 268 | /// decided after the description; see the crate header, "Reach, |
| 269 | /// focus and the focus ring", for the three terms and who owns each. |
| 270 | /// |
| 271 | /// `#[non_exhaustive]` for the reason [`Fill`] and [`Depth`] carry it: growth |
| 272 | /// must not be a lockstep event across the three renderers. |
| 273 | |
| 274 | |
| 275 | |
| 276 | /// Present, visible, and not answering. |
| 277 | /// |
| 278 | /// Not the same as absent, and deliberately not a [`Fill`]: a disabled |
| 279 | /// control keeps the surface it always had and stops responding, so what |
| 280 | /// changes is its content and its interactivity rather than what it is. |
| 281 | Disabled, |
| 282 | |
| 283 | |
| 284 | |
| 285 | /// Whether a region in this state stops answering the pointer. |
| 286 | /// |
| 287 | /// Stated in the description rather than left to each renderer, on the same |
| 288 | /// reasoning as [`Bevel::pressed`]: a cascade carries it for free and an |
| 289 | /// immediate-mode renderer resolves it per call site, so leaving it unsaid |
| 290 | /// means resolving it once per consumer and disagreeing. |
| 291 | |
| 292 | pub const |
| 293 | // A match rather than a bare `true`, so a member added to this |
| 294 | // `#[non_exhaustive]` axis has to answer the question rather than |
| 295 | // inheriting an answer. |
| 296 | match self |
| 297 | SelfDisabled => true, |
| 298 | |
| 299 | |
| 300 | |
| 301 | |
| 302 | |
| 303 | |
| 304 | match self |
| 305 | // Reusing the muted content intent rather than minting a |
| 306 | // `disabled` colour. Disabled is a reduction and not a status, and |
| 307 | // `makeover-webview`'s progress rules already record the reading |
| 308 | // that `content-muted` is what disabled looks like. |
| 309 | SelfDisabled => "content-muted", |
| 310 | |
| 311 | |
| 312 | |
| 313 |