nandi/jolt-nativepublic Fork 0
258bbc5161b93d580a4c84363acabe63b0624e88
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 · 609 lines · 22.4 KBRust Blame HistoryRaw
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d 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
13use crate::screen::Rect;
14use 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
66/// Break `text` to `width` columns, on spaces where it can and mid-word where
67/// it must. Explicit newlines are always breaks.
68pub fn wrap(text: &str, width: u16) -> Vec<String> {
69 if width == 0 {
70 return Vec::new();
71 }
72 let width = width as usize;
73 let mut lines = Vec::new();
74 for paragraph in text.split('\n') {
75 let mut line = String::new();
76 let mut len = 0usize;
77 for word in paragraph.split(' ') {
78 let word_len = word.chars().count();
79 if len > 0 && len + 1 + word_len > width {
80 lines.push(std::mem::take(&mut line));
81 len = 0;
82 }
83 if word_len > width {
84 // Longer than the whole line: break it where the line ends
85 // rather than let it run off the edge.
86 for ch in word.chars() {
87 if len == width {
88 lines.push(std::mem::take(&mut line));
89 len = 0;
90 }
91 line.push(ch);
92 len += 1;
93 }
94 continue;
95 }
96 if len > 0 {
97 line.push(' ');
98 len += 1;
99 }
100 line.push_str(word);
101 len += word_len;
102 }
103 lines.push(line);
104 }
105 lines
106}
107
108fn columns(text: &str) -> u16 {
109 text.split('\n')
110 .map(|line| line.chars().count())
111 .max()
112 .unwrap_or(0)
113 .min(u16::MAX as usize) as u16
114}
115
116/// The longest single word — a label cannot usefully be narrower than this.
117fn longest_word(text: &str) -> u16 {
118 text.split([' ', '\n'])
119 .map(|w| w.chars().count())
120 .max()
121 .unwrap_or(0)
122 .min(u16::MAX as usize) as u16
123}
124
125/// The text an entry shows: its own, or its placeholder when it has none.
126pub fn entry_text(props: &Props) -> String {
127 let text = props.str("text");
128 if text.is_empty() {
129 props.str("placeholder").to_owned()
130 } else {
131 text.to_owned()
132 }
133}
134
135/// A node's content size before its own request or inset is applied.
136fn intrinsic_width(tree: &Tree, id: u32, minimum: bool) -> u16 {
137 let tag = tree.tag(id);
138 let props = tree.props(id);
139 let text = props.label();
140 match tag {
141 Tag::Button => columns(text).saturating_add(4),
142 Tag::CheckButton => columns(text).saturating_add(4),
143 Tag::Entry => {
144 let want = columns(&entry_text(&props)).saturating_add(1).max(12);
145 if minimum {
146 want.min(6)
147 } else {
148 want
149 }
150 }
151 Tag::Label | Tag::Title | Tag::DimLabel => {
152 if minimum {
153 longest_word(text)
154 } else {
155 columns(text)
156 }
157 }
Share a column's rows out at the width it has, not at its own height 5acc801 nandi 17d ago158 // An unknown tag with nothing under it paints its own text, so it has
159 // to be measured as the label it turns out to be — a widget given no
160 // 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 17d ago161 //
162 // Unless it names a picture. An `:avatar`'s label is the nick behind
163 // the face and an `:image`'s is its alt text: words for something that
164 // cannot be drawn here, and in frq's case words already on the row
165 // beside it, which is how every sender came out named twice.
166 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 17d ago167 if minimum {
168 longest_word(props.label())
169 } else {
170 columns(props.label())
171 }
172 }
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago173 Tag::Separator => 1,
174 Tag::Spacer => props.cells("size", 1),
175 Tag::Progress => {
176 if minimum {
177 4
178 } else {
179 20
180 }
181 }
182 Tag::Spinner => 1,
183 Tag::Listbox => tree
184 .children(id)
185 .iter()
186 .map(|c| intrinsic_width(tree, *c, minimum).saturating_add(2))
187 .max()
188 .unwrap_or(0),
189 // Every container measures its children the same way; only the axis
190 // the sum runs along differs.
191 Tag::Box | Tag::Window | Tag::Frame | Tag::Scroll | Tag::Overlay | Tag::Unknown(_) => {
192 let children = tree.children(id);
193 let spacing = props.cells("spacing", 0);
194 let sizes = children
195 .iter()
196 .map(|c| width(tree, *c, minimum))
197 .collect::<Vec<_>>();
198 let content = if horizontal(&props) && matches!(tag, Tag::Box) {
199 let gaps = spacing.saturating_mul(sizes.len().saturating_sub(1) as u16);
200 sizes.iter().fold(gaps, |a, b| a.saturating_add(*b))
201 } else {
202 sizes.into_iter().max().unwrap_or(0)
203 };
204 // A frame's heading sits in its top edge, so it is part of how wide
205 // the frame has to be — a box narrower than its own label reads as
206 // a truncated one.
207 if matches!(tag, Tag::Frame | Tag::Overlay) {
208 content.max(columns(props.label()).saturating_add(2))
209 } else {
210 content
211 }
212 }
213 }
214}
215
Let a backlog keep its place, and leave room for what is under it 68910bd nandi 17d ago216/// True for a node whose label describes a picture rather than being text to
217/// paint — an `:avatar`, an `:image`, a live `:feed`.
218fn has_picture(props: &Props) -> bool {
219 props.has("src") || props.has("feed")
220}
221
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago222/// 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 17d ago223///
224/// A `width-request` is the width, not a floor under it. In a window it can be
225/// a floor, because a label wraps to whatever it is given and a column's
226/// natural width is therefore whatever the layout decides. Here a label's
227/// natural width is its whole line, so a column that holds one is as wide as
228/// the longest thing anybody ever said in it — and `max` then hands the
229/// sidebar the screen and leaves the conversation beside it ten cells to wrap
230/// in. Asking for a width is the caller saying how wide the column is; nothing
231/// else in a terminal can say it for them.
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago232pub fn width(tree: &Tree, id: u32, minimum: bool) -> u16 {
233 let props = tree.props(id);
Let a backlog keep its place, and leave room for what is under it 68910bd nandi 17d ago234 let requested = props.cells("width-request", 0);
235 if requested > 0 {
236 return requested;
237 }
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago238 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 17d ago239 intrinsic_width(tree, id, minimum).saturating_add(pad)
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago240}
241
242/// How tall `id` is when laid out `avail` columns wide.
243///
244/// Height depends on width — that is what wrapping means — so there is no
245/// natural height to ask for on its own.
246pub fn height_for_width(tree: &Tree, id: u32, avail: u16) -> u16 {
247 let tag = tree.tag(id);
248 let props = tree.props(id);
249 let pad = inset(&tag, &props);
250 let inner = avail.saturating_sub(pad.saturating_mul(2));
251 let content = match tag {
252 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 17d ago253 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 17d ago254 wrap(props.label(), inner).len() as u16
255 }
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago256 Tag::Button | Tag::CheckButton | Tag::Separator | Tag::Progress | Tag::Spinner => 1,
257 Tag::Entry => props.cells("rows", 1).max(1),
258 Tag::Spacer => props.cells("size", 1),
259 Tag::Listbox => tree.child_count(id) as u16,
260 Tag::Box | Tag::Window | Tag::Frame | Tag::Scroll | Tag::Overlay | Tag::Unknown(_) => {
261 let children = tree.children(id);
262 let spacing = props.cells("spacing", 0);
263 if horizontal(&props) && matches!(tag, Tag::Box) {
264 // 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 17d ago265 let shares = share(tree, id, inner, true, 0);
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago266 children
267 .iter()
268 .zip(shares)
269 .map(|(c, w)| height_for_width(tree, *c, w))
270 .max()
271 .unwrap_or(0)
272 } else {
273 let gaps = spacing.saturating_mul(children.len().saturating_sub(1) as u16);
274 children
275 .iter()
276 .map(|c| height_for_width(tree, *c, inner))
277 .fold(gaps, |a, b| a.saturating_add(b))
278 }
279 }
280 };
281 content
282 .saturating_add(pad.saturating_mul(2))
283 .max(props.cells("height-request", 0))
284}
285
Let a backlog keep its place, and leave room for what is under it 68910bd nandi 17d ago286/// The least `id` can be squeezed to at `avail` columns wide.
287///
288/// Down the page almost nothing can be shorter than it is: a label wrapped to
289/// four lines needs four. A `:scroll` is the exception, and the reason there is
290/// one — it is a viewport, so its height is whatever it is given and its
291/// content moves inside it.
292///
293/// It has to recurse, because the viewport is rarely the child being measured.
294/// In frq's chat screen the backlog is a scroll inside a column inside a row
295/// inside the screen, and a column that reported its natural height all the
296/// way up gave the layout nothing to take: the backlog kept every row it asked
297/// for and the separator and compose bar under it were painted past the bottom
298/// edge — a conversation you cannot type into.
299pub fn min_height_for_width(tree: &Tree, id: u32, avail: u16) -> u16 {
300 let tag = tree.tag(id);
301 let props = tree.props(id);
302 let pad = inset(&tag, &props);
303 let inner = avail.saturating_sub(pad.saturating_mul(2));
304 let content = match tag {
305 Tag::Scroll => 1,
306 Tag::Box | Tag::Window | Tag::Frame | Tag::Overlay | Tag::Unknown(_)
307 if tree.child_count(id) > 0 =>
308 {
309 let children = tree.children(id);
310 let spacing = props.cells("spacing", 0);
311 if horizontal(&props) && matches!(tag, Tag::Box) {
312 let shares = share(tree, id, inner, true, 0);
313 children
314 .iter()
315 .zip(shares)
316 .map(|(c, w)| min_height_for_width(tree, *c, w))
317 .max()
318 .unwrap_or(0)
319 } else {
320 let gaps = spacing.saturating_mul(children.len().saturating_sub(1) as u16);
321 children
322 .iter()
323 .map(|c| min_height_for_width(tree, *c, inner))
324 .fold(gaps, |a, b| a.saturating_add(b))
325 }
326 }
327 // Everything else is as short as it is tall.
328 _ => return height_for_width(tree, id, avail),
329 };
330 content
331 .saturating_add(pad.saturating_mul(2))
332 .max(props.cells("height-request", 0))
333}
334
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago335/// Share `avail` out among the children of `id` along one axis.
336///
337/// `across` picks the axis: true for a horizontal box sharing columns, false
338/// for a vertical one sharing rows. The rule is the same either way — natural
339/// sizes first, shrink proportionally toward the minimums when short, and the
340/// surplus to whoever set `:hexpand` / `:vexpand`.
Share a column's rows out at the width it has, not at its own height 5acc801 nandi 17d ago341///
342/// `cross` is the extent on the *other* axis, and sharing rows out cannot be
343/// done without it: how tall a child is depends on how wide it is, because
344/// that is what wrapping means. Passing the rows in its place measures every
345/// label at a column count of two or three, wraps it to a paragraph, and the
346/// overrun is then taken off the end — which paints a box's first child and
347/// drops every sibling after it. Unused when `across`, where a width does not
348/// depend on a height.
349pub 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 17d ago350 let children = tree.children(id);
351 if children.is_empty() {
352 return Vec::new();
353 }
354 let props = tree.props(id);
355 let spacing = props.cells("spacing", 0);
356 let gaps = spacing.saturating_mul((children.len() - 1) as u16);
357 let room = avail.saturating_sub(gaps) as i64;
358
359 let measure = |child: u32, minimum: bool| -> i64 {
360 if across {
361 width(tree, child, minimum) as i64
Let a backlog keep its place, and leave room for what is under it 68910bd nandi 17d ago362 } else if minimum {
363 min_height_for_width(tree, child, cross) as i64
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago364 } else {
Let a backlog keep its place, and leave room for what is under it 68910bd nandi 17d ago365 // A child's height depends on the width it gets, which the caller
366 // 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 17d ago367 height_for_width(tree, child, cross) as i64
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago368 }
369 };
370
371 let nat: Vec<i64> = children.iter().map(|c| measure(*c, false)).collect();
372 let min: Vec<i64> = children
373 .iter()
374 .zip(&nat)
375 .map(|(c, n)| measure(*c, true).min(*n))
376 .collect();
377 let total: i64 = nat.iter().sum();
378 let mut out = nat.clone();
379
380 if total > room {
381 // Short: take the overrun out of whatever each child is willing to give
382 // up, in proportion to how much that is.
383 let mut over = total - room;
384 let slack: i64 = nat.iter().zip(&min).map(|(n, m)| n - m).sum();
385 if slack > 0 {
386 for i in 0..out.len() {
387 let give = ((nat[i] - min[i]) * over.min(slack)) / slack;
388 out[i] -= give;
389 }
390 over -= nat.iter().zip(&out).map(|(n, o)| n - o).sum::<i64>();
391 }
392 // Rounding, and children with no slack at all: take the rest off the
393 // end, which is where a terminal clips anyway.
394 let mut i = out.len();
395 while over > 0 && i > 0 {
396 i -= 1;
397 let give = (out[i] - min[i]).min(over);
398 out[i] -= give;
399 over -= give;
400 }
401 } else if total < room {
402 let key = if across { "hexpand" } else { "vexpand" };
403 let greedy: Vec<usize> = children
404 .iter()
405 .enumerate()
406 .filter(|(_, c)| tree.props(**c).bool(key, false))
407 .map(|(i, _)| i)
408 .collect();
409 if !greedy.is_empty() {
410 let extra = room - total;
411 let each = extra / greedy.len() as i64;
412 let mut rest = extra % greedy.len() as i64;
413 for i in greedy {
414 out[i] += each + if rest > 0 { 1 } else { 0 };
415 rest -= 1;
416 }
417 }
418 }
419 out.into_iter()
420 .map(|n| n.clamp(0, u16::MAX as i64) as u16)
421 .collect()
422}
423
424/// The rect a child of `size` gets inside `avail` on its cross axis.
425pub fn place(align: Align, size: u16, avail: u16) -> (u16, u16) {
426 match align {
427 Align::Fill => (0, avail),
428 other => {
429 let size = size.min(avail);
430 (other.offset(size, avail), size)
431 }
432 }
433}
434
435/// Lay the children of a box out inside `area`.
436pub fn children_rects(tree: &Tree, id: u32, area: Rect) -> Vec<Rect> {
437 let props = tree.props(id);
438 let across = horizontal(&props) && matches!(tree.tag(id), Tag::Box);
439 let spacing = props.cells("spacing", 0);
440 let children = tree.children(id);
Share a column's rows out at the width it has, not at its own height 5acc801 nandi 17d ago441 let shares = share(
442 tree,
443 id,
444 if across { area.w } else { area.h },
445 across,
446 if across { area.h } else { area.w },
447 );
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago448
449 let mut out = Vec::with_capacity(children.len());
450 let mut at = 0u16;
451 for (child, main) in children.iter().zip(shares) {
452 let cprops = tree.props(*child);
453 let rect = if across {
454 let want = height_for_width(tree, *child, main).max(cprops.cells("height-request", 0));
455 let (dy, h) = place(Align::parse(cprops.str("valign")), want, area.h);
456 Rect::new(
457 area.x.saturating_add(at),
458 area.y.saturating_add(dy),
459 main,
460 h,
461 )
462 } else {
463 let want = width(tree, *child, false);
464 let (dx, w) = place(Align::parse(cprops.str("halign")), want, area.w);
465 Rect::new(
466 area.x.saturating_add(dx),
467 area.y.saturating_add(at),
468 w,
469 main,
470 )
471 };
472 out.push(rect);
473 at = at.saturating_add(main).saturating_add(spacing);
474 }
475 out
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481 use crate::tree::Value;
482
483 fn label(tree: &mut Tree, parent: u32, text: &str) -> u32 {
484 let id = tree.new_node("label");
485 tree.set(id, "label", Value::Str(text.into()));
486 tree.append(parent, id);
487 id
488 }
489
490 #[test]
491 fn wrapping_breaks_on_spaces_and_mid_word_when_it_must() {
492 assert_eq!(wrap("one two three", 7), vec!["one two", "three"]);
493 assert_eq!(
494 wrap("antidisestablishment", 6),
495 vec!["antidi", "sestab", "lishme", "nt"]
496 );
497 assert_eq!(wrap("a\nb", 10), vec!["a", "b"]);
498 }
499
500 #[test]
501 fn a_label_is_as_wide_as_its_text_and_as_narrow_as_its_longest_word() {
502 let mut tree = Tree::new();
503 let root = tree.root();
504 let id = label(&mut tree, root, "one two three");
505 assert_eq!(width(&tree, id, false), 13);
506 assert_eq!(width(&tree, id, true), 5);
507 assert_eq!(height_for_width(&tree, id, 7), 2);
508 }
509
510 #[test]
511 fn a_width_request_is_a_floor_on_both_sizes() {
512 let mut tree = Tree::new();
513 let root = tree.root();
514 let id = label(&mut tree, root, "hi");
515 tree.set(id, "width-request", Value::Num(20.0));
516 assert_eq!(width(&tree, id, false), 20);
517 assert_eq!(width(&tree, id, true), 20);
518 }
519
520 #[test]
521 fn a_height_request_of_four_rows_gets_four_rows() {
522 let mut tree = Tree::new();
523 let root = tree.root();
524 let id = label(&mut tree, root, "hi");
525 tree.set(id, "height-request", Value::Num(4.0));
526 assert_eq!(height_for_width(&tree, id, 10), 4);
527 }
528
529 #[test]
530 fn a_horizontal_box_gives_the_surplus_to_whoever_expands() {
531 let mut tree = Tree::new();
532 let row = tree.new_node("hbox");
533 tree.set(row, "orientation", Value::Str("horizontal".into()));
534 let root = tree.root();
535 tree.append(root, row);
536 let a = label(&mut tree, row, "aa");
537 let b = label(&mut tree, row, "bb");
538 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 17d ago539 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 17d ago540 let _ = a;
541 }
542
543 #[test]
544 fn a_short_box_shrinks_toward_the_minimums_rather_than_clipping_the_first_child() {
545 let mut tree = Tree::new();
546 let row = tree.new_node("hbox");
547 tree.set(row, "orientation", Value::Str("horizontal".into()));
548 let root = tree.root();
549 tree.append(root, row);
550 label(&mut tree, row, "one two");
551 label(&mut tree, row, "three four");
552 // 17 natural, 10 offered: both give up some, neither goes under its
553 // longest word.
Share a column's rows out at the width it has, not at its own height 5acc801 nandi 17d ago554 let shares = share(&tree, row, 10, true, 1);
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago555 assert_eq!(shares.iter().sum::<u16>(), 10);
556 assert!(shares[0] >= 3 && shares[1] >= 5, "{shares:?}");
557 }
558
559 #[test]
560 fn spacing_comes_off_the_room_before_it_is_shared() {
561 let mut tree = Tree::new();
562 let row = tree.new_node("hbox");
563 tree.set(row, "orientation", Value::Str("horizontal".into()));
564 tree.set(row, "spacing", Value::Num(2.0));
565 let root = tree.root();
566 tree.append(root, row);
567 let a = label(&mut tree, row, "aa");
568 let b = label(&mut tree, row, "bb");
569 tree.set(a, "hexpand", Value::Bool(true));
570 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 17d ago571 assert_eq!(share(&tree, row, 12, true, 1), vec![5, 5]);
572 }
573
574 #[test]
575 fn a_column_shares_its_rows_out_at_the_width_it_has() {
576 // A column two rows tall and thirty columns wide holds two labels, and
577 // each is one row at that width. Measured against the rows instead —
578 // as this did — "the second line" wraps to five, the overrun comes off
579 // the end, and the second child is handed nothing: a box that paints
580 // its first child and drops the rest, which is what the chats list did
581 // to every Open button in it.
582 let mut tree = Tree::new();
583 let col = tree.new_node("vbox");
584 tree.set(col, "orientation", Value::Str("vertical".into()));
585 let root = tree.root();
586 tree.append(root, col);
587 label(&mut tree, col, "the first line");
588 label(&mut tree, col, "the second line");
589 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 17d ago590 }
591
592 #[test]
593 fn a_centred_child_sits_in_the_middle_of_its_row() {
594 assert_eq!(place(Align::Center, 4, 10), (3, 4));
595 assert_eq!(place(Align::End, 4, 10), (6, 4));
596 assert_eq!(place(Align::Fill, 4, 10), (0, 10));
597 }
598
599 #[test]
600 fn a_frame_spends_a_cell_a_side_on_its_border() {
601 let mut tree = Tree::new();
602 let frame = tree.new_node("frame");
603 let root = tree.root();
604 tree.append(root, frame);
605 label(&mut tree, frame, "hi");
606 assert_eq!(width(&tree, frame, false), 4);
607 assert_eq!(height_for_width(&tree, frame, 4), 3);
608 }
609}