1use std::borrow::Cow;
19
20#[derive(Clone, Debug, PartialEq, Eq)]
29pub enum LayoutDecision {
30 #[non_exhaustive]
35 PriorityDrop {
36 id: Cow<'static, str>,
37 priority: u8,
40 terminal_width: u16,
43 overflow: u32,
47 dropped_width: u16,
51 },
52 #[non_exhaustive]
57 ShrinkApplied {
58 id: Cow<'static, str>,
59 from: u16,
60 to: u16,
61 target: u16,
62 },
63 #[non_exhaustive]
66 ReflowApplied {
67 id: Cow<'static, str>,
68 from: u16,
69 to: u16,
70 target: u16,
71 },
72 #[non_exhaustive]
76 WidthBoundUnderMinDrop {
77 id: Cow<'static, str>,
78 rendered_width: u16,
79 min: u16,
80 },
81 #[non_exhaustive]
87 WidthBoundOverMaxTruncate {
88 id: Cow<'static, str>,
89 rendered_width: u16,
90 max: u16,
91 },
92}
93
94impl LayoutDecision {
95 #[must_use]
105 pub fn remediation(&self) -> Option<&'static str> {
106 match self {
107 Self::ShrinkApplied { .. } => Some("Set `width.max` to clamp earlier"),
108 Self::WidthBoundOverMaxTruncate { .. } => {
109 Some("Increase `width.max` or lower `priority`")
110 }
111 Self::PriorityDrop { .. } => None,
112 Self::ReflowApplied { .. } => None,
113 Self::WidthBoundUnderMinDrop { .. } => None,
114 }
115 }
116
117 #[must_use]
124 pub(crate) fn priority_drop(
125 id: Cow<'static, str>,
126 priority: u8,
127 terminal_width: u16,
128 overflow: u32,
129 dropped_width: u16,
130 ) -> Self {
131 debug_assert!(
132 priority > 0 && overflow >= 1,
133 "PriorityDrop invariants: priority>0, overflow>=1 (got priority={priority}, overflow={overflow})"
134 );
135 Self::PriorityDrop {
136 id,
137 priority,
138 terminal_width,
139 overflow,
140 dropped_width,
141 }
142 }
143
144 #[must_use]
147 pub(crate) fn shrink_applied(id: Cow<'static, str>, from: u16, to: u16, target: u16) -> Self {
148 debug_assert!(
149 to <= target && target < from,
150 "ShrinkApplied requires to <= target < from (got from={from}, to={to}, target={target})"
151 );
152 Self::ShrinkApplied {
153 id,
154 from,
155 to,
156 target,
157 }
158 }
159
160 #[must_use]
162 pub(crate) fn reflow_applied(id: Cow<'static, str>, from: u16, to: u16, target: u16) -> Self {
163 debug_assert!(
164 to <= target && target < from,
165 "ReflowApplied requires to <= target < from (got from={from}, to={to}, target={target})"
166 );
167 Self::ReflowApplied {
168 id,
169 from,
170 to,
171 target,
172 }
173 }
174
175 #[must_use]
176 pub(crate) fn width_bound_under_min_drop(
177 id: Cow<'static, str>,
178 rendered_width: u16,
179 min: u16,
180 ) -> Self {
181 debug_assert!(
182 rendered_width < min,
183 "WidthBoundUnderMinDrop requires rendered_width < min (got rendered_width={rendered_width}, min={min})"
184 );
185 Self::WidthBoundUnderMinDrop {
186 id,
187 rendered_width,
188 min,
189 }
190 }
191
192 #[must_use]
193 pub(crate) fn width_bound_over_max_truncate(
194 id: Cow<'static, str>,
195 rendered_width: u16,
196 max: u16,
197 ) -> Self {
198 debug_assert!(
199 rendered_width > max,
200 "WidthBoundOverMaxTruncate requires rendered_width > max (got rendered_width={rendered_width}, max={max})"
201 );
202 Self::WidthBoundOverMaxTruncate {
203 id,
204 rendered_width,
205 max,
206 }
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn remediation_pins_per_variant_phrasing() {
216 let shrink = LayoutDecision::shrink_applied(Cow::Borrowed("git"), 20, 17, 17);
220 let over_max = LayoutDecision::width_bound_over_max_truncate(Cow::Borrowed("git"), 50, 30);
221 let drop = LayoutDecision::priority_drop(Cow::Borrowed("git"), 200, 80, 12, 6);
222 let reflow = LayoutDecision::reflow_applied(Cow::Borrowed("git"), 20, 17, 17);
223 let under_min = LayoutDecision::width_bound_under_min_drop(Cow::Borrowed("git"), 3, 5);
224
225 assert_eq!(
226 shrink.remediation(),
227 Some("Set `width.max` to clamp earlier")
228 );
229 assert_eq!(
230 over_max.remediation(),
231 Some("Increase `width.max` or lower `priority`")
232 );
233 assert!(drop.remediation().is_none());
234 assert!(reflow.remediation().is_none());
235 assert!(under_min.remediation().is_none());
236 }
237
238 #[test]
239 fn constructors_accept_invariant_boundaries() {
240 let _ = LayoutDecision::shrink_applied(Cow::Borrowed("a"), 20, 17, 17);
246 let _ = LayoutDecision::reflow_applied(Cow::Borrowed("a"), 20, 17, 17);
247 let _ = LayoutDecision::shrink_applied(Cow::Borrowed("a"), 20, 16, 17);
249 let _ = LayoutDecision::reflow_applied(Cow::Borrowed("a"), 20, 16, 17);
250
251 let _ = LayoutDecision::priority_drop(Cow::Borrowed("a"), 1, 80, 1, 1);
255
256 let _ = LayoutDecision::width_bound_under_min_drop(Cow::Borrowed("a"), 4, 5);
259 let _ = LayoutDecision::width_bound_over_max_truncate(Cow::Borrowed("a"), 6, 5);
260 }
261
262 const _ASSERT_EQ_DERIVED: fn() = || {
267 fn assert_eq<T: Eq>() {}
268 assert_eq::<LayoutDecision>();
269 };
270
271 #[test]
272 fn derives_clone_debug_partial_eq() {
273 let d = LayoutDecision::shrink_applied(Cow::Borrowed("git"), 20, 17, 17);
274 let cloned = d.clone();
275 assert_eq!(d, cloned);
276 let dbg = format!("{d:?}");
277 assert!(dbg.contains("ShrinkApplied"), "got {dbg:?}");
278 assert!(dbg.contains("id: \"git\""), "got {dbg:?}");
282 }
283
284 #[test]
289 #[cfg(debug_assertions)]
290 #[should_panic(expected = "PriorityDrop invariants: priority>0, overflow>=1")]
291 fn priority_drop_panics_in_debug_when_priority_is_zero() {
292 let _ = LayoutDecision::priority_drop(Cow::Borrowed("git"), 0, 80, 12, 6);
293 }
294
295 #[test]
296 #[cfg(debug_assertions)]
297 #[should_panic(expected = "PriorityDrop invariants: priority>0, overflow>=1")]
298 fn priority_drop_panics_when_overflow_is_zero() {
299 let _ = LayoutDecision::priority_drop(Cow::Borrowed("git"), 200, 80, 0, 6);
300 }
301
302 #[test]
303 fn priority_drop_accepts_zero_dropped_width() {
304 let _ = LayoutDecision::priority_drop(Cow::Borrowed("empty"), 200, 80, 5, 0);
308 }
309
310 #[test]
311 #[cfg(debug_assertions)]
312 #[should_panic(expected = "ShrinkApplied requires to <= target < from")]
313 fn shrink_applied_panics_when_target_not_below_from() {
314 let _ = LayoutDecision::shrink_applied(Cow::Borrowed("git"), 20, 17, 20);
316 }
317
318 #[test]
319 #[cfg(debug_assertions)]
320 #[should_panic(expected = "ShrinkApplied requires to <= target < from")]
321 fn shrink_applied_panics_when_to_above_target() {
322 let _ = LayoutDecision::shrink_applied(Cow::Borrowed("git"), 20, 18, 17);
323 }
324
325 #[test]
326 #[cfg(debug_assertions)]
327 #[should_panic(expected = "ReflowApplied requires to <= target < from")]
328 fn reflow_applied_panics_when_target_not_below_from() {
329 let _ = LayoutDecision::reflow_applied(Cow::Borrowed("git"), 20, 17, 20);
330 }
331
332 #[test]
333 #[cfg(debug_assertions)]
334 #[should_panic(expected = "ReflowApplied requires to <= target < from")]
335 fn reflow_applied_panics_when_to_above_target() {
336 let _ = LayoutDecision::reflow_applied(Cow::Borrowed("git"), 20, 18, 17);
337 }
338
339 #[test]
340 #[cfg(debug_assertions)]
341 #[should_panic(expected = "ShrinkApplied requires to <= target < from")]
342 fn shrink_applied_panics_when_target_above_from() {
343 let _ = LayoutDecision::shrink_applied(Cow::Borrowed("git"), 20, 17, 21);
347 }
348
349 #[test]
350 #[cfg(debug_assertions)]
351 #[should_panic(expected = "ReflowApplied requires to <= target < from")]
352 fn reflow_applied_panics_when_target_above_from() {
353 let _ = LayoutDecision::reflow_applied(Cow::Borrowed("git"), 20, 17, 21);
354 }
355
356 #[test]
357 #[cfg(debug_assertions)]
358 #[should_panic(expected = "WidthBoundUnderMinDrop requires rendered_width < min")]
359 fn under_min_drop_panics_when_rendered_at_or_above_min() {
360 let _ = LayoutDecision::width_bound_under_min_drop(Cow::Borrowed("git"), 5, 5);
361 }
362
363 #[test]
364 #[cfg(debug_assertions)]
365 #[should_panic(expected = "WidthBoundOverMaxTruncate requires rendered_width > max")]
366 fn over_max_truncate_panics_when_rendered_at_or_below_max() {
367 let _ = LayoutDecision::width_bound_over_max_truncate(Cow::Borrowed("git"), 30, 30);
368 }
369}