Skip to main content

max / makeover-layout

0.17.0: the layer names a control, not only the slot it sits in RowPart::Actions has named a slot since 0.2.0 and nothing named what goes in it, so every consumer that wanted a button wrote its own. quasi-tui had drawn one for months. Act is that shape, beside Meter and Figure for the reason those are here: a renderer handed the parts decides how to say them, and a renderer handed a finished string has had the decision made for it. Four fields, and three of them are refusals held to. No address. Where a control goes is the app's business and every host follows it differently, an hx-get against a protocol URL against a function call, so the description says what the control is and the caller keeps what it does. Same split Choice makes. No confirmation flag, and that one is a finding rather than an omission: a question asked after a control is pressed belongs to whatever holds the interaction, and a renderer drawing it would be asking before there was anything to answer. key is the one member written for a terminal before there was one. A webview hangs it off accesskey or ignores it; a terminal has nothing else to offer, so it is the whole of how a control is reached there. state carries the distinction every hand-rolled button in the tree had to remember: focus does not stop a control answering and disabled does, which is what suppresses_interaction already said and nothing could reach from a description. Tree-wide by construction, per the links guard added at 0.16.0.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-12 19:15 UTC
Signed with PGP, not checked
Commit: eb63ba384be8d3f3fb36ea1b3ab8970843e0a581
Parent: e1a489b
2 files changed, +100 insertions, -1 deletion
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-layout"
3 - version = "0.16.0"
3 + version = "0.17.0"
4 4 edition = "2024"
5 5 # One copy of this vocabulary per dependency graph, enforced by cargo rather
6 6 # than by remembering. Two versions of a description layer in one build means
M src/lib.rs +99
@@ -1156,6 +1156,85 @@
1156 1156 }
1157 1157 }
1158 1158
1159 + /// Something the user can do, and what it costs to say so.
1160 + ///
1161 + /// Added 0.17.0, out of `quasi-tui`: the terminal renderer had drawn one of
1162 + /// these for months and every other consumer that wanted a button had written
1163 + /// its own, because this layer named [`RowPart::Actions`] as a *slot* and never
1164 + /// named the thing that goes in it. Beside [`Meter`] and [`Figure`] for the
1165 + /// reason those are here: a renderer that is handed the parts has to decide how
1166 + /// to say them, and a renderer that is handed a finished string has already had
1167 + /// the decision made for it.
1168 + ///
1169 + /// No address. Where a control goes is the app's business and every host
1170 + /// follows it differently — an `hx-get`, a protocol URL, a function call — so
1171 + /// the description says what the control *is* and the caller keeps what it
1172 + /// does. That is the same split [`Choice`] makes.
1173 + ///
1174 + /// No confirmation flag either, and that one is a finding rather than an
1175 + /// omission: a question asked *after* a control is pressed belongs to whatever
1176 + /// is holding the interaction, and a renderer that drew it would be asking
1177 + /// before there was anything to answer.
1178 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1179 + pub struct Act<'a> {
1180 + /// What the control says.
1181 + pub label: &'a str,
1182 + /// The key that reaches it where a host has keys.
1183 + ///
1184 + /// The one member written for a terminal before there was one. A webview
1185 + /// hangs it off `accesskey` or ignores it; a terminal has nothing else to
1186 + /// offer, so this is the whole of how a control is reached there.
1187 + pub key: Option<&'a str>,
1188 + /// What pressing it means. [`Tone::Danger`] is the destructive one.
1189 + pub tone: Tone,
1190 + /// Focus, disabled, or neither.
1191 + ///
1192 + /// [`State::Disabled`] is the one that changes what a renderer may do:
1193 + /// see [`State::suppresses_interaction`], which is what says a disabled
1194 + /// control is drawn and not reachable.
1195 + pub state: Option<State>,
1196 + }
1197 +
1198 + impl<'a> Act<'a> {
1199 + /// An ordinary control, reachable, with no key.
1200 + #[must_use]
1201 + pub const fn new(label: &'a str) -> Self {
1202 + Self {
1203 + label,
1204 + key: None,
1205 + tone: Tone::Neutral,
1206 + state: None,
1207 + }
1208 + }
1209 +
1210 + /// The key that reaches it.
1211 + #[must_use]
1212 + pub const fn key(mut self, key: &'a str) -> Self {
1213 + self.key = Some(key);
1214 + self
1215 + }
1216 +
1217 + /// What pressing it means.
1218 + #[must_use]
1219 + pub const fn tone(mut self, tone: Tone) -> Self {
1220 + self.tone = tone;
1221 + self
1222 + }
1223 +
1224 + /// Focus, or disabled.
1225 + #[must_use]
1226 + pub const fn state(mut self, state: State) -> Self {
1227 + self.state = Some(state);
1228 + self
1229 + }
1230 +
1231 + /// Whether the control is drawn and does not answer.
1232 + #[must_use]
1233 + pub fn disabled(&self) -> bool {
1234 + self.state.is_some_and(State::suppresses_interaction)
1235 + }
1236 + }
1237 +
1159 1238 /// A named part of a screen.
1160 1239 ///
1161 1240 /// The thing `makeover-geometry` deliberately does not name: it names the space
@@ -2037,6 +2116,26 @@
2037 2116 assert_eq!(Meter::new(9, 10).tone, Tone::Neutral);
2038 2117 }
2039 2118
2119 + #[test]
2120 + fn an_act_is_reachable_until_it_is_disabled() {
2121 + // The one member a renderer must branch on. Focus is a state and does
2122 + // not stop the control answering, which is the distinction `State`
2123 + // makes and every hand-rolled button in the tree had to remember.
2124 + assert!(!Act::new("Save").disabled());
2125 + assert!(!Act::new("Save").state(State::Focus).disabled());
2126 + assert!(Act::new("Save").state(State::Disabled).disabled());
2127 + }
2128 +
2129 + #[test]
2130 + fn an_act_carries_its_key_because_a_terminal_has_nothing_else() {
2131 + // No key is the ordinary case, and the webview hosts that ignore it
2132 + // are why it stayed optional.
2133 + assert_eq!(Act::new("Delete").key, None);
2134 + let quit = Act::new("Quit").key("q").tone(Tone::Danger);
2135 + assert_eq!(quit.key, Some("q"));
2136 + assert_eq!(quit.tone, Tone::Danger);
2137 + }
2138 +
2040 2139 #[test]
2041 2140 fn a_meter_does_not_overflow_on_large_counts() {
2042 2141 // done * 100 in u32 would wrap somewhere past 42 million. Counts that