| 382 |
382 |
|
//! that one page: audiofiles' library browser and goingson's filters are the
|
| 383 |
383 |
|
//! same shape.
|
| 384 |
384 |
|
//!
|
|
385 |
+ |
//! 0.35.0 gives a suggestion its second line. [`Candidate`] is the entry in a
|
|
386 |
+ |
//! field's suggestion list: a value, a label, and the [`detail`] that orients
|
|
387 |
+ |
//! it. Ruled by Max 2026-08-21 (`1fcf2e9b`) after the combobox member shipped
|
|
388 |
+ |
//! and was then held against the two sites it was designed from, which is an
|
|
389 |
+ |
//! order worth not repeating.
|
|
390 |
+ |
//!
|
|
391 |
+ |
//! - **A candidate is not a [`Choice`], and the difference is in the reading
|
|
392 |
+ |
//! rather than the writing.** Both submit one string and read as another. An
|
|
393 |
+ |
//! option is picked out of a set the user can see whole; a candidate is
|
|
394 |
+ |
//! offered out of a set nobody can see, so it has to say what tells it from
|
|
395 |
+ |
//! its neighbours. Both measured sites draw that second string today, by
|
|
396 |
+ |
//! hand, in a second span.
|
|
397 |
+ |
//! - **It is its own type rather than a member on [`Choice`].** `Choice` is the
|
|
398 |
+ |
//! most-consumed struct in the vocabulary and the member would have landed on
|
|
399 |
+ |
//! every option list in the tree the day it shipped, including the ones with
|
|
400 |
+ |
//! nowhere to draw it.
|
|
401 |
+ |
//! - **[`Candidate`] is `#[non_exhaustive]` from birth**, which is the whole of
|
|
402 |
+ |
//! what 0.28.0 cost and is not being paid twice.
|
|
403 |
+ |
//! - No `unavailable`. A suggestion that cannot be picked is a row a route
|
|
404 |
+ |
//! should not have offered.
|
|
405 |
+ |
//!
|
|
406 |
+ |
//! [`detail`]: Candidate::detail
|
|
407 |
+ |
//!
|
| 385 |
408 |
|
//! 0.34.0 gives an interval a description. [`FieldKind::Interval`] and
|
| 386 |
409 |
|
//! [`Field::upper_name`] say that two values are one question with two ends.
|
| 387 |
410 |
|
//! Ruled by Max 2026-08-21 against audiofiles' six filter axes and the MNW
|
| 3844 |
3867 |
|
}
|
| 3845 |
3868 |
|
}
|
| 3846 |
3869 |
|
|
|
3870 |
+ |
/// One entry in a field's suggestion list.
|
|
3871 |
+ |
///
|
|
3872 |
+ |
/// A suggestion-only type rather than a fourth member on [`Choice`], ruled by
|
|
3873 |
+ |
/// Max 2026-08-21 (`1fcf2e9b`). The two are near-identical and that is the
|
|
3874 |
+ |
/// drift risk the ruling accepted, so the mitigation is written here: **an
|
|
3875 |
+ |
/// option and a candidate are submitted the same way and read differently.**
|
|
3876 |
+ |
/// An option is a thing you pick from a known set, and the set is the whole of
|
|
3877 |
+ |
/// what there is. A candidate is a thing you are being *oriented* toward out of
|
|
3878 |
+ |
/// a set nobody can see, which is why it carries [`detail`](Self::detail) and
|
|
3879 |
+ |
/// an option does not.
|
|
3880 |
+ |
///
|
|
3881 |
+ |
/// This reverses a position quasi-router stated in its own doc, that a
|
|
3882 |
+ |
/// candidate is [`Choice`] "because a candidate is submitted under one string
|
|
3883 |
+ |
/// and read under another, which is what an option is". True and not
|
|
3884 |
+ |
/// sufficient: how a thing is submitted was never the half that differed.
|
|
3885 |
+ |
///
|
|
3886 |
+ |
/// # Why the second string is not folded into the label
|
|
3887 |
+ |
///
|
|
3888 |
+ |
/// Because every renderer wants it separately, and the two measured sites both
|
|
3889 |
+ |
/// draw it by hand today. The MNW server's tag box computes its context as the
|
|
3890 |
+ |
/// parent path -- "the parent path orients an otherwise ambiguous leaf:
|
|
3891 |
+ |
/// 'Format' appears under audio, software, writing, and video" -- and a list of
|
|
3892 |
+ |
/// four identical rows reading "Format" is not a usable list. In a webview the
|
|
3893 |
+ |
/// second string is styled differently, in a terminal it wants the remaining
|
|
3894 |
+ |
/// columns rather than a dash, and in neither is it part of what the typed
|
|
3895 |
+ |
/// value matches against. `Choice::new(slug, format!("{label} - {context}"))`
|
|
3896 |
+ |
/// loses all three of those facts, which is the condition this type exists to
|
|
3897 |
+ |
/// end.
|
|
3898 |
+ |
///
|
|
3899 |
+ |
/// # No `unavailable`
|
|
3900 |
+ |
///
|
|
3901 |
+ |
/// [`Choice::unavailable`] has no counterpart here, and the omission is the
|
|
3902 |
+ |
/// implementer's call recorded rather than an oversight. A suggestion that
|
|
3903 |
+ |
/// cannot be picked is arguably not a suggestion: an option list is a fixed set
|
|
3904 |
+ |
/// a user is owed an explanation about, and a candidate list is whatever a
|
|
3905 |
+ |
/// route decided to offer, so a route with nothing to say simply does not offer
|
|
3906 |
+ |
/// the row. Add it if a measured site ever wants it.
|
|
3907 |
+ |
///
|
|
3908 |
+ |
/// # What it does not carry, and where that lives
|
|
3909 |
+ |
///
|
|
3910 |
+ |
/// What *happens* when a candidate is picked. Picking is local by default -- it
|
|
3911 |
+ |
/// writes [`value`](Self::value) into the field that owns the list -- and a
|
|
3912 |
+ |
/// candidate that does something else says so with an action. An action is not
|
|
3913 |
+ |
/// a word this crate has, exactly as [`Field`] here has no `suggests` member,
|
|
3914 |
+ |
/// so both live on the router's owned mirror of this type. Ruled the same day
|
|
3915 |
+ |
/// (`ed1fa86f`).
|
|
3916 |
+ |
///
|
|
3917 |
+ |
/// `#[non_exhaustive]` from birth. Non-negotiable and the reason is on the
|
|
3918 |
+ |
/// sibling: [`Choice`] took it at 0.28.0 only after `unavailable` broke 40
|
|
3919 |
+ |
/// literal sites in six repos, and a new type repeating that would be the third
|
|
3920 |
+ |
/// time the tree learned it.
|
|
3921 |
+ |
///
|
|
3922 |
+ |
/// Added 0.35.0.
|
|
3923 |
+ |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
3924 |
+ |
#[non_exhaustive]
|
|
3925 |
+ |
pub struct Candidate<'a> {
|
|
3926 |
+ |
/// What is submitted, and what picking writes into the field.
|
|
3927 |
+ |
pub value: &'a str,
|
|
3928 |
+ |
/// What is read.
|
|
3929 |
+ |
pub label: &'a str,
|
|
3930 |
+ |
/// The second line: what orients this candidate among rows that read alike.
|
|
3931 |
+ |
///
|
|
3932 |
+ |
/// Optional because a candidate list whose labels are already distinct
|
|
3933 |
+ |
/// wants nothing here, and a renderer given [`None`] draws one line rather
|
|
3934 |
+ |
/// than an empty second one.
|
|
3935 |
+ |
pub detail: Option<&'a str>,
|
|
3936 |
+ |
}
|
|
3937 |
+ |
|
|
3938 |
+ |
impl<'a> Candidate<'a> {
|
|
3939 |
+ |
/// A candidate whose submitted value is also its label.
|
|
3940 |
+ |
#[must_use]
|
|
3941 |
+ |
pub const fn plain(value: &'a str) -> Self {
|
|
3942 |
+ |
Self::new(value, value)
|
|
3943 |
+ |
}
|
|
3944 |
+ |
|
|
3945 |
+ |
/// A candidate that submits one string and reads as another.
|
|
3946 |
+ |
///
|
|
3947 |
+ |
/// A constructor rather than a literal, which is what `#[non_exhaustive]`
|
|
3948 |
+ |
/// costs and buys: outside this crate the struct cannot be built by naming
|
|
3949 |
+ |
/// its members, so every call site goes through here and the next member
|
|
3950 |
+ |
/// added breaks none of them.
|
|
3951 |
+ |
#[must_use]
|
|
3952 |
+ |
pub const fn new(value: &'a str, label: &'a str) -> Self {
|
|
3953 |
+ |
Self {
|
|
3954 |
+ |
value,
|
|
3955 |
+ |
label,
|
|
3956 |
+ |
detail: None,
|
|
3957 |
+ |
}
|
|
3958 |
+ |
}
|
|
3959 |
+ |
|
|
3960 |
+ |
/// The same candidate, with the line that tells it from its neighbours.
|
|
3961 |
+ |
#[must_use]
|
|
3962 |
+ |
pub const fn detailed(mut self, detail: &'a str) -> Self {
|
|
3963 |
+ |
self.detail = Some(detail);
|
|
3964 |
+ |
self
|
|
3965 |
+ |
}
|
|
3966 |
+ |
}
|
|
3967 |
+ |
|
| 3847 |
3968 |
|
/// One field of a form.
|
| 3848 |
3969 |
|
///
|
| 3849 |
3970 |
|
/// Borrowed rather than owned: a description is built, read once by a renderer,
|
| 6230 |
6351 |
|
);
|
| 6231 |
6352 |
|
}
|
| 6232 |
6353 |
|
|
|
6354 |
+ |
#[test]
|
|
6355 |
+ |
fn a_candidate_carries_the_line_that_tells_it_from_its_neighbours() {
|
|
6356 |
+ |
// The gap this type was born for: two candidates whose labels read
|
|
6357 |
+ |
// alike, told apart by the second string and by nothing else. The
|
|
6358 |
+ |
// measured site is the MNW tag box, where "Format" is a leaf under
|
|
6359 |
+ |
// audio, software, writing and video.
|
|
6360 |
+ |
let audio = Candidate::new("audio/format", "Format").detailed("Audio");
|
|
6361 |
+ |
let writing = Candidate::new("writing/format", "Format").detailed("Writing");
|
|
6362 |
+ |
|
|
6363 |
+ |
assert_eq!(audio.label, writing.label);
|
|
6364 |
+ |
assert_ne!(audio.detail, writing.detail);
|
|
6365 |
+ |
assert_ne!(
|
|
6366 |
+ |
audio, writing,
|
|
6367 |
+ |
"two rows a user cannot tell apart are two rows the type can"
|
|
6368 |
+ |
);
|
|
6369 |
+ |
}
|
|
6370 |
+ |
|
|
6371 |
+ |
#[test]
|
|
6372 |
+ |
fn a_candidate_is_read_differently_from_an_option_and_written_the_same() {
|
|
6373 |
+ |
// The ruling's own distinction, held as a test so the two types do not
|
|
6374 |
+ |
// drift back together. Submitting is identical; the second line is the
|
|
6375 |
+ |
// whole of what differs, and it is absent by default because a list of
|
|
6376 |
+ |
// distinct labels wants nothing there.
|
|
6377 |
+ |
let candidate = Candidate::plain("rust");
|
|
6378 |
+ |
assert_eq!((candidate.value, candidate.label), ("rust", "rust"));
|
|
6379 |
+ |
assert_eq!(
|
|
6380 |
+ |
candidate.detail, None,
|
|
6381 |
+ |
"one line unless the route says otherwise"
|
|
6382 |
+ |
);
|
|
6383 |
+ |
|
|
6384 |
+ |
let option = Choice::plain("rust");
|
|
6385 |
+ |
assert_eq!(
|
|
6386 |
+ |
(candidate.value, candidate.label),
|
|
6387 |
+ |
(option.value, option.label)
|
|
6388 |
+ |
);
|
|
6389 |
+ |
}
|
|
6390 |
+ |
|
| 6233 |
6391 |
|
#[test]
|
| 6234 |
6392 |
|
fn a_radio_asks_the_same_question_as_a_select_and_is_not_the_same_kind() {
|
| 6235 |
6393 |
|
// Both offer a fixed set and both read `options`, so the two
|