use crate::{ geometry::{ rect::RectF, vector::{vec2f, Vector2F}, }, json::json, AnyElement, Element, MouseRegion, PaintContext, SizeConstraint, ViewContext, }; use std::{cell::RefCell, collections::VecDeque, fmt::Debug, ops::Range, rc::Rc}; use sum_tree::{Bias, SumTree}; pub struct List { state: ListState, } pub struct ListState(Rc>>); #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Orientation { Top, Bottom, } struct StateInner { last_layout_width: Option, render_item: Box) -> AnyElement>, rendered_range: Range, items: SumTree>, logical_scroll_top: Option, orientation: Orientation, overdraw: f32, #[allow(clippy::type_complexity)] scroll_handler: Option, &mut V, &mut ViewContext)>>, } #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct ListOffset { pub item_ix: usize, pub offset_in_item: f32, } enum ListItem { Unrendered, Rendered(Rc>>), Removed(f32), } impl Clone for ListItem { fn clone(&self) -> Self { match self { Self::Unrendered => Self::Unrendered, Self::Rendered(element) => Self::Rendered(element.clone()), Self::Removed(height) => Self::Removed(*height), } } } impl Debug for ListItem { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Unrendered => write!(f, "Unrendered"), Self::Rendered(_) => f.debug_tuple("Rendered").finish(), Self::Removed(height) => f.debug_tuple("Removed").field(height).finish(), } } } #[derive(Clone, Debug, Default, PartialEq)] struct ListItemSummary { count: usize, rendered_count: usize, unrendered_count: usize, height: f32, } #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] struct Count(usize); #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] struct RenderedCount(usize); #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] struct UnrenderedCount(usize); #[derive(Clone, Debug, Default)] struct Height(f32); impl List { pub fn new(state: ListState) -> Self { Self { state } } } impl Element for List { type LayoutState = ListOffset; type PaintState = (); fn layout( &mut self, constraint: SizeConstraint, view: &mut V, cx: &mut ViewContext, ) -> (Vector2F, Self::LayoutState) { let state = &mut *self.state.0.borrow_mut(); let size = constraint.max; let mut item_constraint = constraint; item_constraint.min.set_y(0.); item_constraint.max.set_y(f32::INFINITY); if cx.refreshing() || state.last_layout_width != Some(size.x()) { state.rendered_range = 0..0; state.items = SumTree::from_iter( (0..state.items.summary().count).map(|_| ListItem::Unrendered), &(), ) } let old_items = state.items.clone(); let mut new_items = SumTree::new(); let mut rendered_items = VecDeque::new(); let mut rendered_height = 0.; let mut scroll_top = state.logical_scroll_top(); // Render items after the scroll top, including those in the trailing overdraw. let mut cursor = old_items.cursor::(); cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); for (ix, item) in cursor.by_ref().enumerate() { let visible_height = rendered_height - scroll_top.offset_in_item; if visible_height >= size.y() + state.overdraw { break; } // Force re-render if the item is visible, but attempt to re-use an existing one // if we are inside the overdraw. let existing_element = if visible_height >= size.y() { Some(item) } else { None }; if let Some(element) = state.render_item( scroll_top.item_ix + ix, existing_element, item_constraint, view, cx, ) { rendered_height += element.borrow().size().y(); rendered_items.push_back(ListItem::Rendered(element)); } } // Prepare to start walking upward from the item at the scroll top. cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); // If the rendered items do not fill the visible region, then adjust // the scroll top upward. if rendered_height - scroll_top.offset_in_item < size.y() { while rendered_height < size.y() { cursor.prev(&()); if cursor.item().is_some() { if let Some(element) = state.render_item(cursor.start().0, None, item_constraint, view, cx) { rendered_height += element.borrow().size().y(); rendered_items.push_front(ListItem::Rendered(element)); } } else { break; } } scroll_top = ListOffset { item_ix: cursor.start().0, offset_in_item: rendered_height - size.y(), }; match state.orientation { Orientation::Top => { scroll_top.offset_in_item = scroll_top.offset_in_item.max(0.); state.logical_scroll_top = Some(scroll_top); } Orientation::Bottom => { scroll_top = ListOffset { item_ix: cursor.start().0, offset_in_item: rendered_height - size.y(), }; state.logical_scroll_top = None; } }; } // Render items in the leading overdraw. let mut leading_overdraw = scroll_top.offset_in_item; while leading_overdraw < state.overdraw { cursor.prev(&()); if let Some(item) = cursor.item() { if let Some(element) = state.render_item(cursor.start().0, Some(item), item_constraint, view, cx) { leading_overdraw += element.borrow().size().y(); rendered_items.push_front(ListItem::Rendered(element)); } } else { break; } } let new_rendered_range = cursor.start().0..(cursor.start().0 + rendered_items.len()); let mut cursor = old_items.cursor::(); if state.rendered_range.start < new_rendered_range.start { new_items.append( cursor.slice(&Count(state.rendered_range.start), Bias::Right, &()), &(), ); let remove_to = state.rendered_range.end.min(new_rendered_range.start); while cursor.start().0 < remove_to { new_items.push(cursor.item().unwrap().remove(), &()); cursor.next(&()); } } new_items.append( cursor.slice(&Count(new_rendered_range.start), Bias::Right, &()), &(), ); new_items.extend(rendered_items, &()); cursor.seek(&Count(new_rendered_range.end), Bias::Right, &()); if new_rendered_range.end < state.rendered_range.start { new_items.append( cursor.slice(&Count(state.rendered_range.start), Bias::Right, &()), &(), ); } while cursor.start().0 < state.rendered_range.end { new_items.push(cursor.item().unwrap().remove(), &()); cursor.next(&()); } new_items.append(cursor.suffix(&()), &()); state.items = new_items; state.rendered_range = new_rendered_range; state.last_layout_width = Some(size.x()); (size, scroll_top) } fn paint( &mut self, bounds: RectF, visible_bounds: RectF, scroll_top: &mut ListOffset, view: &mut V, cx: &mut PaintContext, ) { let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default(); cx.scene().push_layer(Some(visible_bounds)); let view_id = cx.view_id(); cx.scene() .push_mouse_region(MouseRegion::new::(view_id, 0, bounds).on_scroll({ let state = self.state.clone(); let height = bounds.height(); let scroll_top = scroll_top.clone(); move |e, view, cx| { state.0.borrow_mut().scroll( &scroll_top, height, *e.platform_event.delta.raw(), e.platform_event.delta.precise(), view, cx, ) } })); let state = &mut *self.state.0.borrow_mut(); for (element, origin) in state.visible_elements(bounds, scroll_top) { element.borrow_mut().paint(origin, visible_bounds, view, cx); } cx.scene().pop_layer(); } fn rect_for_text_range( &self, range_utf16: Range, bounds: RectF, _: RectF, scroll_top: &Self::LayoutState, _: &Self::PaintState, view: &V, cx: &ViewContext, ) -> Option { let state = self.state.0.borrow(); let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item); let mut cursor = state.items.cursor::(); cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); while let Some(item) = cursor.item() { if item_origin.y() > bounds.max_y() { break; } if let ListItem::Rendered(element) = item { if let Some(rect) = element .borrow() .rect_for_text_range(range_utf16.clone(), view, cx) { return Some(rect); } item_origin.set_y(item_origin.y() + element.borrow().size().y()); cursor.next(&()); } else { unreachable!(); } } None } fn debug( &self, bounds: RectF, scroll_top: &Self::LayoutState, _: &(), view: &V, cx: &ViewContext, ) -> serde_json::Value { let state = self.state.0.borrow_mut(); let visible_elements = state .visible_elements(bounds, scroll_top) .map(|e| e.0.borrow().debug(view, cx)) .collect::>(); let visible_range = scroll_top.item_ix..(scroll_top.item_ix + visible_elements.len()); json!({ "visible_range": visible_range, "visible_elements": visible_elements, "scroll_top": state.logical_scroll_top.map(|top| (top.item_ix, top.offset_in_item)), }) } } impl ListState { pub fn new( element_count: usize, orientation: Orientation, overdraw: f32, mut render_item: F, ) -> Self where D: Element, F: 'static + FnMut(&mut V, usize, &mut ViewContext) -> D, { let mut items = SumTree::new(); items.extend((0..element_count).map(|_| ListItem::Unrendered), &()); Self(Rc::new(RefCell::new(StateInner { last_layout_width: None, render_item: Box::new(move |view, ix, cx| render_item(view, ix, cx).into_any()), rendered_range: 0..0, items, logical_scroll_top: None, orientation, overdraw, scroll_handler: None, }))) } pub fn reset(&self, element_count: usize) { let state = &mut *self.0.borrow_mut(); state.rendered_range = 0..0; state.logical_scroll_top = None; state.items = SumTree::new(); state .items .extend((0..element_count).map(|_| ListItem::Unrendered), &()); } pub fn splice(&self, old_range: Range, count: usize) { let state = &mut *self.0.borrow_mut(); if let Some(ListOffset { item_ix, offset_in_item, }) = state.logical_scroll_top.as_mut() { if old_range.contains(item_ix) { *item_ix = old_range.start; *offset_in_item = 0.; } else if old_range.end <= *item_ix { *item_ix = *item_ix - (old_range.end - old_range.start) + count; } } let new_end = old_range.start + count; if old_range.start < state.rendered_range.start { state.rendered_range.start = new_end + state.rendered_range.start.saturating_sub(old_range.end); } if old_range.start < state.rendered_range.end { state.rendered_range.end = new_end + state.rendered_range.end.saturating_sub(old_range.end); } let mut old_heights = state.items.cursor::(); let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &()); old_heights.seek_forward(&Count(old_range.end), Bias::Right, &()); new_heights.extend((0..count).map(|_| ListItem::Unrendered), &()); new_heights.append(old_heights.suffix(&()), &()); drop(old_heights); state.items = new_heights; } pub fn set_scroll_handler( &mut self, handler: impl FnMut(Range, &mut V, &mut ViewContext) + 'static, ) { self.0.borrow_mut().scroll_handler = Some(Box::new(handler)) } pub fn logical_scroll_top(&self) -> ListOffset { self.0.borrow().logical_scroll_top() } pub fn scroll_to(&self, mut scroll_top: ListOffset) { let state = &mut *self.0.borrow_mut(); let item_count = state.items.summary().count; if scroll_top.item_ix >= item_count { scroll_top.item_ix = item_count; scroll_top.offset_in_item = 0.; } state.logical_scroll_top = Some(scroll_top); } } impl Clone for ListState { fn clone(&self) -> Self { Self(self.0.clone()) } } impl StateInner { fn render_item( &mut self, ix: usize, existing_element: Option<&ListItem>, constraint: SizeConstraint, view: &mut V, cx: &mut ViewContext, ) -> Option>>> { if let Some(ListItem::Rendered(element)) = existing_element { Some(element.clone()) } else { let mut element = (self.render_item)(view, ix, cx); element.layout(constraint, view, cx); Some(Rc::new(RefCell::new(element))) } } fn visible_range(&self, height: f32, scroll_top: &ListOffset) -> Range { let mut cursor = self.items.cursor::(); cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); let start_y = cursor.start().height + scroll_top.offset_in_item; cursor.seek_forward(&Height(start_y + height), Bias::Left, &()); scroll_top.item_ix..cursor.start().count + 1 } fn visible_elements<'a>( &'a self, bounds: RectF, scroll_top: &ListOffset, ) -> impl Iterator>>, Vector2F)> + 'a { let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item); let mut cursor = self.items.cursor::(); cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); std::iter::from_fn(move || { while let Some(item) = cursor.item() { if item_origin.y() > bounds.max_y() { break; } if let ListItem::Rendered(element) = item { let result = (element.clone(), item_origin); item_origin.set_y(item_origin.y() + element.borrow().size().y()); cursor.next(&()); return Some(result); } cursor.next(&()); } None }) } fn scroll( &mut self, scroll_top: &ListOffset, height: f32, mut delta: Vector2F, precise: bool, view: &mut V, cx: &mut ViewContext, ) { if !precise { delta *= 20.; } let scroll_max = (self.items.summary().height - height).max(0.); let new_scroll_top = (self.scroll_top(scroll_top) - delta.y()) .max(0.) .min(scroll_max); if self.orientation == Orientation::Bottom && new_scroll_top == scroll_max { self.logical_scroll_top = None; } else { let mut cursor = self.items.cursor::(); cursor.seek(&Height(new_scroll_top), Bias::Right, &()); let item_ix = cursor.start().count; let offset_in_item = new_scroll_top - cursor.start().height; self.logical_scroll_top = Some(ListOffset { item_ix, offset_in_item, }); } if self.scroll_handler.is_some() { let visible_range = self.visible_range(height, scroll_top); self.scroll_handler.as_mut().unwrap()(visible_range, view, cx); } cx.notify(); } fn logical_scroll_top(&self) -> ListOffset { self.logical_scroll_top .unwrap_or_else(|| match self.orientation { Orientation::Top => ListOffset { item_ix: 0, offset_in_item: 0., }, Orientation::Bottom => ListOffset { item_ix: self.items.summary().count, offset_in_item: 0., }, }) } fn scroll_top(&self, logical_scroll_top: &ListOffset) -> f32 { let mut cursor = self.items.cursor::(); cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &()); cursor.start().height + logical_scroll_top.offset_in_item } } impl ListItem { fn remove(&self) -> Self { match self { ListItem::Unrendered => ListItem::Unrendered, ListItem::Rendered(element) => ListItem::Removed(element.borrow().size().y()), ListItem::Removed(height) => ListItem::Removed(*height), } } } impl sum_tree::Item for ListItem { type Summary = ListItemSummary; fn summary(&self) -> Self::Summary { match self { ListItem::Unrendered => ListItemSummary { count: 1, rendered_count: 0, unrendered_count: 1, height: 0., }, ListItem::Rendered(element) => ListItemSummary { count: 1, rendered_count: 1, unrendered_count: 0, height: element.borrow().size().y(), }, ListItem::Removed(height) => ListItemSummary { count: 1, rendered_count: 0, unrendered_count: 1, height: *height, }, } } } impl sum_tree::Summary for ListItemSummary { type Context = (); fn add_summary(&mut self, summary: &Self, _: &()) { self.count += summary.count; self.rendered_count += summary.rendered_count; self.unrendered_count += summary.unrendered_count; self.height += summary.height; } } impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count { fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) { self.0 += summary.count; } } impl<'a> sum_tree::Dimension<'a, ListItemSummary> for RenderedCount { fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) { self.0 += summary.rendered_count; } } impl<'a> sum_tree::Dimension<'a, ListItemSummary> for UnrenderedCount { fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) { self.0 += summary.unrendered_count; } } impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height { fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) { self.0 += summary.height; } } impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Count { fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering { self.0.partial_cmp(&other.count).unwrap() } } impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height { fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering { self.0.partial_cmp(&other.height).unwrap() } } #[cfg(test)] mod tests { use super::*; use crate::{elements::Empty, geometry::vector::vec2f, Entity, PaintContext}; use rand::prelude::*; use std::env; #[crate::test(self)] fn test_layout(cx: &mut crate::AppContext) { cx.add_window(Default::default(), |cx| { let mut view = TestView; let constraint = SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.)); let elements = Rc::new(RefCell::new(vec![(0, 20.), (1, 30.), (2, 100.)])); let state = ListState::new(elements.borrow().len(), Orientation::Top, 1000.0, { let elements = elements.clone(); move |_, ix, _| { let (id, height) = elements.borrow()[ix]; TestElement::new(id, height).into_any() } }); let mut list = List::new(state.clone()); let (size, _) = list.layout(constraint, &mut view, cx); assert_eq!(size, vec2f(100., 40.)); assert_eq!( state.0.borrow().items.summary().clone(), ListItemSummary { count: 3, rendered_count: 3, unrendered_count: 0, height: 150. } ); state.0.borrow_mut().scroll( &ListOffset { item_ix: 0, offset_in_item: 0., }, 40., vec2f(0., -54.), true, &mut view, cx, ); let (_, logical_scroll_top) = list.layout(constraint, &mut view, cx); assert_eq!( logical_scroll_top, ListOffset { item_ix: 2, offset_in_item: 4. } ); assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 54.); elements.borrow_mut().splice(1..2, vec![(3, 40.), (4, 50.)]); elements.borrow_mut().push((5, 60.)); state.splice(1..2, 2); state.splice(4..4, 1); assert_eq!( state.0.borrow().items.summary().clone(), ListItemSummary { count: 5, rendered_count: 2, unrendered_count: 3, height: 120. } ); let (size, logical_scroll_top) = list.layout(constraint, &mut view, cx); assert_eq!(size, vec2f(100., 40.)); assert_eq!( state.0.borrow().items.summary().clone(), ListItemSummary { count: 5, rendered_count: 5, unrendered_count: 0, height: 270. } ); assert_eq!( logical_scroll_top, ListOffset { item_ix: 3, offset_in_item: 4. } ); assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 114.); view }); } #[crate::test(self, iterations = 10)] fn test_random(cx: &mut crate::AppContext, mut rng: StdRng) { let operations = env::var("OPERATIONS") .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); cx.add_window(Default::default(), |cx| { let mut view = TestView; let mut next_id = 0; let elements = Rc::new(RefCell::new( (0..rng.gen_range(0..=20)) .map(|_| { let id = next_id; next_id += 1; (id, rng.gen_range(0..=200) as f32 / 2.0) }) .collect::>(), )); let orientation = *[Orientation::Top, Orientation::Bottom] .choose(&mut rng) .unwrap(); let overdraw = rng.gen_range(1..=100) as f32; let state = ListState::new(elements.borrow().len(), orientation, overdraw, { let elements = elements.clone(); move |_, ix, _| { let (id, height) = elements.borrow()[ix]; TestElement::new(id, height).into_any() } }); let mut width = rng.gen_range(0..=2000) as f32 / 2.; let mut height = rng.gen_range(0..=2000) as f32 / 2.; log::info!("orientation: {:?}", orientation); log::info!("overdraw: {}", overdraw); log::info!("elements: {:?}", elements.borrow()); log::info!("size: ({:?}, {:?})", width, height); log::info!("=================="); let mut last_logical_scroll_top = None; for _ in 0..operations { match rng.gen_range(0..=100) { 0..=29 if last_logical_scroll_top.is_some() => { let delta = vec2f(0., rng.gen_range(-overdraw..=overdraw)); log::info!( "Scrolling by {:?}, previous scroll top: {:?}", delta, last_logical_scroll_top.unwrap() ); state.0.borrow_mut().scroll( last_logical_scroll_top.as_ref().unwrap(), height, delta, true, &mut view, cx, ); } 30..=34 => { width = rng.gen_range(0..=2000) as f32 / 2.; log::info!("changing width: {:?}", width); } 35..=54 => { height = rng.gen_range(0..=1000) as f32 / 2.; log::info!("changing height: {:?}", height); } _ => { let mut elements = elements.borrow_mut(); let end_ix = rng.gen_range(0..=elements.len()); let start_ix = rng.gen_range(0..=end_ix); let new_elements = (0..rng.gen_range(0..10)) .map(|_| { let id = next_id; next_id += 1; (id, rng.gen_range(0..=200) as f32 / 2.) }) .collect::>(); log::info!("splice({:?}, {:?})", start_ix..end_ix, new_elements); state.splice(start_ix..end_ix, new_elements.len()); elements.splice(start_ix..end_ix, new_elements); for (ix, item) in state.0.borrow().items.cursor::<()>().enumerate() { if let ListItem::Rendered(element) = item { let (expected_id, _) = elements[ix]; element.borrow().with_metadata(|metadata: Option<&usize>| { assert_eq!(*metadata.unwrap(), expected_id); }); } } } } let mut list = List::new(state.clone()); let window_size = vec2f(width, height); let (size, logical_scroll_top) = list.layout( SizeConstraint::new(vec2f(0., 0.), window_size), &mut view, cx, ); assert_eq!(size, window_size); last_logical_scroll_top = Some(logical_scroll_top); let state = state.0.borrow(); log::info!("items {:?}", state.items.items(&())); let scroll_top = state.scroll_top(&logical_scroll_top); let rendered_top = (scroll_top - overdraw).max(0.); let rendered_bottom = scroll_top + height + overdraw; let mut item_top = 0.; log::info!( "rendered top {:?}, rendered bottom {:?}, scroll top {:?}", rendered_top, rendered_bottom, scroll_top, ); let mut first_rendered_element_top = None; let mut last_rendered_element_bottom = None; assert_eq!(state.items.summary().count, elements.borrow().len()); for (ix, item) in state.items.cursor::<()>().enumerate() { match item { ListItem::Unrendered => { let item_bottom = item_top; assert!(item_bottom <= rendered_top || item_top >= rendered_bottom); item_top = item_bottom; } ListItem::Removed(height) => { let (id, expected_height) = elements.borrow()[ix]; assert_eq!( *height, expected_height, "element {} height didn't match", id ); let item_bottom = item_top + height; assert!(item_bottom <= rendered_top || item_top >= rendered_bottom); item_top = item_bottom; } ListItem::Rendered(element) => { let (expected_id, expected_height) = elements.borrow()[ix]; let element = element.borrow(); element.with_metadata(|metadata: Option<&usize>| { assert_eq!(*metadata.unwrap(), expected_id); }); assert_eq!(element.size().y(), expected_height); let item_bottom = item_top + element.size().y(); first_rendered_element_top.get_or_insert(item_top); last_rendered_element_bottom = Some(item_bottom); assert!(item_bottom > rendered_top || item_top < rendered_bottom); item_top = item_bottom; } } } match orientation { Orientation::Top => { if let Some(first_rendered_element_top) = first_rendered_element_top { assert!(first_rendered_element_top <= scroll_top); } } Orientation::Bottom => { if let Some(last_rendered_element_bottom) = last_rendered_element_bottom { assert!(last_rendered_element_bottom >= scroll_top + height); } } } } view }); } struct TestView; impl Entity for TestView { type Event = (); } impl crate::View for TestView { fn ui_name() -> &'static str { "TestView" } fn render(&mut self, _: &mut ViewContext) -> AnyElement { Empty::new().into_any() } } struct TestElement { id: usize, size: Vector2F, } impl TestElement { fn new(id: usize, height: f32) -> Self { Self { id, size: vec2f(100., height), } } } impl Element for TestElement { type LayoutState = (); type PaintState = (); fn layout( &mut self, _: SizeConstraint, _: &mut V, _: &mut ViewContext, ) -> (Vector2F, ()) { (self.size, ()) } fn paint(&mut self, _: RectF, _: RectF, _: &mut (), _: &mut V, _: &mut PaintContext) { unimplemented!() } fn rect_for_text_range( &self, _: Range, _: RectF, _: RectF, _: &Self::LayoutState, _: &Self::PaintState, _: &V, _: &ViewContext, ) -> Option { unimplemented!() } fn debug(&self, _: RectF, _: &(), _: &(), _: &V, _: &ViewContext) -> serde_json::Value { self.id.into() } fn metadata(&self) -> Option<&dyn std::any::Any> { Some(&self.id) } } }