Skip to main content

max / makeover-webview

Emit the unchosen option, and stop assuming a fill implies a bevel Release 0.3.0, taking makeover-layout 0.3.0. Selector now describes both states, so the three selectors emit a base rule as well as a chosen one. A tab recedes to --surface-sunken and its chosen state comes forward raised; a segment and a toggle stand up raised and their chosen state is held in. Before this only the chosen half emitted, which left goingson's tab strip hand-writing the recess that makes its chosen tab read as forward. depth_declarations required BOTH a fill and a bevel and returned nothing otherwise, which silently dropped the fill for Depth::Sunken -- a fill with no edge. The two halves emit independently now. Still paired: both come off one Depth and cannot disagree about what the region is, and here one half is legitimately absent. Two tests rewritten rather than deleted, because their assertions stopped being valid while their intent did not: - The pressed-fill guard asserted the whole sheet never contains surface-sunken, using that as a proxy for "the app's pressed fill did not leak". An unchosen tab is legitimately surface-sunken now, so the check is scoped to the pressed rules. - The unchosen-option test asserted no `.tab` base rule exists at all. It now asserts the rule recesses without an edge, which is what "does not look picked" was reaching for.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-29 21:54 UTC
Signed with PGP, not checked
Commit: 7d13a93f6cf65ce714987870e90412336b400135
Parent: b0684f3
2 files changed, +59 insertions, -20 deletions
M Cargo.toml +2 -2
@@ -1,13 +1,13 @@
1 1 [package]
2 2 name = "makeover-webview"
3 - version = "0.2.0"
3 + version = "0.3.0"
4 4 edition = "2024"
5 5 description = "The webview renderer for makeover-layout. Emits CSS, and is the one renderer that needs no palette: var() is the late binding, so resolution stays with the browser."
6 6 license = "MIT"
7 7 repository = "https://makenot.work/git/max/makeover-webview"
8 8
9 9 [dependencies]
10 - makeover-layout = "0.2.0"
10 + makeover-layout = "0.3.0"
11 11
12 12 [lints.rust]
13 13 unused = "warn"
M src/lib.rs +57 -18
@@ -157,6 +157,7 @@
157 157 Depth::Flat => return None,
158 158 Depth::Raised => "raised",
159 159 Depth::Well => "well",
160 + Depth::Sunken => "sunken",
160 161 };
161 162 Some(format!("{}{name}", opts.class_prefix))
162 163 }
@@ -172,16 +173,22 @@
172 173 /// Callers lean on the emptiness to skip the rule rather than emit a class that
173 174 /// sets nothing: a class that sets no properties is a class that means "I
174 175 /// thought about this", which is what comments are for.
176 + ///
177 + /// The two halves are emitted independently because [`Depth::Sunken`] has a
178 + /// fill and no bevel. Requiring both, which this did before makeover-layout
179 + /// 0.3.0, silently dropped the fill for exactly that case. Independent does not
180 + /// mean unpaired: both halves still come off one `Depth`, so they cannot
181 + /// disagree about what the region is.
175 182 #[must_use]
176 183 pub fn depth_declarations(depth: Depth) -> String {
177 - let (Some(fill), Some(bevel)) = (depth.fill(), depth.bevel()) else {
178 - return String::new();
179 - };
180 - format!(
181 - " background: {};\n box-shadow: var({});\n",
182 - fill_var(fill),
183 - bevel_var(bevel)
184 - )
184 + let mut css = String::new();
185 + if let Some(fill) = depth.fill() {
186 + let _ = writeln!(css, " background: {};", fill_var(fill));
187 + }
188 + if let Some(bevel) = depth.bevel() {
189 + let _ = writeln!(css, " box-shadow: var({});", bevel_var(bevel));
190 + }
191 + css
185 192 }
186 193
187 194 /// One rule giving a selector a depth, or nothing when the depth declares
@@ -319,6 +326,11 @@
319 326 ///
320 327 /// [`Selector::abutting`] is not emitted: whether the options touch is
321 328 /// spacing, and spacing is `makeover-geometry`'s question to answer.
329 + ///
330 + /// Both states emit as of makeover-layout 0.3.0. Before it the description
331 + /// named only the chosen option, so an unchosen one fell through to
332 + /// [`Depth::Flat`] and nothing was drawn for it, which left goingson's tab
333 + /// strip hand-writing the recess that makes its chosen tab read as forward.
322 334 fn selector_rules(opts: &Emit) -> String {
323 335 let mut css = String::new();
324 336 for (selector, name) in [
@@ -326,9 +338,8 @@
326 338 (Selector::Segmented, "segment"),
327 339 (Selector::Toggle, "toggle"),
328 340 ] {
329 - // The unchosen option is flat and emits no depth of its own. Giving it
330 - // an edge would make every option look picked.
331 341 let c = class(name, opts);
342 + css.push_str(&depth_rule(&c, selector.unchosen()));
332 343 css.push_str(&interactive_rules(&c));
333 344 css.push_str(&depth_rule(&format!("{c}.chosen"), selector.chosen()));
334 345 }
@@ -510,11 +521,24 @@
510 521 // raised region reads as a well, and makeover says outright that
511 522 // surface-sunken cannot serve as one, so the app is the thing that
512 523 // moves.
524 + //
525 + // Scoped to the pressed rules rather than to the whole sheet: since
526 + // makeover-layout 0.3.0 an unchosen tab is legitimately
527 + // --surface-sunken, so the token appearing somewhere in the output no
528 + // longer means the app's choice leaked in.
513 529 let css = stylesheet(&Emit::default());
514 - assert!(
515 - !css.contains("surface-sunken"),
516 - "the app's pressed fill leaked into the generated sheet"
517 - );
530 + let mut checked = 0;
531 + for rule in css.split("}\n") {
532 + if !rule.contains(":active") {
533 + continue;
534 + }
535 + checked += 1;
536 + assert!(
537 + !rule.contains("surface-sunken"),
538 + "a pressed rule took the app's fill: {rule}"
539 + );
540 + }
541 + assert!(checked > 0, "no pressed rules found to check");
518 542 assert_eq!(
519 543 Depth::Raised.pressed().fill(),
520 544 Some(Fill::Well),
@@ -595,14 +619,29 @@
595 619 }
596 620
597 621 #[test]
598 - fn an_unchosen_option_has_no_edge_of_its_own() {
622 + fn an_unchosen_tab_recedes_without_looking_picked() {
599 623 let css = selector_rules(&Emit::default());
600 - // `.tab {` with a body would make every option look picked. Only the
601 - // interaction states and the chosen rule may exist.
602 - assert!(!css.contains(".tab {\n"));
624 + // Recessed by colour and given no edge. An edge would make every option
625 + // look picked; flat would leave the chosen one nothing to come forward
626 + // from, which is the gap makeover-layout 0.3.0 closed.
627 + assert!(
628 + css.contains(".tab {\n background: var(--surface-sunken);\n}"),
629 + "unchosen tab is not recessed: {css}"
630 + );
631 + assert_eq!(Selector::Tabs.unchosen(), Depth::Sunken);
603 632 assert!(css.contains(".tab:hover {"));
604 633 }
605 634
635 + #[test]
636 + fn a_segment_stands_up_so_the_chosen_one_can_be_held_in() {
637 + // The inverse of the tab, and why the three selectors are not one
638 + // member with a flag.
639 + let css = selector_rules(&Emit::default());
640 + assert!(css.contains(".segment {\n background: var(--surface-raised);"));
641 + assert_eq!(Selector::Segmented.unchosen(), Depth::Raised);
642 + assert_eq!(Selector::Segmented.chosen(), Depth::Well);
643 + }
644 +
606 645 #[test]
607 646 fn row_actions_are_revealed_without_moving_the_row() {
608 647 let css = row_rules(&Emit::default());