use cranpose_core::key;
use cranpose_render_common::{
graph::{LayerNode, RenderNode},
scene_builder::build_graph_from_applier,
};
use cranpose_ui::Layout;
use cranpose_ui_layout::{
Constraints, Measurable, MeasurePolicy, MeasureResult, MeasureScope, Placement,
};
use super::*;
#[path = "liquid_lens_frame_tests.rs"]
mod liquid_lens_frame_tests;
#[path = "draw_observation_identity_tests.rs"]
mod draw_observation_identity_tests;
const FIRST_TRADE: u64 = 100;
const TAPE_ROWS: u64 = 4;
const ROW_HEIGHT: f32 = 18.0;
thread_local! {
static TAPE_HEAD: RefCell<Option<MutableState<u64>>> = const { RefCell::new(None) };
static PEEK_SHOWN: RefCell<Option<MutableState<bool>>> = const { RefCell::new(None) };
}
#[composable]
fn TradeTape() {
let head = rememberMutableStateOf(|| FIRST_TRADE + TAPE_ROWS);
TAPE_HEAD.with(|slot| *slot.borrow_mut() = Some(head));
let newest = head.get();
Column(
Modifier::empty().fill_max_size().padding(8.0),
ColumnSpec::default(),
move || {
Column(
Modifier::empty().fill_max_width(),
ColumnSpec::default(),
move || {
for trade in (FIRST_TRADE..=newest).rev() {
key(trade, || TradeRow(trade));
}
},
);
Box(
Modifier::empty()
.size_points(60.0, 6.0)
.background(Color(0.5, 0.5, 0.5, 1.0)),
BoxSpec::default(),
|| {},
);
},
);
}
#[composable]
fn TradeRow(trade: u64) {
Row(
Modifier::empty().fill_max_width().height(ROW_HEIGHT),
RowSpec::default(),
move || {
Text(
format!("trade {trade}"),
Modifier::empty(),
TextStyle::default(),
);
Box(
Modifier::empty()
.size_points(2.0, 12.0)
.background(Color(0.2, 0.8, 0.3, 1.0)),
BoxSpec::default(),
|| {},
);
},
);
}
#[derive(Clone, Copy, PartialEq)]
struct PeekPolicy {
shown: bool,
}
impl MeasurePolicy for PeekPolicy {
fn measure(
&self,
_scope: &dyn MeasureScope,
measurables: &[Box<dyn Measurable>],
constraints: Constraints,
) -> MeasureResult {
let mut placements = Vec::with_capacity(measurables.len());
for (index, measurable) in measurables.iter().enumerate() {
let mut child = constraints.loosen();
if !self.shown {
child.max_width -= 1.0;
}
let placeable = measurable.measure(child);
if index == 0 || self.shown {
placements.push(Placement::new(
placeable.node_id(),
0.0,
index as f32 * 20.0,
0,
));
}
}
MeasureResult::new(
Size {
width: 100.0,
height: 40.0,
},
placements,
)
}
fn min_intrinsic_width(&self, _measurables: &[Box<dyn Measurable>], _height: f32) -> f32 {
100.0
}
fn max_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
self.min_intrinsic_width(measurables, height)
}
fn min_intrinsic_height(&self, _measurables: &[Box<dyn Measurable>], _width: f32) -> f32 {
40.0
}
fn max_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
self.min_intrinsic_height(measurables, width)
}
}
#[composable]
fn PeekBox() {
let shown = rememberMutableStateOf(|| true);
PEEK_SHOWN.with(|slot| *slot.borrow_mut() = Some(shown));
let visible = shown.get();
Column(
Modifier::empty().fill_max_size(),
ColumnSpec::default(),
move || {
Layout(Modifier::empty(), PeekPolicy { shown: visible }, || {
Text(
"always".to_string(),
Modifier::empty(),
TextStyle::default(),
);
Box(
Modifier::empty()
.size_points(30.0, 10.0)
.background(Color(0.1, 0.2, 0.9, 1.0)),
BoxSpec::default(),
|| {},
);
});
Box(
Modifier::empty()
.size_points(80.0, if visible { 6.0 } else { 8.0 })
.background(Color(0.5, 0.5, 0.5, 1.0)),
BoxSpec::default(),
|| {},
);
},
);
}
fn scene_picture(layer: &LayerNode, depth: usize, out: &mut Vec<String>) {
out.push(format!(
"{depth} layer {:?} wraps {:?} bounds {:?} node {:?} transform {:?} content {:?} \
origin {:?} clip {} shadow {:?} graphics {:?} translated {} {:?}",
layer.node_id,
layer.wraps,
layer.local_bounds,
layer.node_bounds,
layer.transform_to_parent,
layer.content_offset,
layer.origin_in_parent,
layer.clip_to_bounds,
layer.shadow_clip,
layer.graphics_layer,
layer.translated_content_context,
layer.translated_content_offset,
));
for child in &layer.children {
match child {
RenderNode::Layer(child) => scene_picture(child, depth + 1, out),
RenderNode::Primitive(entry) => out.push(format!("{depth} primitive {entry:?}")),
RenderNode::DrawRun(run) => {
for primitive in run.primitives() {
out.push(format!("{depth} run {:?} {primitive:?}", run.phase));
}
}
}
}
}
fn drawn_picture(shell: &AppShell<ScopedUpdateCountingRenderer>) -> Vec<String> {
let graph = shell.surfaces[0]
.renderer
.scene()
.graph
.as_ref()
.expect("the frame should have built a scene");
let mut picture = Vec::new();
scene_picture(&graph.root, 0, &mut picture);
picture
}
fn rebuilt_picture(shell: &mut AppShell<ScopedUpdateCountingRenderer>) -> Vec<String> {
let root = shell.app.composition.root().expect("composition root");
let app_context = Rc::clone(&shell.app.app_context);
let graph = app_context
.enter(|| build_graph_from_applier(&shell.app.composition.applier_mut(), root, 1.0))
.expect("a whole-scene rebuild should lower the root");
let mut picture = Vec::new();
scene_picture(&graph.root, 0, &mut picture);
picture
}
fn assert_scene_matches_rebuild(shell: &mut AppShell<ScopedUpdateCountingRenderer>, frame: &str) {
let drawn = drawn_picture(shell);
let rebuilt = rebuilt_picture(shell);
if let Some(line) = drawn.iter().zip(&rebuilt).position(|(a, b)| a != b) {
let (drawn, rebuilt) = (&drawn[line], &rebuilt[line]);
let at = drawn
.bytes()
.zip(rebuilt.bytes())
.position(|(a, b)| a != b)
.unwrap_or(0);
let near = |text: &str| {
text.get(at.saturating_sub(160)..(at + 160).min(text.len()))
.unwrap_or(text)
.to_string()
};
panic!(
"{frame}: the scoped scene differs from a rebuild at line {line}, byte {at}:\n drawn: {}\n rebuilt: {}",
near(drawn),
near(rebuilt)
);
}
assert_eq!(
drawn.len(),
rebuilt.len(),
"{frame}: the scoped scene and a rebuild draw a different number of things"
);
}
fn drawn_texts(shell: &AppShell<ScopedUpdateCountingRenderer>) -> Vec<String> {
super::graph_scene_text_values(shell.surfaces[0].renderer.scene())
}
struct CountedShell {
shell: AppShell<ScopedUpdateCountingRenderer>,
rebuilds: Rc<Cell<usize>>,
updates: Rc<Cell<usize>>,
}
impl CountedShell {
fn settled(root_key: cranpose_core::Key, content: impl FnMut() + 'static) -> Self {
let rebuilds = Rc::new(Cell::new(0));
let updates = Rc::new(Cell::new(0));
let mut shell = AppShell::new(
ScopedUpdateCountingRenderer::new(
Rc::clone(&rebuilds),
Rc::clone(&updates),
Rc::new(RefCell::new(Vec::new())),
),
root_key,
content,
);
shell.set_buffer_size(320, 240);
shell.set_viewport(320.0, 240.0);
shell.update();
Self {
shell,
rebuilds,
updates,
}
}
fn frame(&mut self) -> (usize, usize) {
self.rebuilds.set(0);
self.updates.set(0);
self.shell.update();
(self.rebuilds.get(), self.updates.get())
}
}
#[test]
fn rows_inserted_at_the_top_of_a_keyed_column_ride_the_scoped_update() {
let _guard = test_guard();
TAPE_HEAD.with(|slot| slot.borrow_mut().take());
let mut tape = CountedShell::settled(location_key(file!(), line!(), column!()), TradeTape);
let head = TAPE_HEAD
.with(|slot| *slot.borrow())
.expect("the tape exposes its newest trade");
for _ in 0..3 {
let newest = head.get_non_reactive() + 1;
head.set_value(newest);
let (rebuilds, updates) = tape.frame();
assert_eq!(
(rebuilds, updates),
(0, 1),
"a trade inserted at the top must reach the scoped update, not a rebuild"
);
let texts = drawn_texts(&tape.shell);
assert_eq!(
texts.first().map(String::as_str),
Some(format!("trade {newest}").as_str()),
"the new trade must draw first: {texts:?}"
);
assert_scene_matches_rebuild(&mut tape.shell, "keyed insertion");
}
}
#[test]
fn an_unchanged_layout_pass_preserves_the_scene_without_recording_it() {
let _guard = test_guard();
let mut host = CountedShell::settled(location_key(file!(), line!(), column!()), TradeTape);
host.frame();
let picture = drawn_picture(&host.shell);
host.shell.app.request_layout_pass();
assert_eq!(host.frame(), (0, 0));
assert_eq!(drawn_picture(&host.shell), picture);
assert_scene_matches_rebuild(&mut host.shell, "unchanged layout");
}
#[test]
fn a_node_its_parent_stops_placing_leaves_the_scene_and_returns_where_it_was() {
let _guard = test_guard();
PEEK_SHOWN.with(|slot| slot.borrow_mut().take());
let mut peek = CountedShell::settled(location_key(file!(), line!(), column!()), PeekBox);
let shown = PEEK_SHOWN
.with(|slot| *slot.borrow())
.expect("the box exposes whether it shows its second child");
let settled = drawn_picture(&peek.shell);
for show in [false, true, false, true] {
shown.set_value(show);
peek.frame();
assert_scene_matches_rebuild(
&mut peek.shell,
if show {
"a child placed again at its old spot"
} else {
"a child no longer placed"
},
);
let picture = drawn_picture(&peek.shell);
if show {
assert_eq!(
picture, settled,
"the child must come back exactly as it was"
);
} else {
assert!(
picture.len() < settled.len(),
"the unplaced child must leave the scene"
);
}
}
}