use std::rc::Rc;
use rustc_hash::FxHashMap;
use crate::jobs::{JobRequest, request_job};
use crate::layout::{Constraints, Size};
use crate::reactive::{OwnerId, dispose_owner_now, under_owner};
use crate::renderer::PaintContext;
use crate::tree::{LayoutCtx, Tree, WidgetId};
use super::Widget;
use super::widget::{Event, EventResponse, LayoutHints};
type ItemsFn = dyn Fn(&Leaving) -> Vec<DynItem>;
enum SegmentType {
Static(usize),
Dynamic {
items_fn: Rc<ItemsFn>,
cached: FxHashMap<u64, WidgetId>,
current_keys: Vec<u64>,
leaving: FxHashMap<u64, WidgetId>,
has_run: bool,
},
}
#[derive(Default)]
pub struct ChildrenSource {
merged: Vec<WidgetId>,
pending_static: Vec<Box<dyn Widget>>,
segments: Vec<SegmentType>,
container_id: Option<WidgetId>,
initial_reconcile_done: bool,
}
impl ChildrenSource {
pub fn set_container_id(&mut self, id: WidgetId) {
self.container_id = Some(id);
}
pub fn add_static(&mut self, widget: Box<dyn Widget>) {
self.pending_static.push(widget);
if let Some(SegmentType::Static(count)) = self.segments.last_mut() {
*count += 1;
} else {
self.segments.push(SegmentType::Static(1));
}
}
pub(crate) fn append(&mut self, mut other: ChildrenSource) {
debug_assert!(
other.merged.is_empty() && !other.initial_reconcile_done,
"a source is forwarded while the tree is being built, before \
anything in it has reached a widget id"
);
let mut statics = std::mem::take(&mut other.pending_static).into_iter();
for segment in std::mem::take(&mut other.segments) {
match segment {
SegmentType::Static(count) => {
for widget in statics.by_ref().take(count) {
self.add_static(widget);
}
}
dynamic => self.segments.push(dynamic),
}
}
}
pub fn register_pending(&mut self, tree: &mut Tree, parent_id: WidgetId) {
for widget in self.pending_static.drain(..) {
let widget_id = tree.register(widget);
tree.set_parent(widget_id, parent_id);
self.merged.push(widget_id);
tree.with_widget_mut(widget_id, |widget, id, tree| {
widget.register_children(tree, id);
});
}
}
pub fn add_dynamic(&mut self, items_fn: impl Fn(&Leaving) -> Vec<DynItem> + 'static) {
self.segments.push(SegmentType::Dynamic {
items_fn: Rc::new(items_fn),
cached: FxHashMap::default(),
current_keys: Vec::new(),
leaving: FxHashMap::default(),
has_run: false,
});
}
pub fn has_dynamic(&self) -> bool {
self.segments
.iter()
.any(|s| matches!(s, SegmentType::Dynamic { .. }))
}
fn reconcile(&mut self, tree: &mut Tree, parent_id: WidgetId) {
let dirty = crate::reactive::invalidation::take_dirty_segments(parent_id);
let mut segments_with_changes: Vec<(usize, Option<Vec<DynItem>>)> = Vec::new();
for (idx, segment) in self.segments.iter_mut().enumerate() {
if let SegmentType::Dynamic {
items_fn,
current_keys,
leaving,
has_run,
..
} = segment
{
let needs_run = !*has_run
|| dirty.as_ref().is_some_and(|d| {
d.contains(&(idx as u32))
|| d.contains(&crate::reactive::invalidation::EVERY_SEGMENT)
});
let settled = leaving.values().any(|&id| !is_exiting(tree, id));
if needs_run {
let items_fn = Rc::clone(items_fn);
let new_items = crate::reactive::invalidation::with_segment_tracking(
parent_id,
idx as u32,
|| items_fn(&Leaving(leaving)),
);
*has_run = true;
let new_keys = new_items.iter().map(|i| i.key);
let unchanged = if leaving.is_empty() {
new_keys.eq(current_keys.iter().copied())
} else {
let live = current_keys.iter().filter(|k| !leaving.contains_key(k));
new_keys.eq(live.copied())
};
if !unchanged {
segments_with_changes.push((idx, Some(new_items)));
continue;
}
}
if settled {
segments_with_changes.push((idx, None));
}
}
}
if segments_with_changes.is_empty() {
return;
}
let old_merged = std::mem::take(&mut self.merged);
let mut old_merged_iter = old_merged.into_iter();
let mut new_merged = Vec::with_capacity(old_merged_iter.len());
let mut change_idx = 0;
for (idx, segment) in self.segments.iter_mut().enumerate() {
match segment {
SegmentType::Static(count) => {
for _ in 0..*count {
if let Some(widget_id) = old_merged_iter.next() {
new_merged.push(widget_id);
}
}
}
SegmentType::Dynamic {
cached,
current_keys,
leaving,
..
} => {
let has_changes = change_idx < segments_with_changes.len()
&& segments_with_changes[change_idx].0 == idx;
if has_changes {
let (_, new_items) = std::mem::take(&mut segments_with_changes[change_idx]);
change_idx += 1;
let old: Vec<(u64, WidgetId)> = current_keys
.drain(..)
.filter_map(|key| old_merged_iter.next().map(|id| (key, id)))
.collect();
let mut rows = Rows {
tree: &mut *tree,
parent_id,
cached,
keys: current_keys,
leaving,
merged: &mut new_merged,
};
match new_items {
Some(new_items) => rows.replace(old, new_items),
None => rows.dispose_settled(old),
}
} else {
for _ in 0..current_keys.len() {
if let Some(widget_id) = old_merged_iter.next() {
new_merged.push(widget_id);
}
}
}
}
}
}
self.merged = new_merged;
}
pub fn reconcile_with_tracking(&mut self, tree: &mut Tree) -> bool {
if !self.has_dynamic() {
return false;
}
let container_id = self
.container_id
.expect("container_id must be set before reconcile_with_tracking");
let prev_count = self.merged.len();
self.reconcile(tree, container_id);
self.initial_reconcile_done = true;
prev_count != self.merged.len()
}
pub fn reconcile_and_get(&mut self, tree: &mut Tree) -> &Vec<WidgetId> {
if self.has_dynamic() && !self.initial_reconcile_done {
if let Some(container_id) = self.container_id {
self.reconcile(tree, container_id);
self.initial_reconcile_done = true;
} else {
}
}
&self.merged
}
pub fn get(&self) -> &Vec<WidgetId> {
&self.merged
}
pub fn get_mut(&mut self) -> &mut Vec<WidgetId> {
&mut self.merged
}
pub fn is_empty(&self) -> bool {
self.merged.is_empty()
}
pub fn len(&self) -> usize {
self.merged.len()
}
}
pub struct Leaving<'a>(&'a FxHashMap<u64, WidgetId>);
impl Leaving<'_> {
pub fn contains(&self, key: u64) -> bool {
self.0.contains_key(&key)
}
}
fn is_exiting(tree: &Tree, id: WidgetId) -> bool {
tree.with_widget(id, |widget| widget.is_exiting())
.unwrap_or(false)
}
struct Rows<'a> {
tree: &'a mut Tree,
parent_id: WidgetId,
cached: &'a mut FxHashMap<u64, WidgetId>,
keys: &'a mut Vec<u64>,
leaving: &'a mut FxHashMap<u64, WidgetId>,
merged: &'a mut Vec<WidgetId>,
}
impl Rows<'_> {
fn replace(&mut self, old: Vec<(u64, WidgetId)>, new_items: Vec<DynItem>) {
let start = self.merged.len();
for &(key, id) in &old {
self.cached.insert(key, id);
}
for item in new_items {
let widget_id = match self.cached.remove(&item.key) {
Some(widget_id) => {
if self.leaving.remove(&item.key).is_some() {
self.tree.with_widget_mut(widget_id, |widget, id, tree| {
widget.cancel_exit(tree, id)
});
crate::jobs::reattach_widget_subtree(self.tree, widget_id);
}
widget_id
}
None => {
let widget = (item.widget_fn)();
let widget_id = self.tree.register(widget);
self.tree.set_parent(widget_id, self.parent_id);
self.tree.with_widget_mut(widget_id, |widget, id, tree| {
widget.register_children(tree, id);
});
widget_id
}
};
self.keys.push(item.key);
self.merged.push(widget_id);
}
let mut after: FxHashMap<Option<u64>, Vec<(u64, WidgetId)>> = FxHashMap::default();
let mut before = None;
for (key, id) in old {
if self.cached.remove(&key).is_none() {
before = Some(key);
continue;
}
let was_leaving = self.leaving.contains_key(&key);
let stays = if was_leaving {
is_exiting(self.tree, id)
} else {
self.begin_leaving(id)
};
if stays {
self.leaving.insert(key, id);
after.entry(before).or_default().push((key, id));
} else if was_leaving {
self.leaving.remove(&key);
crate::jobs::teardown_widget_subtree(self.tree, id);
} else {
crate::jobs::teardown_widget_subtree(self.tree, id);
}
}
if after.is_empty() {
return;
}
let keys = std::mem::take(self.keys);
let ids = self.merged.split_off(start);
let front = after.remove(&None).into_iter().flatten();
let rows = keys.into_iter().zip(ids).flat_map(|row| {
let behind = after.remove(&Some(row.0)).into_iter().flatten();
std::iter::once(row).chain(behind)
});
for (key, id) in front.chain(rows) {
self.keys.push(key);
self.merged.push(id);
}
}
fn dispose_settled(&mut self, old: Vec<(u64, WidgetId)>) {
for (key, id) in old {
if self.leaving.contains_key(&key) && !is_exiting(self.tree, id) {
self.leaving.remove(&key);
crate::jobs::teardown_widget_subtree(self.tree, id);
} else {
self.keys.push(key);
self.merged.push(id);
}
}
}
fn begin_leaving(&mut self, id: WidgetId) -> bool {
let began = self
.tree
.with_widget_mut(id, |widget, id, tree| widget.begin_exit(tree, id))
.unwrap_or(false);
if !began || !is_exiting(self.tree, id) {
return false;
}
crate::reactive::diagnostics::snapshot_zone(|| {
self.tree.with_widget_mut(id, |widget, id, tree| {
widget.event(tree, id, &Event::MouseLeave)
});
self.tree.deliver_hover_changes();
});
crate::jobs::detach_widget_subtree(self.tree, id);
true
}
}
impl Drop for ChildrenSource {
fn drop(&mut self) {
for widget_id in self.merged.drain(..) {
request_job(widget_id, JobRequest::Unregister);
}
for segment in &mut self.segments {
if let SegmentType::Dynamic { cached, .. } = segment {
for widget_id in cached.values() {
request_job(*widget_id, JobRequest::Unregister);
}
cached.clear();
}
}
}
}
pub struct DynItem {
pub key: u64,
pub widget_fn: Box<dyn FnOnce() -> Box<dyn Widget>>,
}
impl DynItem {
pub fn new<W: Widget + 'static>(key: u64, widget_fn: impl FnOnce() -> W + 'static) -> Self {
Self {
key,
widget_fn: Box::new(move || Box::new(widget_fn())),
}
}
}
pub struct OwnedWidget {
inner: Box<dyn Widget>,
owner: OwnerHandle,
}
enum OwnerHandle {
Exclusive(OwnerId),
Shared(SharedOwner),
}
#[derive(Clone)]
pub struct SharedOwner {
guard: Rc<OwnerGuard>,
}
struct OwnerGuard(OwnerId);
impl Drop for OwnerGuard {
fn drop(&mut self) {
dispose_owner_now(self.0);
}
}
impl SharedOwner {
pub fn new(owner_id: OwnerId) -> Self {
Self {
guard: Rc::new(OwnerGuard(owner_id)),
}
}
}
impl OwnedWidget {
pub fn new(inner: Box<dyn Widget>, owner_id: OwnerId) -> Self {
Self {
inner,
owner: OwnerHandle::Exclusive(owner_id),
}
}
pub fn new_shared(inner: Box<dyn Widget>, shared: SharedOwner) -> Self {
Self {
inner,
owner: OwnerHandle::Shared(shared),
}
}
}
impl OwnerHandle {
fn id(&self) -> OwnerId {
match self {
OwnerHandle::Exclusive(id) => *id,
OwnerHandle::Shared(shared) => shared.guard.0,
}
}
}
impl Drop for OwnedWidget {
fn drop(&mut self) {
if let OwnerHandle::Exclusive(owner_id) = self.owner {
dispose_owner_now(owner_id);
}
}
}
impl Widget for OwnedWidget {
fn advance_animations(&mut self, tree: &mut Tree, id: WidgetId) -> bool {
self.inner.advance_animations(tree, id)
}
fn reconcile_children(&mut self, tree: &mut Tree, id: WidgetId) -> bool {
self.inner.reconcile_children(tree, id)
}
fn begin_exit(&mut self, tree: &mut Tree, id: WidgetId) -> bool {
self.inner.begin_exit(tree, id)
}
fn is_exiting(&self) -> bool {
self.inner.is_exiting()
}
fn cancel_exit(&mut self, tree: &mut Tree, id: WidgetId) {
self.inner.cancel_exit(tree, id)
}
fn owned_scope(&self) -> Option<OwnerId> {
Some(self.owner.id())
}
fn refresh_paint_bounds(&self, tree: &mut Tree, id: WidgetId) {
self.inner.refresh_paint_bounds(tree, id)
}
fn placement(&self, tree: &Tree, id: WidgetId) -> crate::widgets::Placement {
self.inner.placement(tree, id)
}
fn layout_hints(&self) -> LayoutHints {
self.inner.layout_hints()
}
fn register_children(&mut self, tree: &mut Tree, id: WidgetId) {
under_owner(self.owner.id(), || self.inner.register_children(tree, id))
}
fn layout(&mut self, ctx: &mut LayoutCtx, constraints: Constraints) -> Size {
under_owner(self.owner.id(), || self.inner.layout(ctx, constraints))
}
fn paint(&self, ctx: &mut PaintContext) {
self.inner.paint(ctx)
}
fn event(&mut self, tree: &mut Tree, id: WidgetId, event: &Event) -> EventResponse {
self.inner.event(tree, id, event)
}
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use super::*;
use crate::jobs::pump_and_layout;
use crate::reactive::storage::live_signal_count;
use crate::reactive::{RwSignal, create_signal};
use crate::widgets::{AnyWidget, Scroll, container, keyed};
struct Rows {
tree: Tree,
root: WidgetId,
}
impl Rows {
fn new(widget: impl Widget + 'static) -> Self {
let mut tree = Tree::new();
let root = tree.register(Box::new(widget));
tree.with_widget_mut(root, |w, id, t| w.register_children(t, id));
let mut rows = Self { tree, root };
rows.frame();
rows
}
fn click(&mut self, x: f32, y: f32) {
let root = self.root;
crate::reactive::diagnostics::snapshot_zone(|| {
for event in [
Event::mouse_down(x, y, crate::widgets::widget::MouseButton::Left),
Event::mouse_up(x, y, crate::widgets::widget::MouseButton::Left),
] {
self.tree.dispatch(root, &event);
}
});
}
fn frame(&mut self) {
pump_and_layout(
&mut self.tree,
self.root,
Constraints::new(0.0, 0.0, 200.0, 200.0),
);
}
}
fn churn(rows: &mut Rows, keys: RwSignal<Vec<u32>>, round: u32) {
let before = rows.tree.get_children(rows.root).to_vec();
keys.set((0..ROWS).map(|i| round * 100 + i).collect());
rows.frame();
assert_ne!(
before,
rows.tree.get_children(rows.root).to_vec(),
"round {round} replaced no rows, so nothing after it measures anything"
);
}
const ROWS: u32 = 4;
#[test]
fn a_signal_built_while_a_row_registers_dies_with_the_row() {
let keys = create_signal(vec![0u32, 1, 2, 3]);
let outer = create_signal(true);
let mut rows = Rows::new(
container()
.width(200.0)
.height(200.0)
.enabled(outer)
.children(keyed(
move || keys.get(),
|k| *k,
move |_| {
let own = create_signal(true);
container().width(50.0).height(10.0).enabled(own).into_any()
},
)),
);
churn(&mut rows, keys, 1);
let settled = live_signal_count();
for round in 2..=10u32 {
churn(&mut rows, keys, round);
}
assert_eq!(
live_signal_count(),
settled,
"thirty-six rows built and discarded left signals behind"
);
}
#[test]
fn a_signal_a_handler_creates_outlives_the_row_that_created_it() {
let keys = create_signal(vec![0u32, 1, 2, 3]);
let escaped: Rc<RefCell<Option<RwSignal<u32>>>> = Rc::new(RefCell::new(None));
let sink = escaped.clone();
let mut rows = Rows::new(container().width(200.0).height(200.0).children(keyed(
move || keys.get(),
|k| *k,
move |k| {
let sink = sink.clone();
container()
.width(50.0)
.height(10.0)
.on_click(move || *sink.borrow_mut() = Some(create_signal(k)))
.into_any()
},
)));
rows.click(0.0, 0.0);
let handed_out = escaped.borrow().expect("the handler ran");
assert_eq!(handed_out.get_untracked(), 0);
for round in 1..=3u32 {
churn(&mut rows, keys, round);
}
assert_eq!(
handed_out.get_untracked(),
0,
"the row's disposal took a signal its handler had given away"
);
}
#[test]
fn a_row_that_builds_nothing_at_registration_was_never_the_problem() {
let keys = create_signal(vec![0u32, 1, 2, 3]);
let mut rows = Rows::new(container().width(200.0).height(200.0).children(keyed(
move || keys.get(),
|k| *k,
move |_| -> AnyWidget { container().width(50.0).height(10.0).into_any() },
)));
churn(&mut rows, keys, 1);
let settled = live_signal_count();
for round in 2..=10u32 {
churn(&mut rows, keys, round);
}
assert_eq!(live_signal_count(), settled);
}
#[test]
fn a_list_inside_a_row_is_inside_the_row_s_scope() {
let outer_keys = create_signal(vec![0u32, 1, 2, 3]);
let enabled = create_signal(true);
let mut rows = Rows::new(
container()
.width(200.0)
.height(200.0)
.enabled(enabled)
.children(keyed(
move || outer_keys.get(),
|k| *k,
move |k| {
let inner_keys = create_signal(vec![k * 10, k * 10 + 1]);
let own = create_signal(true);
container()
.width(100.0)
.height(50.0)
.enabled(own)
.children(keyed(
move || inner_keys.get(),
|x| *x,
move |_| {
let deep = create_signal(true);
container().width(10.0).height(5.0).enabled(deep).into_any()
},
))
.into_any()
},
)),
);
churn(&mut rows, outer_keys, 1);
let settled = live_signal_count();
for round in 2..=10u32 {
churn(&mut rows, outer_keys, round);
}
assert_eq!(
live_signal_count(),
settled,
"a nested list left its rows' signals behind"
);
}
#[test]
fn a_scrollbar_built_while_a_row_lays_out_dies_with_the_row() {
let keys = create_signal(vec![0u32, 1, 2, 3]);
let mut rows = Rows::new(container().width(200.0).height(200.0).children(keyed(
move || keys.get(),
|k| *k,
move |_| {
container()
.width(50.0)
.height(20.0)
.scroll(Scroll::vertical())
.children(
(0..8)
.map(|_| container().width(50.0).height(20.0).into_any())
.collect::<Vec<_>>(),
)
.into_any()
},
)));
churn(&mut rows, keys, 1);
let settled = live_signal_count();
for round in 2..=10u32 {
churn(&mut rows, keys, round);
}
assert_eq!(
live_signal_count(),
settled,
"a scroller left its scrollbar's signals behind"
);
}
}