|
1 |
+ |
//! What each element of a description may hold.
|
|
2 |
+ |
//!
|
|
3 |
+ |
//! <!-- wiki: quasi-overview -->
|
|
4 |
+ |
//!
|
|
5 |
+ |
//! # Why this exists
|
|
6 |
+ |
//!
|
|
7 |
+ |
//! Before this, composability was a per-pairing enumeration: every
|
|
8 |
+ |
//! primitive-inside-container pairing was a member, three renderer arms and a
|
|
9 |
+ |
//! release. `RowPart::Tokens` put a tag in a row, `Cell::tokens` put the same
|
|
10 |
+ |
//! tag in a cell, `RowPart::Proportion` put a meter in a row, and a meter in a
|
|
11 |
+ |
//! cell was simply not sayable. Three of the seven pairings in that matrix were
|
|
12 |
+ |
//! filled in two days, which is the measurement that decided this: the rate was
|
|
13 |
+ |
//! rising, not falling.
|
|
14 |
+ |
//!
|
|
15 |
+ |
//! The rule the enumeration was defending -- "a row holds no nodes, the door
|
|
16 |
+ |
//! through which a description becomes a templating language" -- was a rider on
|
|
17 |
+ |
//! a different 2026-08-08 decision (the one that shipped `RowPart::Tokens`),
|
|
18 |
+ |
//! promoted to a constitution by later sessions and cited six times in
|
|
19 |
+ |
//! `screen.rs` as standing law. It also did not do what it claimed:
|
|
20 |
+ |
//!
|
|
21 |
+ |
//! 1. It bounded one seam rather than the tree. `Node::Region(Slot)` holding
|
|
22 |
+ |
//! `Vec<Node>` holding `Node::Region(Slot)` is unbounded nesting and has
|
|
23 |
+ |
//! always been accepted.
|
|
24 |
+ |
//! 2. It was enforced by the absence of a type rather than by a type, so
|
|
25 |
+ |
//! nothing stopped [`Cell`](crate::Cell) growing a member per release until
|
|
26 |
+ |
//! it was a node under another name -- which it was already doing, going
|
|
27 |
+ |
//! from a `String` to four fields in one release.
|
|
28 |
+ |
//! 3. The thing worth bounding is what a constrained renderer must be able to
|
|
29 |
+ |
//! draw, not how deep the tree goes. The two got conflated.
|
|
30 |
+ |
//!
|
|
31 |
+ |
//! So the bound moves from a doc comment to a property every element declares
|
|
32 |
+ |
//! and one test checks. The 2026-08-08 rider was never checkable; this is.
|
|
33 |
+ |
//!
|
|
34 |
+ |
//! # The bound
|
|
35 |
+ |
//!
|
|
36 |
+ |
//! Three rules, all asserted by `the_containment_ladder_only_goes_down`:
|
|
37 |
+ |
//!
|
|
38 |
+ |
//! - **An inline run may not reach blocks or collections.** Depth below a line
|
|
39 |
+ |
//! is one, so a run is always drawable on one wrapped line. That is the
|
|
40 |
+ |
//! constrained-consumer test: a terminal can draw any run in a cell without
|
|
41 |
+ |
//! knowing what is in it.
|
|
42 |
+ |
//! - **A collection's element type is fixed and non-recursive.** A [`Cell`] is
|
|
43 |
+ |
//! never a [`Row`]. The container names what it holds, once.
|
|
44 |
+ |
//! - **Blocks may contain blocks.** Already true, already accepted, and a
|
|
45 |
+ |
//! nested region is a nested rect on every host.
|
|
46 |
+ |
//!
|
|
47 |
+ |
//! # What this is not
|
|
48 |
+ |
//!
|
|
49 |
+ |
//! Not a type-erased tree. Every element stays a closed enum, so a renderer
|
|
50 |
+ |
//! still matches exhaustively to pick a drawing and the compiler still says
|
|
51 |
+ |
//! when a member is added. A `Vec<Box<dyn Container>>` would take that away and
|
|
52 |
+ |
//! cost `Clone` and `PartialEq` besides. [`Element`] is a query surface over a
|
|
53 |
+ |
//! closed set, which is what `makeover_layout::Intent` already is.
|
|
54 |
+ |
|
|
55 |
+ |
use crate::screen::{
|
|
56 |
+ |
Act, Cell, Cells, Choice, Column, Field, Figure, Meter, Node, Prose, RegionKind, Row, Slot, Tag,
|
|
57 |
+ |
};
|
|
58 |
+ |
|
|
59 |
+ |
/// What an element may hold.
|
|
60 |
+ |
///
|
|
61 |
+ |
/// The default is [`Text`](Self::Text), because most of the vocabulary is a
|
|
62 |
+ |
/// label. [`Heading`](Node::Heading), [`Text`](Node::Text), [`Token`](Node::Token)
|
|
63 |
+ |
/// and the rest declare nothing and get it.
|
|
64 |
+ |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
65 |
+ |
#[non_exhaustive]
|
|
66 |
+ |
pub enum Containment {
|
|
67 |
+ |
/// Prose, and only prose.
|
|
68 |
+ |
///
|
|
69 |
+ |
/// [`Prose`] rather than `String`, so the markdown answer that
|
|
70 |
+ |
/// `df246b95` settled for a row part comes along free everywhere else: a
|
|
71 |
+ |
/// leaf says which of the two kinds of string it is, and a renderer that
|
|
72 |
+ |
/// can draw markdown does.
|
|
73 |
+ |
Text,
|
|
74 |
+ |
/// A run of things on a line, holding no blocks.
|
|
75 |
+ |
Inlines,
|
|
76 |
+ |
/// Other blocks, regions included.
|
|
77 |
+ |
Blocks,
|
|
78 |
+ |
/// A homogeneous sequence whose element type the container fixes.
|
|
79 |
+ |
Collection(Of),
|
|
80 |
+ |
/// The app fills it; the description names the place and stops.
|
|
81 |
+ |
///
|
|
82 |
+ |
/// What [`RegionKind::Bespoke`](crate::RegionKind::Bespoke) always was.
|
|
83 |
+ |
/// Under the enumeration it was an exception to a rule; here it is an
|
|
84 |
+ |
/// answer to the same question every other element answers.
|
|
85 |
+ |
Opaque,
|
|
86 |
+ |
}
|
|
87 |
+ |
|
|
88 |
+ |
/// The element type a [`Containment::Collection`] holds.
|
|
89 |
+ |
///
|
|
90 |
+ |
/// One variant per element type rather than the five the review sketched,
|
|
91 |
+ |
/// because the tree has two kinds of row: a list holds [`Row`], and a table
|
|
92 |
+ |
/// holds [`Cells`], which is a row of [`Cell`]. Folding those into one variant
|
|
93 |
+ |
/// would make "the element type is fixed by the container" false on the first
|
|
94 |
+ |
/// container anyone checked.
|
|
95 |
+ |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
96 |
+ |
#[non_exhaustive]
|
|
97 |
+ |
pub enum Of {
|
|
98 |
+ |
/// A list's rows. [`Row`].
|
|
99 |
+ |
Rows,
|
|
100 |
+ |
/// A table's rows. [`Cells`].
|
|
101 |
+ |
TableRows,
|
|
102 |
+ |
/// A table row's cells. [`Cell`].
|
|
103 |
+ |
Cells,
|
|
104 |
+ |
/// A form's questions. [`Field`].
|
|
105 |
+ |
Fields,
|
|
106 |
+ |
/// A stats strip's figures. [`Figure`].
|
|
107 |
+ |
Figures,
|
|
108 |
+ |
/// A control's options. [`Choice`].
|
|
109 |
+ |
Choices,
|
|
110 |
+ |
}
|
|
111 |
+ |
|
|
112 |
+ |
impl Of {
|
|
113 |
+ |
/// What one element of this collection may itself hold.
|
|
114 |
+ |
///
|
|
115 |
+ |
/// The recursion check reads this rather than reaching for the element's
|
|
116 |
+ |
/// own [`Element`] impl, because an `Of` names a type and not a value and
|
|
117 |
+ |
/// there is nothing to call the trait on.
|
|
118 |
+ |
#[must_use]
|
|
119 |
+ |
pub fn element_containment(self) -> Containment {
|
|
120 |
+ |
match self {
|
|
121 |
+ |
// A row is a run: parts on a line, none of them a block.
|
|
122 |
+ |
Self::Rows | Self::TableRows | Self::Cells => Containment::Inlines,
|
|
123 |
+ |
// A field holds its options when it has any, and text otherwise.
|
|
124 |
+ |
// The looser of the two is what the bound has to hold against.
|
|
125 |
+ |
Self::Fields => Containment::Collection(Of::Choices),
|
|
126 |
+ |
Self::Figures | Self::Choices => Containment::Text,
|
|
127 |
+ |
}
|
|
128 |
+ |
}
|
|
129 |
+ |
}
|
|
130 |
+ |
|
|
131 |
+ |
/// How far down the containment ladder a rung sits.
|
|
132 |
+ |
///
|
|
133 |
+ |
/// [`Opaque`](Containment::Opaque) is off the ladder rather than on top of it:
|
|
134 |
+ |
/// the description stops there, so there is nothing below it to bound and
|
|
135 |
+ |
/// nothing it can reach. Giving it a rank would make it either a block that
|
|
136 |
+ |
/// must not appear in a run, which is right by accident, or a leaf, which is
|
|
137 |
+ |
/// wrong.
|
|
138 |
+ |
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
|
139 |
+ |
pub enum Level {
|
|
140 |
+ |
/// [`Containment::Text`].
|
|
141 |
+ |
Leaf,
|
|
142 |
+ |
/// [`Containment::Inlines`].
|
|
143 |
+ |
Run,
|
|
144 |
+ |
/// [`Containment::Blocks`] and [`Containment::Collection`].
|
|
145 |
+ |
Block,
|
|
146 |
+ |
}
|
|
147 |
+ |
|
|
148 |
+ |
impl Containment {
|
|
149 |
+ |
/// Where this sits on the ladder, if it is on it.
|
|
150 |
+ |
#[must_use]
|
|
151 |
+ |
pub fn level(self) -> Option<Level> {
|
|
152 |
+ |
match self {
|
|
153 |
+ |
Self::Text => Some(Level::Leaf),
|
|
154 |
+ |
Self::Inlines => Some(Level::Run),
|
|
155 |
+ |
Self::Blocks | Self::Collection(_) => Some(Level::Block),
|
|
156 |
+ |
Self::Opaque => None,
|
|
157 |
+ |
}
|
|
158 |
+ |
}
|
|
159 |
+ |
}
|
|
160 |
+ |
|
|
161 |
+ |
/// What an element of a description may hold.
|
|
162 |
+ |
///
|
|
163 |
+ |
/// Implemented for every element rather than for a bucket of them, which is the
|
|
164 |
+ |
/// difference between this and the two-tier shape the review also costed. Two
|
|
165 |
+ |
/// tiers leave [`Node::List`], [`Node::Table`] and [`Node::Form`] in neither,
|
|
166 |
+ |
/// so all three become special-cased members with hardcoded children and the
|
|
167 |
+ |
/// enumeration problem moves up a level. [`Containment::Collection`] states
|
|
168 |
+ |
/// outright what that shape has to special-case.
|
|
169 |
+ |
pub trait Element {
|
|
170 |
+ |
/// What this may hold. Most of the vocabulary is a label and takes the
|
|
171 |
+ |
/// default.
|
|
172 |
+ |
fn containment(&self) -> Containment {
|
|
173 |
+ |
Containment::Text
|
|
174 |
+ |
}
|
|
175 |
+ |
}
|
|
176 |
+ |
|
|
177 |
+ |
impl Element for Node {
|
|
178 |
+ |
fn containment(&self) -> Containment {
|
|
179 |
+ |
match self {
|
|
180 |
+ |
// Labels. The default, spelled out here only because a match has to
|
|
181 |
+ |
// be exhaustive.
|
|
182 |
+ |
Self::Heading { .. } | Self::Text { .. } | Self::Rich { .. } | Self::Token(_) => {
|
|
183 |
+ |
Containment::Text
|
|
184 |
+ |
}
|
|
185 |
+ |
// An act is a label and an address. The address is not content.
|
|
186 |
+ |
Self::Act(_) => Containment::Text,
|
|
187 |
+ |
// A notice is a sentence with a tone. It was never allowed to hold
|
|
188 |
+ |
// anything and this does not change that.
|
|
189 |
+ |
Self::Notice { .. } => Containment::Text,
|
|
190 |
+ |
// A sentence and, sometimes, the way out. Two things on a line.
|
|
191 |
+ |
Self::StandIn { .. } => Containment::Inlines,
|
|
192 |
+ |
// One field, so this is the field's own answer rather than a
|
|
193 |
+ |
// collection of one.
|
|
194 |
+ |
Self::Field(field) => field.containment(),
|
|
195 |
+ |
Self::Form { .. } => Containment::Collection(Of::Fields),
|
|
196 |
+ |
Self::List { .. } => Containment::Collection(Of::Rows),
|
|
197 |
+ |
// The columns are the table's schema and not its content, the way
|
|
198 |
+ |
// `Screen::discovery` is metadata rather than a region. What a
|
|
199 |
+ |
// table *holds* is rows.
|
|
200 |
+ |
Self::Table { .. } => Containment::Collection(Of::TableRows),
|
|
201 |
+ |
Self::Select { .. } => Containment::Collection(Of::Choices),
|
|
202 |
+ |
Self::Meter(meter) => meter.containment(),
|
|
203 |
+ |
Self::Stats { .. } => Containment::Collection(Of::Figures),
|
|
204 |
+ |
Self::Region(slot) => slot.containment(),
|
|
205 |
+ |
}
|
|
206 |
+ |
}
|
|
207 |
+ |
}
|
|
208 |
+ |
|
|
209 |
+ |
impl Element for Slot {
|
|
210 |
+ |
fn containment(&self) -> Containment {
|
|
211 |
+ |
// The one place `Opaque` is reached, and the reason it exists. A
|
|
212 |
+ |
// bespoke region is filled by the host, so the description names the
|
|
213 |
+ |
// place and says nothing about what goes in it. Every other region
|
|
214 |
+ |
// holds blocks, regions included, which is the nesting that was always
|
|
215 |
+ |
// accepted and that the row rule never touched.
|
|
216 |
+ |
if matches!(self.kind, RegionKind::Bespoke { .. }) {
|
|
217 |
+ |
Containment::Opaque
|
|
218 |
+ |
} else {
|
|
219 |
+ |
Containment::Blocks
|
|
220 |
+ |
}
|
|
221 |
+ |
}
|
|
222 |
+ |
}
|
|
223 |
+ |
|
|
224 |
+ |
impl Element for Row {
|
|
225 |
+ |
fn containment(&self) -> Containment {
|
|
226 |
+ |
Containment::Inlines
|
|
227 |
+ |
}
|
|
228 |
+ |
}
|
|
229 |
+ |
|
|
230 |
+ |
impl Element for Cells {
|
|
231 |
+ |
fn containment(&self) -> Containment {
|
|
232 |
+ |
Containment::Collection(Of::Cells)
|
|
233 |
+ |
}
|
|
234 |
+ |
}
|
|
235 |
+ |
|
|
236 |
+ |
impl Element for Cell {
|
|
237 |
+ |
fn containment(&self) -> Containment {
|
|
238 |
+ |
Containment::Inlines
|
|
239 |
+ |
}
|
|
240 |
+ |
}
|
|
241 |
+ |
|
|
242 |
+ |
impl Element for Field {
|
|
243 |
+ |
fn containment(&self) -> Containment {
|
|
244 |
+ |
if self.kind.offers_options() {
|
|
245 |
+ |
Containment::Collection(Of::Choices)
|
|
246 |
+ |
} else {
|
|
247 |
+ |
Containment::Text
|
|
248 |
+ |
}
|
|
249 |
+ |
}
|
|
250 |
+ |
}
|
|
251 |
+ |
|
|
252 |
+ |
// The leaves. Each takes the default, and each says so by name rather than by
|
|
253 |
+ |
// silence, because "nobody wrote an impl" and "this holds a label" look the
|
|
254 |
+ |
// same from outside and only one of them is a decision.
|
|
255 |
+ |
impl Element for Act {}
|
|
256 |
+ |
impl Element for Tag {}
|
|
257 |
+ |
impl Element for Figure {}
|
|
258 |
+ |
impl Element for Meter {}
|
|
259 |
+ |
impl Element for Choice {}
|
|
260 |
+ |
impl Element for Column {}
|
|
261 |
+ |
impl Element for Prose {}
|
|
262 |
+ |
|
|
263 |
+ |
#[cfg(test)]
|
|
264 |
+ |
mod tests {
|
|
265 |
+ |
use super::*;
|
|
266 |
+ |
use crate::layout;
|
|
267 |
+ |
use crate::screen::Action;
|
|
268 |
+ |
|
|
269 |
+ |
/// Every element type, as a value, so the ladder test can ask each one.
|
|
270 |
+ |
///
|
|
271 |
+ |
/// A list rather than a derive because the question is about the vocabulary
|
|
272 |
+ |
/// and not about any one screen: adding a `Node` member and not adding it
|
|
273 |
+ |
/// here is caught by the exhaustive match in [`Element for Node`], which is
|
|
274 |
+ |
/// where a new member has to be answered for anyway.
|
|
275 |
+ |
fn every_node() -> Vec<Node> {
|
|
276 |
+ |
vec![
|
|
277 |
+ |
Node::page("t"),
|
|
278 |
+ |
Node::text("t"),
|
|
279 |
+ |
Node::rich("*t*"),
|
|
280 |
+ |
Node::Act(Act::new("go", Action::get("/"))),
|
|
281 |
+ |
Node::Token(Tag::badge("tag")),
|
|
282 |
+ |
Node::Notice {
|
|
283 |
+ |
kind: layout::Notice::Banner,
|
|
284 |
+ |
tone: layout::Tone::Neutral,
|
|
285 |
+ |
text: "t".into(),
|
|
286 |
+ |
},
|
|
287 |
+ |
Node::StandIn {
|
|
288 |
+ |
state: layout::Readiness::Empty,
|
|
289 |
+ |
message: "nothing yet".into(),
|
|
290 |
+ |
act: None,
|
|
291 |
+ |
},
|
|
292 |
+ |
Node::Field(Box::new(Field::new(
|
|
293 |
+ |
layout::FieldKind::Text,
|
|
294 |
+ |
"name",
|
|
295 |
+ |
"Name",
|
|
296 |
+ |
))),
|
|
297 |
+ |
Node::Form {
|
|
298 |
+ |
action: Action::post("/"),
|
|
299 |
+ |
submit: "Save".into(),
|
|
300 |
+ |
fields: Vec::new(),
|
|
301 |
+ |
},
|
|
302 |
+ |
Node::List {
|
|
303 |
+ |
rows: Vec::new(),
|
|
304 |
+ |
more: None,
|
|
305 |
+ |
},
|
|
306 |
+ |
Node::Table {
|
|
307 |
+ |
columns: Vec::new(),
|
|
308 |
+ |
rows: Vec::new(),
|
|
309 |
+ |
},
|
|
310 |
+ |
Node::Select {
|
|
311 |
+ |
kind: layout::Selector::Tabs,
|
|
312 |
+ |
options: Vec::new(),
|
|
313 |
+ |
chosen: None,
|
|
314 |
+ |
action: None,
|
|
315 |
+ |
},
|
|
316 |
+ |
Node::Meter(Meter::new(1, 2)),
|
|
317 |
+ |
Node::Stats {
|
|
318 |
+ |
figures: Vec::new(),
|
|
319 |
+ |
},
|
|
320 |
+ |
Node::Region(Slot::new("r", RegionKind::Pane)),
|
|
321 |
+ |
]
|
|
322 |
+ |
}
|
|
323 |
+ |
|
|
324 |
+ |
#[test]
|
|
325 |
+ |
fn the_containment_ladder_only_goes_down() {
|
|
326 |
+ |
// Rule 1. An inline run may not reach blocks or collections, so depth
|
|
327 |
+ |
// below a line is one and a run is always drawable on one wrapped line.
|
|
328 |
+ |
// This is the constrained-consumer test: a terminal draws a cell
|
|
329 |
+ |
// without knowing what is in it.
|
|
330 |
+ |
for node in every_node() {
|
|
331 |
+ |
if node.containment() != Containment::Text {
|
|
332 |
+ |
continue;
|
|
333 |
+ |
}
|
|
334 |
+ |
// A node that may sit in a run is a leaf, by construction. Nothing
|
|
335 |
+ |
// to assert beyond the classification itself being total.
|
|
336 |
+ |
assert_eq!(node.containment().level(), Some(Level::Leaf));
|
|
337 |
+ |
}
|
|
338 |
+ |
|
|
339 |
+ |
// Rule 2. A collection's element type is fixed and its element sits
|
|
340 |
+ |
// strictly below a collection: never another collection, never blocks.
|
|
341 |
+ |
for of in [
|
|
342 |
+ |
Of::Rows,
|
|
343 |
+ |
Of::TableRows,
|
|
344 |
+ |
Of::Cells,
|
|
345 |
+ |
Of::Fields,
|
|
346 |
+ |
Of::Figures,
|
|
347 |
+ |
Of::Choices,
|
|
348 |
+ |
] {
|
|
349 |
+ |
let inner = of.element_containment();
|
|
350 |
+ |
// Fields are the one collection whose element is itself a
|
|
351 |
+ |
// collection, and it is the terminating one: a choice is a label.
|
|
352 |
+ |
if let Containment::Collection(nested) = inner {
|
|
353 |
+ |
assert_eq!(
|
|
354 |
+ |
nested.element_containment(),
|
|
355 |
+ |
Containment::Text,
|
|
356 |
+ |
"{of:?} nests {nested:?}, which must terminate in text"
|
|
357 |
+ |
);
|
|
358 |
+ |
assert_ne!(nested, of, "{of:?} holds itself");
|
|
359 |
+ |
continue;
|
|
360 |
+ |
}
|
|
361 |
+ |
assert!(
|
|
362 |
+ |
matches!(inner, Containment::Text | Containment::Inlines),
|
|
363 |
+ |
"{of:?} holds {inner:?}, which is not below a collection"
|
|
364 |
+ |
);
|
|
365 |
+ |
}
|
|
366 |
+ |
|
|
367 |
+ |
// Rule 3. Blocks may contain blocks, and a region is the one that does.
|
|
368 |
+ |
let region = Node::Region(Slot::new("r", RegionKind::Pane));
|
|
369 |
+ |
assert_eq!(region.containment(), Containment::Blocks);
|
|
370 |
+ |
}
|
|
371 |
+ |
|
|
372 |
+ |
#[test]
|
|
373 |
+ |
fn a_run_holds_no_blocks() {
|
|
374 |
+ |
// The rule stated the other way round, over the members that are runs.
|
|
375 |
+ |
// A row and a cell hold parts on a line; neither may hold a list.
|
|
376 |
+ |
for run in [Row::new("r").containment(), Cell::new("c").containment()] {
|
|
377 |
+ |
assert_eq!(run, Containment::Inlines);
|
|
378 |
+ |
assert_eq!(run.level(), Some(Level::Run));
|
|
379 |
+ |
assert!(run.level() < Containment::Blocks.level());
|
|
380 |
+ |
}
|
|
381 |
+ |
}
|
|
382 |
+ |
|
|
383 |
+ |
#[test]
|
|
384 |
+ |
fn most_of_the_vocabulary_declares_nothing_and_is_text() {
|
|
385 |
+ |
// The default is what makes this cheap: a leaf writes no impl body.
|
|
386 |
+ |
assert_eq!(Tag::badge("t").containment(), Containment::Text);
|
|
387 |
+ |
assert_eq!(
|
|
388 |
+ |
Act::new("go", Action::get("/")).containment(),
|
|
389 |
+ |
Containment::Text
|
|
390 |
+ |
);
|
|
391 |
+ |
assert_eq!(Figure::new("7", "tasks").containment(), Containment::Text);
|
|
392 |
+ |
assert_eq!(Meter::new(1, 2).containment(), Containment::Text);
|
|
393 |
+ |
}
|
|
394 |
+ |
|
|
395 |
+ |
#[test]
|
|
396 |
+ |
fn a_field_holds_its_options_only_when_it_has_any() {
|
|
397 |
+ |
// The one element whose containment depends on its own state, which is
|
|
398 |
+ |
// why the trait takes `&self` rather than being an associated const.
|
|
399 |
+ |
let plain = Field::new(layout::FieldKind::Text, "name", "Name");
|
|
400 |
+ |
assert_eq!(plain.containment(), Containment::Text);
|
|
401 |
+ |
|
|
402 |
+ |
let choosing = Field::select("priority", "Priority", vec![Choice::plain("high")]);
|
|
403 |
+ |
assert_eq!(choosing.containment(), Containment::Collection(Of::Choices));
|
|
404 |
+ |
assert!(choosing.kind.offers_options());
|
|
405 |
+ |
}
|
|
406 |
+ |
|
|
407 |
+ |
#[test]
|
|
408 |
+ |
fn a_bespoke_region_is_opaque_rather_than_an_exception() {
|
|
409 |
+ |
// Under the enumeration this was a hole in the rule. Here it answers
|
|
410 |
+ |
// the same question every other element answers, and the answer is
|
|
411 |
+ |
// "the app fills it".
|
|
412 |
+ |
let bespoke = Node::Region(Slot::bespoke("canvas", "editor"));
|
|
413 |
+ |
assert_eq!(bespoke.containment(), Containment::Opaque);
|
|
414 |
+ |
// Off the ladder, so it can neither be reached into nor reach out.
|
|
415 |
+ |
assert_eq!(bespoke.containment().level(), None);
|
|
416 |
+ |
}
|
|
417 |
+ |
}
|