Share a column's rows out at the width it has, not at its own height
A box asked how tall each child was by measuring it at `avail` — which down the page is the rows it has to give away, not the columns it has to give them in. So a child was measured two or three cells wide, every label wrapped to a paragraph, the total ran far past the room, and the overrun came off the end: the first child painted and every sibling after it was handed nothing. It hid at the top of a tree, where the window is as wide as it is tall and the two numbers are close enough to pass. Two levels down it is the whole screen — frq's chats list painted a channel's name and dropped the Open button under it, which reads as a list you cannot click rather than a list that was never drawn. And an unknown tag paints as a box so its children still show, which loses the text of one that has no children: `:status` and `:link` are widgets the caller has and this has not, and painting the box without the label took a link out of the middle of a message. A leaf paints its own text, and is measured as the label it turns out to be. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5acc801 parent: 3cfee15 modified
crates/jolt-tui/src/layout.rs +52 -7 | @@ -155,6 +155,16 @@ fn intrinsic_width(tree: &Tree, id: u32, minimum: bool) -> u16 { | ||
| 155 | 155 | columns(text) |
| 156 | 156 | } |
| 157 | 157 | } |
| 158 | + // 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. | |
| 161 | + Tag::Unknown(_) if tree.child_count(id) == 0 => { | |
| 162 | + if minimum { | |
| 163 | + longest_word(props.label()) | |
| 164 | + } else { | |
| 165 | + columns(props.label()) | |
| 166 | + } | |
| 167 | + } | |
| 158 | 168 | Tag::Separator => 1, |
| 159 | 169 | Tag::Spacer => props.cells("size", 1), |
| 160 | 170 | Tag::Progress => { |
| @@ -217,6 +227,9 @@ pub fn height_for_width(tree: &Tree, id: u32, avail: u16) -> u16 { | ||
| 217 | 227 | let inner = avail.saturating_sub(pad.saturating_mul(2)); |
| 218 | 228 | let content = match tag { |
| 219 | 229 | Tag::Label | Tag::Title | Tag::DimLabel => wrap(props.label(), inner).len() as u16, |
| 230 | + Tag::Unknown(_) if tree.child_count(id) == 0 => { | |
| 231 | + wrap(props.label(), inner).len() as u16 | |
| 232 | + } | |
| 220 | 233 | Tag::Button | Tag::CheckButton | Tag::Separator | Tag::Progress | Tag::Spinner => 1, |
| 221 | 234 | Tag::Entry => props.cells("rows", 1).max(1), |
| 222 | 235 | Tag::Spacer => props.cells("size", 1), |
| @@ -226,7 +239,7 @@ pub fn height_for_width(tree: &Tree, id: u32, avail: u16) -> u16 { | ||
| 226 | 239 | let spacing = props.cells("spacing", 0); |
| 227 | 240 | if horizontal(&props) && matches!(tag, Tag::Box) { |
| 228 | 241 | // Across: each child is measured at the width it will get. |
| 229 | - let shares = share(tree, id, inner, true); | |
| 242 | + let shares = share(tree, id, inner, true, 0); | |
| 230 | 243 | children |
| 231 | 244 | .iter() |
| 232 | 245 | .zip(shares) |
| @@ -253,7 +266,15 @@ pub fn height_for_width(tree: &Tree, id: u32, avail: u16) -> u16 { | ||
| 253 | 266 | /// for a vertical one sharing rows. The rule is the same either way — natural |
| 254 | 267 | /// sizes first, shrink proportionally toward the minimums when short, and the |
| 255 | 268 | /// surplus to whoever set `:hexpand` / `:vexpand`. |
| 256 | -pub fn share(tree: &Tree, id: u32, avail: u16, across: bool) -> Vec<u16> { | |
| 269 | +/// | |
| 270 | +/// `cross` is the extent on the *other* axis, and sharing rows out cannot be | |
| 271 | +/// done without it: how tall a child is depends on how wide it is, because | |
| 272 | +/// that is what wrapping means. Passing the rows in its place measures every | |
| 273 | +/// label at a column count of two or three, wraps it to a paragraph, and the | |
| 274 | +/// overrun is then taken off the end — which paints a box's first child and | |
| 275 | +/// drops every sibling after it. Unused when `across`, where a width does not | |
| 276 | +/// depend on a height. | |
| 277 | +pub fn share(tree: &Tree, id: u32, avail: u16, across: bool, cross: u16) -> Vec<u16> { | |
| 257 | 278 | let children = tree.children(id); |
| 258 | 279 | if children.is_empty() { |
| 259 | 280 | return Vec::new(); |
| @@ -269,7 +290,7 @@ pub fn share(tree: &Tree, id: u32, avail: u16, across: bool) -> Vec<u16> { | ||
| 269 | 290 | } else { |
| 270 | 291 | // Down the page a child's height depends on the width it gets, |
| 271 | 292 | // which the caller has already fixed by the time it asks. |
| 272 | - height_for_width(tree, child, avail) as i64 | |
| 293 | + height_for_width(tree, child, cross) as i64 | |
| 273 | 294 | } |
| 274 | 295 | }; |
| 275 | 296 | |
| @@ -343,7 +364,13 @@ pub fn children_rects(tree: &Tree, id: u32, area: Rect) -> Vec<Rect> { | ||
| 343 | 364 | let across = horizontal(&props) && matches!(tree.tag(id), Tag::Box); |
| 344 | 365 | let spacing = props.cells("spacing", 0); |
| 345 | 366 | let children = tree.children(id); |
| 346 | - let shares = share(tree, id, if across { area.w } else { area.h }, across); | |
| 367 | + let shares = share( | |
| 368 | + tree, | |
| 369 | + id, | |
| 370 | + if across { area.w } else { area.h }, | |
| 371 | + across, | |
| 372 | + if across { area.h } else { area.w }, | |
| 373 | + ); | |
| 347 | 374 | |
| 348 | 375 | let mut out = Vec::with_capacity(children.len()); |
| 349 | 376 | let mut at = 0u16; |
| @@ -435,7 +462,7 @@ mod tests { | ||
| 435 | 462 | let a = label(&mut tree, row, "aa"); |
| 436 | 463 | let b = label(&mut tree, row, "bb"); |
| 437 | 464 | tree.set(b, "hexpand", Value::Bool(true)); |
| 438 | - assert_eq!(share(&tree, row, 20, true), vec![2, 18]); | |
| 465 | + assert_eq!(share(&tree, row, 20, true, 1), vec![2, 18]); | |
| 439 | 466 | let _ = a; |
| 440 | 467 | } |
| 441 | 468 | |
| @@ -450,7 +477,7 @@ mod tests { | ||
| 450 | 477 | label(&mut tree, row, "three four"); |
| 451 | 478 | // 17 natural, 10 offered: both give up some, neither goes under its |
| 452 | 479 | // longest word. |
| 453 | - let shares = share(&tree, row, 10, true); | |
| 480 | + let shares = share(&tree, row, 10, true, 1); | |
| 454 | 481 | assert_eq!(shares.iter().sum::<u16>(), 10); |
| 455 | 482 | assert!(shares[0] >= 3 && shares[1] >= 5, "{shares:?}"); |
| 456 | 483 | } |
| @@ -467,7 +494,25 @@ mod tests { | ||
| 467 | 494 | let b = label(&mut tree, row, "bb"); |
| 468 | 495 | tree.set(a, "hexpand", Value::Bool(true)); |
| 469 | 496 | tree.set(b, "hexpand", Value::Bool(true)); |
| 470 | - assert_eq!(share(&tree, row, 12, true), vec![5, 5]); | |
| 497 | + assert_eq!(share(&tree, row, 12, true, 1), vec![5, 5]); | |
| 498 | + } | |
| 499 | + | |
| 500 | + #[test] | |
| 501 | + fn a_column_shares_its_rows_out_at_the_width_it_has() { | |
| 502 | + // A column two rows tall and thirty columns wide holds two labels, and | |
| 503 | + // each is one row at that width. Measured against the rows instead — | |
| 504 | + // as this did — "the second line" wraps to five, the overrun comes off | |
| 505 | + // the end, and the second child is handed nothing: a box that paints | |
| 506 | + // its first child and drops the rest, which is what the chats list did | |
| 507 | + // to every Open button in it. | |
| 508 | + let mut tree = Tree::new(); | |
| 509 | + let col = tree.new_node("vbox"); | |
| 510 | + tree.set(col, "orientation", Value::Str("vertical".into())); | |
| 511 | + let root = tree.root(); | |
| 512 | + tree.append(root, col); | |
| 513 | + label(&mut tree, col, "the first line"); | |
| 514 | + label(&mut tree, col, "the second line"); | |
| 515 | + assert_eq!(share(&tree, col, 2, false, 30), vec![1, 1]); | |
| 471 | 516 | } |
| 472 | 517 | |
| 473 | 518 | #[test] |
| @@ -155,6 +155,16 @@ fn intrinsic_width(tree: &Tree, id: u32, minimum: bool) -> u16 { | |||
| 155 | columns(text) | 155 | columns(text) |
| 156 | } | 156 | } |
| 157 | } | 157 | } |
| 158 | + // 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. | ||
| 161 | + Tag::Unknown(_) if tree.child_count(id) == 0 => { | ||
| 162 | + if minimum { | ||
| 163 | + longest_word(props.label()) | ||
| 164 | + } else { | ||
| 165 | + columns(props.label()) | ||
| 166 | + } | ||
| 167 | + } | ||
| 158 | Tag::Separator => 1, | 168 | Tag::Separator => 1, |
| 159 | Tag::Spacer => props.cells("size", 1), | 169 | Tag::Spacer => props.cells("size", 1), |
| 160 | Tag::Progress => { | 170 | Tag::Progress => { |
| @@ -217,6 +227,9 @@ pub fn height_for_width(tree: &Tree, id: u32, avail: u16) -> u16 { | |||
| 217 | let inner = avail.saturating_sub(pad.saturating_mul(2)); | 227 | let inner = avail.saturating_sub(pad.saturating_mul(2)); |
| 218 | let content = match tag { | 228 | let content = match tag { |
| 219 | Tag::Label | Tag::Title | Tag::DimLabel => wrap(props.label(), inner).len() as u16, | 229 | Tag::Label | Tag::Title | Tag::DimLabel => wrap(props.label(), inner).len() as u16, |
| 230 | + Tag::Unknown(_) if tree.child_count(id) == 0 => { | ||
| 231 | + wrap(props.label(), inner).len() as u16 | ||
| 232 | + } | ||
| 220 | Tag::Button | Tag::CheckButton | Tag::Separator | Tag::Progress | Tag::Spinner => 1, | 233 | Tag::Button | Tag::CheckButton | Tag::Separator | Tag::Progress | Tag::Spinner => 1, |
| 221 | Tag::Entry => props.cells("rows", 1).max(1), | 234 | Tag::Entry => props.cells("rows", 1).max(1), |
| 222 | Tag::Spacer => props.cells("size", 1), | 235 | Tag::Spacer => props.cells("size", 1), |
| @@ -226,7 +239,7 @@ pub fn height_for_width(tree: &Tree, id: u32, avail: u16) -> u16 { | |||
| 226 | let spacing = props.cells("spacing", 0); | 239 | let spacing = props.cells("spacing", 0); |
| 227 | if horizontal(&props) && matches!(tag, Tag::Box) { | 240 | if horizontal(&props) && matches!(tag, Tag::Box) { |
| 228 | // Across: each child is measured at the width it will get. | 241 | // Across: each child is measured at the width it will get. |
| 229 | - let shares = share(tree, id, inner, true); | 242 | + let shares = share(tree, id, inner, true, 0); |
| 230 | children | 243 | children |
| 231 | .iter() | 244 | .iter() |
| 232 | .zip(shares) | 245 | .zip(shares) |
| @@ -253,7 +266,15 @@ pub fn height_for_width(tree: &Tree, id: u32, avail: u16) -> u16 { | |||
| 253 | /// for a vertical one sharing rows. The rule is the same either way — natural | 266 | /// for a vertical one sharing rows. The rule is the same either way — natural |
| 254 | /// sizes first, shrink proportionally toward the minimums when short, and the | 267 | /// sizes first, shrink proportionally toward the minimums when short, and the |
| 255 | /// surplus to whoever set `:hexpand` / `:vexpand`. | 268 | /// surplus to whoever set `:hexpand` / `:vexpand`. |
| 256 | -pub fn share(tree: &Tree, id: u32, avail: u16, across: bool) -> Vec<u16> { | 269 | +/// |
| 270 | +/// `cross` is the extent on the *other* axis, and sharing rows out cannot be | ||
| 271 | +/// done without it: how tall a child is depends on how wide it is, because | ||
| 272 | +/// that is what wrapping means. Passing the rows in its place measures every | ||
| 273 | +/// label at a column count of two or three, wraps it to a paragraph, and the | ||
| 274 | +/// overrun is then taken off the end — which paints a box's first child and | ||
| 275 | +/// drops every sibling after it. Unused when `across`, where a width does not | ||
| 276 | +/// depend on a height. | ||
| 277 | +pub fn share(tree: &Tree, id: u32, avail: u16, across: bool, cross: u16) -> Vec<u16> { | ||
| 257 | let children = tree.children(id); | 278 | let children = tree.children(id); |
| 258 | if children.is_empty() { | 279 | if children.is_empty() { |
| 259 | return Vec::new(); | 280 | return Vec::new(); |
| @@ -269,7 +290,7 @@ pub fn share(tree: &Tree, id: u32, avail: u16, across: bool) -> Vec<u16> { | |||
| 269 | } else { | 290 | } else { |
| 270 | // Down the page a child's height depends on the width it gets, | 291 | // Down the page a child's height depends on the width it gets, |
| 271 | // which the caller has already fixed by the time it asks. | 292 | // which the caller has already fixed by the time it asks. |
| 272 | - height_for_width(tree, child, avail) as i64 | 293 | + height_for_width(tree, child, cross) as i64 |
| 273 | } | 294 | } |
| 274 | }; | 295 | }; |
| 275 | 296 | ||
| @@ -343,7 +364,13 @@ pub fn children_rects(tree: &Tree, id: u32, area: Rect) -> Vec<Rect> { | |||
| 343 | let across = horizontal(&props) && matches!(tree.tag(id), Tag::Box); | 364 | let across = horizontal(&props) && matches!(tree.tag(id), Tag::Box); |
| 344 | let spacing = props.cells("spacing", 0); | 365 | let spacing = props.cells("spacing", 0); |
| 345 | let children = tree.children(id); | 366 | let children = tree.children(id); |
| 346 | - let shares = share(tree, id, if across { area.w } else { area.h }, across); | 367 | + let shares = share( |
| 368 | + tree, | ||
| 369 | + id, | ||
| 370 | + if across { area.w } else { area.h }, | ||
| 371 | + across, | ||
| 372 | + if across { area.h } else { area.w }, | ||
| 373 | + ); | ||
| 347 | 374 | ||
| 348 | let mut out = Vec::with_capacity(children.len()); | 375 | let mut out = Vec::with_capacity(children.len()); |
| 349 | let mut at = 0u16; | 376 | let mut at = 0u16; |
| @@ -435,7 +462,7 @@ mod tests { | |||
| 435 | let a = label(&mut tree, row, "aa"); | 462 | let a = label(&mut tree, row, "aa"); |
| 436 | let b = label(&mut tree, row, "bb"); | 463 | let b = label(&mut tree, row, "bb"); |
| 437 | tree.set(b, "hexpand", Value::Bool(true)); | 464 | tree.set(b, "hexpand", Value::Bool(true)); |
| 438 | - assert_eq!(share(&tree, row, 20, true), vec![2, 18]); | 465 | + assert_eq!(share(&tree, row, 20, true, 1), vec![2, 18]); |
| 439 | let _ = a; | 466 | let _ = a; |
| 440 | } | 467 | } |
| 441 | 468 | ||
| @@ -450,7 +477,7 @@ mod tests { | |||
| 450 | label(&mut tree, row, "three four"); | 477 | label(&mut tree, row, "three four"); |
| 451 | // 17 natural, 10 offered: both give up some, neither goes under its | 478 | // 17 natural, 10 offered: both give up some, neither goes under its |
| 452 | // longest word. | 479 | // longest word. |
| 453 | - let shares = share(&tree, row, 10, true); | 480 | + let shares = share(&tree, row, 10, true, 1); |
| 454 | assert_eq!(shares.iter().sum::<u16>(), 10); | 481 | assert_eq!(shares.iter().sum::<u16>(), 10); |
| 455 | assert!(shares[0] >= 3 && shares[1] >= 5, "{shares:?}"); | 482 | assert!(shares[0] >= 3 && shares[1] >= 5, "{shares:?}"); |
| 456 | } | 483 | } |
| @@ -467,7 +494,25 @@ mod tests { | |||
| 467 | let b = label(&mut tree, row, "bb"); | 494 | let b = label(&mut tree, row, "bb"); |
| 468 | tree.set(a, "hexpand", Value::Bool(true)); | 495 | tree.set(a, "hexpand", Value::Bool(true)); |
| 469 | tree.set(b, "hexpand", Value::Bool(true)); | 496 | tree.set(b, "hexpand", Value::Bool(true)); |
| 470 | - assert_eq!(share(&tree, row, 12, true), vec![5, 5]); | 497 | + assert_eq!(share(&tree, row, 12, true, 1), vec![5, 5]); |
| 498 | + } | ||
| 499 | + | ||
| 500 | + #[test] | ||
| 501 | + fn a_column_shares_its_rows_out_at_the_width_it_has() { | ||
| 502 | + // A column two rows tall and thirty columns wide holds two labels, and | ||
| 503 | + // each is one row at that width. Measured against the rows instead — | ||
| 504 | + // as this did — "the second line" wraps to five, the overrun comes off | ||
| 505 | + // the end, and the second child is handed nothing: a box that paints | ||
| 506 | + // its first child and drops the rest, which is what the chats list did | ||
| 507 | + // to every Open button in it. | ||
| 508 | + let mut tree = Tree::new(); | ||
| 509 | + let col = tree.new_node("vbox"); | ||
| 510 | + tree.set(col, "orientation", Value::Str("vertical".into())); | ||
| 511 | + let root = tree.root(); | ||
| 512 | + tree.append(root, col); | ||
| 513 | + label(&mut tree, col, "the first line"); | ||
| 514 | + label(&mut tree, col, "the second line"); | ||
| 515 | + assert_eq!(share(&tree, col, 2, false, 30), vec![1, 1]); | ||
| 471 | } | 516 | } |
| 472 | 517 | ||
| 473 | #[test] | 518 | #[test] |
modified
crates/jolt-tui/src/paint.rs +16 -1 | @@ -123,7 +123,22 @@ impl Painter<'_> { | ||
| 123 | 123 | self.children(id, inner, style, enabled); |
| 124 | 124 | } |
| 125 | 125 | Tag::Scroll => self.scroll(id, inner, style, enabled), |
| 126 | - Tag::Box | Tag::Window | Tag::Unknown(_) => self.children(id, inner, style, enabled), | |
| 126 | + Tag::Box | Tag::Window => self.children(id, inner, style, enabled), | |
| 127 | + // A tag this backend has not learned paints as a vertical box, so | |
| 128 | + // whatever is under it still reaches the screen. When there is | |
| 129 | + // nothing under it, its own text does instead: an unknown *leaf* | |
| 130 | + // is a widget the caller has and this has not — frq's `:status` | |
| 131 | + // badge, its `:link` — and painting the box and not the label is | |
| 132 | + // the one outcome that loses the text altogether. A link vanishing | |
| 133 | + // out of the middle of a message is not a missing widget; it is a | |
| 134 | + // missing sentence. | |
| 135 | + Tag::Unknown(_) => { | |
| 136 | + if self.tree.child_count(id) == 0 { | |
| 137 | + self.wrapped(inner, props.label(), style); | |
| 138 | + } else { | |
| 139 | + self.children(id, inner, style, enabled); | |
| 140 | + } | |
| 141 | + } | |
| 127 | 142 | Tag::Label => self.wrapped(inner, props.label(), style), |
| 128 | 143 | Tag::Title => self.wrapped(inner, props.label(), style.with(attr::BOLD)), |
| 129 | 144 | Tag::DimLabel => self.wrapped(inner, props.label(), style.with(attr::DIM)), |
| @@ -123,7 +123,22 @@ impl Painter<'_> { | |||
| 123 | self.children(id, inner, style, enabled); | 123 | self.children(id, inner, style, enabled); |
| 124 | } | 124 | } |
| 125 | Tag::Scroll => self.scroll(id, inner, style, enabled), | 125 | Tag::Scroll => self.scroll(id, inner, style, enabled), |
| 126 | - Tag::Box | Tag::Window | Tag::Unknown(_) => self.children(id, inner, style, enabled), | 126 | + Tag::Box | Tag::Window => self.children(id, inner, style, enabled), |
| 127 | + // A tag this backend has not learned paints as a vertical box, so | ||
| 128 | + // whatever is under it still reaches the screen. When there is | ||
| 129 | + // nothing under it, its own text does instead: an unknown *leaf* | ||
| 130 | + // is a widget the caller has and this has not — frq's `:status` | ||
| 131 | + // badge, its `:link` — and painting the box and not the label is | ||
| 132 | + // the one outcome that loses the text altogether. A link vanishing | ||
| 133 | + // out of the middle of a message is not a missing widget; it is a | ||
| 134 | + // missing sentence. | ||
| 135 | + Tag::Unknown(_) => { | ||
| 136 | + if self.tree.child_count(id) == 0 { | ||
| 137 | + self.wrapped(inner, props.label(), style); | ||
| 138 | + } else { | ||
| 139 | + self.children(id, inner, style, enabled); | ||
| 140 | + } | ||
| 141 | + } | ||
| 127 | Tag::Label => self.wrapped(inner, props.label(), style), | 142 | Tag::Label => self.wrapped(inner, props.label(), style), |
| 128 | Tag::Title => self.wrapped(inner, props.label(), style.with(attr::BOLD)), | 143 | Tag::Title => self.wrapped(inner, props.label(), style.with(attr::BOLD)), |
| 129 | Tag::DimLabel => self.wrapped(inner, props.label(), style.with(attr::DIM)), | 144 | Tag::DimLabel => self.wrapped(inner, props.label(), style.with(attr::DIM)), |
modified
crates/jolt-tui/src/tests.rs +31 -0 | @@ -369,3 +369,34 @@ fn focus_survives_a_repaint_and_lands_somewhere_when_its_widget_is_unmounted() { | ||
| 369 | 369 | ui.frame(); |
| 370 | 370 | assert_eq!(ui.focus(), first); |
| 371 | 371 | } |
| 372 | + | |
| 373 | +#[test] | |
| 374 | +fn an_unknown_leaf_paints_its_own_text() { | |
| 375 | + // A tag this backend does not know paints as a box so its children still | |
| 376 | + // show — but a leaf has none, and a `:status` badge or a `:link` in the | |
| 377 | + // middle of a message would otherwise be a hole in the sentence. | |
| 378 | + let mut ui = ui(); | |
| 379 | + let root = ui.tree.root(); | |
| 380 | + node(&mut ui, root, "status", &[("label", "joined")]); | |
| 381 | + node(&mut ui, root, "label", &[("label", "after")]); | |
| 382 | + ui.frame(); | |
| 383 | + assert_eq!(ui.screen.line(0), "joined"); | |
| 384 | + assert_eq!(ui.screen.line(1), "after"); | |
| 385 | +} | |
| 386 | + | |
| 387 | +#[test] | |
| 388 | +fn a_nested_column_paints_every_child_and_not_only_the_first() { | |
| 389 | + // Its rows were shared out by measuring each child at the *rows* it had | |
| 390 | + // rather than the columns, so a label wrapped to a paragraph, the overrun | |
| 391 | + // came off the end, and everything after the first child was handed | |
| 392 | + // nothing. Two levels down is where it showed: the top box is as wide as | |
| 393 | + // the screen and as tall, so the two numbers were close enough to hide it. | |
| 394 | + let mut ui = ui(); | |
| 395 | + let root = ui.tree.root(); | |
| 396 | + let col = node(&mut ui, root, "vbox", &[("orientation", "vertical")]); | |
| 397 | + node(&mut ui, col, "label", &[("label", "the first line")]); | |
| 398 | + node(&mut ui, col, "button", &[("label", "Open")]); | |
| 399 | + ui.frame(); | |
| 400 | + assert_eq!(ui.screen.line(0), "the first line"); | |
| 401 | + assert_eq!(ui.screen.line(1), "[ Open ]"); | |
| 402 | +} | |
| @@ -369,3 +369,34 @@ fn focus_survives_a_repaint_and_lands_somewhere_when_its_widget_is_unmounted() { | |||
| 369 | ui.frame(); | 369 | ui.frame(); |
| 370 | assert_eq!(ui.focus(), first); | 370 | assert_eq!(ui.focus(), first); |
| 371 | } | 371 | } |
| 372 | + | ||
| 373 | +#[test] | ||
| 374 | +fn an_unknown_leaf_paints_its_own_text() { | ||
| 375 | + // A tag this backend does not know paints as a box so its children still | ||
| 376 | + // show — but a leaf has none, and a `:status` badge or a `:link` in the | ||
| 377 | + // middle of a message would otherwise be a hole in the sentence. | ||
| 378 | + let mut ui = ui(); | ||
| 379 | + let root = ui.tree.root(); | ||
| 380 | + node(&mut ui, root, "status", &[("label", "joined")]); | ||
| 381 | + node(&mut ui, root, "label", &[("label", "after")]); | ||
| 382 | + ui.frame(); | ||
| 383 | + assert_eq!(ui.screen.line(0), "joined"); | ||
| 384 | + assert_eq!(ui.screen.line(1), "after"); | ||
| 385 | +} | ||
| 386 | + | ||
| 387 | +#[test] | ||
| 388 | +fn a_nested_column_paints_every_child_and_not_only_the_first() { | ||
| 389 | + // Its rows were shared out by measuring each child at the *rows* it had | ||
| 390 | + // rather than the columns, so a label wrapped to a paragraph, the overrun | ||
| 391 | + // came off the end, and everything after the first child was handed | ||
| 392 | + // nothing. Two levels down is where it showed: the top box is as wide as | ||
| 393 | + // the screen and as tall, so the two numbers were close enough to hide it. | ||
| 394 | + let mut ui = ui(); | ||
| 395 | + let root = ui.tree.root(); | ||
| 396 | + let col = node(&mut ui, root, "vbox", &[("orientation", "vertical")]); | ||
| 397 | + node(&mut ui, col, "label", &[("label", "the first line")]); | ||
| 398 | + node(&mut ui, col, "button", &[("label", "Open")]); | ||
| 399 | + ui.frame(); | ||
| 400 | + assert_eq!(ui.screen.line(0), "the first line"); | ||
| 401 | + assert_eq!(ui.screen.line(1), "[ Open ]"); | ||
| 402 | +} | ||