use teksilo_canvas::{Point, Rect, Size, SizeProposal};
use crate::binding::BindingLevel;
use crate::build_context::BuildContext;
use crate::signal::Signal;
use crate::widget::{LayoutContext, LayoutResponse, Widget, WidgetPlacement};
use crate::widget_id::WidgetId;
pub struct DeferredSubtree {
pending: Option<Box<dyn Widget>>,
child: Option<WidgetId>,
reveal: Option<Signal<bool>>,
forced: bool,
}
impl DeferredSubtree {
pub(crate) fn new(reveal: Option<Signal<bool>>, content: Box<dyn Widget>) -> Self {
Self {
pending: Some(content),
child: None,
reveal,
forced: false,
}
}
pub fn force(&mut self) {
self.forced = true;
}
pub fn is_materialized(&self) -> bool {
self.child.is_some()
}
pub fn materialized_child(&self) -> Option<WidgetId> {
self.child
}
}
impl std::fmt::Debug for DeferredSubtree {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DeferredSubtree")
.field("materialized", &self.child.is_some())
.finish()
}
}
impl Widget for DeferredSubtree {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
if let Some(reveal) = &self.reveal {
reveal.bind_to(ctx.self_id(), ctx.binding_registry(), BindingLevel::Rebuild);
}
if let Some(id) = self.child {
return vec![id];
}
if !self.forced && !self.reveal.as_ref().is_some_and(|r| r.get()) {
return Vec::new();
}
let Some(content) = self.pending.take() else {
return Vec::new();
};
let id = ctx.add_boxed(content);
self.child = Some(id);
vec![id]
}
fn layout_response(&self, proposal: SizeProposal, ctx: &LayoutContext) -> LayoutResponse {
match self.child {
Some(id) => ctx
.child_size(id, proposal)
.unwrap_or(Size::new(0.0, 0.0))
.into(),
None => Size::new(0.0, 0.0).into(),
}
}
fn place_children(
&self,
bounds: Rect,
_proposal: SizeProposal,
children: &mut [WidgetPlacement],
_ctx: &LayoutContext,
) {
for child in children.iter_mut() {
child.origin = Point::new(bounds.x, bounds.y);
child.size = bounds.size();
}
}
fn preserves_children_on_rebuild(&self) -> bool {
true
}
fn accessibility(&self, builder: &mut crate::accessibility::AccessNodeBuilder) {
if let Some(pending) = &self.pending {
pending.accessibility(builder);
}
}
fn tooltip_has_content(&self) -> bool {
match &self.pending {
Some(pending) => pending.tooltip_has_content(),
None => true,
}
}
fn as_any(&self) -> Option<&dyn std::any::Any> {
Some(self)
}
fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
Some(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::widget_tree::WidgetTree;
#[derive(Debug)]
struct BuildCounter {
builds: Signal<u32>,
}
impl Widget for BuildCounter {
fn build(&mut self, _ctx: &mut BuildContext) -> Vec<WidgetId> {
self.builds.set(self.builds.get() + 1);
Vec::new()
}
fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
proposal.resolve(20.0, 12.0).into()
}
}
#[derive(Debug)]
struct Host {
reveal: Signal<bool>,
builds: Signal<u32>,
host_builds: Signal<u32>,
child: Option<WidgetId>,
}
impl Widget for Host {
fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
self.host_builds.set(self.host_builds.get() + 1);
self.reveal
.bind_to(ctx.self_id(), ctx.binding_registry(), BindingLevel::Rebuild);
let id = self.child.unwrap_or_else(|| {
ctx.add_deferred(
self.reveal.clone(),
BuildCounter {
builds: self.builds.clone(),
},
)
});
self.child = Some(id);
vec![id]
}
fn layout_response(&self, proposal: SizeProposal, ctx: &LayoutContext) -> LayoutResponse {
self.child
.and_then(|id| ctx.child_size(id, proposal))
.unwrap_or_else(|| proposal.resolve(0.0, 0.0))
.into()
}
fn preserves_children_on_rebuild(&self) -> bool {
true
}
}
fn tree_with_host() -> (WidgetTree, Signal<bool>, Signal<u32>, Signal<u32>, WidgetId) {
let reveal = Signal::new(false);
let builds = Signal::new(0);
let host_builds = Signal::new(0);
let mut tree = WidgetTree::new();
let id = tree.add(Host {
reveal: reveal.clone(),
builds: builds.clone(),
host_builds: host_builds.clone(),
child: None,
});
tree.layout(SizeProposal::exact(200.0, 100.0));
(tree, reveal, builds, host_builds, id)
}
#[test]
fn content_is_not_built_until_it_is_revealed() {
let (_tree, _reveal, builds, _host_builds, _id) = tree_with_host();
assert_eq!(builds.get(), 0, "content built while it was never revealed");
}
#[test]
fn rebuilding_the_host_does_not_build_unrevealed_content() {
let (mut tree, _reveal, builds, host_builds, id) = tree_with_host();
for _ in 0..5 {
tree.arena_mark_needs_rebuild_for_testing(id);
tree.layout(SizeProposal::exact(200.0, 100.0));
}
assert!(
host_builds.get() >= 5,
"the host itself must really have rebuilt; got {}",
host_builds.get()
);
assert_eq!(
builds.get(),
0,
"the host rebuilt {} times and dragged its unopened content along",
host_builds.get()
);
}
#[test]
fn revealing_builds_the_content_once_and_keeps_it() {
let (mut tree, reveal, builds, _host_builds, id) = tree_with_host();
reveal.set(true);
tree.layout(SizeProposal::exact(200.0, 100.0));
assert_eq!(builds.get(), 1, "revealing must build the content");
reveal.set(false);
tree.layout(SizeProposal::exact(200.0, 100.0));
reveal.set(true);
tree.layout(SizeProposal::exact(200.0, 100.0));
assert_eq!(
builds.get(),
1,
"content was rebuilt on reopen — its state would have been lost"
);
tree.arena_mark_needs_rebuild_for_testing(id);
tree.layout(SizeProposal::exact(200.0, 100.0));
assert_eq!(builds.get(), 1);
}
#[test]
fn the_host_reports_the_size_of_its_child_and_nothing_before_that() {
let (mut tree, reveal, _builds, _host_builds, host) = tree_with_host();
tree.layout(SizeProposal::with_width(200.0));
assert_eq!(
tree.bounds(host).height,
0.0,
"an unbuilt deferred subtree must take no space"
);
reveal.set(true);
tree.layout(SizeProposal::with_width(200.0));
assert_eq!(
tree.bounds(host).height,
12.0,
"once built it is layout-transparent — the child's size, not its own"
);
}
}