Skip to main content

max / makeover-webview

0.42.0: emit a time axis track_rules, for makeover-layout 0.24.0's Track. The container, its gridlines and ruler, and the placed entries. One rule does the work: an entry is absolutely positioned and reads --track-at and --track-for, percentages the caller sets inline from Track::fraction so the arithmetic happens once rather than in three renderers. The stylesheet stays static while the items move; the rejected shape was a rule per item, which is a stylesheet that grows with the data. Overlap arrives as --track-lane and --track-lanes and the rule divides the width. Both default, so an entry naming neither is full width and a renderer that would rather stack gets that for free. The description never declares an overlap: Placement::overlaps derives it from the times. The track gets position: relative and no height. 96 quarter-hour slots at some slot height is a size and sizes are makeover-geometry's, so the app says how tall a day is and the percentages resolve against it. A test asserts no bare px, rem, em or vh survives outside a var(), because a gridline is exactly where a hardcoded 1px would look reasonable. the_whole_sheet_still_names_every_colour gains relative and absolute, with the argument written where the allowlist can see it: position is the one property whose job is where a thing sits, and neither keyword names a magnitude. Takes makeover-layout 0.24.0 and makeover-touch 0.16.0.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-15 15:42 UTC
Signed with PGP, not checked
Commit: cb2ac60f1eb9307af656126c3033fd47e6da4c8b
Parent: f47e461
2 files changed, +130 insertions, -3 deletions
M Cargo.toml +3 -3
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-webview"
3 - version = "0.41.0"
3 + version = "0.42.0"
4 4 edition = "2024"
5 5 # One copy of this renderer per dependency graph, enforced by cargo rather than
6 6 # by remembering. Two versions means the generated stylesheet and the emitted
@@ -17,7 +17,7 @@
17 17 # patch satisfy the requirement and still fail to compile. That happened once
18 18 # with `form::radio_html` calling `FieldKind::Radio`, and makeover-build is where
19 19 # it surfaced, one release later.
20 - makeover-layout = "0.23.0"
20 + makeover-layout = "0.24.0"
21 21 # The capability axis. `makeover-touch` decides whether a hover rule should be
22 22 # gated at all; `makeover-geometry` spells the gate as a media condition. Both
23 23 # answers are owned elsewhere and neither is re-derived here.
@@ -27,7 +27,7 @@
27 27 # satisfies "0.8" and keeps a second makeover-geometry in the graph next to the
28 28 # 0.7 this crate asks for. `Density` is nominally distinct across the two and
29 29 # the build fails on a type that reads as identical.
30 - makeover-touch = "0.15.0"
30 + makeover-touch = "0.16.0"
31 31 makeover-geometry = "0.7"
32 32
33 33 [lints.rust]
M src/lib.rs +127
@@ -1166,6 +1166,81 @@
1166 1166 css
1167 1167 }
1168 1168
1169 + /// A time axis: the container, its gridlines and ruler, and the placed things.
1170 + ///
1171 + /// `makeover-layout` 0.24.0's [`Track`](makeover_layout::Track), and the first
1172 + /// member here whose whole point is *position*. That makes the magnitude line
1173 + /// this crate keeps worth restating rather than assuming.
1174 + ///
1175 + /// # Where the numbers come from
1176 + ///
1177 + /// Every value that varies per item is a custom property the caller sets
1178 + /// inline, and every rule here reads one. `--track-at` and `--track-for` are
1179 + /// percentages of the span, which
1180 + /// [`Track::fraction`](makeover_layout::Track::fraction) computes once so three
1181 + /// renderers cannot disagree about it. Nothing here knows a pixel.
1182 + ///
1183 + /// That is what lets the stylesheet stay static while the items move: an entry
1184 + /// carries `style="--track-at: 37.5%; --track-for: 4.166%"` and the rule below
1185 + /// turns it into a box. The alternative was emitting a rule per item, which is
1186 + /// a stylesheet that grows with the data.
1187 + ///
1188 + /// **The height of the track is the app's**, not this crate's. 96 quarter-hour
1189 + /// slots at some slot height is a size, and a size is `makeover-geometry`'s
1190 + /// question -- the same refusal `figure_rules` and `placeholder_rules` make.
1191 + /// Percentages need a resolved height above them, so `.track` gets
1192 + /// `position: relative` and nothing else; the app says how tall a day is.
1193 + ///
1194 + /// # Overlap
1195 + ///
1196 + /// Two things at the same time is intrinsic to an axis and has no analogue in a
1197 + /// list. The description does not declare it -- `Placement::overlaps` derives it
1198 + /// from the times -- so what arrives here is a lane index and a lane count, and
1199 + /// the rule divides the width. A renderer that would rather stack them ignores
1200 + /// both properties and the defaults give it one full-width lane.
1201 + fn track_rules(opts: &Emit) -> String {
1202 + let track = class("track", opts);
1203 + let slot = class("track-slot", opts);
1204 + let tick = class("track-tick", opts);
1205 + let entry = class("track-entry", opts);
1206 + let mut css = String::new();
1207 +
1208 + // The positioning context every entry resolves against, and the whole of
1209 + // what this crate says about the container. No height: see the doc above.
1210 + let _ = writeln!(css, ".{track} {{\n position: relative;\n}}");
1211 +
1212 + // Gridlines and the ruler are the axis reading itself back, which is
1213 + // `picture-caption` and `showing-position`'s claim: about the thing rather
1214 + // than one of the things.
1215 + //
1216 + // `border_width` rather than a literal, the way `depth_rule` writes its
1217 + // edge. A gridline is the one place a timeline would most naturally reach
1218 + // for a hardcoded 1px, and a hardcoded 1px is this crate naming a size.
1219 + let _ = writeln!(
1220 + css,
1221 + ".{slot} {{\n border-top: {} solid var(--border);\n}}",
1222 + opts.border_width
1223 + );
1224 + let _ = writeln!(css, ".{tick} {{\n color: var(--content-muted);\n}}");
1225 +
1226 + // The one rule that does real work. Top and height are the placement;
1227 + // left and width are the lane. Both lane properties default so an entry
1228 + // that names neither is full width, which is the common case and the one a
1229 + // renderer gets for free.
1230 + let _ = writeln!(
1231 + css,
1232 + ".{entry} {{\n \
1233 + position: absolute;\n \
1234 + top: var(--track-at, 0%);\n \
1235 + height: var(--track-for, 100%);\n \
1236 + left: calc(var(--track-lane, 0) / var(--track-lanes, 1) * 100%);\n \
1237 + width: calc(100% / var(--track-lanes, 1));\n\
1238 + }}"
1239 + );
1240 +
1241 + css
1242 + }
1243 +
1169 1244 /// A region's stand-in, and the header of a table that can be reordered.
1170 1245 ///
1171 1246 /// Both are 0.12.0 members and both are colour and affordance only, which is
@@ -1370,6 +1445,7 @@
1370 1445 css.push_str(&figure_rules(opts));
1371 1446 css.push_str(&picture_rules(opts));
1372 1447 css.push_str(&showing_rules(opts));
1448 + css.push_str(&track_rules(opts));
1373 1449 css.push_str(&state_rules(opts));
1374 1450 css.push_str(&table_rules(opts));
1375 1451 css
@@ -2149,6 +2225,33 @@
2149 2225 assert!(!css.contains("nth-child"), "{css}");
2150 2226 }
2151 2227
2228 + #[test]
2229 + fn a_track_places_by_custom_property_and_never_by_a_size() {
2230 + let css = track_rules(&Emit::default());
2231 +
2232 + // Placement arrives from the caller, computed once by Track::fraction.
2233 + // If either of these becomes a literal, three renderers have started
2234 + // disagreeing about where 09:30 is.
2235 + assert!(css.contains("top: var(--track-at"), "{css}");
2236 + assert!(css.contains("height: var(--track-for"), "{css}");
2237 +
2238 + // Overlap lanes default so an entry naming neither is full width.
2239 + assert!(css.contains("--track-lane, 0"), "{css}");
2240 + assert!(css.contains("--track-lanes, 1"), "{css}");
2241 +
2242 + // The refusal that matters. A slot height here would be this crate
2243 + // deciding how tall a quarter of an hour is, which is the thing
2244 + // makeover-geometry owns and the reason `.track` gets no height at all.
2245 + assert!(!css.contains("height: var(--track-slot"), "{css}");
2246 + for size in ["px", "rem", "em", "vh"] {
2247 + let bare = css
2248 + .lines()
2249 + .filter(|l| !l.contains("var(--"))
2250 + .any(|l| l.contains(size));
2251 + assert!(!bare, "track_rules named a {size} outside a var(): {css}");
2252 + }
2253 + }
2254 +
2152 2255 #[test]
2153 2256 fn the_whole_sheet_still_names_every_colour() {
2154 2257 // The crate's founding property, asserted over the component layer and
@@ -2228,6 +2331,30 @@
2228 2331 | "nowrap"
2229 2332 | "100%"
2230 2333 | "1%"
2334 + // The time axis, 0.42.0. `position` is the one
2335 + // property whose whole job is where a thing sits,
2336 + // which is exactly what this crate spent its life
2337 + // refusing to say -- so it is worth being exact
2338 + // about why these two are not that refusal
2339 + // breaking.
2340 + //
2341 + // Neither names a magnitude. `relative` says the
2342 + // track is what its entries resolve against, and
2343 + // `absolute` says an entry is placed rather than
2344 + // flowed. *Where* each entry lands is
2345 + // `--track-at` and `--track-for`, custom
2346 + // properties the caller sets from
2347 + // `Track::fraction`, and they are skipped by the
2348 + // `var(--` arm above like every other value this
2349 + // crate refuses to decide.
2350 + //
2351 + // The rule that would break the refusal is a slot
2352 + // height, and there is none: the track's height is
2353 + // the app's, so the percentages have something to
2354 + // resolve against and this crate still never says
2355 + // how tall a day is.
2356 + | "relative"
2357 + | "absolute"
2231 2358 // A picture's three, 0.36.0. `block` is structure
2232 2359 // for the reason `table` is: an inline image sits
2233 2360 // on the baseline and carries a descender's worth