| 64 |
64 |
|
//! deliberately two members wide, because hover and pressed belong where they
|
| 65 |
65 |
|
//! already are. [`State`]'s own docs carry that argument.
|
| 66 |
66 |
|
//!
|
|
67 |
+ |
//! 0.8.0 finishes [`Field`], which described a field well enough to label it and
|
|
68 |
+ |
//! not well enough to draw it. Writing `makeover-webview`'s form emitter found
|
|
69 |
+ |
//! three things missing and the renderer supplied all three from outside: the
|
|
70 |
+ |
//! current value, a select's options, and the placeholder. Two of those move
|
|
71 |
+ |
//! here and one does not.
|
|
72 |
+ |
//!
|
|
73 |
+ |
//! - [`Field::placeholder`] is user-facing text sitting beside `label` and
|
|
74 |
+ |
//! `hint`. There was never a reading on which it was renderer state; it was
|
|
75 |
+ |
//! outside only because adding a field to a published struct is breaking.
|
|
76 |
+ |
//! - [`Field::options`] moves because every renderer needs them and each was
|
|
77 |
+ |
//! going to invent its own shape. [`Choice`] is the shape `makeover-webview`
|
|
78 |
+ |
//! already arrived at, taken as-is rather than redesigned.
|
|
79 |
+ |
//! - The current value stays renderer-side and is not coming here. It is the
|
|
80 |
+ |
//! one of the three that is genuinely state: a webview reads it out of the
|
|
81 |
+ |
//! DOM, an immediate-mode renderer holds a `&mut` to the app's own field, and
|
|
82 |
+ |
//! a description that carried it would be a form model.
|
|
83 |
+ |
//!
|
| 67 |
84 |
|
//! # Where the description stops
|
| 68 |
85 |
|
//!
|
| 69 |
86 |
|
//! The bespoke widgets, a day-plan timeline and a kanban board and a calendar,
|
| 858 |
875 |
|
}
|
| 859 |
876 |
|
}
|
| 860 |
877 |
|
|
|
878 |
+ |
/// One option a [`FieldKind::Select`] offers.
|
|
879 |
+ |
///
|
|
880 |
+ |
/// Two strings, because the submitted value and the read label are different
|
|
881 |
+ |
/// facts and every renderer that has tried to collapse them has had to
|
|
882 |
+ |
/// un-collapse them later. `makeover-webview` invented this shape writing its
|
|
883 |
+ |
/// form emitter and it is taken here unchanged; moving it down rather than
|
|
884 |
+ |
/// re-deriving it is the point, since the second and third renderers were each
|
|
885 |
+ |
/// going to arrive at a near-miss of it.
|
|
886 |
+ |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
887 |
+ |
pub struct Choice<'a> {
|
|
888 |
+ |
/// What is submitted.
|
|
889 |
+ |
pub value: &'a str,
|
|
890 |
+ |
/// What is read.
|
|
891 |
+ |
pub label: &'a str,
|
|
892 |
+ |
}
|
|
893 |
+ |
|
|
894 |
+ |
impl<'a> Choice<'a> {
|
|
895 |
+ |
/// An option whose submitted value is also its label.
|
|
896 |
+ |
#[must_use]
|
|
897 |
+ |
pub const fn plain(value: &'a str) -> Self {
|
|
898 |
+ |
Self {
|
|
899 |
+ |
value,
|
|
900 |
+ |
label: value,
|
|
901 |
+ |
}
|
|
902 |
+ |
}
|
|
903 |
+ |
}
|
|
904 |
+ |
|
| 861 |
905 |
|
/// One field of a form.
|
| 862 |
906 |
|
///
|
| 863 |
907 |
|
/// Borrowed rather than owned: a description is built, read once by a renderer,
|
| 864 |
908 |
|
/// and dropped. Nothing here outlives the screen it describes.
|
|
909 |
+ |
///
|
|
910 |
+ |
/// # What it carries, and what it does not
|
|
911 |
+ |
///
|
|
912 |
+ |
/// Stated here so the next renderer does not re-ask, which is what the first
|
|
913 |
+ |
/// two both did. It carries everything a renderer needs to *draw* the field:
|
|
914 |
+ |
/// its kind, what it is called, what it is asked for, its standing help, what
|
|
915 |
+ |
/// is wrong with it now, whether it is compulsory, whether it hides behind a
|
|
916 |
+ |
/// disclosure, its ghost text, and the options it offers.
|
|
917 |
+ |
///
|
|
918 |
+ |
/// It does not carry the **current value**, and it is not going to. That is the
|
|
919 |
+ |
/// one thing here that is genuinely renderer state: a webview reads it back out
|
|
920 |
+ |
/// of the DOM, an immediate-mode renderer holds a `&mut` to the app's own field
|
|
921 |
+ |
/// and writes through it, and a terminal keeps an edit buffer. A description
|
|
922 |
+ |
/// that carried the value would have to carry a way to write it back, at which
|
|
923 |
+ |
/// point it is a form model and no longer a description.
|
|
924 |
+ |
///
|
|
925 |
+ |
/// **Validation** is absent for the reason [`FieldKind`] records: [`error`] is
|
|
926 |
+ |
/// the *result* of validating, supplied by whoever validated. Nothing here
|
|
927 |
+ |
/// decides that a value is wrong.
|
|
928 |
+ |
///
|
|
929 |
+ |
/// [`error`]: Field::error
|
| 865 |
930 |
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
| 866 |
931 |
|
pub struct Field<'a> {
|
| 867 |
932 |
|
/// What kind of value it takes.
|
| 874 |
939 |
|
pub hint: Option<&'a str>,
|
| 875 |
940 |
|
/// What is currently wrong with the value.
|
| 876 |
941 |
|
pub error: Option<&'a str>,
|
|
942 |
+ |
/// Ghost text shown while the field is empty.
|
|
943 |
+ |
///
|
|
944 |
+ |
/// User-facing text, and it sits with `label` and `hint` rather than with
|
|
945 |
+ |
/// the value because it is a property of the *question* and not of the
|
|
946 |
+ |
/// answer. It lived renderer-side in `makeover-webview` until 0.8.0 for one
|
|
947 |
+ |
/// reason and it was not a reading on where it belonged: adding a field to
|
|
948 |
+ |
/// a published struct is a breaking change.
|
|
949 |
+ |
///
|
|
950 |
+ |
/// Not a substitute for a label. A field labelled only by its placeholder
|
|
951 |
+ |
/// loses its label the moment anything is typed, and no renderer here can
|
|
952 |
+ |
/// make that not happen, so the description keeps both.
|
|
953 |
+ |
pub placeholder: Option<&'a str>,
|
|
954 |
+ |
/// The options offered, in the order they are offered.
|
|
955 |
+ |
///
|
|
956 |
+ |
/// Empty for every kind but [`FieldKind::Select`]. A select described with
|
|
957 |
+ |
/// no options is sayable on purpose: it is what an app with an
|
|
958 |
+ |
/// unfinished-loading option list actually has, and a renderer showing an
|
|
959 |
+ |
/// empty select says so on screen rather than in a log.
|
|
960 |
+ |
///
|
|
961 |
+ |
/// Which option is *current* is not here. That is the value, and the value
|
|
962 |
+ |
/// is renderer state.
|
|
963 |
+ |
pub options: &'a [Choice<'a>],
|
| 877 |
964 |
|
/// Whether the form refuses to submit without it.
|
| 878 |
965 |
|
pub required: bool,
|
| 879 |
966 |
|
/// Whether the field lives behind a "more options" disclosure.
|
| 890 |
977 |
|
label,
|
| 891 |
978 |
|
hint: None,
|
| 892 |
979 |
|
error: None,
|
|
980 |
+ |
placeholder: None,
|
|
981 |
+ |
options: &[],
|
|
982 |
+ |
required: false,
|
|
983 |
+ |
extended: false,
|
|
984 |
+ |
}
|
|
985 |
+ |
}
|
|
986 |
+ |
|
|
987 |
+ |
/// A select offering the given options.
|
|
988 |
+ |
///
|
|
989 |
+ |
/// The one kind that is under-described by [`Field::new`], so it gets a
|
|
990 |
+ |
/// constructor rather than leaving every call site to remember that a
|
|
991 |
+ |
/// select with an empty `options` renders as an empty select.
|
|
992 |
+ |
#[must_use]
|
|
993 |
+ |
pub const fn select(name: &'a str, label: &'a str, options: &'a [Choice<'a>]) -> Self {
|
|
994 |
+ |
Self {
|
|
995 |
+ |
kind: FieldKind::Select,
|
|
996 |
+ |
name,
|
|
997 |
+ |
label,
|
|
998 |
+ |
hint: None,
|
|
999 |
+ |
error: None,
|
|
1000 |
+ |
placeholder: None,
|
|
1001 |
+ |
options,
|
| 893 |
1002 |
|
required: false,
|
| 894 |
1003 |
|
extended: false,
|
| 895 |
1004 |
|
}
|
| 1278 |
1387 |
|
assert!(!FieldKind::Text.labels_itself());
|
| 1279 |
1388 |
|
}
|
| 1280 |
1389 |
|
|
|
1390 |
+ |
#[test]
|
|
1391 |
+ |
fn a_plain_field_offers_nothing_and_a_select_offers_its_options() {
|
|
1392 |
+ |
let text = Field::new(FieldKind::Text, "title", "Title");
|
|
1393 |
+ |
assert!(text.options.is_empty());
|
|
1394 |
+ |
assert_eq!(text.placeholder, None);
|
|
1395 |
+ |
|
|
1396 |
+ |
let sizes = [Choice::plain("small"), Choice::plain("large")];
|
|
1397 |
+ |
let select = Field::select("size", "Size", &sizes);
|
|
1398 |
+ |
assert_eq!(select.kind, FieldKind::Select);
|
|
1399 |
+ |
assert_eq!(select.options.len(), 2);
|
|
1400 |
+ |
}
|
|
1401 |
+ |
|
|
1402 |
+ |
#[test]
|
|
1403 |
+ |
fn a_choice_says_what_submits_and_what_is_read_apart() {
|
|
1404 |
+ |
// The whole reason it is two strings. `plain` is the case where they
|
|
1405 |
+ |
// coincide, and it is a shorthand rather than the general shape.
|
|
1406 |
+ |
let plain = Choice::plain("7");
|
|
1407 |
+ |
assert_eq!((plain.value, plain.label), ("7", "7"));
|
|
1408 |
+ |
|
|
1409 |
+ |
let spelled = Choice {
|
|
1410 |
+ |
value: "7",
|
|
1411 |
+ |
label: "One week",
|
|
1412 |
+ |
};
|
|
1413 |
+ |
assert_ne!(spelled.value, spelled.label);
|
|
1414 |
+ |
}
|
|
1415 |
+ |
|
|
1416 |
+ |
#[test]
|
|
1417 |
+ |
fn a_select_with_no_options_is_sayable() {
|
|
1418 |
+ |
// An app whose option list has not loaded has exactly this. Making it
|
|
1419 |
+ |
// unrepresentable would push the state somewhere less visible, and a
|
|
1420 |
+ |
// renderer drawing an empty select reports it on screen.
|
|
1421 |
+ |
let loading = Field::select("project", "Project", &[]);
|
|
1422 |
+ |
assert!(loading.options.is_empty());
|
|
1423 |
+ |
}
|
|
1424 |
+ |
|
|
1425 |
+ |
#[test]
|
|
1426 |
+ |
fn the_description_carries_the_question_and_never_the_answer() {
|
|
1427 |
+ |
// The line 0.8.0 drew. Placeholder and options are properties of what
|
|
1428 |
+ |
// is being asked; the current value is what came back, and no field
|
|
1429 |
+ |
// here holds one.
|
|
1430 |
+ |
let f = Field {
|
|
1431 |
+ |
placeholder: Some("yyyy-mm-dd"),
|
|
1432 |
+ |
..Field::new(FieldKind::Text, "due", "Due")
|
|
1433 |
+ |
};
|
|
1434 |
+ |
assert_eq!(f.placeholder, Some("yyyy-mm-dd"));
|
|
1435 |
+ |
// A placeholder is not a label, and having one does not excuse the
|
|
1436 |
+ |
// field from carrying the other.
|
|
1437 |
+ |
assert_eq!(f.label, "Due");
|
|
1438 |
+ |
}
|
|
1439 |
+ |
|
| 1281 |
1440 |
|
#[test]
|
| 1282 |
1441 |
|
fn a_field_reports_its_own_error_state() {
|
| 1283 |
1442 |
|
let mut f = Field::new(FieldKind::Text, "title", "Title");
|