Skip to main content

max / makeover-immediate

0.32.0: a number draws its unit makeover-layout 0.33.0's `Field::unit`, and this is the host the member was argued from. egui's Slider already draws a suffix beside its readout, which is where these controls put the unit before they were described and is somewhere a label cannot reach, so a slider takes it as a suffix inside the control. A typed number has no readout of its own and takes it as a muted label after the box. Every other kind ignores it, and which those are is the description's answer through `FieldKind::measurable` rather than a `matches!` kept here.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-22 00:27 UTC
Signed with PGP, not checked
Commit: 85721987db06e75fccdea06595725948e423ffbb
Parent: 21f701e
2 files changed, +71 insertions, -3 deletions
M Cargo.toml +2 -2
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-immediate"
3 - version = "0.31.0"
3 + version = "0.32.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.32.0"
15 + makeover-layout = "0.33.0"
16 16
17 17 [lints.rust]
18 18 unused = "warn"
M src/lib.rs +69 -1
@@ -154,6 +154,18 @@
154 154 //! looks for it, and because the silent version reads as "egui does not need
155 155 //! a palette" rather than "nobody has built the renderer yet".
156 156
157 + //! # 0.32.0: a number draws its unit
158 + //!
159 + //! `makeover-layout` 0.33.0's [`Field::unit`], and this host is the one the
160 + //! member was argued from: egui's `Slider` already draws a suffix beside its
161 + //! readout, which is where these controls put the unit before they were
162 + //! described and is somewhere a label cannot reach.
163 + //!
164 + //! So a slider takes it as a suffix, inside the control. A typed number has no
165 + //! readout of its own and takes it as a muted label after the box. Every other
166 + //! kind ignores it, and the description says which those are --
167 + //! `FieldKind::measurable`, rather than a `matches!` kept here.
168 + //!
157 169 //! # 0.31.0: the slider's track is a curve
158 170 //!
159 171 //! `makeover-layout` 0.32.0 says what a slider is: a fraction and a function
@@ -674,6 +686,17 @@
674 686 }
675 687 }
676 688
689 + /// The unit to draw beside this field's value, if there is one to draw.
690 + ///
691 + /// Two conditions rather than one: the field has to carry a unit and its kind
692 + /// has to be one that means anything by it. `FieldKind::measurable` is the
693 + /// description answering the second, so this renderer does not keep its own
694 + /// list of which kinds are quantities -- which is the drift that predicate
695 + /// exists to stop.
696 + fn unit_of<'a>(field: &Field<'a>) -> Option<&'a str> {
697 + field.unit.filter(|_| field.kind.measurable())
698 + }
699 +
677 700 /// The two ends of a range, as egui wants them.
678 701 ///
679 702 /// `None` when either end is missing or is not a number this host can read.
@@ -816,6 +839,13 @@
816 839 if field.curve.is_ratio(*extent.start(), *extent.end()) {
817 840 slider = slider.logarithmic(true);
818 841 }
842 + // The unit goes inside the control, beside the readout egui already
843 + // draws. That placement is the argument `Field::unit` was decided
844 + // on: it is where these controls put it before they were described,
845 + // and it is the one a label could never reach.
846 + if let Some(unit) = unit_of(field) {
847 + slider = slider.suffix(format!(" {unit}"));
848 + }
819 849 let response = ui.add(slider);
820 850 if response.changed() {
821 851 *value = match decimals(step) {
@@ -851,7 +881,17 @@
851 881 if let Some(ghost) = field.placeholder {
852 882 edit = edit.hint_text(RichText::new(ghost).color(palette.content_muted));
853 883 }
854 - frame(ui, Depth::Well, palette, style.frame, |ui| ui.add(edit))
884 + let response = frame(ui, Depth::Well, palette, style.frame, |ui| ui.add(edit));
885 + // A typed number has no readout of its own to sit beside, so the
886 + // unit follows the box. Muted, because it is a fact about the value
887 + // rather than a second thing to read.
888 + match unit_of(field) {
889 + Some(unit) => {
890 + ui.label(RichText::new(unit).color(palette.content_muted));
891 + response
892 + }
893 + None => response,
894 + }
855 895 }
856 896 Control::Toggled => {
857 897 let on = match filling {
@@ -1075,6 +1115,34 @@
1075 1115 /// The cast is egui's own shadow type carrying the theme's tone, which is
1076 1116 /// the whole of what this crate had to decide for it: unlike a bevel, egui
1077 1117 /// already knows how to paint one.
1118 + #[test]
1119 + fn a_unit_is_drawn_only_where_the_kind_is_a_quantity() {
1120 + // egui-drawing has no harness here, so what is tested is the decision
1121 + // that precedes it: which fields have a unit to draw at all. The kind
1122 + // half comes from the description rather than from a `matches!` in this
1123 + // crate, which is the drift `FieldKind::measurable` exists to stop.
1124 + let ranged = makeover_layout::Field {
1125 + unit: Some("s"),
1126 + ..makeover_layout::Field::range("attack", "Attack", "0", "5")
1127 + };
1128 + assert_eq!(unit_of(&ranged), Some("s"));
1129 +
1130 + let typed = makeover_layout::Field {
1131 + unit: Some("ms"),
1132 + ..makeover_layout::Field::new(makeover_layout::FieldKind::Number, "fade", "Fade")
1133 + };
1134 + assert_eq!(unit_of(&typed), Some("ms"));
1135 +
1136 + let worded = makeover_layout::Field {
1137 + unit: Some("s"),
1138 + ..makeover_layout::Field::new(makeover_layout::FieldKind::Text, "name", "Name")
1139 + };
1140 + assert_eq!(unit_of(&worded), None);
1141 +
1142 + let bare = makeover_layout::Field::range("attack", "Attack", "0", "5");
1143 + assert_eq!(unit_of(&bare), None);
1144 + }
1145 +
1078 1146 #[test]
1079 1147 fn the_cast_hands_egui_the_themes_tone() {
1080 1148 let p = palette(Color32::from_rgb(9, 9, 9));