Skip to main content

max / makeover-immediate

Resolve a sunken surface, and let an unknown fill go unpainted Fill became non_exhaustive in makeover-layout 0.4.0, which a total function over it cannot survive honestly: staying total would mean inventing a colour for a member this crate has never heard of, and inventing colours is what 0.2.0 removed. So fill returns Option and the wildcard answers None. No external caller existed to break; Palette::fill had callers only in this crate's own tests. Fill::Sunken gets a real answer rather than the wildcard. A terminal cell has one background and makeover-tui declines it for that reason, but an immediate-mode renderer paints an arbitrary rect and has no such excuse. Palette gains a required sunken member, on the same footing as well: every one of the 31 themes makeover embeds authors surface-sunken. In frame, the two ways there is nothing to paint now collapse: the depth names no fill, or names one that cannot be resolved. Both leave the frame unfilled and let the bevel carry the depth, which is the rule this module already documented for Depth::Flat. 0.3.0: Palette gained a public field and fill changed return type.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-30 12:29 UTC
Signed with PGP, not checked
Commit: 476b2ec2e2168f25f80ae04bc54c7d9fb09e2fdb
Parent: b2c0f63
2 files changed, +56 insertions, -20 deletions
M Cargo.toml +2 -2
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-immediate"
3 - version = "0.2.0"
3 + version = "0.3.0"
4 4 edition = "2024"
5 5 description = "The immediate-mode renderer for makeover-layout. Immediate mode is the constraint that matters, not the library: no cascade, no retained tree, one stroke per widget. Backed by egui."
6 6 license = "MIT"
@@ -8,7 +8,7 @@
8 8
9 9 [dependencies]
10 10 egui = { version = "0.35", default-features = false }
11 - makeover-layout = "0.1.0"
11 + makeover-layout = "0.4.0"
12 12
13 13 [lints.rust]
14 14 unused = "warn"
M src/lib.rs +54 -18
@@ -61,6 +61,16 @@
61 61 /// the page; `makeover-tui` keeps its own `Option` for a different reason,
62 62 /// since a terminal can have the colour and still be unable to show it.
63 63 pub well: Color32,
64 + /// `surface-sunken`.
65 + ///
66 + /// A surface set back from the one it sits on, by colour and nothing else.
67 + /// Not a well: a well is a hole with an edge, and this has no edge. An
68 + /// immediate-mode renderer paints an arbitrary rect, so unlike
69 + /// `makeover-tui` it has no excuse for declining this one.
70 + ///
71 + /// Required rather than optional, on the same footing as `well`: all 31
72 + /// themes makeover embeds author it.
73 + pub sunken: Color32,
64 74 /// `bevel-light`.
65 75 pub bevel_light: Color32,
66 76 /// `bevel-dark`.
@@ -68,18 +78,27 @@
68 78 }
69 79
70 80 impl Palette {
71 - /// Resolve a surface intent.
81 + /// Resolve a surface intent, or `None` for one this renderer does not know.
72 82 ///
73 - /// A plain lookup. There is no substitution here any more: it existed only
74 - /// while `surface-well` was underived, and every consumer now reads the
75 - /// real token.
83 + /// A plain lookup. There is still no substitution: the old one existed only
84 + /// while `surface-well` was underived, and every consumer reads the real
85 + /// token now.
86 + ///
87 + /// `Option` since 0.3.0, because [`Fill`] became `#[non_exhaustive]` in
88 + /// `makeover-layout` 0.4.0 and a total function over an open enum can only
89 + /// stay total by inventing a colour for a member it has never heard of.
90 + /// That is the substitution this crate spent 0.2.0 removing, so the return
91 + /// type moved instead. Every member the description has today is answered
92 + /// with `Some`.
76 93 #[must_use]
77 - pub const fn fill(&self, fill: Fill) -> Color32 {
94 + pub const fn fill(&self, fill: Fill) -> Option<Color32> {
78 95 match fill {
79 - Fill::Page => self.page,
80 - Fill::Raised => self.raised,
81 - Fill::Overlay => self.overlay,
82 - Fill::Well => self.well,
96 + Fill::Page => Some(self.page),
97 + Fill::Raised => Some(self.raised),
98 + Fill::Overlay => Some(self.overlay),
99 + Fill::Well => Some(self.well),
100 + Fill::Sunken => Some(self.sunken),
101 + _ => None,
83 102 }
84 103 }
85 104
@@ -161,8 +180,13 @@
161 180 let mut f = egui::Frame::new()
162 181 .corner_radius(style.radius)
163 182 .inner_margin(style.margin);
164 - if let Some(fill) = depth.fill() {
165 - f = f.fill(palette.fill(fill));
183 + // Two ways there is no fill to paint, and they collapse to the same
184 + // outcome: the depth names none (Depth::Flat), or it names one this
185 + // renderer cannot resolve. Either way the frame goes unfilled and the
186 + // bevel below carries the depth on its own, which is the rule this
187 + // module already documents for Flat.
188 + if let Some(fill) = depth.fill().and_then(|f| palette.fill(f)) {
189 + f = f.fill(fill);
166 190 }
167 191 let framed = f.show(ui, add_contents);
168 192 if let Some(bevel) = depth.bevel() {
@@ -187,6 +211,7 @@
187 211 raised: Color32::from_rgb(2, 2, 2),
188 212 overlay: Color32::from_rgb(3, 3, 3),
189 213 well,
214 + sunken: Color32::from_rgb(4, 4, 4),
190 215 bevel_light: Color32::WHITE,
191 216 bevel_dark: Color32::BLACK,
192 217 }
@@ -198,24 +223,35 @@
198 223 // token that did not exist yet; it exists now.
199 224 let w = Color32::from_rgb(9, 9, 9);
200 225 let p = palette(w);
201 - assert_eq!(p.fill(Fill::Well), w);
202 - assert_ne!(p.fill(Fill::Well), p.page);
226 + assert_eq!(p.fill(Fill::Well), Some(w));
227 + assert_ne!(p.fill(Fill::Well), Some(p.page));
203 228 }
204 229
205 230 #[test]
206 231 fn every_intent_is_a_plain_lookup() {
207 232 let p = palette(Color32::from_rgb(9, 9, 9));
208 - assert_eq!(p.fill(Fill::Page), p.page);
209 - assert_eq!(p.fill(Fill::Raised), p.raised);
210 - assert_eq!(p.fill(Fill::Overlay), p.overlay);
233 + assert_eq!(p.fill(Fill::Page), Some(p.page));
234 + assert_eq!(p.fill(Fill::Raised), Some(p.raised));
235 + assert_eq!(p.fill(Fill::Overlay), Some(p.overlay));
236 + }
237 +
238 + /// Sunken is its own colour, not the well's and not the page's. The two
239 + /// are authored in opposite directions and an earlier cut of the
240 + /// description conflated them.
241 + #[test]
242 + fn sunken_is_neither_the_well_nor_the_page() {
243 + let p = palette(Color32::from_rgb(9, 9, 9));
244 + assert_eq!(p.fill(Fill::Sunken), Some(p.sunken));
245 + assert_ne!(p.fill(Fill::Sunken), p.fill(Fill::Well));
246 + assert_ne!(p.fill(Fill::Sunken), p.fill(Fill::Page));
211 247 }
212 248
213 249 #[test]
214 250 fn a_raised_region_never_resolves_to_the_well_fill() {
215 251 // The cross-app bug, asserted at the renderer boundary this time.
216 252 let p = palette(Color32::from_rgb(9, 9, 9));
217 - let raised = Depth::Raised.fill().map(|f| p.fill(f));
218 - let well = Depth::Well.fill().map(|f| p.fill(f));
253 + let raised = Depth::Raised.fill().and_then(|f| p.fill(f));
254 + let well = Depth::Well.fill().and_then(|f| p.fill(f));
219 255 assert_eq!(raised, Some(p.raised));
220 256 assert_ne!(raised, well);
221 257 }