use super::{Harness, rect_at};
use crate::{Align, Button, Color, Icon, Rect, Row, Size, Spacer, Stack, Style, Text};
#[test]
fn the_first_layout_damages_every_node_and_then_nothing_is_dirty() {
let mut h = Harness::new();
let row = Row::new().child(Icon::new("a").size(10));
let window = h.window(h.panel().child(row), 200.0, 30.0);
let damage = window.take_damage();
assert!(damage.contains(&Rect::new(0.0, 0.0, 200.0, 30.0)));
assert!(damage.contains(&rect_at(&window, &[0, 0])));
h.frame(&window);
assert!(!window.needs_layout());
assert!(!window.has_damage());
}
#[test]
fn a_paint_change_repaints_only_that_widget() {
let mut h = Harness::new();
let dark = h.cx.signal(true);
let row = Row::new()
.padding(4, 8)
.child(Icon::new("a").size(10))
.child(Button::new(Icon::new("b").size(10)).style(move || {
let background = if dark.get() { 0x11 } else { 0xee };
Style::new().background(Color::rgb(background, background, background))
}));
let window = h.window(h.panel().child(row), 200.0, 30.0);
window.take_damage();
dark.set(false);
h.runtime.flush();
assert!(!window.needs_layout());
assert_eq!(window.take_damage(), vec![rect_at(&window, &[0, 1])]);
}
#[test]
fn setting_an_equal_value_invalidates_nothing() {
let mut h = Harness::new();
let label = h.cx.signal(String::from("same"));
let style = h.cx.signal(Style::new().background(Color::rgb(1, 2, 3)));
let row = Row::new()
.style(move || style.get())
.child(Text::new(move || label.get()));
let window = h.window(h.panel().child(row), 200.0, 30.0);
window.take_damage();
label.set(String::from("same"));
style.set(Style::new().background(Color::rgb(1, 2, 3)));
h.runtime.flush();
assert!(!window.needs_layout());
assert!(!window.has_damage());
}
#[test]
fn a_layout_change_repaints_what_moved_at_both_positions() {
let mut h = Harness::new();
let size = h.cx.signal(10);
let row = Row::new()
.align(Align::Start)
.child(Icon::new("before").size(8))
.child(Icon::new("grows").size(move || size.get()))
.child(Icon::new("after").size(8));
let window = h.window(h.panel().child(row), 200.0, 30.0);
window.take_damage();
let before = rect_at(&window, &[0, 0]);
let old_grows = rect_at(&window, &[0, 1]);
let old_after = rect_at(&window, &[0, 2]);
size.set(20);
h.runtime.flush();
assert!(window.needs_layout());
assert!(!window.has_damage());
window.layout(&mut h.text);
let new_grows = rect_at(&window, &[0, 1]);
let new_after = rect_at(&window, &[0, 2]);
assert_eq!(new_after.x, old_after.x + 10.0);
let damage = window.take_damage();
for rect in [old_grows, new_grows, old_after, new_after] {
assert!(damage.contains(&rect), "{rect:?} missing from {damage:?}");
}
assert!(!damage.contains(&before), "{damage:?}");
assert!(!window.needs_layout());
}
#[test]
fn new_text_is_shaped_and_measured_again() {
let mut h = Harness::new();
let label = h.cx.signal(String::from("a"));
let row = Row::new()
.align(Align::Start)
.child(Text::new(move || label.get()))
.child(Icon::new("after").size(8));
let window = h.window(h.panel().child(row), 400.0, 30.0);
let short = rect_at(&window, &[0, 0]);
label.set(String::from("a much longer label"));
h.frame(&window);
let long = rect_at(&window, &[0, 0]);
assert!(long.width > short.width + 50.0, "{short:?} {long:?}");
assert_eq!(rect_at(&window, &[0, 1]).x, long.right());
let view = window.view();
let crate::Content::Text(text) = super::node_at(view.root(), &[0, 0]).content() else {
panic!("a text node has text content");
};
let drawn: Vec<&str> = text.runs().map(|run| run.text).collect();
assert_eq!(drawn, ["a much longer label"]);
}
#[test]
fn a_layout_change_that_moves_nothing_still_repaints_its_widget() {
let mut h = Harness::new();
let label = h.cx.signal(String::from("ab"));
let row = Row::new()
.align(Align::Start)
.child(Text::new(move || label.get()))
.child(Icon::new("after").size(8));
let window = h.window(h.panel().child(row), 200.0, 30.0);
window.take_damage();
let text = rect_at(&window, &[0, 0]);
label.set(String::from("ba"));
h.frame(&window);
assert_eq!(rect_at(&window, &[0, 0]), text);
let bounds = super::node_at(window.view().root(), &[0, 0]).bounds();
assert!(
bounds.x < text.x && bounds.right() > text.right(),
"{bounds:?}"
);
assert_eq!(window.take_damage(), vec![bounds]);
}
#[test]
fn a_style_change_to_padding_needs_a_layout() {
let mut h = Harness::new();
let wide = h.cx.signal(false);
let button = Button::new(Icon::new("a").size(10)).style(move || {
Style::new()
.background(Color::rgb(1, 1, 1))
.padding(0, if wide.get() { 8 } else { 2 })
});
let window = h.window(h.panel().child(Row::new().child(button)), 200.0, 30.0);
window.take_damage();
assert_eq!(rect_at(&window, &[0, 0]).width, 14.0);
wide.set(true);
h.runtime.flush();
assert!(window.needs_layout());
window.layout(&mut h.text);
assert_eq!(rect_at(&window, &[0, 0]).width, 26.0);
assert!(window.has_damage());
}
#[test]
fn a_change_to_one_sides_padding_lays_out_and_repaints_both_places() {
let mut h = Harness::new();
let wide = h.cx.signal(false);
let button = Button::new(Icon::new("a").size(10)).style(move || {
Style::new()
.background(Color::rgb(1, 1, 1))
.padding(0, 2)
.padding_left(if wide.get() { 8 } else { 2 })
});
let window = h.window(h.panel().child(Row::new().child(button)), 200.0, 30.0);
window.take_damage();
assert_eq!(rect_at(&window, &[0, 0, 0]).x, 2.0);
wide.set(true);
h.runtime.flush();
assert!(window.needs_layout());
window.layout(&mut h.text);
assert_eq!(rect_at(&window, &[0, 0]).width, 20.0);
assert_eq!(rect_at(&window, &[0, 0, 0]).x, 8.0);
let damage = window.take_damage();
let reach = damage
.iter()
.fold(Rect::default(), |all, rect| all.union(*rect));
assert_eq!(reach, Rect::new(0.0, 0.0, 20.0, 30.0), "{damage:?}");
}
#[test]
fn a_font_change_on_an_ancestor_measures_its_text_again() {
use crate::Font;
let mut h = Harness::new();
let big = h.cx.signal(false);
let row = Row::new()
.align(Align::Start)
.style(move || Style::new().font(Font::new("sans-serif", if big.get() { 20.0 } else { 10.0 })))
.child(Text::new("hello"));
let window = h.window(h.panel().child(row), 400.0, 40.0);
let small = rect_at(&window, &[0, 0]);
assert_eq!(small.height, 12.0);
big.set(true);
h.frame(&window);
let large = rect_at(&window, &[0, 0]);
assert_eq!(large.height, 24.0);
assert!(large.width > small.width, "{small:?} {large:?}");
}
#[test]
fn a_new_size_lays_out_again_and_repaints_old_and_new_area() {
let mut h = Harness::new();
let window = h.window(h.panel().child(Row::new()), 200.0, 30.0);
window.take_damage();
window.set_size(Size::new(200.0, 30.0));
assert!(!window.needs_layout());
window.set_size(Size::new(300.0, 30.0));
assert!(window.needs_layout());
window.layout(&mut h.text);
let damage = window.take_damage();
assert!(damage.contains(&Rect::new(0.0, 0.0, 200.0, 30.0)));
assert!(damage.contains(&Rect::new(0.0, 0.0, 300.0, 30.0)));
}
#[test]
fn a_color_change_repaints_the_descendants_that_inherit_it() {
let mut h = Harness::new();
let red = h.cx.signal(false);
let row = Row::new()
.style(move || Style::new().color(Color::rgb(if red.get() { 255 } else { 0 }, 0, 0)))
.child(Button::new(Text::new("overflowing text")));
let window = h.window(h.panel().child(row), 30.0, 30.0);
window.take_damage();
let text = rect_at(&window, &[0, 0, 0]);
assert_eq!(rect_at(&window, &[0]).width, 30.0);
assert!(text.right() > 30.0, "{text:?}");
red.set(true);
h.runtime.flush();
let damage = window.take_damage();
assert_eq!(damage.len(), 1);
assert!(damage[0].right() >= text.right(), "{damage:?} {text:?}");
}
#[test]
fn empty_and_repeated_areas_are_left_out() {
let mut h = Harness::new();
let row = Row::new()
.child(Spacer::new())
.child(Stack::new().child(Icon::new("a").size(10)));
let window = h.window(h.panel().child(row), 10.0, 10.0);
let damage = window.take_damage();
assert_eq!(damage, vec![Rect::new(0.0, 0.0, 10.0, 10.0)]);
}
fn line_box(window: &crate::Window, path: &[usize]) -> (f32, f32) {
let view = window.view();
let crate::Content::Text(text) = super::node_at(view.root(), path).content() else {
panic!("a text node has text content");
};
let top = text.origin().y;
(
top,
top + text.runs().map(|run| run.line_height).sum::<f32>(),
)
}
fn covered(damage: &[Rect]) -> Rect {
damage
.iter()
.fold(Rect::default(), |all, rect| all.union(*rect))
}
#[test]
fn text_taller_than_its_box_is_repainted_where_it_is_drawn() {
let mut h = Harness::new();
let label = h.cx.signal(String::from("gy"));
let row = Row::new()
.padding(4, 8)
.child(Text::new(move || label.get()));
let window = h.window(h.panel().child(row), 200.0, 20.0);
window.take_damage();
let (top, bottom) = line_box(&window, &[0, 0]);
assert!(top < rect_at(&window, &[0, 0]).y, "the line overflows");
label.set(String::from("yg"));
h.frame(&window);
let damage = covered(&window.take_damage());
assert!(damage.y <= top && damage.bottom() >= bottom, "{damage:?}");
}
#[test]
fn a_paint_change_covers_the_texts_overflow() {
let mut h = Harness::new();
let red = h.cx.signal(false);
let row = Row::new().padding(4, 8).child(
Text::new("gy")
.style(move || Style::new().color(Color::rgb(if red.get() { 255 } else { 0 }, 0, 0))),
);
let window = h.window(h.panel().child(row), 200.0, 20.0);
window.take_damage();
let (top, bottom) = line_box(&window, &[0, 0]);
red.set(true);
h.runtime.flush();
let damage = covered(&window.take_damage());
assert!(damage.y <= top && damage.bottom() >= bottom, "{damage:?}");
}
#[test]
fn the_damage_covers_every_glyph_pixel() {
use cosmic_text::SwashCache;
let mut h = Harness::new();
let label = h.cx.signal(String::from("\u{1FA}gjy"));
let row = Row::new()
.align(Align::Center)
.child(Text::new(move || label.get()));
let window = h.window(h.panel().child(row), 200.0, 30.0);
window.take_damage();
label.set(String::from("yjg\u{1FA}"));
h.frame(&window);
let damage = covered(&window.take_damage());
let mut cache = SwashCache::new();
let view = window.view();
let crate::Content::Text(text) = super::node_at(view.root(), &[0, 0]).content() else {
panic!("a text node has text content");
};
let (mut top, mut bottom) = (f32::MAX, f32::MIN);
for run in text.runs() {
let baseline = (text.origin().y + run.line_y).round();
for glyph in run.glyphs {
let key = glyph.physical((0.0, 0.0), 1.0).cache_key;
if let Some(image) = cache.get_image_uncached(h.text.font_system(), key) {
let glyph_top = baseline - image.placement.top as f32;
top = top.min(glyph_top);
bottom = bottom.max(glyph_top + image.placement.height as f32);
}
}
}
assert!(top < bottom, "some glyph was drawn");
assert!(
damage.y <= top && damage.bottom() >= bottom,
"glyphs {top}..{bottom}, {damage:?}"
);
}
#[test]
fn an_icon_bigger_than_its_box_is_repainted_whole() {
let mut h = Harness::new();
let first = h.cx.signal(true);
let row = Row::new().padding(10, 4).child(
Icon::new(move || crate::IconSource::Name(if first.get() { "one" } else { "two" }.to_owned()))
.size(24),
);
let window = h.window(h.panel().child(row), 60.0, 36.0);
window.take_damage();
first.set(false);
h.runtime.flush();
let damage = covered(&window.take_damage());
assert!(damage.y <= 6.0 && damage.bottom() >= 30.0, "{damage:?}");
assert!(damage.x <= 4.0 && damage.right() >= 28.0, "{damage:?}");
}
fn ink(window: &crate::Window, text: &mut crate::TextSystem, path: &[usize], scale: f32) -> Rect {
use cosmic_text::SwashCache;
let view = window.view();
let crate::Content::Text(view_text) = super::node_at(view.root(), path).content() else {
panic!("a text node has text content");
};
let origin = view_text.origin();
let mut cache = SwashCache::new();
let mut ink = Rect::default();
for run in view_text.runs() {
let baseline = ((origin.y + run.line_y) * scale).round();
for glyph in run.glyphs {
let physical = glyph.physical((origin.x * scale, baseline), scale);
let Some(image) = cache.get_image_uncached(text.font_system(), physical.cache_key) else {
continue;
};
let left = (physical.x + image.placement.left) as f32;
let top = (physical.y - image.placement.top) as f32;
let (width, height) = (image.placement.width as f32, image.placement.height as f32);
if width > 0.0 && height > 0.0 {
ink = ink.union(Rect::new(
left / scale,
top / scale,
width / scale,
height / scale,
));
}
}
}
ink
}
fn covers(outer: Rect, inner: Rect) -> bool {
outer.x <= inner.x
&& outer.y <= inner.y
&& outer.right() >= inner.right()
&& outer.bottom() >= inner.bottom()
}
#[test]
fn glyph_ink_outside_the_line_box_is_repainted() {
for (from, to) in [("\u{1ED6}", "O"), ("\u{1EB6}", "A"), ("\u{303}x", "x")] {
for scale in [1.0, 1.25, 1.5, 1.75, 2.0] {
let mut h = Harness::new();
let label = h.cx.signal(String::from(from));
let row = Row::new()
.padding(8, 8)
.align(Align::Center)
.child(Text::new(move || label.get()));
let window = h.window(h.panel().child(row), 60.0, 40.0);
window.take_damage();
let old = ink(&window, &mut h.text, &[0, 0], scale);
label.set(String::from(to));
h.frame(&window);
let new = ink(&window, &mut h.text, &[0, 0], scale);
let damage = covered(&window.take_damage());
assert!(
covers(damage, old) && covers(damage, new),
"{from:?} to {to:?} at {scale}: ink {old:?} and {new:?}, damage {damage:?}"
);
}
}
}
fn bounds_at(window: &crate::Window, path: &[usize]) -> Rect {
super::node_at(window.view().root(), path).bounds()
}
#[test]
fn a_spacer_that_resizes_adds_no_damage() {
let mut h = Harness::new();
let label = h.cx.signal(String::from("12:34"));
let row = Row::new()
.child(Spacer::new())
.child(Text::new(move || label.get()));
let window = h.window(h.panel().child(row), 300.0, 30.0);
window.take_damage();
let old_spacer = rect_at(&window, &[0, 0]);
let old_label = bounds_at(&window, &[0, 1]);
label.set(String::from("1:05"));
h.frame(&window);
assert_ne!(rect_at(&window, &[0, 0]), old_spacer, "the spacer resized");
let new_label = bounds_at(&window, &[0, 1]);
let mut damage = window.take_damage();
damage.sort_by(|a, b| a.x.total_cmp(&b.x));
let mut expected = vec![old_label, new_label];
expected.sort_by(|a, b| a.x.total_cmp(&b.x));
assert_eq!(damage, expected);
}
#[test]
fn a_background_added_without_a_layout_is_repainted_when_the_widget_moves() {
let mut h = Harness::new();
let label = h.cx.signal(String::from("ab"));
let shaded = h.cx.signal(false);
let row = Row::new()
.align(Align::Start)
.child(Text::new(move || label.get()))
.child(
Row::new()
.style(move || {
let style = Style::new();
if shaded.get() {
style.background(Color::rgb(9, 9, 9))
} else {
style
}
})
.padding(6, 6)
.child(Icon::new("x").size(8)),
);
let window = h.window(h.panel().child(row), 300.0, 30.0);
window.take_damage();
shaded.set(true);
h.runtime.flush();
assert!(!window.needs_layout());
let old = rect_at(&window, &[0, 1]);
assert!(window.take_damage().contains(&old));
label.set(String::from("a much longer label"));
h.frame(&window);
let new = rect_at(&window, &[0, 1]);
assert!(new.x > old.x + 20.0, "{old:?} {new:?}");
let damage = window.take_damage();
for rect in [old, new] {
assert!(damage.contains(&rect), "{rect:?} missing from {damage:?}");
}
}
#[test]
fn a_widget_that_stops_drawing_is_repainted_where_it_was() {
let mut h = Harness::new();
let plain = h.cx.signal(false);
let row = Row::new().align(Align::Start).child(
Row::new()
.style(move || {
if plain.get() {
Style::new().padding(2, 2)
} else {
Style::new().background(Color::rgb(9, 9, 9)).padding(6, 6)
}
})
.child(Icon::new("x").size(8)),
);
let window = h.window(h.panel().child(row), 300.0, 30.0);
window.take_damage();
let old = rect_at(&window, &[0, 0]);
plain.set(true);
h.frame(&window);
let damage = window.take_damage();
assert!(damage.contains(&old), "{old:?} missing from {damage:?}");
assert!(!damage.contains(&rect_at(&window, &[0, 0])), "{damage:?}");
}
#[test]
fn a_widget_with_a_border_gradient_or_shadow_that_moves_is_repainted_at_both_places() {
let black = Color::rgb(9, 9, 9);
for style in [
Style::new().border(1.0, black),
Style::new().gradient(crate::Gradient::radial(black, black)),
Style::new().shadow(crate::Shadow::new(0.0, 0.0, 0.0, black)),
] {
let mut h = Harness::new();
let label = h.cx.signal(String::from("ab"));
let row = Row::new()
.align(Align::Start)
.child(Text::new(move || label.get()))
.child(
Row::new()
.style(style)
.padding(6, 6)
.child(Icon::new("x").size(8)),
);
let window = h.window(h.panel().child(row), 300.0, 30.0);
window.take_damage();
let old = rect_at(&window, &[0, 1]);
label.set(String::from("a much longer label"));
h.frame(&window);
let damage = window.take_damage();
for rect in [old, rect_at(&window, &[0, 1])] {
assert!(
damage.contains(&rect),
"{style:?}: {rect:?} missing from {damage:?}"
);
}
}
}
#[test]
fn a_shadow_widens_the_painted_area_and_a_new_one_is_laid_out() {
use crate::Shadow;
let mut h = Harness::new();
let lifted = h.cx.signal(false);
let row = Row::new().padding(20, 20).align(Align::Start).child(
Row::new()
.style(move || {
let style = Style::new().background(Color::rgb(9, 9, 9));
if lifted.get() {
style.shadow(Shadow::new(0.0, 4.0, 4.0, Color::rgb(0, 0, 0)))
} else {
style
}
})
.padding(6, 6),
);
let window = h.window(h.panel().child(row), 300.0, 60.0);
window.take_damage();
let rect = rect_at(&window, &[0, 0]);
lifted.set(true);
h.runtime.flush();
assert!(window.needs_layout(), "the painted area grows");
h.frame(&window);
let reach = Rect::new(
rect.x - 6.0,
rect.y - 2.0,
rect.width + 12.0,
rect.height + 12.0,
);
let bounds = super::node_at(window.view().root(), &[0, 0]).bounds();
assert_eq!(bounds, rect.union(reach));
let damage = window.take_damage();
assert!(
damage.iter().any(|area| area.union(reach) == *area),
"{reach:?} missing from {damage:?}"
);
}