| 1055 |
1055 |
|
/// webview apps already carry it as a `data-color` attribute.
|
| 1056 |
1056 |
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
| 1057 |
1057 |
|
pub enum Tone {
|
| 1058 |
|
- |
/// No status. Reads as ordinary de-emphasised content.
|
|
1058 |
+ |
/// No status.
|
|
1059 |
+ |
///
|
|
1060 |
+ |
/// Ordinary content, at full weight. It does not also mean muted: a
|
|
1061 |
+ |
/// badge reads quiet because [`Token::Badge`] answers no click, which is
|
|
1062 |
+ |
/// the renderer's knowledge and not this axis's. A renderer wanting a
|
|
1063 |
+ |
/// muted badge reaches for [`Token::interactive`] itself rather than
|
|
1064 |
+ |
/// expecting `Neutral` to have muted it.
|
| 1059 |
1065 |
|
Neutral,
|
| 1060 |
1066 |
|
/// Something worth knowing and nothing to do about it.
|
| 1061 |
1067 |
|
Info,
|
| 1070 |
1076 |
|
impl Intent for Tone {
|
| 1071 |
1077 |
|
fn token(self) -> &'static str {
|
| 1072 |
1078 |
|
match self {
|
| 1073 |
|
- |
// Neutral has no status token of its own. It takes the muted
|
| 1074 |
|
- |
// content intent, which is what both webview apps already spell as
|
| 1075 |
|
- |
// `data-color="muted"`.
|
| 1076 |
|
- |
Self::Neutral => "content-muted",
|
|
1079 |
+ |
// Neutral has no status token of its own, so it takes the plain
|
|
1080 |
+ |
// content intent. It used to answer `content-muted`, which read
|
|
1081 |
+ |
// "no status" as "de-emphasised" and muted every figure value in
|
|
1082 |
+ |
// the webview. Muting is a renderer's call about a particular
|
|
1083 |
+ |
// token, not something the status axis knows.
|
|
1084 |
+ |
Self::Neutral => "content",
|
| 1077 |
1085 |
|
Self::Info => "info",
|
| 1078 |
1086 |
|
Self::Success => "success",
|
| 1079 |
1087 |
|
Self::Warning => "warning",
|
| 4194 |
4202 |
|
pub hint: Option<&'a str>,
|
| 4195 |
4203 |
|
/// What is currently wrong with the value.
|
| 4196 |
4204 |
|
pub error: Option<&'a str>,
|
|
4205 |
+ |
/// A consequence of the answer the user has given, carrying its own tone.
|
|
4206 |
+ |
///
|
|
4207 |
+ |
/// The third message channel, between [`hint`](Self::hint) and
|
|
4208 |
+ |
/// [`error`](Self::error) and overlapping neither. A hint is standing help
|
|
4209 |
+ |
/// that does not depend on the value; an error says the value is not
|
|
4210 |
+ |
/// acceptable. A note is the case in the middle: the value is perfectly
|
|
4211 |
+ |
/// acceptable and choosing it costs something the user should know about.
|
|
4212 |
+ |
///
|
|
4213 |
+ |
/// The first consumer is audiofiles' export Format field, where choosing
|
|
4214 |
+ |
/// WAV or AIFF over Original re-encodes and silently drops embedded BWF,
|
|
4215 |
+ |
/// iXML, loop points, cue markers and ID3. That is not a validation
|
|
4216 |
+ |
/// failure and it is not standing help — it is true of one answer to one
|
|
4217 |
+ |
/// question — and it was hand-drawn in the app's own draw callback for
|
|
4218 |
+ |
/// want of anywhere to say it.
|
|
4219 |
+ |
///
|
|
4220 |
+ |
/// The tone is carried rather than fixed at [`Tone::Warning`] because the
|
|
4221 |
+ |
/// channel is not only for warnings: the same slot says "this is the
|
|
4222 |
+ |
/// recommended one" ([`Tone::Success`]) and "this is what that setting
|
|
4223 |
+ |
/// implies" ([`Tone::Info`]). A renderer gets the announcement behaviour
|
|
4224 |
+ |
/// off the tone for free — makeover-webview emits `data-tone` and treats
|
|
4225 |
+ |
/// Warning and Danger as assertive for `aria-live`.
|
|
4226 |
+ |
///
|
|
4227 |
+ |
/// It does **not** make the field invalid. [`invalid`](Self::invalid) stays
|
|
4228 |
+ |
/// `error.is_some()`, so a note never marks the group as a problem.
|
|
4229 |
+ |
///
|
|
4230 |
+ |
/// # Precedence, for a renderer with room for one
|
|
4231 |
+ |
///
|
|
4232 |
+ |
/// Error, then note, then hint. A renderer that shows every message shows
|
|
4233 |
+ |
/// them in that order too. makeover-tui is the one with room for exactly
|
|
4234 |
+ |
/// one line, and it is why the order is decided here rather than three
|
|
4235 |
+ |
/// times: what is wrong outranks what it costs, which outranks how it
|
|
4236 |
+ |
/// works.
|
|
4237 |
+ |
///
|
|
4238 |
+ |
/// Added 0.36.0.
|
|
4239 |
+ |
pub note: Option<(Tone, &'a str)>,
|
| 4197 |
4240 |
|
/// Ghost text shown while the field is empty.
|
| 4198 |
4241 |
|
///
|
| 4199 |
4242 |
|
/// User-facing text, and it sits with `label` and `hint` rather than with
|
| 4348 |
4391 |
|
label,
|
| 4349 |
4392 |
|
hint: None,
|
| 4350 |
4393 |
|
error: None,
|
|
4394 |
+ |
note: None,
|
| 4351 |
4395 |
|
placeholder: None,
|
| 4352 |
4396 |
|
options: &[],
|
| 4353 |
4397 |
|
accept: &[],
|
| 4468 |
4512 |
|
/// terminal) cannot find the group from the message. goingson already marks
|
| 4469 |
4513 |
|
/// the group and Balanced Breakfast does not, so goingson's shape is the
|
| 4470 |
4514 |
|
/// one taken here.
|
|
4515 |
+ |
///
|
|
4516 |
+ |
/// [`note`](Self::note) is deliberately not consulted. A note says the
|
|
4517 |
+ |
/// answer costs something, not that it is unacceptable, and a field the
|
|
4518 |
+ |
/// user may submit as it stands is not invalid.
|
| 4471 |
4519 |
|
#[must_use]
|
| 4472 |
4520 |
|
pub const fn invalid(&self) -> bool {
|
| 4473 |
4521 |
|
self.error.is_some()
|
| 5795 |
5843 |
|
assert_ne!(Depth::Overlay.fill(), Depth::Flat.fill());
|
| 5796 |
5844 |
|
}
|
| 5797 |
5845 |
|
|
|
5846 |
+ |
#[test]
|
|
5847 |
+ |
fn a_note_is_neither_a_hint_nor_an_error() {
|
|
5848 |
+ |
// audiofiles' export Format field: choosing WAV over Original
|
|
5849 |
+ |
// re-encodes and drops the embedded metadata. Perfectly valid, and it
|
|
5850 |
+ |
// costs something.
|
|
5851 |
+ |
let format = Field {
|
|
5852 |
+ |
note: Some((Tone::Warning, "Re-encoding drops embedded BWF and iXML")),
|
|
5853 |
+ |
..Field::select("format", "Format", &[])
|
|
5854 |
+ |
};
|
|
5855 |
+ |
assert!(!format.invalid(), "a note is not a validation failure");
|
|
5856 |
+ |
assert!(format.hint.is_none());
|
|
5857 |
+ |
assert!(format.error.is_none());
|
|
5858 |
+ |
assert_eq!(format.note.unwrap().0, Tone::Warning);
|
|
5859 |
+ |
|
|
5860 |
+ |
// The default is no note, so the 15 in-tree `..Field::new(..)`
|
|
5861 |
+ |
// literals absorb the member with no call-site edit.
|
|
5862 |
+ |
assert!(Field::new(FieldKind::Text, "title", "Title").note.is_none());
|
|
5863 |
+ |
}
|
|
5864 |
+ |
|
|
5865 |
+ |
#[test]
|
|
5866 |
+ |
fn neutral_is_content_not_muted_content() {
|
|
5867 |
+ |
// Neutral means "no status", and that is all it means. It answered
|
|
5868 |
+ |
// `content-muted` until 2026-08-27, which muted a figure's headline
|
|
5869 |
+ |
// number to the colour of its own caption. What makes a badge quiet
|
|
5870 |
+ |
// is `Token::Badge` answering no click, which lives on the renderer.
|
|
5871 |
+ |
assert_eq!(Tone::Neutral.token(), "content");
|
|
5872 |
+ |
assert!(!Token::Badge.interactive());
|
|
5873 |
+ |
assert_eq!(State::Disabled.token(), "content-muted");
|
|
5874 |
+ |
}
|
|
5875 |
+ |
|
| 5798 |
5876 |
|
#[test]
|
| 5799 |
5877 |
|
fn intents_name_makeover_tokens_and_nothing_else() {
|
| 5800 |
5878 |
|
assert_eq!(Edge::Light.token(), "bevel-light");
|