use super::*;
use crate::base::{Rect, Size};
use crate::layout::Style as LayoutStyle;
use crate::reactive::Signal;
use crate::theme::default_theme;
use crate::ui::{text, Element, UiTree};
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 focusable_rows(offset: Signal<i32>) -> 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()).focusable();
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_focused_visible(tree: &UiTree, offset: Signal<i32>, view_h: i32) {
let Some(id) = tree.focused() else { return };
let r: Rect = tree.rect_of(id);
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);
}
}
#[test]
fn keyboard_selection_can_ensure_visible_today_with_no_new_engine_api() {
let size = Size::new(VIEW_W, VIEW_H);
let mut offset_holder: Option<Signal<i32>> = None;
let (_root, mut tree) = mount_widget(size, |cx| {
let offset = cx.signal(0);
offset_holder = Some(offset);
focusable_rows(offset)(cx)
});
let offset = offset_holder.expect("built");
settle(&mut tree, size);
tree.focus_first();
settle(&mut tree, size);
for step in 0..=TARGET_ROW {
ensure_focused_visible(&tree, offset, VIEW_H);
settle(&mut tree, size);
let id = tree.focused().expect("focus stays inside the column");
let r = tree.rect_of(id);
assert!(
r.y >= 0 && r.y + r.h <= VIEW_H,
"step {step}: the selected child must be FULLY visible in \
the {VIEW_H}-row viewport, got {r:?} at offset {}",
offset.get_untracked()
);
if step < TARGET_ROW {
tree.focus_next();
settle(&mut tree, size);
}
}
assert!(
offset.get_untracked() > 0,
"control: selection walked past the fold, so the offset must \
have moved; it is still {}",
offset.get_untracked()
);
let id = tree.focused().expect("focus stayed in the column");
let content_top = tree.rect_of(id).y + offset.get_untracked();
let uniform_guess = TARGET_ROW;
assert_ne!(
content_top, uniform_guess,
"control: with mixed heights the true content offset of child \
{TARGET_ROW} must differ from the uniform-height guess, or \
this test does not exercise the case 0910 is about"
);
}
#[test]
fn focus_alone_does_not_scroll_the_selected_child_into_view() {
let size = Size::new(VIEW_W, VIEW_H);
let mut offset_holder: Option<Signal<i32>> = None;
let (_root, mut tree) = mount_widget(size, |cx| {
let offset = cx.signal(0);
offset_holder = Some(offset);
focusable_rows(offset)(cx)
});
let offset = offset_holder.expect("built");
settle(&mut tree, size);
tree.focus_first();
settle(&mut tree, size);
let first = tree.rect_of(tree.focused().expect("focused"));
assert!(
first.y >= 0 && first.y < VIEW_H,
"sanity: the first row starts inside the viewport, got {first:?}"
);
for _ in 0..TARGET_ROW {
tree.focus_next();
settle(&mut tree, size);
}
let r = tree.rect_of(tree.focused().expect("focus stayed in the column"));
assert_eq!(
offset.get_untracked(),
0,
"the engine did not scroll: the bound offset never moved"
);
assert!(
r.y >= VIEW_H,
"THE GAP 0910 IS ABOUT: focus is on a child below the fold and \
the viewport did not follow it — {r:?} against a {VIEW_H}-row \
viewport. rect_of reports it correctly, which is what makes \
the app-side clamp possible; nothing in the engine applies it."
);
}