max / makeover-layout
| 1 | use crateTone; |
| 2 | |
| 3 | // Names this module's prose links to, resolved for rustdoc. |
| 4 | |
| 5 | use crate::; |
| 6 | |
| 7 | /// How much of a set is done. |
| 8 | /// |
| 9 | /// Nine sites across the two webview apps drew a bar and nothing here named |
| 10 | /// one, so every described screen concatenated the two numbers into its |
| 11 | /// heading text instead: "Subtasks 3/7", "Time Tracking 45m tracked / 30m est, |
| 12 | /// over". Every fact survives that and the reading does not, which is the same |
| 13 | /// loss `RowPart::Tokens` closed when a toned status badge became prose. |
| 14 | /// |
| 15 | /// # Why a pair and not a percentage |
| 16 | /// |
| 17 | /// Both numbers, not the percentage the apps compute from them. The percentage |
| 18 | /// was the obvious shape and it had already been tried: goingson's |
| 19 | /// `Task::time_progress` divides, rounds, and then clamps to 100, which throws |
| 20 | /// away the one case the bar exists to show — 45 minutes tracked against a |
| 21 | /// 30-minute estimate. It carries a separate `is_over_estimate` boolean beside |
| 22 | /// it to recover the fact the clamp dropped. A pair keeps the over-run without a |
| 23 | /// companion flag, and [`percent`](Meter::percent) is still one call away for a |
| 24 | /// renderer that wants it. |
| 25 | /// |
| 26 | /// The pair is also what the apps already have at every site. All seven |
| 27 | /// determinate bars write the ratio into the accessible layer and never the |
| 28 | /// percentage: `title="3/7 subtasks"`, `aria-label="3 of 7 subtasks completed"`, |
| 29 | /// a milestone's own `3/7` span. Given 43 nothing can recover "3 of 7", so a |
| 30 | /// percentage member would have made [`label`](Meter::label) mandatory at every |
| 31 | /// call site, which is the concatenated text this member removes, moved one |
| 32 | /// layer down. |
| 33 | /// |
| 34 | /// # What this is not |
| 35 | /// |
| 36 | /// The progress of an *operation*. Two of the nine sites are that — goingson's |
| 37 | /// focus timer, Balanced Breakfast's feed fetch — and they get nothing here, on |
| 38 | /// purpose. Both are imperative controllers over a live handle, driven by a tick |
| 39 | /// or an event stream, and a description is built once and dropped. Holding one |
| 40 | /// would mean growing a way to update a description between renders, which is a |
| 41 | /// different feature. [`Readiness::Pending`] and a [`Notice::Toast`] carry the |
| 42 | /// honest part. |
| 43 | /// |
| 44 | /// The two cases are distinguishable in the markup rather than by taste: every |
| 45 | /// determinate bar in both apps carries a tone, and neither operation bar |
| 46 | /// carries one. Two codebases drew that line the same way without coordinating. |
| 47 | |
| 48 | |
| 49 | /// How much is done. May exceed [`total`](Self::total), and that is the |
| 50 | /// case worth drawing. |
| 51 | pub done: u32, |
| 52 | /// How much there is to do. Zero means there is no set, not that the set is |
| 53 | /// complete. |
| 54 | pub total: u32, |
| 55 | /// What the proportion means right now. |
| 56 | /// |
| 57 | /// Carried rather than derived, because no renderer can work it out. The |
| 58 | /// same 90% is [`Tone::Success`] on a subtask rollup and [`Tone::Danger`] on |
| 59 | /// a time estimate, and goingson picks between them from `is_over_estimate`, |
| 60 | /// a fact about the data and not about the number. |
| 61 | pub tone: Tone, |
| 62 | /// What is being counted, if the bar says so: "subtasks", "tasks". |
| 63 | /// |
| 64 | /// The noun, not the ratio. A renderer builds "3 of 7 subtasks" from this |
| 65 | /// and the two numbers; handing it the assembled string would put the |
| 66 | /// sentence order in the description, where a terminal at one line and a |
| 67 | /// tooltip want different ones. |
| 68 | pub label: , |
| 69 | |
| 70 | |
| 71 | |
| 72 | /// A proportion with no tone and no label. |
| 73 | |
| 74 | pub const |
| 75 | Self |
| 76 | done, |
| 77 | total, |
| 78 | tone: Neutral, |
| 79 | label: None, |
| 80 | |
| 81 | |
| 82 | |
| 83 | /// What the proportion means. |
| 84 | |
| 85 | pub const |
| 86 | self.tone = tone; |
| 87 | self |
| 88 | |
| 89 | |
| 90 | /// What is being counted. |
| 91 | |
| 92 | pub const |
| 93 | self.label = Some; |
| 94 | self |
| 95 | |
| 96 | |
| 97 | /// How full the bar is, 0 to 100, clamped. |
| 98 | /// |
| 99 | /// For drawing, which is the only thing a clamped number is good for. Ask |
| 100 | /// [`overflowing`](Self::overflowing) before reporting it as a fact, or this |
| 101 | /// is `time_progress`'s bug again with the clamp moved. |
| 102 | /// |
| 103 | /// An empty set reads as 0. Nothing is done, because there is nothing to do |
| 104 | /// and no bar to fill; the apps guard on the count before drawing at all. |
| 105 | |
| 106 | pub const |
| 107 | if self.total == 0 |
| 108 | return 0; |
| 109 | |
| 110 | let scaled = / self.total as u64; |
| 111 | if scaled > 100 else |
| 112 | |
| 113 | |
| 114 | /// Whether more is done than there was to do. |
| 115 | /// |
| 116 | /// The fact [`percent`](Self::percent) destroys, kept reachable so a |
| 117 | /// renderer can mark the over-run rather than drawing a full bar and |
| 118 | /// implying it landed exactly. |
| 119 | |
| 120 | pub const |
| 121 | self.done > self.total |
| 122 | |
| 123 | |
| 124 | /// Whether there is a set at all. |
| 125 | /// |
| 126 | /// A meter over nothing is sayable on purpose, for the same reason a field |
| 127 | /// with no options is: it is what an app with an unloaded count actually |
| 128 | /// has, and a renderer that shows an empty bar says so on screen rather than |
| 129 | /// dividing by zero. |
| 130 | |
| 131 | pub const |
| 132 | self.total == 0 |
| 133 | |
| 134 | |
| 135 | |
| 136 | /// One figure with a caption: a number and what it counts. |
| 137 | /// |
| 138 | /// The dashboard shape. A large value over a small caption, several of them in |
| 139 | /// a strip: a current streak, a completion rate, a total. Four put the value |
| 140 | /// above the caption and one inverts it, which is drift inside the shape |
| 141 | /// rather than a second shape. |
| 142 | /// |
| 143 | /// # Why the value is text |
| 144 | /// |
| 145 | /// "17", "84%", "12/30", "3d". A figure is whatever the app computed, already |
| 146 | /// formatted, and the formatting is the app's because only it knows whether the |
| 147 | /// number is a percentage, a duration or a ratio. This carries none of the |
| 148 | /// arithmetic [`Meter`] carries, and that is the difference between them: a |
| 149 | /// meter is a proportion a renderer draws, and a figure is a fact a renderer |
| 150 | /// sets in type. |
| 151 | /// |
| 152 | /// # Tone is carried, for [`Meter`]'s reason |
| 153 | /// |
| 154 | /// Three of the five sites tone the figure by their own means — `red`/`blue` on |
| 155 | /// the weekly review, a `${type}` class on the monthly one, `sync-stat-warn` on |
| 156 | /// sync. So tone is carried at every site that needs it and derived at none, and |
| 157 | /// no renderer can work out that a streak of zero is worth colouring. |
| 158 | /// |
| 159 | /// # What is not here |
| 160 | /// |
| 161 | /// Whether the figure answers a click. One of the five is a control — sync's |
| 162 | /// "Not Applied: 3" opens the list — and an action is not something this crate |
| 163 | /// can name: nothing here knows what a route is. That belongs beside the figure |
| 164 | /// in whatever layer holds the actions, the same way a row's activation sits |
| 165 | /// beside its parts rather than inside them. |
| 166 | /// |
| 167 | /// The arrangement is not here either. Several figures in a strip is a set, and |
| 168 | /// a renderer given them one at a time cannot tell it is looking at one; the |
| 169 | /// layer that holds the tree is where the set gets said. |
| 170 | |
| 171 | |
| 172 | /// The number, formatted the way the app means it to read. |
| 173 | pub value: &'a str, |
| 174 | /// What it counts. The caption under the value. |
| 175 | pub caption: &'a str, |
| 176 | /// How the value has moved, if the app is tracking that. |
| 177 | /// |
| 178 | /// Text, for [`value`](Self::value)'s reason: only the app knows whether a |
| 179 | /// move reads as `+12.5%`, `+3` or `2x`, and a renderer handed a number |
| 180 | /// would have to guess. |
| 181 | /// |
| 182 | /// This is what [`tone`](Self::tone) was for and had no consumer of. The MNW |
| 183 | /// server has four screens whose stat card is a label, a value and a delta, |
| 184 | /// and the delta is the toned part: the figure itself is an ordinary fact |
| 185 | /// and it is the movement that reads as good or bad. Without this the delta |
| 186 | /// has to be folded into the caption, which loses the tone and reads as a |
| 187 | /// longer caption rather than as a second, smaller line. |
| 188 | pub change: , |
| 189 | /// What the figure means right now. [`Tone::Neutral`] is an ordinary fact. |
| 190 | /// |
| 191 | /// Applies to [`change`](Self::change) where there is one, since that is the |
| 192 | /// part that carries the judgement, and to the value where there is not. |
| 193 | pub tone: Tone, |
| 194 | |
| 195 | |
| 196 | |
| 197 | /// A figure that is an ordinary fact. |
| 198 | |
| 199 | pub const |
| 200 | Self |
| 201 | value, |
| 202 | caption, |
| 203 | change: None, |
| 204 | tone: Neutral, |
| 205 | |
| 206 | |
| 207 | |
| 208 | /// How the value has moved. |
| 209 | |
| 210 | pub const |
| 211 | self.change = Some; |
| 212 | self |
| 213 | |
| 214 | |
| 215 | /// What the figure means. |
| 216 | |
| 217 | pub const |
| 218 | self.tone = tone; |
| 219 | self |
| 220 | |
| 221 | |
| 222 | |
| 223 | /// Something the user can do, and what it costs to say so. |
| 224 | /// |
| 225 | /// Beside [`Meter`] and [`Figure`] for the reason those are here: a renderer |
| 226 | /// that is handed the parts has to decide how to say them, and a renderer that |
| 227 | /// is handed a finished string has already had the decision made for it. |
| 228 | /// |
| 229 | /// No address. Where a control goes is the app's business and every host |
| 230 | /// follows it differently — an `hx-get`, a protocol URL, a function call — so |
| 231 | /// the description says what the control *is* and the caller keeps what it |
| 232 | /// does. That is the same split [`Choice`] makes. |
| 233 | /// |
| 234 | /// No confirmation flag either, and that one is a finding rather than an |
| 235 | /// omission: a question asked *after* a control is pressed belongs to whatever |
| 236 | /// is holding the interaction, and a renderer that drew it would be asking |
| 237 | /// before there was anything to answer. |
| 238 | /// How a picture sits in the box it is given. |
| 239 | /// |
| 240 | /// An intent rather than a value, so a renderer picks the expression it has: |
| 241 | /// `object-fit` in a webview, a texture's UV rect in egui, and in a terminal a |
| 242 | /// choice about how many cells the blit gets. Named because MNW already makes |
| 243 | /// the distinction deliberately at 17 sites and makes it three different ways, |
| 244 | /// which is a policy the app decided rather than one a shared crate would be |
| 245 | /// picking by accident. |
| 246 | |
| 247 | |
| 248 | |
| 249 | /// The picture's own proportions, and the box takes the height they imply. |
| 250 | /// |
| 251 | /// The default because it is the only one that shows the whole picture at |
| 252 | /// its own shape, so a renderer that ignores this enum entirely is still |
| 253 | /// right about the common case. A screenshot wants this; the shipped MNW |
| 254 | /// carousel sets no `object-fit` at all, which is this. |
| 255 | |
| 256 | Natural, |
| 257 | /// Fill the box and crop whatever does not fit. |
| 258 | /// |
| 259 | /// For a picture in a slot whose shape the layout fixed: a thumbnail, an |
| 260 | /// avatar, cover art. 15 of MNW's 17 sites. |
| 261 | Cover, |
| 262 | /// Fit inside the box whole, leaving space on two sides. |
| 263 | /// |
| 264 | /// The letterbox. For when the whole picture matters more than filling the |
| 265 | /// space, and the space is not the picture's shape. |
| 266 | Contain, |
| 267 | |
| 268 | |
| 269 | /// A picture's own pixel dimensions. |
| 270 | /// |
| 271 | /// Deliberately not [`makeover_geometry`]'s business. Geometry answers *how |
| 272 | /// much space a thing should get*, which is a scale question with the same |
| 273 | /// answer on every screen. This is the intrinsic size of one asset, which is a |
| 274 | /// fact about that asset and varies per picture. |
| 275 | /// |
| 276 | /// [`makeover_geometry`]: https://docs.rs/makeover-geometry |
| 277 | |
| 278 | |
| 279 | /// Width in the picture's own pixels. |
| 280 | pub width: u32, |
| 281 | /// Height in the picture's own pixels. |
| 282 | pub height: u32, |
| 283 | |
| 284 | |
| 285 | |
| 286 | /// A picture's dimensions. |
| 287 | |
| 288 | pub const |
| 289 | Self |
| 290 | |
| 291 | |
| 292 | /// Width over height, or `None` if either side is zero. |
| 293 | /// |
| 294 | /// The form a renderer actually reserves space with: a box that knows its |
| 295 | /// proportion holds the right height at any width, which is what a |
| 296 | /// responsive picture needs and what a fixed pixel height cannot give. |
| 297 | |
| 298 | |
| 299 | .then |
| 300 | |
| 301 | |
| 302 |