Skip to main content

max / makeover-layout

0.33.0: a number carries what it is measured in `Field::unit` and `FieldKind::measurable`. Decided by Max 2026-08-21 (`32215e21`) against eight sites across four audiofiles files that had each independently put the unit in parentheses at the end of the label -- three of them written while the gap was a known open question. A unit is a fact about the value rather than part of the question's name, and the two readings come apart the moment anything reads a field back instead of drawing it: a bound of -96 and a bound of -96 dBFS are the same number and not the same answer. The convention this replaces also froze the worst placement, because a label is the sentence above the control and that is the one place it could go -- including on the host that had somewhere better, since egui's slider already draws a suffix beside its readout. A string rather than a closed family, which is Curve's argument inverted and correctly so: a curve is a mapping this crate computes, a unit is a symbol it only carries, and this crate does not know what the next consumer measures in. `measurable` is `takes_files`' footing: the renderers ask before they decide where a unit goes, and a `matches!` per renderer is where the next measurable kind would go missing. Additive. Absent is what every field meant before.
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: a177ee66254129236f3362e89d1b769c252c9106
Parent: 863c03b
2 files changed, +111 insertions, -1 deletion
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-layout"
3 - version = "0.32.0"
3 + version = "0.33.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 +110
@@ -382,6 +382,27 @@
382 382 //! that one page: audiofiles' library browser and goingson's filters are the
383 383 //! same shape.
384 384 //!
385 + //! 0.33.0 gives a number its unit. [`Field::unit`] carries what the value is
386 + //! measured in, and [`FieldKind::measurable`] says which kinds read it. Decided
387 + //! by Max 2026-08-21 (`32215e21`) against eight sites across four audiofiles
388 + //! files that had each independently put the unit in parentheses at the end of
389 + //! the label -- three of them written while the gap was a known open question.
390 + //!
391 + //! - **A unit is a fact about the value, not part of the question's name.** The
392 + //! two readings come apart the moment anything reads a field back rather than
393 + //! drawing it, which is the argument that decided it.
394 + //! - **The convention it replaces froze the worst placement.** A label is the
395 + //! sentence above the control, so unit-in-label was the same answer on every
396 + //! host -- including the host that had somewhere better, since egui's slider
397 + //! already draws a suffix beside the readout, which is what these controls did
398 + //! before they were described.
399 + //! - A string rather than a closed family, which is [`Curve`]'s argument
400 + //! inverted and correctly so: a curve is a mapping this crate computes, and a
401 + //! unit is a symbol it only carries. The measured set is `GiB`, `dBFS`, `s`
402 + //! and `ms`, and this crate does not know what the next consumer measures in.
403 + //!
404 + //! Additive: absent is what every field meant before.
405 + //!
385 406 //! 0.32.0 is the slider's real shape. **The data of a slider is a fraction and
386 407 //! a function taking numbers to numbers** (Max, 2026-08-21), so [`Curve`]
387 408 //! arrives and [`Field::curve`] with it. [`Field::min`] and [`Field::max`] were
@@ -3509,6 +3530,23 @@
3509 3530 pub const fn takes_files(self) -> bool {
3510 3531 matches!(self, Self::File)
3511 3532 }
3533 +
3534 + /// Whether the value is a quantity, so [`Field::unit`] means something.
3535 + ///
3536 + /// The two numeric kinds and nothing else. A date is a quantity in the sense
3537 + /// that it is ordered, and it is not one in the sense that matters here:
3538 + /// its unit is fixed by the kind, so `Date` carrying `days` would be the
3539 + /// description restating what [`kind`](Field::kind) already said.
3540 + ///
3541 + /// [`takes_files`](Self::takes_files)'s footing, and for its reason: the
3542 + /// renderers ask this before they decide where a unit goes, and a
3543 + /// `matches!` per renderer is where the next measurable kind goes missing.
3544 + ///
3545 + /// Added 0.33.0 with [`Field::unit`].
3546 + #[must_use]
3547 + pub const fn measurable(self) -> bool {
3548 + matches!(self, Self::Number | Self::Range)
3549 + }
3512 3550 }
3513 3551
3514 3552 /// A family of media a file can belong to.
@@ -4033,6 +4071,46 @@
4033 4071 ///
4034 4072 /// Added 0.32.0.
4035 4073 pub curve: Curve<'a>,
4074 + /// What the number is measured in: `s`, `ms`, `dB`, `GiB`.
4075 + ///
4076 + /// A fact about the value, not part of the question's name, and that
4077 + /// distinction is the whole reason it is a member. The two readings come
4078 + /// apart the moment anything reads a field back rather than drawing it: a
4079 + /// [`max`](Self::max) of `-96` and a bound of `-96 dBFS` are the same number
4080 + /// and not the same answer, and under the convention this replaces the unit
4081 + /// could only be recovered by parsing it back out of a label.
4082 + ///
4083 + /// # Where a renderer draws it
4084 + ///
4085 + /// Beside the value, wherever that host puts a value. Not in the label: the
4086 + /// label is the sentence above the control and that is the one place the
4087 + /// convention could put it, which is why it read the same on every host and
4088 + /// was wrong on the one host that had somewhere better. egui puts it inside
4089 + /// the slider where the readout already is, a terminal appends it to the
4090 + /// value in the edit line, a webview sets it adjacent to the input.
4091 + ///
4092 + /// # Which kinds read it
4093 + ///
4094 + /// [`FieldKind::measurable`] answers, and it is
4095 + /// [`takes_files`](FieldKind::takes_files)'s footing: three renderers ask
4096 + /// before they can decide whether to draw this, and a `matches!` per
4097 + /// renderer is where the next measurable kind goes missing. A unit on a kind
4098 + /// that rejects it is sayable and ignored, the same way
4099 + /// [`options`](Self::options) is on a kind that offers none.
4100 + ///
4101 + /// # Why a string
4102 + ///
4103 + /// The measured sites are `GiB`, `dBFS`, `s` and `ms`. An enum would have to
4104 + /// grow a member for every unit any consumer ever wants, and this crate does
4105 + /// not know them; it knows that a number has one.
4106 + ///
4107 + /// Written as the symbol alone, with no brackets and no leading space. The
4108 + /// spacing is the renderer's, because a slider's readout and a sentence want
4109 + /// different answers.
4110 + ///
4111 + /// Added 0.33.0, `32215e21`, on eight sites across four files that had each
4112 + /// arrived at "Attack (s)" separately.
4113 + pub unit: Option<&'a str>,
4036 4114 /// Whether the field lives behind a "more options" disclosure.
4037 4115 pub extended: bool,
4038 4116 }
@@ -4057,6 +4135,7 @@
4057 4135 max: None,
4058 4136 step: None,
4059 4137 curve: Curve::Linear { step: None },
4138 + unit: None,
4060 4139 extended: false,
4061 4140 }
4062 4141 }
@@ -4849,6 +4928,37 @@
4849 4928 assert!(FieldKind::Rich.visible());
4850 4929 }
4851 4930
4931 + #[test]
4932 + fn only_the_two_numeric_kinds_are_measurable() {
4933 + assert!(FieldKind::Number.measurable());
4934 + assert!(FieldKind::Range.measurable());
4935 + // A date is ordered and is not a quantity with a unit to choose: its
4936 + // unit is fixed by the kind, so saying one would restate `kind`.
4937 + for kind in [
4938 + FieldKind::Text,
4939 + FieldKind::Date,
4940 + FieldKind::DateTime,
4941 + FieldKind::Select,
4942 + FieldKind::Checkbox,
4943 + FieldKind::File,
4944 + ] {
4945 + assert!(!kind.measurable(), "{kind:?}");
4946 + }
4947 + }
4948 +
4949 + #[test]
4950 + fn a_field_carries_no_unit_until_one_is_given() {
4951 + // Additive: absent is what every field described before 0.33.0 meant.
4952 + let plain = Field::new(FieldKind::Number, "attack", "Attack");
4953 + assert_eq!(plain.unit, None);
4954 + let measured = Field {
4955 + unit: Some("s"),
4956 + ..Field::range("attack", "Attack", "0.001", "5")
4957 + };
4958 + assert_eq!(measured.unit, Some("s"));
4959 + assert!(measured.kind.measurable());
4960 + }
4961 +
4852 4962 #[test]
4853 4963 fn only_a_subtree_prunes_and_only_the_listing_modes_offer_values() {
4854 4964 // Excluding a value from a flat facet is the same fact as not picking