Skip to main content

max / makeover-immediate

0.27.0: a heading that offers to reorder says so, and stops claiming it is dead Three states, three tones, per wiki three-tone-convention, pulled out of press into heading_color so the choice is one readable match and a test can assert it. A sortable-but-unsorted heading took content_muted, which is what State::Disabled resolves to, so a control the user could press told them it would not answer. It takes content_secondary now. The unsortable arm keeps muted, where it is true. The heading also draws the idle caret, in the ascending spelling, so pressing one no longer widens its column. Both caret fields default to Sort::glyph (makeover-layout 0.27.5) instead of this crate's own literals, and the leading space moved into heading(), written once for all three states. They stay knobs.
Author: Max Johnson <me@maxj.phd> · 2026-08-16 21:54 UTC
Signed with PGP, not checked
Commit: f83068dfe207d2d126a149d423ae5a460ba6c2d7
Parent: ecee946
2 files changed, +72 insertions, -27 deletions
M Cargo.toml +2 -2
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-immediate"
3 - version = "0.26.0"
3 + version = "0.27.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"
@@ -12,7 +12,7 @@
12 12 # Exact patch rather than the minor, as the rest of the suite pins: a
13 13 # minor-only requirement is satisfied by a consumer lock holding an earlier
14 14 # patch, which then fails to compile against an API added in a later one.
15 - makeover-layout = "0.27.0"
15 + makeover-layout = "0.27.5"
16 16
17 17 [lints.rust]
18 18 unused = "warn"
M src/table.rs +70 -25
@@ -98,7 +98,13 @@
98 98 pub header_height: f32,
99 99 /// The height of a body row.
100 100 pub row_height: f32,
101 - /// Drawn after the heading of an ascending column.
101 + /// The caret drawn after the heading of an ascending column.
102 + ///
103 + /// Defaults to [`Sort::glyph`], which is where the spelling lives now:
104 + /// three renderers holding the same literal agreed by coincidence. Bare,
105 + /// with no leading space -- the gap is [`heading`]'s, written once for all
106 + /// three states rather than baked into two strings and forgotten in the
107 + /// third.
102 108 pub ascending: &'static str,
103 109 /// Drawn after the heading of a descending column.
104 110 pub descending: &'static str,
@@ -133,10 +139,8 @@
133 139 Self {
134 140 header_height: 20.0,
135 141 row_height: 18.0,
136 - // The pair audiofiles already draws, so a sorted column points the
137 - // same way here as it does in a terminal.
138 - ascending: " \u{25B2}",
139 - descending: " \u{25BC}",
142 + ascending: Sort::Ascending.glyph(),
143 + descending: Sort::Descending.glyph(),
140 144 striped: false,
141 145 resizable: false,
142 146 }
@@ -244,11 +248,17 @@
244 248 /// cannot change is a real thing, and the caret is how it says so.
245 249 #[must_use]
246 250 pub fn heading(column: &Column<'_>, style: &TableStyle) -> String {
247 - match column.sorted {
248 - Some(Sort::Ascending) => format!("{}{}", column.name, style.ascending),
249 - Some(Sort::Descending) => format!("{}{}", column.name, style.descending),
250 - None => column.name.to_owned(),
251 - }
251 + let caret = match column.sorted {
252 + Some(Sort::Ascending) => style.ascending,
253 + Some(Sort::Descending) => style.descending,
254 + // Sortable and not sorted draws the idle mark, in the ascending
255 + // spelling because that is the direction a first press takes. What
256 + // separates it from the column in force is the tone, which is
257 + // [`press`]'s to pick.
258 + None if column.sortable => style.ascending,
259 + None => return column.name.to_owned(),
260 + };
261 + format!("{} {caret}", column.name)
252 262 }
253 263
254 264 /// How wide a column asks to be at its narrowest, in points.
@@ -391,23 +401,36 @@
391 401 pressed.get()
392 402 }
393 403
404 + /// What one heading is drawn in.
405 + ///
406 + /// Three states, three tones (wiki `three-tone-convention`). The column in force
407 + /// is the emphasised thing; a column offering to reorder is inactive but usable,
408 + /// because it answers a press; a column that is not a control at all is inert.
409 + ///
410 + /// The middle one used to take `content_muted`, which is what
411 + /// [`State::Disabled`](makeover_layout::State::Disabled) resolves to, so a
412 + /// heading the user could press claimed it would not answer. The same lie the
413 + /// unchosen option in a choice field was telling before 0.26.0.
414 + fn heading_color(column: &Column<'_>, palette: &Palette) -> egui::Color32 {
415 + match (column.sorted, column.sortable) {
416 + (Some(_), _) => palette.content,
417 + (None, true) => palette.content_secondary,
418 + (None, false) => palette.content_muted,
419 + }
420 + }
421 +
394 422 /// One heading, and whether it was pressed.
395 423 fn press(ui: &mut Ui, column: &Column<'_>, palette: &Palette, style: &TableStyle) -> bool {
396 - let text = RichText::new(heading(column, style)).strong();
424 + let text = RichText::new(heading(column, style)).color(heading_color(column, palette));
397 425 if !column.sortable {
398 - // Muted, and not sensed. A heading a user cannot press must not look
399 - // like one they can, which is the affordance `Column::sortable` exists
400 - // to carry.
401 - ui.label(text.color(palette.content_muted));
426 + // Not sensed. A heading a user cannot press must not look like one they
427 + // can, which is the affordance `Column::sortable` exists to carry, and
428 + // the tone above is half of saying so.
429 + ui.label(text.strong());
402 430 return false;
403 431 }
404 - let tone = if column.sorted.is_some() {
405 - palette.content
406 - } else {
407 - palette.content_muted
408 - };
409 432 let response: Response = ui
410 - .add(egui::Label::new(text.color(tone)).sense(Sense::click()))
433 + .add(egui::Label::new(text.strong()).sense(Sense::click()))
411 434 .on_hover_cursor(egui::CursorIcon::PointingHand);
412 435 response.clicked()
413 436 }
@@ -566,14 +589,31 @@
566 589 }
567 590
568 591 #[test]
569 - fn the_ordered_column_draws_a_caret_and_the_others_do_not() {
592 + fn a_heading_carries_a_caret_when_it_is_ordered_by_or_offers_to_be() {
570 593 let style = TableStyle::default();
571 594 let cols = columns();
572 595 assert_eq!(heading(&cols[0], &style), "name \u{25B2}");
573 - assert_eq!(heading(&cols[1], &style), "size");
596 + // Sortable and idle. It draws the mark a first press would give, which
597 + // is what stops the press from widening the column and shifting the
598 + // ones after it.
599 + assert_eq!(heading(&cols[1], &style), "size \u{25B2}");
600 + // Not a control. Nothing to mark.
574 601 assert_eq!(heading(&cols[2], &style), "note");
575 602 }
576 603
604 + #[test]
605 + fn the_three_states_of_a_heading_are_three_tones() {
606 + // wiki `three-tone-convention`. The middle state used to take
607 + // content_muted, which is what `State::Disabled` resolves to, so a
608 + // heading the user could press claimed it would not answer. The arm
609 + // that keeps muted is the one where it is true.
610 + let p = palette();
611 + let cols = columns();
612 + assert_eq!(heading_color(&cols[0], &p), p.content);
613 + assert_eq!(heading_color(&cols[1], &p), p.content_secondary);
614 + assert_eq!(heading_color(&cols[2], &p), p.content_muted);
615 + }
616 +
577 617 #[test]
578 618 fn a_column_sorted_without_being_sortable_still_draws_its_caret() {
579 619 // A list ordered by a key the user cannot change is a real thing to
@@ -593,9 +633,14 @@
593 633 // Two crates, one glyph pair, and no dependency between them to enforce
594 634 // it. A description sorted ascending must not point up in a window and
595 635 // down in a terminal.
636 + // Composition rather than agreement since makeover-layout 0.27.5: both
637 + // read `Sort::glyph`, so a fourth spelling cannot appear in one crate.
596 638 let style = TableStyle::default();
597 - assert_eq!(style.ascending, " \u{25B2}");
598 - assert_eq!(style.descending, " \u{25BC}");
639 + assert_eq!(style.ascending, Sort::Ascending.glyph());
640 + assert_eq!(style.descending, Sort::Descending.glyph());
641 + // Bare. The gap is `heading`'s, so a consumer swapping the glyph for an
642 + // ASCII one does not have to remember to bring a space with it.
643 + assert_eq!(style.ascending.trim(), style.ascending);
599 644 }
600 645
601 646 #[test]