use std::cell::Cell;
use std::rc::Rc;
use super::*;
use crate::base::{Rect, Size};
use crate::layout::Style as LayoutStyle;
use crate::reactive::{flush_effects, Signal};
use crate::theme::default_theme;
use crate::ui::{dyn_view_scoped, text, Element};
use crate::widgets::itest_util::{mount_widget, settle};
const VIEW_W: i32 = 12;
const VIEW_H: i32 = 4;
const ROWS: i32 = 20;
const TARGET_ROW: i32 = 14;
fn child_height(i: i32) -> i32 {
if i % 3 == 0 {
3
} else {
1
}
}
fn content_height() -> i32 {
(0..ROWS).map(child_height).sum()
}
fn column_with_binding(
offset: Signal<i32>,
bound: i32,
sig: Signal<Option<Rect>>,
painted: Rc<Cell<bool>>,
) -> impl FnOnce(crate::reactive::Scope) -> View {
move |cx| {
let t = &default_theme().tokens;
let mut col = Element::new().style(LayoutStyle::column());
for i in 0..ROWS {
let mut card = Element::new().style(LayoutStyle::column());
if i == bound {
let painted = painted.clone();
card = card
.rect_signal(sig)
.draw(move |_canvas, _rect| painted.set(true));
}
for line in 0..child_height(i) {
card = card.child(text(format!("r{i}.{line}")));
}
col = col.child(card.build());
}
Scroll::new(col.build())
.content_size(VIEW_W - 1, content_height())
.offset_y(offset)
.element(cx, t)
.build()
}
}
fn ensure_visible(sig: Signal<Option<Rect>>, offset: Signal<i32>, view_h: i32) -> bool {
let Some(r) = sig.get_untracked() else {
return false;
};
let cur = offset.get_untracked();
let top = r.y + cur;
let bottom = top + r.h;
let mut next = cur;
if top < next {
next = top;
}
if bottom > next + view_h {
next = bottom - view_h;
}
if next != cur {
offset.set(next);
}
true
}
#[test]
fn an_off_screen_child_publishes_its_true_solved_rect() {
let size = Size::new(VIEW_W, VIEW_H);
let painted = Rc::new(Cell::new(false));
let mut wires: Option<(Signal<i32>, Signal<Option<Rect>>)> = None;
let (_root, mut tree) = mount_widget(size, {
let painted = painted.clone();
|cx| {
let offset = cx.signal(0);
let sig = cx.signal(None);
wires = Some((offset, sig));
column_with_binding(offset, TARGET_ROW, sig, painted)(cx)
}
});
let (offset, sig) = wires.expect("built");
settle(&mut tree, size);
let published = sig.get_untracked().expect("the binding published a rect");
assert!(
published.y >= VIEW_H,
"control: the bound child must start below the {VIEW_H}-row \
fold, got {published:?}"
);
assert!(
!painted.get(),
"control: the bound child must NOT have painted while below \
the fold, or this test says nothing about publishing from \
layout rather than from paint"
);
assert!(
published.h == child_height(TARGET_ROW),
"the published rect is the SOLVED one: child {TARGET_ROW} is \
{} rows tall, got {published:?}",
child_height(TARGET_ROW)
);
assert!(ensure_visible(sig, offset, VIEW_H), "a rect was available");
settle(&mut tree, size);
let after = sig
.get_untracked()
.expect("still published after scrolling");
assert!(
painted.get(),
"control on the control: the bound child paints once it is \
visible, so `painted` is a real signal of paint"
);
assert!(
after.y >= 0 && after.y + after.h <= VIEW_H,
"the bound child must be FULLY visible after one clamp, got \
{after:?} at offset {}",
offset.get_untracked()
);
let content_top = after.y + offset.get_untracked();
assert_ne!(
content_top, TARGET_ROW,
"control: mixed heights must make the true offset of child \
{TARGET_ROW} differ from the uniform guess"
);
}
#[test]
fn a_collapsed_child_publishes_none_rather_than_the_origin() {
let size = Size::new(VIEW_W, VIEW_H);
let mut wires: Option<(Signal<bool>, Signal<Option<Rect>>)> = None;
let (_root, mut tree) = mount_widget(size, |cx| {
let collapsed = cx.signal(false);
let sig = cx.signal(None);
wires = Some((collapsed, sig));
let card = Element::new()
.style(LayoutStyle::column())
.rect_signal(sig)
.style_signal(move || {
let mut s = LayoutStyle::column();
s.height = if collapsed.get() {
crate::layout::Dimension::Cells(0)
} else {
crate::layout::Dimension::Cells(2)
};
s
})
.child(text("card"));
Element::new()
.style(LayoutStyle::column())
.child(card.build())
.child(text("sibling"))
.build()
});
let (collapsed, sig) = wires.expect("built");
settle(&mut tree, size);
let open = sig.get_untracked().expect("an open card has a position");
assert_eq!(
open.h, 2,
"sanity: the card is 2 rows before it collapses, got {open:?}"
);
collapsed.set(true);
settle(&mut tree, size);
assert_eq!(
sig.get_untracked(),
None,
"a zero-area child must publish None: it is CLEAN ABSENCE, and \
a 0x0 rect reaching an ensure-visible clamp scrolls to the \
origin — which reads as a jump to the top, not as a collapse"
);
collapsed.set(false);
settle(&mut tree, size);
assert_eq!(
sig.get_untracked().map(|r| r.h),
Some(2),
"re-expanding republishes a real rect"
);
}
#[test]
fn a_publish_scheduled_before_disposal_does_not_land_after_it() {
let size = Size::new(VIEW_W, VIEW_H);
let mut wires: Option<(Signal<bool>, Signal<Option<Rect>>)> = None;
let (_root, mut tree) = mount_widget(size, |cx| {
let show = cx.signal(true);
let sig = cx.signal(None);
wires = Some((show, sig));
dyn_view_scoped(LayoutStyle::column(), move |_| {
if show.get() {
Element::new()
.style(LayoutStyle::column())
.rect_signal(sig)
.child(text("doomed"))
.build()
} else {
text("replacement")
}
})
});
let (show, sig) = wires.expect("built");
flush_effects();
tree.layout();
assert_eq!(
sig.get_untracked(),
None,
"precondition: the publish is deferred, so nothing has landed \
yet — if this is already Some, the race below is not armed"
);
show.set(false);
settle(&mut tree, size);
assert_eq!(
sig.get_untracked(),
None,
"an element disposed between scheduling and firing published \
its rect anyway — the caller's signal is alive, which is not \
the question that decides this"
);
}
#[test]
fn a_rebuild_moves_the_binding_to_the_newly_selected_card() {
let size = Size::new(VIEW_W, VIEW_H);
let mut wires: Option<(Signal<i32>, Signal<Option<Rect>>)> = None;
let (_root, mut tree) = mount_widget(size, |cx| {
let selected = cx.signal(0);
let sig = cx.signal(None);
wires = Some((selected, sig));
dyn_view_scoped(LayoutStyle::column(), move |_| {
let sel = selected.get(); let mut col = Element::new().style(LayoutStyle::column());
for i in 0..3 {
let mut card = Element::new().style(LayoutStyle::column());
if i == sel {
card = card.rect_signal(sig);
}
card = card.child(text(format!("c{i}")));
col = col.child(card.build());
}
col.build()
})
});
let (selected, sig) = wires.expect("built");
settle(&mut tree, size);
let first = sig.get_untracked().expect("the selected card published");
assert_eq!(first.y, 0, "card 0 is the top row, got {first:?}");
selected.set(2);
settle(&mut tree, size);
let third = sig.get_untracked().expect("the new selection published");
assert_eq!(
third.y, 2,
"the binding must follow the selection to card 2 (row 2), not \
report the previous card's rect: got {third:?}"
);
}