//! Where a frame puts things, with no window to put them in. use iris::harness::{Harness, assert_corners}; use iris::prelude::*; /// A fixed 100 wide, and the rest of the 400 to its neighbour. fn two_rects(h: &mut Harness) -> (WidgetId, WidgetId) { let left = rect(Color::RED).width(100).add(&mut h.rsc); let right = rect(Color::BLUE).add(&mut h.rsc); h.set_root((left, right).span(Dir::RIGHT)); (left.id(), right.id()) } #[test] fn a_span_gives_each_child_the_width_it_asked_for() { let mut h = Harness::new((400, 200)); let (left, right) = two_rects(&mut h); assert_corners!(h, left, (0, 0), (100, 200)); assert_corners!(h, right, (100, 0), (400, 200)); } #[test] fn resizing_relays_out_against_the_new_output() { let mut h = Harness::new((400, 200)); let (left, right) = two_rects(&mut h); h.resize((800, 100)); assert!(h.needs_redraw()); h.frame(); assert_corners!(h, left, (0, 0), (100, 100)); assert_corners!(h, right, (100, 0), (800, 100)); } #[test] fn an_empty_widget_takes_a_share_of_a_span() { let mut h = Harness::new((400, 200)); let gap = ().add(&mut h.rsc); let right = rect(Color::BLUE).width(100).add(&mut h.rsc); h.set_root((gap, right).span(Dir::RIGHT)); assert_corners!(h, gap, (0, 0), (300, 200)); assert_corners!(h, right, (300, 0), (400, 200)); } #[test] fn a_child_drawn_twice_moves_once() { let mut h = Harness::new((400, 200)); // `Aligned` draws its child twice; listing it twice would move it twice. let inner = rect(Color::BLUE).add(&mut h.rsc); let centered = inner.center().width(200).add(&mut h.rsc); let left = rect(Color::RED).width(100).add(&mut h.rsc); h.set_root((left, centered).span(Dir::RIGHT)); assert_corners!(h, inner, (100, 0), (300, 200)); h.rsc[left].x = Some(Len::px(150)); h.frame(); assert_corners!(h, inner, (150, 0), (350, 200)); } #[test] fn a_resize_lands_where_a_cold_start_would() { let build = |h: &mut Harness| { let para = wtext( "Wrapping shapes one source into as many lines as its container leaves room \ for, so the height of a paragraph is an answer rather than a setting.", ) .size(20) .wrap(true) .pad(16) .add(&mut h.rsc); let below = rect(Color::RED).add(&mut h.rsc); let root = (para, below).span(Dir::DOWN).pad(12); h.set_root(root); (para, below) }; let mut cold = Harness::new((900, 1200)); let (cold_para, cold_below) = build(&mut cold); let mut resized = Harness::new((1920, 1200)); let (para, below) = build(&mut resized); resized.resize((900, 1200)); resized.frame(); assert_eq!(resized.region(¶), cold.region(&cold_para), "paragraph"); assert_eq!(resized.region(&below), cold.region(&cold_below), "below"); } #[test] fn a_fixed_box_is_drawn_again_rather_than_stretched() { let mut h = Harness::new((400, 400)); // The panel fills a stack sized by its sibling, so it is drawn in the // whole box and then placed in the shorter one. Reusing it in that fixed // box afterwards would leave it whatever height it happened to have. let panel = rect(Color::BLUE).add(&mut h.rsc); let leaf = rect(Color::RED).height(100).add(&mut h.rsc); let stack = (panel, leaf) .stack() .size(StackSize::Child(1)) .add(&mut h.rsc); h.set_root(stack.align(Align::TOP)); assert_corners!(h, panel, (0, 0), (400, 100)); h.rsc[leaf].y = Some(Len::px(250)); h.frame(); assert_corners!(h, panel, (0, 0), (400, 250)); } #[test] fn a_moved_subtree_takes_its_children_with_it() { let mut h = Harness::new((400, 400)); let first = rect(Color::RED).height(40).add(&mut h.rsc); let inner = rect(Color::BLUE).add(&mut h.rsc); let row = inner.pad(10).height(40).add(&mut h.rsc); h.set_root((first, row).span(Dir::DOWN)); assert_corners!(h, inner, (10, 50), (390, 70)); h.rsc[first].y = Some(Len::px(80)); h.frame(); // The row is the same shape somewhere else, so one slot moved it and // `inner`'s own region was never rewritten. assert_corners!(h, inner, (10, 90), (390, 110)); } #[test] fn a_fixed_length_child_keeps_it_when_the_box_around_it_grows() { let mut h = Harness::new((400, 200)); let fixed = rect(Color::BLUE).width(50).add(&mut h.rsc); let leftover = rect(Color::GREEN).add(&mut h.rsc); let panel = (fixed, leftover).span(Dir::RIGHT).add(&mut h.rsc); // Changing the bar's width is the only thing that changes the box the // panel and everything under it was drawn for. let bar = rect(Color::RED).width(100).add(&mut h.rsc); h.set_root((bar, panel).span(Dir::RIGHT)); assert_corners!(h, fixed, (100, 0), (150, 200)); assert_corners!(h, leftover, (150, 0), (400, 200)); h.rsc[bar].x = Some(Len::px(200)); h.frame(); // The panel's box is 100 shorter, so the fixed child is the same 50 wide // against its new start and the one taking what is left absorbs the change. assert_corners!(h, fixed, (200, 0), (250, 200)); assert_corners!(h, leftover, (250, 0), (400, 200)); } #[test] fn a_box_with_a_fixed_length_can_be_stretched_on_its_other_axis() { let mut h = Harness::new((400, 200)); // The row is 40 tall whatever happens, which used to make its drawing // impossible to take out of: recovering a fraction of a box needs a // relative extent, and it has none on that axis. let inner = rect(Color::BLUE).add(&mut h.rsc); let row = inner.pad(10).height(40).add(&mut h.rsc); let filler = rect(Color::GREEN).add(&mut h.rsc); let column = (row, filler).span(Dir::DOWN).add(&mut h.rsc); let bar = rect(Color::RED).width(100).add(&mut h.rsc); h.set_root((bar, column).span(Dir::RIGHT)); assert_corners!(h, inner, (110, 10), (390, 30)); h.rsc[bar].x = Some(Len::px(200)); h.frame(); assert_corners!(h, inner, (210, 10), (390, 30)); } #[test] fn only_a_container_that_places_its_children_lengthens_the_chain() { let mut h = Harness::new((400, 200)); let leaf = rect(Color::BLUE).add(&mut h.rsc); // Four widgets between the span and the leaf, none of which places what // it draws, so all of them share the span's slot. let buried = leaf.pad(4).pad(4).pad(4).pad(4).add(&mut h.rsc); let bar = rect(Color::RED).width(100).add(&mut h.rsc); h.set_root((bar, buried).span(Dir::RIGHT)); let slot = h.render.active[&leaf.id()].parent_move; assert_eq!( h.render.moves.depth(slot), 2, "the span above the leaf, and the root the window is held in" ); } /// A span that sizes from its children passes their `leftover` weight up /// than collapsing it to one share, so nesting divides the same space instead /// of re-dividing a share of it. #[test] fn nested_spans_divide_the_space_once_however_deep_the_nesting_is() { let mut h = Harness::new((400, 200)); let (a, b, c, d) = ( rect(Color::RED).add(&mut h.rsc), rect(Color::BLUE).add(&mut h.rsc), rect(Color::GREEN).add(&mut h.rsc), rect(Color::WHITE).add(&mut h.rsc), ); let left = (a, b).span(Dir::RIGHT).add(&mut h.rsc); let right = (c, d).span(Dir::RIGHT).add(&mut h.rsc); h.set_root((left, right).span(Dir::RIGHT)); for (i, id) in [a, b, c, d].into_iter().enumerate() { let x = i as f32 * 100.0; assert_corners!(h, id, (x, 0), (x + 100.0, 200)); } } /// The same space, unevenly nested: weights carried up mean a share is a /// share of the whole, not of whatever branch a widget happens to sit in. #[test] fn an_uneven_nesting_still_gives_every_share_the_same_length() { let mut h = Harness::new((400, 200)); let (a, b, c, d) = ( rect(Color::RED).add(&mut h.rsc), rect(Color::BLUE).add(&mut h.rsc), rect(Color::GREEN).add(&mut h.rsc), rect(Color::WHITE).add(&mut h.rsc), ); let one = (a,).span(Dir::RIGHT).add(&mut h.rsc); let three = (b, c, d).span(Dir::RIGHT).add(&mut h.rsc); h.set_root((one, three).span(Dir::RIGHT)); for (i, id) in [a, b, c, d].into_iter().enumerate() { let x = i as f32 * 100.0; assert_corners!(h, id, (x, 0), (x + 100.0, 200)); } } /// Where the shader puts an edge: the two parts of a scalar are floored /// apart, so a fraction and a pixel offset snap independently. fn drawn_edges(h: &Harness, id: WidgetId, axis: Axis) -> (f32, f32) { let active = &h.render.active[&id]; let region = h.render.moves.resolve(active.parent_move, active.region); let dim = h.size().axis(axis); let edge = |s: UiScalar| (s.rel * dim).floor() + s.px.floor(); let span = region.axis(axis); (edge(span.start), edge(span.end)) } fn hairline(h: &mut Harness, marks: &mut Vec) -> StrongWidget { let inner = rect(Color::RED).add_strong(&mut h.rsc); let mark = SetSize { inner, x: Some(Len::px(1.0)), y: None, } .add_strong(&mut h.rsc); marks.push(mark.id()); mark } fn share(h: &mut Harness, inner: StrongWidget, ratio: f32) -> StrongWidget { SetSize { inner, x: Some(Len::leftover(ratio)), y: None, } .add_strong(&mut h.rsc) } /// Shares in weights no binary fraction lands on, a padding on one branch /// and not the other, so an edge falls near an integer as often as it can. fn hairlines(h: &mut Harness, depth: usize, marks: &mut Vec) -> StrongWidget { let mut span = Span::empty(Dir::RIGHT); if depth == 0 { let left = rect(Color::BLUE).add_strong(&mut h.rsc); let left = share(h, left, 3.0); span.push(left); let mark = hairline(h, marks); span.push(mark); let right = rect(Color::BLUE).add_strong(&mut h.rsc); let right = share(h, right, 7.0); span.push(right); return span.add_strong(&mut h.rsc); } let first = hairlines(h, depth - 1, marks); let first = share(h, first, 3.0); span.push(first); let second = hairlines(h, depth - 1, marks); let second = Pad { padding: Padding { left: 3.0, right: 7.0, top: 0.0, bottom: 0.0, }, inner: second, } .add_strong(&mut h.rsc); let second = share(h, second, 5.0); span.push(second); span.add_strong(&mut h.rsc) } /// A one-pixel line is a pixel wherever it is drawn. Both edges of a fixed /// length share their box's fraction, so composing the chain moves them /// together and the shader's `floor` cannot round the pixel between them /// away -- only shift it. A separator that disappeared at one window size /// would be a defect no size comparison catches. #[test] fn a_one_pixel_line_keeps_its_pixel_through_a_chain() { let mut h = Harness::new((1920, 1200)); let mut marks = Vec::new(); let root = hairlines(&mut h, 4, &mut marks); h.state.set_root(root); h.frame(); assert_eq!(marks.len(), 16); for size in [(1920, 1200), (1919, 1201), (997, 1003), (1367, 733)] { h.resize(size); h.frame(); for mark in &marks { let (start, end) = drawn_edges(&h, *mark, Axis::X); assert_eq!(end - start, 1.0, "at {size:?}, mark {mark:?}"); } } } /// A span short of room takes it from its shares, which go to nothing and /// then to nothing wider; the fixed lengths between them keep their pixels. /// Collapsing those to make room would delete a separator the caller asked /// for, which is worse than overflowing. #[test] fn a_span_out_of_room_shrinks_its_shares_and_not_its_fixed_lengths() { let mut h = Harness::new((400, 20)); let mut marks = Vec::new(); let mut span = Span::empty(Dir::RIGHT); for _ in 0..3 { let share_of = rect(Color::BLUE).add_strong(&mut h.rsc); let share_of = share(&mut h, share_of, 1.0); span.push(share_of); let mark = hairline(&mut h, &mut marks); span.push(mark); } let root = span.add_strong(&mut h.rsc); h.state.set_root(root); h.frame(); for width in [400, 10, 3, 1] { h.resize((width, 20)); h.frame(); for mark in &marks { let (start, end) = drawn_edges(&h, *mark, Axis::X); assert_eq!(end - start, 1.0, "at {width} wide, mark {mark:?}"); } } }