nandi/jolt-nativepublic Fork 0
3dd441e764bc6b0123d9b33fe9871dcf7ac150b6
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

layout.rs · 624 lines · 23.2 KBRust Blame HistoryRaw
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago1//! Two sizes per node, and how a box shares out what it has.
2//!
3//! Every node answers a *natural* size — what it would like — and a *minimum*
4//! — what it can survive on. A container hands out its natural sizes when there
5//! is room, shrinks them proportionally toward the minimums when there is not,
6//! and gives the surplus to whoever asked to expand. `:width-request` and
7//! `:height-request` are a floor on both numbers, so asking for four rows gets
8//! four rows even when space is short.
9//!
10//! Nothing here touches a terminal or the screen grid: sizes are arithmetic on
11//! the tree, which is why the layout tests below need no TTY.
12
Keep a glyph whole, selector and all, and copy it across whole 3dd441e nandi 17d ago13use crate::screen::{glyph_cols, glyphs, text_cols, Rect};
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago14use crate::tree::{Props, Tag, Tree};
15
16/// How a child that is not filling its cross axis sits in the space it was
17/// given. `:halign` and `:valign` in the props.
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum Align {
20 Fill,
21 Start,
22 Center,
23 End,
24}
25
26impl Align {
27 pub fn parse(text: &str) -> Self {
28 match text {
29 "start" => Self::Start,
30 "center" | "centre" => Self::Center,
31 "end" => Self::End,
32 _ => Self::Fill,
33 }
34 }
35
36 /// Where a span of `size` sits inside `avail`.
37 fn offset(self, size: u16, avail: u16) -> u16 {
38 let slack = avail.saturating_sub(size);
39 match self {
40 Self::Fill | Self::Start => 0,
41 Self::Center => slack / 2,
42 Self::End => slack,
43 }
44 }
45}
46
47/// Whether a box stacks its children across or down.
48pub fn horizontal(props: &Props) -> bool {
49 props.str("orientation") == "horizontal"
50}
51
52/// The cells a node gives up on each side before its content starts. `:margin`
53/// and `:padding` are one inset here — a terminal cell has no border between
54/// them to tell them apart, and a caller that sets both means both.
55pub fn inset(tag: &Tag, props: &Props) -> u16 {
56 let own = props.cells("margin", 0) + props.cells("padding", 0);
57 // A frame — and an overlay, which is a frame that floats — spends a cell a
58 // side on its border.
59 own + if matches!(tag, Tag::Frame | Tag::Overlay) {
60 1
61 } else {
62 0
63 }
64}
65
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 17d ago66/// What a `:reaction` reads as: the glyph, and the tally when there is one.
67/// A pill with no count is the chip you press to put one there — the same
68/// picture the picker offers, which is the point of it being the same node.
69pub fn pill_text(props: &Props) -> String {
70 let glyph = props.str("emoji");
71 match props.cells("count", 0) {
72 0 => glyph.to_owned(),
73 n => format!("{glyph} {n}"),
74 }
75}
76
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago77/// Break `text` to `width` columns, on spaces where it can and mid-word where
78/// it must. Explicit newlines are always breaks.
79pub fn wrap(text: &str, width: u16) -> Vec<String> {
80 if width == 0 {
81 return Vec::new();
82 }
83 let width = width as usize;
84 let mut lines = Vec::new();
85 for paragraph in text.split('\n') {
86 let mut line = String::new();
87 let mut len = 0usize;
88 for word in paragraph.split(' ') {
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 17d ago89 // In columns, not characters: an emoji is drawn two cells wide, so
90 // a line of them measured by character is twice the width it was
91 // wrapped to and runs off the edge.
92 let word_len = text_cols(word) as usize;
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago93 if len > 0 && len + 1 + word_len > width {
94 lines.push(std::mem::take(&mut line));
95 len = 0;
96 }
97 if word_len > width {
98 // Longer than the whole line: break it where the line ends
99 // rather than let it run off the edge.
Keep a glyph whole, selector and all, and copy it across whole 3dd441e nandi 17d ago100 for glyph in glyphs(word) {
101 let cols = glyph_cols(&glyph) as usize;
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 17d ago102 if len + cols > width && len > 0 {
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago103 lines.push(std::mem::take(&mut line));
104 len = 0;
105 }
Keep a glyph whole, selector and all, and copy it across whole 3dd441e nandi 17d ago106 line.push_str(&glyph);
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 17d ago107 len += cols;
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago108 }
109 continue;
110 }
111 if len > 0 {
112 line.push(' ');
113 len += 1;
114 }
115 line.push_str(word);
116 len += word_len;
117 }
118 lines.push(line);
119 }
120 lines
121}
122
123fn columns(text: &str) -> u16 {
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 17d ago124 text.split('\n').map(text_cols).max().unwrap_or(0)
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago125}
126
127/// The longest single word — a label cannot usefully be narrower than this.
128fn longest_word(text: &str) -> u16 {
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 17d ago129 text.split([' ', '\n']).map(text_cols).max().unwrap_or(0)
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago130}
131
132/// The text an entry shows: its own, or its placeholder when it has none.
133pub fn entry_text(props: &Props) -> String {
134 let text = props.str("text");
135 if text.is_empty() {
136 props.str("placeholder").to_owned()
137 } else {
138 text.to_owned()
139 }
140}
141
142/// A node's content size before its own request or inset is applied.
143fn intrinsic_width(tree: &Tree, id: u32, minimum: bool) -> u16 {
144 let tag = tree.tag(id);
145 let props = tree.props(id);
146 let text = props.label();
147 match tag {
148 Tag::Button => columns(text).saturating_add(4),
149 Tag::CheckButton => columns(text).saturating_add(4),
150 Tag::Entry => {
151 let want = columns(&entry_text(&props)).saturating_add(1).max(12);
152 if minimum {
153 want.min(6)
154 } else {
155 want
156 }
157 }
158 Tag::Label | Tag::Title | Tag::DimLabel => {
159 if minimum {
160 longest_word(text)
161 } else {
162 columns(text)
163 }
164 }
Share a column's rows out at the width it has, not at its own height 5acc801 nandi 18d ago165 // An unknown tag with nothing under it paints its own text, so it has
166 // to be measured as the label it turns out to be — a widget given no
167 // room is as invisible as one that was never painted.
Let a backlog keep its place, and leave room for what is under it 68910bd nandi 18d ago168 //
169 // Unless it names a picture. An `:avatar`'s label is the nick behind
170 // the face and an `:image`'s is its alt text: words for something that
171 // cannot be drawn here, and in frq's case words already on the row
172 // beside it, which is how every sender came out named twice.
173 Tag::Unknown(_) if tree.child_count(id) == 0 && !has_picture(&props) => {
Share a column's rows out at the width it has, not at its own height 5acc801 nandi 18d ago174 if minimum {
175 longest_word(props.label())
176 } else {
177 columns(props.label())
178 }
179 }
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago180 Tag::Separator => 1,
181 Tag::Spacer => props.cells("size", 1),
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 17d ago182 Tag::Emoji => text_cols(props.str("emoji")),
183 Tag::Reaction => text_cols(&pill_text(&props)),
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago184 Tag::Progress => {
185 if minimum {
186 4
187 } else {
188 20
189 }
190 }
191 Tag::Spinner => 1,
192 Tag::Listbox => tree
193 .children(id)
194 .iter()
195 .map(|c| intrinsic_width(tree, *c, minimum).saturating_add(2))
196 .max()
197 .unwrap_or(0),
198 // Every container measures its children the same way; only the axis
199 // the sum runs along differs.
200 Tag::Box | Tag::Window | Tag::Frame | Tag::Scroll | Tag::Overlay | Tag::Unknown(_) => {
201 let children = tree.children(id);
202 let spacing = props.cells("spacing", 0);
203 let sizes = children
204 .iter()
205 .map(|c| width(tree, *c, minimum))
206 .collect::<Vec<_>>();
207 let content = if horizontal(&props) && matches!(tag, Tag::Box) {
208 let gaps = spacing.saturating_mul(sizes.len().saturating_sub(1) as u16);
209 sizes.iter().fold(gaps, |a, b| a.saturating_add(*b))
210 } else {
211 sizes.into_iter().max().unwrap_or(0)
212 };
213 // A frame's heading sits in its top edge, so it is part of how wide
214 // the frame has to be — a box narrower than its own label reads as
215 // a truncated one.
216 if matches!(tag, Tag::Frame | Tag::Overlay) {
217 content.max(columns(props.label()).saturating_add(2))
218 } else {
219 content
220 }
221 }
222 }
223}
224
Let a backlog keep its place, and leave room for what is under it 68910bd nandi 18d ago225/// True for a node whose label describes a picture rather than being text to
226/// paint — an `:avatar`, an `:image`, a live `:feed`.
227fn has_picture(props: &Props) -> bool {
228 props.has("src") || props.has("feed")
229}
230
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago231/// A node's natural or minimum width, requests and insets included.
Let a backlog keep its place, and leave room for what is under it 68910bd nandi 18d ago232///
233/// A `width-request` is the width, not a floor under it. In a window it can be
234/// a floor, because a label wraps to whatever it is given and a column's
235/// natural width is therefore whatever the layout decides. Here a label's
236/// natural width is its whole line, so a column that holds one is as wide as
237/// the longest thing anybody ever said in it — and `max` then hands the
238/// sidebar the screen and leaves the conversation beside it ten cells to wrap
239/// in. Asking for a width is the caller saying how wide the column is; nothing
240/// else in a terminal can say it for them.
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago241pub fn width(tree: &Tree, id: u32, minimum: bool) -> u16 {
242 let props = tree.props(id);
Let a backlog keep its place, and leave room for what is under it 68910bd nandi 18d ago243 let requested = props.cells("width-request", 0);
244 if requested > 0 {
245 return requested;
246 }
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago247 let pad = inset(&tree.tag(id), &props).saturating_mul(2);
Let a backlog keep its place, and leave room for what is under it 68910bd nandi 18d ago248 intrinsic_width(tree, id, minimum).saturating_add(pad)
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago249}
250
251/// How tall `id` is when laid out `avail` columns wide.
252///
253/// Height depends on width — that is what wrapping means — so there is no
254/// natural height to ask for on its own.
255pub fn height_for_width(tree: &Tree, id: u32, avail: u16) -> u16 {
256 let tag = tree.tag(id);
257 let props = tree.props(id);
258 let pad = inset(&tag, &props);
259 let inner = avail.saturating_sub(pad.saturating_mul(2));
260 let content = match tag {
261 Tag::Label | Tag::Title | Tag::DimLabel => wrap(props.label(), inner).len() as u16,
Let a backlog keep its place, and leave room for what is under it 68910bd nandi 18d ago262 Tag::Unknown(_) if tree.child_count(id) == 0 && !has_picture(&props) => {
Share a column's rows out at the width it has, not at its own height 5acc801 nandi 18d ago263 wrap(props.label(), inner).len() as u16
264 }
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 17d ago265 Tag::Button
266 | Tag::CheckButton
267 | Tag::Separator
268 | Tag::Progress
269 | Tag::Spinner
270 | Tag::Reaction
271 | Tag::Emoji => 1,
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago272 Tag::Entry => props.cells("rows", 1).max(1),
273 Tag::Spacer => props.cells("size", 1),
274 Tag::Listbox => tree.child_count(id) as u16,
275 Tag::Box | Tag::Window | Tag::Frame | Tag::Scroll | Tag::Overlay | Tag::Unknown(_) => {
276 let children = tree.children(id);
277 let spacing = props.cells("spacing", 0);
278 if horizontal(&props) && matches!(tag, Tag::Box) {
279 // Across: each child is measured at the width it will get.
Share a column's rows out at the width it has, not at its own height 5acc801 nandi 18d ago280 let shares = share(tree, id, inner, true, 0);
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago281 children
282 .iter()
283 .zip(shares)
284 .map(|(c, w)| height_for_width(tree, *c, w))
285 .max()
286 .unwrap_or(0)
287 } else {
288 let gaps = spacing.saturating_mul(children.len().saturating_sub(1) as u16);
289 children
290 .iter()
291 .map(|c| height_for_width(tree, *c, inner))
292 .fold(gaps, |a, b| a.saturating_add(b))
293 }
294 }
295 };
296 content
297 .saturating_add(pad.saturating_mul(2))
298 .max(props.cells("height-request", 0))
299}
300
Let a backlog keep its place, and leave room for what is under it 68910bd nandi 18d ago301/// The least `id` can be squeezed to at `avail` columns wide.
302///
303/// Down the page almost nothing can be shorter than it is: a label wrapped to
304/// four lines needs four. A `:scroll` is the exception, and the reason there is
305/// one — it is a viewport, so its height is whatever it is given and its
306/// content moves inside it.
307///
308/// It has to recurse, because the viewport is rarely the child being measured.
309/// In frq's chat screen the backlog is a scroll inside a column inside a row
310/// inside the screen, and a column that reported its natural height all the
311/// way up gave the layout nothing to take: the backlog kept every row it asked
312/// for and the separator and compose bar under it were painted past the bottom
313/// edge — a conversation you cannot type into.
314pub fn min_height_for_width(tree: &Tree, id: u32, avail: u16) -> u16 {
315 let tag = tree.tag(id);
316 let props = tree.props(id);
317 let pad = inset(&tag, &props);
318 let inner = avail.saturating_sub(pad.saturating_mul(2));
319 let content = match tag {
320 Tag::Scroll => 1,
321 Tag::Box | Tag::Window | Tag::Frame | Tag::Overlay | Tag::Unknown(_)
322 if tree.child_count(id) > 0 =>
323 {
324 let children = tree.children(id);
325 let spacing = props.cells("spacing", 0);
326 if horizontal(&props) && matches!(tag, Tag::Box) {
327 let shares = share(tree, id, inner, true, 0);
328 children
329 .iter()
330 .zip(shares)
331 .map(|(c, w)| min_height_for_width(tree, *c, w))
332 .max()
333 .unwrap_or(0)
334 } else {
335 let gaps = spacing.saturating_mul(children.len().saturating_sub(1) as u16);
336 children
337 .iter()
338 .map(|c| min_height_for_width(tree, *c, inner))
339 .fold(gaps, |a, b| a.saturating_add(b))
340 }
341 }
342 // Everything else is as short as it is tall.
343 _ => return height_for_width(tree, id, avail),
344 };
345 content
346 .saturating_add(pad.saturating_mul(2))
347 .max(props.cells("height-request", 0))
348}
349
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago350/// Share `avail` out among the children of `id` along one axis.
351///
352/// `across` picks the axis: true for a horizontal box sharing columns, false
353/// for a vertical one sharing rows. The rule is the same either way — natural
354/// sizes first, shrink proportionally toward the minimums when short, and the
355/// surplus to whoever set `:hexpand` / `:vexpand`.
Share a column's rows out at the width it has, not at its own height 5acc801 nandi 18d ago356///
357/// `cross` is the extent on the *other* axis, and sharing rows out cannot be
358/// done without it: how tall a child is depends on how wide it is, because
359/// that is what wrapping means. Passing the rows in its place measures every
360/// label at a column count of two or three, wraps it to a paragraph, and the
361/// overrun is then taken off the end — which paints a box's first child and
362/// drops every sibling after it. Unused when `across`, where a width does not
363/// depend on a height.
364pub fn share(tree: &Tree, id: u32, avail: u16, across: bool, cross: u16) -> Vec<u16> {
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago365 let children = tree.children(id);
366 if children.is_empty() {
367 return Vec::new();
368 }
369 let props = tree.props(id);
370 let spacing = props.cells("spacing", 0);
371 let gaps = spacing.saturating_mul((children.len() - 1) as u16);
372 let room = avail.saturating_sub(gaps) as i64;
373
374 let measure = |child: u32, minimum: bool| -> i64 {
375 if across {
376 width(tree, child, minimum) as i64
Let a backlog keep its place, and leave room for what is under it 68910bd nandi 18d ago377 } else if minimum {
378 min_height_for_width(tree, child, cross) as i64
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago379 } else {
Let a backlog keep its place, and leave room for what is under it 68910bd nandi 18d ago380 // A child's height depends on the width it gets, which the caller
381 // has already fixed by the time it asks.
Share a column's rows out at the width it has, not at its own height 5acc801 nandi 18d ago382 height_for_width(tree, child, cross) as i64
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago383 }
384 };
385
386 let nat: Vec<i64> = children.iter().map(|c| measure(*c, false)).collect();
387 let min: Vec<i64> = children
388 .iter()
389 .zip(&nat)
390 .map(|(c, n)| measure(*c, true).min(*n))
391 .collect();
392 let total: i64 = nat.iter().sum();
393 let mut out = nat.clone();
394
395 if total > room {
396 // Short: take the overrun out of whatever each child is willing to give
397 // up, in proportion to how much that is.
398 let mut over = total - room;
399 let slack: i64 = nat.iter().zip(&min).map(|(n, m)| n - m).sum();
400 if slack > 0 {
401 for i in 0..out.len() {
402 let give = ((nat[i] - min[i]) * over.min(slack)) / slack;
403 out[i] -= give;
404 }
405 over -= nat.iter().zip(&out).map(|(n, o)| n - o).sum::<i64>();
406 }
407 // Rounding, and children with no slack at all: take the rest off the
408 // end, which is where a terminal clips anyway.
409 let mut i = out.len();
410 while over > 0 && i > 0 {
411 i -= 1;
412 let give = (out[i] - min[i]).min(over);
413 out[i] -= give;
414 over -= give;
415 }
416 } else if total < room {
417 let key = if across { "hexpand" } else { "vexpand" };
418 let greedy: Vec<usize> = children
419 .iter()
420 .enumerate()
421 .filter(|(_, c)| tree.props(**c).bool(key, false))
422 .map(|(i, _)| i)
423 .collect();
424 if !greedy.is_empty() {
425 let extra = room - total;
426 let each = extra / greedy.len() as i64;
427 let mut rest = extra % greedy.len() as i64;
428 for i in greedy {
429 out[i] += each + if rest > 0 { 1 } else { 0 };
430 rest -= 1;
431 }
432 }
433 }
434 out.into_iter()
435 .map(|n| n.clamp(0, u16::MAX as i64) as u16)
436 .collect()
437}
438
439/// The rect a child of `size` gets inside `avail` on its cross axis.
440pub fn place(align: Align, size: u16, avail: u16) -> (u16, u16) {
441 match align {
442 Align::Fill => (0, avail),
443 other => {
444 let size = size.min(avail);
445 (other.offset(size, avail), size)
446 }
447 }
448}
449
450/// Lay the children of a box out inside `area`.
451pub fn children_rects(tree: &Tree, id: u32, area: Rect) -> Vec<Rect> {
452 let props = tree.props(id);
453 let across = horizontal(&props) && matches!(tree.tag(id), Tag::Box);
454 let spacing = props.cells("spacing", 0);
455 let children = tree.children(id);
Share a column's rows out at the width it has, not at its own height 5acc801 nandi 18d ago456 let shares = share(
457 tree,
458 id,
459 if across { area.w } else { area.h },
460 across,
461 if across { area.h } else { area.w },
462 );
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago463
464 let mut out = Vec::with_capacity(children.len());
465 let mut at = 0u16;
466 for (child, main) in children.iter().zip(shares) {
467 let cprops = tree.props(*child);
468 let rect = if across {
469 let want = height_for_width(tree, *child, main).max(cprops.cells("height-request", 0));
470 let (dy, h) = place(Align::parse(cprops.str("valign")), want, area.h);
471 Rect::new(
472 area.x.saturating_add(at),
473 area.y.saturating_add(dy),
474 main,
475 h,
476 )
477 } else {
478 let want = width(tree, *child, false);
479 let (dx, w) = place(Align::parse(cprops.str("halign")), want, area.w);
480 Rect::new(
481 area.x.saturating_add(dx),
482 area.y.saturating_add(at),
483 w,
484 main,
485 )
486 };
487 out.push(rect);
488 at = at.saturating_add(main).saturating_add(spacing);
489 }
490 out
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496 use crate::tree::Value;
497
498 fn label(tree: &mut Tree, parent: u32, text: &str) -> u32 {
499 let id = tree.new_node("label");
500 tree.set(id, "label", Value::Str(text.into()));
501 tree.append(parent, id);
502 id
503 }
504
505 #[test]
506 fn wrapping_breaks_on_spaces_and_mid_word_when_it_must() {
507 assert_eq!(wrap("one two three", 7), vec!["one two", "three"]);
508 assert_eq!(
509 wrap("antidisestablishment", 6),
510 vec!["antidi", "sestab", "lishme", "nt"]
511 );
512 assert_eq!(wrap("a\nb", 10), vec!["a", "b"]);
513 }
514
515 #[test]
516 fn a_label_is_as_wide_as_its_text_and_as_narrow_as_its_longest_word() {
517 let mut tree = Tree::new();
518 let root = tree.root();
519 let id = label(&mut tree, root, "one two three");
520 assert_eq!(width(&tree, id, false), 13);
521 assert_eq!(width(&tree, id, true), 5);
522 assert_eq!(height_for_width(&tree, id, 7), 2);
523 }
524
525 #[test]
526 fn a_width_request_is_a_floor_on_both_sizes() {
527 let mut tree = Tree::new();
528 let root = tree.root();
529 let id = label(&mut tree, root, "hi");
530 tree.set(id, "width-request", Value::Num(20.0));
531 assert_eq!(width(&tree, id, false), 20);
532 assert_eq!(width(&tree, id, true), 20);
533 }
534
535 #[test]
536 fn a_height_request_of_four_rows_gets_four_rows() {
537 let mut tree = Tree::new();
538 let root = tree.root();
539 let id = label(&mut tree, root, "hi");
540 tree.set(id, "height-request", Value::Num(4.0));
541 assert_eq!(height_for_width(&tree, id, 10), 4);
542 }
543
544 #[test]
545 fn a_horizontal_box_gives_the_surplus_to_whoever_expands() {
546 let mut tree = Tree::new();
547 let row = tree.new_node("hbox");
548 tree.set(row, "orientation", Value::Str("horizontal".into()));
549 let root = tree.root();
550 tree.append(root, row);
551 let a = label(&mut tree, row, "aa");
552 let b = label(&mut tree, row, "bb");
553 tree.set(b, "hexpand", Value::Bool(true));
Share a column's rows out at the width it has, not at its own height 5acc801 nandi 18d ago554 assert_eq!(share(&tree, row, 20, true, 1), vec![2, 18]);
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago555 let _ = a;
556 }
557
558 #[test]
559 fn a_short_box_shrinks_toward_the_minimums_rather_than_clipping_the_first_child() {
560 let mut tree = Tree::new();
561 let row = tree.new_node("hbox");
562 tree.set(row, "orientation", Value::Str("horizontal".into()));
563 let root = tree.root();
564 tree.append(root, row);
565 label(&mut tree, row, "one two");
566 label(&mut tree, row, "three four");
567 // 17 natural, 10 offered: both give up some, neither goes under its
568 // longest word.
Share a column's rows out at the width it has, not at its own height 5acc801 nandi 18d ago569 let shares = share(&tree, row, 10, true, 1);
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago570 assert_eq!(shares.iter().sum::<u16>(), 10);
571 assert!(shares[0] >= 3 && shares[1] >= 5, "{shares:?}");
572 }
573
574 #[test]
575 fn spacing_comes_off_the_room_before_it_is_shared() {
576 let mut tree = Tree::new();
577 let row = tree.new_node("hbox");
578 tree.set(row, "orientation", Value::Str("horizontal".into()));
579 tree.set(row, "spacing", Value::Num(2.0));
580 let root = tree.root();
581 tree.append(root, row);
582 let a = label(&mut tree, row, "aa");
583 let b = label(&mut tree, row, "bb");
584 tree.set(a, "hexpand", Value::Bool(true));
585 tree.set(b, "hexpand", Value::Bool(true));
Share a column's rows out at the width it has, not at its own height 5acc801 nandi 18d ago586 assert_eq!(share(&tree, row, 12, true, 1), vec![5, 5]);
587 }
588
589 #[test]
590 fn a_column_shares_its_rows_out_at_the_width_it_has() {
591 // A column two rows tall and thirty columns wide holds two labels, and
592 // each is one row at that width. Measured against the rows instead —
593 // as this did — "the second line" wraps to five, the overrun comes off
594 // the end, and the second child is handed nothing: a box that paints
595 // its first child and drops the rest, which is what the chats list did
596 // to every Open button in it.
597 let mut tree = Tree::new();
598 let col = tree.new_node("vbox");
599 tree.set(col, "orientation", Value::Str("vertical".into()));
600 let root = tree.root();
601 tree.append(root, col);
602 label(&mut tree, col, "the first line");
603 label(&mut tree, col, "the second line");
604 assert_eq!(share(&tree, col, 2, false, 30), vec![1, 1]);
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago605 }
606
607 #[test]
608 fn a_centred_child_sits_in_the_middle_of_its_row() {
609 assert_eq!(place(Align::Center, 4, 10), (3, 4));
610 assert_eq!(place(Align::End, 4, 10), (6, 4));
611 assert_eq!(place(Align::Fill, 4, 10), (0, 10));
612 }
613
614 #[test]
615 fn a_frame_spends_a_cell_a_side_on_its_border() {
616 let mut tree = Tree::new();
617 let frame = tree.new_node("frame");
618 let root = tree.root();
619 tree.append(root, frame);
620 label(&mut tree, frame, "hi");
621 assert_eq!(width(&tree, frame, false), 4);
622 assert_eq!(height_for_width(&tree, frame, 4), 3);
623 }
624}