| 211 |
211 |
|
}
|
| 212 |
212 |
|
return;
|
| 213 |
213 |
|
}
|
| 214 |
|
- |
out.push(field_role(field.kind), field.label.clone(), false);
|
|
214 |
+ |
// An interval is one question with two ends, and both ends are controls.
|
|
215 |
+ |
// makeover-immediate names each of them by the question, which is the
|
|
216 |
+ |
// right answer -- "BPM Range" twice reads correctly to a screen reader
|
|
217 |
+ |
// stepping through them -- so the offering has two.
|
|
218 |
+ |
let ends = if field.kind == layout::FieldKind::Interval {
|
|
219 |
+ |
2
|
|
220 |
+ |
} else {
|
|
221 |
+ |
1
|
|
222 |
+ |
};
|
|
223 |
+ |
for _ in 0..ends {
|
|
224 |
+ |
out.push(field_role(field.kind), field.label.clone(), false);
|
|
225 |
+ |
}
|
| 215 |
226 |
|
}
|
| 216 |
227 |
|
|
| 217 |
228 |
|
/// The role a field of this kind is drawn as.
|
| 220 |
231 |
|
match kind {
|
| 221 |
232 |
|
K::Checkbox => Role::Check,
|
| 222 |
233 |
|
K::Select | K::Radio => Role::Choice,
|
| 223 |
|
- |
K::Number | K::Range | K::Interval => Role::Number,
|
|
234 |
+ |
// A slider and an interval's two ends. A bare `Number` is not here:
|
|
235 |
+ |
// `makeover_immediate::control_shape` sends everything it cannot draw
|
|
236 |
+ |
// natively to `Control::Typed`, so a number is a well you type into and
|
|
237 |
+ |
// reaches the tree as a text input. That is the renderer's answer and a
|
|
238 |
+ |
// true report of the value, so the reader follows it rather than
|
|
239 |
+ |
// insisting on the kind.
|
|
240 |
+ |
K::Range | K::Interval => Role::Number,
|
| 224 |
241 |
|
// Everything else is a box you type into. `File` is the one stretch and
|
| 225 |
242 |
|
// it is the honest answer: a host draws it as a control that opens a
|
| 226 |
243 |
|
// picker, which is a button on some hosts and a path box on others, and
|
| 308 |
325 |
|
}
|
| 309 |
326 |
|
Node::List { rows, .. } => {
|
| 310 |
327 |
|
for row in rows {
|
| 311 |
|
- |
if let Some(activate) = &row.activate {
|
|
328 |
+ |
// A row is claimed when it opens OR when it offers a menu,
|
|
329 |
+ |
// which is `quasi_immediate::node::row`'s own condition: a row
|
|
330 |
+ |
// that only offers a menu still needs somewhere to right-click,
|
|
331 |
+ |
// and it is announced by its first text either way. The menu's
|
|
332 |
+ |
// own acts are not offers here -- they are not on screen until
|
|
333 |
+ |
// the gesture -- but the row that carries them is.
|
|
334 |
+ |
if row.activate.is_some() || !row.menu.is_empty() {
|
| 312 |
335 |
|
let named: Vec<_> = row.parts.iter().map(|part| part.node.clone()).collect();
|
| 313 |
336 |
|
out.push(Role::Button, first_text(&named), false);
|
|
337 |
+ |
}
|
|
338 |
+ |
if let Some(activate) = &row.activate {
|
| 314 |
339 |
|
out.addresses.push(activate.destination.as_str().to_owned());
|
| 315 |
340 |
|
}
|
| 316 |
341 |
|
for part in &row.parts {
|
| 444 |
469 |
|
let Some(update) = output.platform_output.accesskit_update else {
|
| 445 |
470 |
|
panic!("accesskit produced no tree: the panel drew nothing at all");
|
| 446 |
471 |
|
};
|
| 447 |
|
- |
for (_, node) in update.nodes {
|
| 448 |
|
- |
let Some(role) = role_of(&node) else {
|
|
472 |
+ |
let by_id: std::collections::HashMap<_, _> = update.nodes.iter().cloned().collect();
|
|
473 |
+ |
for (_, node) in &update.nodes {
|
|
474 |
+ |
let Some(role) = role_of(node) else {
|
| 449 |
475 |
|
continue;
|
| 450 |
476 |
|
};
|
| 451 |
|
- |
// egui puts a label's text in `value` and every other widget's in
|
| 452 |
|
- |
// `label`, because a `Role::Label` IS its text. See
|
| 453 |
|
- |
// `Response::fill_accesskit_node_from_widget_info`.
|
| 454 |
|
- |
let said = node
|
| 455 |
|
- |
.label()
|
| 456 |
|
- |
.map(str::to_owned)
|
| 457 |
|
- |
.or_else(|| node.value().map(str::to_owned))
|
| 458 |
|
- |
.unwrap_or_default();
|
|
477 |
+ |
let said = named(node, &by_id);
|
| 459 |
478 |
|
out.push(role, undecorated(&said), node.is_disabled());
|
| 460 |
479 |
|
}
|
| 461 |
480 |
|
out
|
| 462 |
481 |
|
}
|
| 463 |
482 |
|
|
|
483 |
+ |
/// What a control is called, the way a client works it out.
|
|
484 |
+ |
///
|
|
485 |
+ |
/// egui puts a label's own text in `value` and every other widget's in `label`,
|
|
486 |
+ |
/// because a `Role::Label` IS its text. A control named by a *separate* label
|
|
487 |
+ |
/// has neither: it carries a `labelled_by` relation naming the node that says
|
|
488 |
+ |
/// it, which is what `Response::labelled_by` sets and what
|
|
489 |
+ |
/// `makeover_immediate::field` uses so a box is announced by its question
|
|
490 |
+ |
/// rather than by its own contents. Following the relation is not a
|
|
491 |
+ |
/// convenience here -- a reader that stopped at `label()` would report every
|
|
492 |
+ |
/// properly-labelled field as nameless, which is the opposite of the truth.
|
|
493 |
+ |
fn named(
|
|
494 |
+ |
node: &egui::accesskit::Node,
|
|
495 |
+ |
by_id: &std::collections::HashMap<egui::accesskit::NodeId, egui::accesskit::Node>,
|
|
496 |
+ |
) -> String {
|
|
497 |
+ |
if let Some(label) = node.label() {
|
|
498 |
+ |
return label.to_owned();
|
|
499 |
+ |
}
|
|
500 |
+ |
if let Some(said) = node
|
|
501 |
+ |
.labelled_by()
|
|
502 |
+ |
.iter()
|
|
503 |
+ |
.find_map(|id| by_id.get(id))
|
|
504 |
+ |
.and_then(|by| by.label().or_else(|| by.value()))
|
|
505 |
+ |
{
|
|
506 |
+ |
return said.to_owned();
|
|
507 |
+ |
}
|
|
508 |
+ |
node.value().unwrap_or_default().to_owned()
|
|
509 |
+ |
}
|
|
510 |
+ |
|
| 464 |
511 |
|
/// A rendered label with the renderer's own decoration taken back off.
|
| 465 |
512 |
|
///
|
| 466 |
513 |
|
/// A sorted column heading is drawn as its name plus a caret, because a glyph
|
| 582 |
629 |
|
.dropping("Close window")
|
| 583 |
630 |
|
}
|
| 584 |
631 |
|
|
| 585 |
|
- |
/// A field the description names and `makeover-immediate` draws unnamed.
|
|
632 |
+ |
/// The numeric readout egui draws inside a slider.
|
| 586 |
633 |
|
///
|
| 587 |
|
- |
/// `makeover_immediate::field` draws `Field::label` as a `ui.label` above
|
| 588 |
|
- |
/// the control and never associates the two, so what reaches the widget tree
|
| 589 |
|
- |
/// is an unnamed box with some text near it. A screen reader announces a
|
| 590 |
|
- |
/// text field with no question, and this reader -- which drops a control
|
| 591 |
|
- |
/// with nothing to say -- sees no control at all.
|
|
634 |
+ |
/// A `Slider` is two widgets: the track, which `makeover_immediate::field`
|
|
635 |
+ |
/// names from the question, and a `DragValue` showing the number, which
|
|
636 |
+ |
/// egui builds inside and never hands back. So a described `Range` reaches
|
|
637 |
+ |
/// the tree as one named control and one unnamed number.
|
| 592 |
638 |
|
///
|
| 593 |
|
- |
/// The same gap the shipped panels have, inherited. It is worse here only
|
| 594 |
|
- |
/// because the description **knows** the label: `Field::label` is right
|
| 595 |
|
- |
/// there, and `egui::Response::labelled_by` is the one call that would
|
| 596 |
|
- |
/// attach it. Filed against makeover-immediate; until it is fixed and
|
| 597 |
|
- |
/// published, every described field is an allowance at a call site.
|
|
639 |
+ |
/// Not a defect to chase: the readout is a second view of a value the
|
|
640 |
+ |
/// question already names, and the alternative is `show_value(false)`,
|
|
641 |
+ |
/// which takes the number off the screen. Named here so it is a claim
|
|
642 |
+ |
/// rather than a silence.
|
| 598 |
643 |
|
#[must_use]
|
| 599 |
|
- |
pub(super) fn unnamed_field(self, label: &str) -> Self {
|
| 600 |
|
- |
self.gaining(label)
|
| 601 |
|
- |
}
|
| 602 |
|
- |
|
| 603 |
|
- |
/// Every field on a screen the renderer draws unnamed, both sides at once.
|
| 604 |
|
- |
///
|
| 605 |
|
- |
/// [`unnamed_field`](Self::unnamed_field)'s bulk form, for the screens where
|
| 606 |
|
- |
/// the gap is most of the diff. `asked` is what the description calls each
|
| 607 |
|
- |
/// question; `shown` is what the unnamed control is announced as instead,
|
| 608 |
|
- |
/// which is its value where it has one and nothing at all where it does not
|
| 609 |
|
- |
/// -- a slider drawn with `show_value(false)` says neither.
|
| 610 |
|
- |
///
|
| 611 |
|
- |
/// Two lists rather than pairs, because they do not pair one to one: a
|
| 612 |
|
- |
/// field whose control shows nothing contributes to `asked` and not to
|
| 613 |
|
- |
/// `shown`. One call is still one claim, which is the point.
|
| 614 |
|
- |
#[must_use]
|
| 615 |
|
- |
pub(super) fn unnamed_fields(mut self, asked: &[&str], shown: &[&str]) -> Self {
|
| 616 |
|
- |
for label in asked {
|
| 617 |
|
- |
self = self.gaining(label);
|
| 618 |
|
- |
}
|
|
644 |
+ |
pub(super) fn slider_readouts(mut self, shown: &[&str]) -> Self {
|
| 619 |
645 |
|
for value in shown {
|
| 620 |
646 |
|
self = self.dropping(value);
|
| 621 |
647 |
|
}
|
| 622 |
648 |
|
self
|
| 623 |
649 |
|
}
|
| 624 |
650 |
|
|
| 625 |
|
- |
/// A chip the description makes pressable and the renderer draws mute.
|
| 626 |
|
- |
///
|
| 627 |
|
- |
/// `makeover_immediate::widget::token` allocated its rect and painted its
|
| 628 |
|
- |
/// own text, registering no `WidgetInfo`, so a chip reached the tree as
|
| 629 |
|
- |
/// nothing: pressable by a mouse and invisible to everything else. Same
|
| 630 |
|
- |
/// class as the row and the field before it.
|
| 631 |
|
- |
///
|
| 632 |
|
- |
/// Fixed in makeover-immediate 0.34.0 and not published, so it is an
|
| 633 |
|
- |
/// allowance here. `drums` runs the other way: the shipped tag chip is an
|
| 634 |
|
- |
/// ordinary button and the described one is a token.
|
| 635 |
|
- |
#[must_use]
|
| 636 |
|
- |
pub(super) fn mute_chips(mut self, labels: &[&str]) -> Self {
|
| 637 |
|
- |
for label in labels {
|
| 638 |
|
- |
self = self.gaining(label);
|
| 639 |
|
- |
}
|
| 640 |
|
- |
self
|
| 641 |
|
- |
}
|
| 642 |
|
- |
|
| 643 |
651 |
|
/// Assert the two sides offer the same thing, panicking with a diff if not.
|
| 644 |
652 |
|
pub(super) fn assert(&self, described: &Offering, shipped: &Offering) {
|
| 645 |
653 |
|
let mut want: BTreeMap<Offer, isize> = BTreeMap::new();
|
| 890 |
898 |
|
}),
|
| 891 |
899 |
|
];
|
| 892 |
900 |
|
|
| 893 |
|
- |
// What each modal asks, and what the renderer does not attach to the box.
|
| 894 |
|
- |
// See `Parity::unnamed_field`.
|
| 895 |
|
- |
let asked = ["Vault name", "New name", "Folder name", "New name"];
|
| 896 |
|
- |
|
| 897 |
|
- |
for ((title, address, show), asked) in modals.into_iter().zip(asked) {
|
|
901 |
+ |
for (title, address, show) in modals {
|
| 898 |
902 |
|
let (mut state, _dir) = fixture();
|
| 899 |
903 |
|
// A folder to rename, which the sample-only fixture does not have.
|
| 900 |
904 |
|
let vault = state.current_vfs_id().unwrap();
|
| 906 |
910 |
|
state.refresh_contents();
|
| 907 |
911 |
|
show(&mut state);
|
| 908 |
912 |
|
|
| 909 |
|
- |
// What a prefilled unnamed box is announced as: its own contents. The
|
| 910 |
|
- |
// other half of `unnamed_field` -- with no label attached, a rename
|
| 911 |
|
- |
// modal's field reports the name it was seeded with.
|
| 912 |
|
- |
let prefilled = state
|
| 913 |
|
- |
.vfs_modal
|
| 914 |
|
- |
.vfs_rename_target
|
| 915 |
|
- |
.as_ref()
|
| 916 |
|
- |
.map(|(_, name)| name.clone())
|
| 917 |
|
- |
.or_else(|| {
|
| 918 |
|
- |
state
|
| 919 |
|
- |
.vfs_modal
|
| 920 |
|
- |
.dir_rename_target
|
| 921 |
|
- |
.as_ref()
|
| 922 |
|
- |
.map(|(_, name)| name.clone())
|
| 923 |
|
- |
});
|
| 924 |
|
- |
|
| 925 |
913 |
|
let address = address.replace("{id}", &real_id(&state, address).to_string());
|
| 926 |
914 |
|
let described = described(&super::panel::described_screen(&state, &address));
|
| 927 |
915 |
|
let drawn = shipped(|ui| {
|
| 929 |
917 |
|
});
|
| 930 |
918 |
|
|
| 931 |
919 |
|
described.addresses_resolve();
|
| 932 |
|
- |
let mut parity = Parity::strict().in_a_window(title).unnamed_field(asked);
|
| 933 |
|
- |
if let Some(prefilled) = &prefilled {
|
| 934 |
|
- |
parity = parity.dropping(prefilled);
|
| 935 |
|
- |
}
|
| 936 |
|
- |
parity.assert(&described, &drawn);
|
|
920 |
+ |
Parity::strict()
|
|
921 |
+ |
.in_a_window(title)
|
|
922 |
+ |
.assert(&described, &drawn);
|
| 937 |
923 |
|
}
|
| 938 |
924 |
|
}
|
| 939 |
925 |
|
|
| 1021 |
1007 |
|
described.addresses_resolve();
|
| 1022 |
1008 |
|
Parity::strict()
|
| 1023 |
1009 |
|
.in_a_window("Sample Forge")
|
| 1024 |
|
- |
// The device picker, unnamed by the renderer and therefore announced by
|
| 1025 |
|
- |
// its own placeholder. See `Parity::unnamed_field`; both lines come out
|
| 1026 |
|
- |
// when makeover-immediate 0.34.0 is published.
|
| 1027 |
|
- |
.unnamed_field("Conform for device")
|
| 1028 |
|
- |
.dropping("Select device...")
|
| 1029 |
1010 |
|
.assert(&described, &drawn);
|
| 1030 |
1011 |
|
}
|
| 1031 |
1012 |
|
|
| 1048 |
1029 |
|
described.addresses_resolve();
|
| 1049 |
1030 |
|
Parity::strict()
|
| 1050 |
1031 |
|
.in_a_window("Sample Editor")
|
| 1051 |
|
- |
// The unnamed-field gap, on the screen that has the most of it. What
|
| 1052 |
|
- |
// the renderer announces is each control's own value, so the right-hand
|
| 1053 |
|
- |
// list is this fixture's numbers rather than anything either side says.
|
| 1054 |
|
- |
// Both lists come out when makeover-immediate 0.34.0 is published.
|
| 1055 |
|
- |
.unnamed_fields(
|
| 1056 |
|
- |
&[
|
| 1057 |
|
- |
"Start",
|
| 1058 |
|
- |
"End",
|
| 1059 |
|
- |
"Gain",
|
| 1060 |
|
- |
"Target",
|
| 1061 |
|
- |
"Duration",
|
| 1062 |
|
- |
"Length",
|
| 1063 |
|
- |
"Insert at",
|
| 1064 |
|
- |
"Remove from",
|
| 1065 |
|
- |
"to",
|
| 1066 |
|
- |
"Curve",
|
| 1067 |
|
- |
],
|
| 1068 |
|
- |
&[
|
| 1069 |
|
- |
"0", "0", "0", "100", "-1.0", "0.0", "0.000", "1.000", "100", "Linear",
|
| 1070 |
|
- |
],
|
| 1071 |
|
- |
)
|
|
1032 |
+ |
.slider_readouts(&["-1.0", "0.0", "0.000", "1.000", "100"])
|
| 1072 |
1033 |
|
.assert(&described, &drawn);
|
| 1073 |
1034 |
|
}
|
| 1074 |
1035 |
|
|
| 1105 |
1066 |
|
described.addresses_resolve();
|
| 1106 |
1067 |
|
// A left pane rather than a window, which is what the shipped panel was, so
|
| 1107 |
1068 |
|
// there is no frame to discount.
|
| 1108 |
|
- |
Parity::strict()
|
| 1109 |
|
- |
// Twenty-four key chips, two spellings of each of twelve notes. See
|
| 1110 |
|
- |
// `Parity::mute_chips`; they come out when makeover-immediate 0.34.0 is
|
| 1111 |
|
- |
// published, and the tag chip goes with them.
|
| 1112 |
|
- |
.mute_chips(&[
|
| 1113 |
|
- |
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", "C", "C#", "D", "D#",
|
| 1114 |
|
- |
"E", "F", "F#", "G", "G#", "A", "A#", "B",
|
| 1115 |
|
- |
])
|
| 1116 |
|
- |
.dropping("drums")
|
| 1117 |
|
- |
// The six intervals, unnamed and therefore announced as their ends.
|
| 1118 |
|
- |
.unnamed_fields(
|
| 1119 |
|
- |
&[
|
| 1120 |
|
- |
"BPM Range",
|
| 1121 |
|
- |
"Duration",
|
| 1122 |
|
- |
"Loudness",
|
| 1123 |
|
- |
"Brightness",
|
| 1124 |
|
- |
"Tonal / Noisy",
|
| 1125 |
|
- |
"Attack",
|
| 1126 |
|
- |
],
|
| 1127 |
|
- |
&[
|
| 1128 |
|
- |
"90", "300", "1.0 s", "600.0 s", "-12 dB", "0 dB", "500 Hz", "20000 Hz", "0.20",
|
| 1129 |
|
- |
"1.00", "1.000 s", "1.000 s",
|
| 1130 |
|
- |
],
|
| 1131 |
|
- |
)
|
| 1132 |
|
- |
// Two text boxes the renderer leaves unnamed and empty, so they are
|
| 1133 |
|
- |
// announced as nothing at all rather than as their contents.
|
| 1134 |
|
- |
.unnamed_field("Collection name")
|
| 1135 |
|
- |
.unnamed_field("Require a tag")
|
| 1136 |
|
- |
.assert(&described, &drawn);
|
|
1069 |
+ |
Parity::strict().assert(&described, &drawn);
|
| 1137 |
1070 |
|
}
|