list.rs

   1use crate::{
   2    geometry::{
   3        rect::RectF,
   4        vector::{vec2f, Vector2F},
   5    },
   6    json::json,
   7    AnyElement, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder, SizeConstraint,
   8    View, ViewContext,
   9};
  10use std::{cell::RefCell, collections::VecDeque, fmt::Debug, ops::Range, rc::Rc};
  11use sum_tree::{Bias, SumTree};
  12
  13pub struct List<V: View> {
  14    state: ListState<V>,
  15}
  16
  17pub struct ListState<V: View>(Rc<RefCell<StateInner<V>>>);
  18
  19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
  20pub enum Orientation {
  21    Top,
  22    Bottom,
  23}
  24
  25struct StateInner<V: View> {
  26    last_layout_width: Option<f32>,
  27    render_item: Box<dyn FnMut(&mut V, usize, &mut ViewContext<V>) -> AnyElement<V>>,
  28    rendered_range: Range<usize>,
  29    items: SumTree<ListItem<V>>,
  30    logical_scroll_top: Option<ListOffset>,
  31    orientation: Orientation,
  32    overdraw: f32,
  33    #[allow(clippy::type_complexity)]
  34    scroll_handler: Option<Box<dyn FnMut(Range<usize>, &mut V, &mut ViewContext<V>)>>,
  35}
  36
  37#[derive(Clone, Copy, Debug, Default, PartialEq)]
  38pub struct ListOffset {
  39    pub item_ix: usize,
  40    pub offset_in_item: f32,
  41}
  42
  43enum ListItem<V: View> {
  44    Unrendered,
  45    Rendered(Rc<RefCell<AnyElement<V>>>),
  46    Removed(f32),
  47}
  48
  49impl<V: View> Clone for ListItem<V> {
  50    fn clone(&self) -> Self {
  51        match self {
  52            Self::Unrendered => Self::Unrendered,
  53            Self::Rendered(element) => Self::Rendered(element.clone()),
  54            Self::Removed(height) => Self::Removed(*height),
  55        }
  56    }
  57}
  58
  59impl<V: View> Debug for ListItem<V> {
  60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  61        match self {
  62            Self::Unrendered => write!(f, "Unrendered"),
  63            Self::Rendered(_) => f.debug_tuple("Rendered").finish(),
  64            Self::Removed(height) => f.debug_tuple("Removed").field(height).finish(),
  65        }
  66    }
  67}
  68
  69#[derive(Clone, Debug, Default, PartialEq)]
  70struct ListItemSummary {
  71    count: usize,
  72    rendered_count: usize,
  73    unrendered_count: usize,
  74    height: f32,
  75}
  76
  77#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
  78struct Count(usize);
  79
  80#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
  81struct RenderedCount(usize);
  82
  83#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
  84struct UnrenderedCount(usize);
  85
  86#[derive(Clone, Debug, Default)]
  87struct Height(f32);
  88
  89impl<V: View> List<V> {
  90    pub fn new(state: ListState<V>) -> Self {
  91        Self { state }
  92    }
  93}
  94
  95impl<V: View> Element<V> for List<V> {
  96    type LayoutState = ListOffset;
  97    type PaintState = ();
  98
  99    fn layout(
 100        &mut self,
 101        constraint: SizeConstraint,
 102        view: &mut V,
 103        cx: &mut LayoutContext<V>,
 104    ) -> (Vector2F, Self::LayoutState) {
 105        let state = &mut *self.state.0.borrow_mut();
 106        let size = constraint.max;
 107        let mut item_constraint = constraint;
 108        item_constraint.min.set_y(0.);
 109        item_constraint.max.set_y(f32::INFINITY);
 110
 111        if cx.refreshing || state.last_layout_width != Some(size.x()) {
 112            state.rendered_range = 0..0;
 113            state.items = SumTree::from_iter(
 114                (0..state.items.summary().count).map(|_| ListItem::Unrendered),
 115                &(),
 116            )
 117        }
 118
 119        let old_items = state.items.clone();
 120        let mut new_items = SumTree::new();
 121        let mut rendered_items = VecDeque::new();
 122        let mut rendered_height = 0.;
 123        let mut scroll_top = state.logical_scroll_top();
 124
 125        // Render items after the scroll top, including those in the trailing overdraw.
 126        let mut cursor = old_items.cursor::<Count>();
 127        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
 128        for (ix, item) in cursor.by_ref().enumerate() {
 129            let visible_height = rendered_height - scroll_top.offset_in_item;
 130            if visible_height >= size.y() + state.overdraw {
 131                break;
 132            }
 133
 134            // Force re-render if the item is visible, but attempt to re-use an existing one
 135            // if we are inside the overdraw.
 136            let existing_element = if visible_height >= size.y() {
 137                Some(item)
 138            } else {
 139                None
 140            };
 141            if let Some(element) = state.render_item(
 142                scroll_top.item_ix + ix,
 143                existing_element,
 144                item_constraint,
 145                view,
 146                cx,
 147            ) {
 148                rendered_height += element.borrow().size().y();
 149                rendered_items.push_back(ListItem::Rendered(element));
 150            }
 151        }
 152
 153        // Prepare to start walking upward from the item at the scroll top.
 154        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
 155
 156        // If the rendered items do not fill the visible region, then adjust
 157        // the scroll top upward.
 158        if rendered_height - scroll_top.offset_in_item < size.y() {
 159            while rendered_height < size.y() {
 160                cursor.prev(&());
 161                if cursor.item().is_some() {
 162                    if let Some(element) =
 163                        state.render_item(cursor.start().0, None, item_constraint, view, cx)
 164                    {
 165                        rendered_height += element.borrow().size().y();
 166                        rendered_items.push_front(ListItem::Rendered(element));
 167                    }
 168                } else {
 169                    break;
 170                }
 171            }
 172
 173            scroll_top = ListOffset {
 174                item_ix: cursor.start().0,
 175                offset_in_item: rendered_height - size.y(),
 176            };
 177
 178            match state.orientation {
 179                Orientation::Top => {
 180                    scroll_top.offset_in_item = scroll_top.offset_in_item.max(0.);
 181                    state.logical_scroll_top = Some(scroll_top);
 182                }
 183                Orientation::Bottom => {
 184                    scroll_top = ListOffset {
 185                        item_ix: cursor.start().0,
 186                        offset_in_item: rendered_height - size.y(),
 187                    };
 188                    state.logical_scroll_top = None;
 189                }
 190            };
 191        }
 192
 193        // Render items in the leading overdraw.
 194        let mut leading_overdraw = scroll_top.offset_in_item;
 195        while leading_overdraw < state.overdraw {
 196            cursor.prev(&());
 197            if let Some(item) = cursor.item() {
 198                if let Some(element) =
 199                    state.render_item(cursor.start().0, Some(item), item_constraint, view, cx)
 200                {
 201                    leading_overdraw += element.borrow().size().y();
 202                    rendered_items.push_front(ListItem::Rendered(element));
 203                }
 204            } else {
 205                break;
 206            }
 207        }
 208
 209        let new_rendered_range = cursor.start().0..(cursor.start().0 + rendered_items.len());
 210
 211        let mut cursor = old_items.cursor::<Count>();
 212
 213        if state.rendered_range.start < new_rendered_range.start {
 214            new_items.append(
 215                cursor.slice(&Count(state.rendered_range.start), Bias::Right, &()),
 216                &(),
 217            );
 218            let remove_to = state.rendered_range.end.min(new_rendered_range.start);
 219            while cursor.start().0 < remove_to {
 220                new_items.push(cursor.item().unwrap().remove(), &());
 221                cursor.next(&());
 222            }
 223        }
 224        new_items.append(
 225            cursor.slice(&Count(new_rendered_range.start), Bias::Right, &()),
 226            &(),
 227        );
 228
 229        new_items.extend(rendered_items, &());
 230        cursor.seek(&Count(new_rendered_range.end), Bias::Right, &());
 231
 232        if new_rendered_range.end < state.rendered_range.start {
 233            new_items.append(
 234                cursor.slice(&Count(state.rendered_range.start), Bias::Right, &()),
 235                &(),
 236            );
 237        }
 238        while cursor.start().0 < state.rendered_range.end {
 239            new_items.push(cursor.item().unwrap().remove(), &());
 240            cursor.next(&());
 241        }
 242
 243        new_items.append(cursor.suffix(&()), &());
 244
 245        state.items = new_items;
 246        state.rendered_range = new_rendered_range;
 247        state.last_layout_width = Some(size.x());
 248        (size, scroll_top)
 249    }
 250
 251    fn paint(
 252        &mut self,
 253        scene: &mut SceneBuilder,
 254        bounds: RectF,
 255        visible_bounds: RectF,
 256        scroll_top: &mut ListOffset,
 257        view: &mut V,
 258        cx: &mut PaintContext<V>,
 259    ) {
 260        let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default();
 261        scene.push_layer(Some(visible_bounds));
 262        scene.push_mouse_region(
 263            MouseRegion::new::<Self>(cx.view_id(), 0, bounds).on_scroll({
 264                let state = self.state.clone();
 265                let height = bounds.height();
 266                let scroll_top = scroll_top.clone();
 267                move |e, view, cx| {
 268                    state.0.borrow_mut().scroll(
 269                        &scroll_top,
 270                        height,
 271                        *e.platform_event.delta.raw(),
 272                        e.platform_event.delta.precise(),
 273                        view,
 274                        cx,
 275                    )
 276                }
 277            }),
 278        );
 279
 280        let state = &mut *self.state.0.borrow_mut();
 281        for (element, origin) in state.visible_elements(bounds, scroll_top) {
 282            element
 283                .borrow_mut()
 284                .paint(scene, origin, visible_bounds, view, cx);
 285        }
 286
 287        scene.pop_layer();
 288    }
 289
 290    fn rect_for_text_range(
 291        &self,
 292        range_utf16: Range<usize>,
 293        bounds: RectF,
 294        _: RectF,
 295        scroll_top: &Self::LayoutState,
 296        _: &Self::PaintState,
 297        view: &V,
 298        cx: &ViewContext<V>,
 299    ) -> Option<RectF> {
 300        let state = self.state.0.borrow();
 301        let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item);
 302        let mut cursor = state.items.cursor::<Count>();
 303        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
 304        while let Some(item) = cursor.item() {
 305            if item_origin.y() > bounds.max_y() {
 306                break;
 307            }
 308
 309            if let ListItem::Rendered(element) = item {
 310                if let Some(rect) =
 311                    element
 312                        .borrow()
 313                        .rect_for_text_range(range_utf16.clone(), view, cx)
 314                {
 315                    return Some(rect);
 316                }
 317
 318                item_origin.set_y(item_origin.y() + element.borrow().size().y());
 319                cursor.next(&());
 320            } else {
 321                unreachable!();
 322            }
 323        }
 324
 325        None
 326    }
 327
 328    fn debug(
 329        &self,
 330        bounds: RectF,
 331        scroll_top: &Self::LayoutState,
 332        _: &(),
 333        view: &V,
 334        cx: &ViewContext<V>,
 335    ) -> serde_json::Value {
 336        let state = self.state.0.borrow_mut();
 337        let visible_elements = state
 338            .visible_elements(bounds, scroll_top)
 339            .map(|e| e.0.borrow().debug(view, cx))
 340            .collect::<Vec<_>>();
 341        let visible_range = scroll_top.item_ix..(scroll_top.item_ix + visible_elements.len());
 342        json!({
 343            "visible_range": visible_range,
 344            "visible_elements": visible_elements,
 345            "scroll_top": state.logical_scroll_top.map(|top| (top.item_ix, top.offset_in_item)),
 346        })
 347    }
 348}
 349
 350impl<V: View> ListState<V> {
 351    pub fn new<D, F>(
 352        element_count: usize,
 353        orientation: Orientation,
 354        overdraw: f32,
 355        mut render_item: F,
 356    ) -> Self
 357    where
 358        D: Element<V>,
 359        F: 'static + FnMut(&mut V, usize, &mut ViewContext<V>) -> D,
 360    {
 361        let mut items = SumTree::new();
 362        items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
 363        Self(Rc::new(RefCell::new(StateInner {
 364            last_layout_width: None,
 365            render_item: Box::new(move |view, ix, cx| render_item(view, ix, cx).into_any()),
 366            rendered_range: 0..0,
 367            items,
 368            logical_scroll_top: None,
 369            orientation,
 370            overdraw,
 371            scroll_handler: None,
 372        })))
 373    }
 374
 375    pub fn reset(&self, element_count: usize) {
 376        let state = &mut *self.0.borrow_mut();
 377        state.rendered_range = 0..0;
 378        state.logical_scroll_top = None;
 379        state.items = SumTree::new();
 380        state
 381            .items
 382            .extend((0..element_count).map(|_| ListItem::Unrendered), &());
 383    }
 384
 385    pub fn splice(&self, old_range: Range<usize>, count: usize) {
 386        let state = &mut *self.0.borrow_mut();
 387
 388        if let Some(ListOffset {
 389            item_ix,
 390            offset_in_item,
 391        }) = state.logical_scroll_top.as_mut()
 392        {
 393            if old_range.contains(item_ix) {
 394                *item_ix = old_range.start;
 395                *offset_in_item = 0.;
 396            } else if old_range.end <= *item_ix {
 397                *item_ix = *item_ix - (old_range.end - old_range.start) + count;
 398            }
 399        }
 400
 401        let new_end = old_range.start + count;
 402        if old_range.start < state.rendered_range.start {
 403            state.rendered_range.start =
 404                new_end + state.rendered_range.start.saturating_sub(old_range.end);
 405        }
 406        if old_range.start < state.rendered_range.end {
 407            state.rendered_range.end =
 408                new_end + state.rendered_range.end.saturating_sub(old_range.end);
 409        }
 410
 411        let mut old_heights = state.items.cursor::<Count>();
 412        let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &());
 413        old_heights.seek_forward(&Count(old_range.end), Bias::Right, &());
 414
 415        new_heights.extend((0..count).map(|_| ListItem::Unrendered), &());
 416        new_heights.append(old_heights.suffix(&()), &());
 417        drop(old_heights);
 418        state.items = new_heights;
 419    }
 420
 421    pub fn set_scroll_handler(
 422        &mut self,
 423        handler: impl FnMut(Range<usize>, &mut V, &mut ViewContext<V>) + 'static,
 424    ) {
 425        self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
 426    }
 427
 428    pub fn logical_scroll_top(&self) -> ListOffset {
 429        self.0.borrow().logical_scroll_top()
 430    }
 431
 432    pub fn scroll_to(&self, mut scroll_top: ListOffset) {
 433        let state = &mut *self.0.borrow_mut();
 434        let item_count = state.items.summary().count;
 435        if scroll_top.item_ix >= item_count {
 436            scroll_top.item_ix = item_count;
 437            scroll_top.offset_in_item = 0.;
 438        }
 439        state.logical_scroll_top = Some(scroll_top);
 440    }
 441}
 442
 443impl<V: View> Clone for ListState<V> {
 444    fn clone(&self) -> Self {
 445        Self(self.0.clone())
 446    }
 447}
 448
 449impl<V: View> StateInner<V> {
 450    fn render_item(
 451        &mut self,
 452        ix: usize,
 453        existing_element: Option<&ListItem<V>>,
 454        constraint: SizeConstraint,
 455        view: &mut V,
 456        cx: &mut LayoutContext<V>,
 457    ) -> Option<Rc<RefCell<AnyElement<V>>>> {
 458        if let Some(ListItem::Rendered(element)) = existing_element {
 459            Some(element.clone())
 460        } else {
 461            let mut element = (self.render_item)(view, ix, cx);
 462            element.layout(constraint, view, cx);
 463            Some(Rc::new(RefCell::new(element)))
 464        }
 465    }
 466
 467    fn visible_range(&self, height: f32, scroll_top: &ListOffset) -> Range<usize> {
 468        let mut cursor = self.items.cursor::<ListItemSummary>();
 469        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
 470        let start_y = cursor.start().height + scroll_top.offset_in_item;
 471        cursor.seek_forward(&Height(start_y + height), Bias::Left, &());
 472        scroll_top.item_ix..cursor.start().count + 1
 473    }
 474
 475    fn visible_elements<'a>(
 476        &'a self,
 477        bounds: RectF,
 478        scroll_top: &ListOffset,
 479    ) -> impl Iterator<Item = (Rc<RefCell<AnyElement<V>>>, Vector2F)> + 'a {
 480        let mut item_origin = bounds.origin() - vec2f(0., scroll_top.offset_in_item);
 481        let mut cursor = self.items.cursor::<Count>();
 482        cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
 483        std::iter::from_fn(move || {
 484            while let Some(item) = cursor.item() {
 485                if item_origin.y() > bounds.max_y() {
 486                    break;
 487                }
 488
 489                if let ListItem::Rendered(element) = item {
 490                    let result = (element.clone(), item_origin);
 491                    item_origin.set_y(item_origin.y() + element.borrow().size().y());
 492                    cursor.next(&());
 493                    return Some(result);
 494                }
 495
 496                cursor.next(&());
 497            }
 498
 499            None
 500        })
 501    }
 502
 503    fn scroll(
 504        &mut self,
 505        scroll_top: &ListOffset,
 506        height: f32,
 507        mut delta: Vector2F,
 508        precise: bool,
 509        view: &mut V,
 510        cx: &mut ViewContext<V>,
 511    ) {
 512        if !precise {
 513            delta *= 20.;
 514        }
 515
 516        let scroll_max = (self.items.summary().height - height).max(0.);
 517        let new_scroll_top = (self.scroll_top(scroll_top) - delta.y())
 518            .max(0.)
 519            .min(scroll_max);
 520
 521        if self.orientation == Orientation::Bottom && new_scroll_top == scroll_max {
 522            self.logical_scroll_top = None;
 523        } else {
 524            let mut cursor = self.items.cursor::<ListItemSummary>();
 525            cursor.seek(&Height(new_scroll_top), Bias::Right, &());
 526            let item_ix = cursor.start().count;
 527            let offset_in_item = new_scroll_top - cursor.start().height;
 528            self.logical_scroll_top = Some(ListOffset {
 529                item_ix,
 530                offset_in_item,
 531            });
 532        }
 533
 534        if self.scroll_handler.is_some() {
 535            let visible_range = self.visible_range(height, scroll_top);
 536            self.scroll_handler.as_mut().unwrap()(visible_range, view, cx);
 537        }
 538
 539        cx.notify();
 540    }
 541
 542    fn logical_scroll_top(&self) -> ListOffset {
 543        self.logical_scroll_top
 544            .unwrap_or_else(|| match self.orientation {
 545                Orientation::Top => ListOffset {
 546                    item_ix: 0,
 547                    offset_in_item: 0.,
 548                },
 549                Orientation::Bottom => ListOffset {
 550                    item_ix: self.items.summary().count,
 551                    offset_in_item: 0.,
 552                },
 553            })
 554    }
 555
 556    fn scroll_top(&self, logical_scroll_top: &ListOffset) -> f32 {
 557        let mut cursor = self.items.cursor::<ListItemSummary>();
 558        cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &());
 559        cursor.start().height + logical_scroll_top.offset_in_item
 560    }
 561}
 562
 563impl<V: View> ListItem<V> {
 564    fn remove(&self) -> Self {
 565        match self {
 566            ListItem::Unrendered => ListItem::Unrendered,
 567            ListItem::Rendered(element) => ListItem::Removed(element.borrow().size().y()),
 568            ListItem::Removed(height) => ListItem::Removed(*height),
 569        }
 570    }
 571}
 572
 573impl<V: View> sum_tree::Item for ListItem<V> {
 574    type Summary = ListItemSummary;
 575
 576    fn summary(&self) -> Self::Summary {
 577        match self {
 578            ListItem::Unrendered => ListItemSummary {
 579                count: 1,
 580                rendered_count: 0,
 581                unrendered_count: 1,
 582                height: 0.,
 583            },
 584            ListItem::Rendered(element) => ListItemSummary {
 585                count: 1,
 586                rendered_count: 1,
 587                unrendered_count: 0,
 588                height: element.borrow().size().y(),
 589            },
 590            ListItem::Removed(height) => ListItemSummary {
 591                count: 1,
 592                rendered_count: 0,
 593                unrendered_count: 1,
 594                height: *height,
 595            },
 596        }
 597    }
 598}
 599
 600impl sum_tree::Summary for ListItemSummary {
 601    type Context = ();
 602
 603    fn add_summary(&mut self, summary: &Self, _: &()) {
 604        self.count += summary.count;
 605        self.rendered_count += summary.rendered_count;
 606        self.unrendered_count += summary.unrendered_count;
 607        self.height += summary.height;
 608    }
 609}
 610
 611impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
 612    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
 613        self.0 += summary.count;
 614    }
 615}
 616
 617impl<'a> sum_tree::Dimension<'a, ListItemSummary> for RenderedCount {
 618    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
 619        self.0 += summary.rendered_count;
 620    }
 621}
 622
 623impl<'a> sum_tree::Dimension<'a, ListItemSummary> for UnrenderedCount {
 624    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
 625        self.0 += summary.unrendered_count;
 626    }
 627}
 628
 629impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
 630    fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
 631        self.0 += summary.height;
 632    }
 633}
 634
 635impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Count {
 636    fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
 637        self.0.partial_cmp(&other.count).unwrap()
 638    }
 639}
 640
 641impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height {
 642    fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
 643        self.0.partial_cmp(&other.height).unwrap()
 644    }
 645}
 646
 647#[cfg(test)]
 648mod tests {
 649    use super::*;
 650    use crate::{elements::Empty, geometry::vector::vec2f, Entity, PaintContext};
 651    use rand::prelude::*;
 652    use std::env;
 653
 654    #[crate::test(self)]
 655    fn test_layout(cx: &mut crate::AppContext) {
 656        cx.add_window(Default::default(), |cx| {
 657            let mut view = TestView;
 658            let constraint = SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.));
 659            let elements = Rc::new(RefCell::new(vec![(0, 20.), (1, 30.), (2, 100.)]));
 660            let state = ListState::new(elements.borrow().len(), Orientation::Top, 1000.0, {
 661                let elements = elements.clone();
 662                move |_, ix, _| {
 663                    let (id, height) = elements.borrow()[ix];
 664                    TestElement::new(id, height).into_any()
 665                }
 666            });
 667
 668            let mut list = List::new(state.clone());
 669            let mut new_parents = Default::default();
 670            let mut notify_views_if_parents_change = Default::default();
 671            let mut layout_cx = LayoutContext::new(
 672                cx,
 673                &mut new_parents,
 674                &mut notify_views_if_parents_change,
 675                false,
 676            );
 677            let (size, _) = list.layout(constraint, &mut view, &mut layout_cx);
 678            assert_eq!(size, vec2f(100., 40.));
 679            assert_eq!(
 680                state.0.borrow().items.summary().clone(),
 681                ListItemSummary {
 682                    count: 3,
 683                    rendered_count: 3,
 684                    unrendered_count: 0,
 685                    height: 150.
 686                }
 687            );
 688
 689            state.0.borrow_mut().scroll(
 690                &ListOffset {
 691                    item_ix: 0,
 692                    offset_in_item: 0.,
 693                },
 694                40.,
 695                vec2f(0., -54.),
 696                true,
 697                &mut view,
 698                cx,
 699            );
 700
 701            let mut layout_cx = LayoutContext::new(
 702                cx,
 703                &mut new_parents,
 704                &mut notify_views_if_parents_change,
 705                false,
 706            );
 707            let (_, logical_scroll_top) = list.layout(constraint, &mut view, &mut layout_cx);
 708            assert_eq!(
 709                logical_scroll_top,
 710                ListOffset {
 711                    item_ix: 2,
 712                    offset_in_item: 4.
 713                }
 714            );
 715            assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 54.);
 716
 717            elements.borrow_mut().splice(1..2, vec![(3, 40.), (4, 50.)]);
 718            elements.borrow_mut().push((5, 60.));
 719            state.splice(1..2, 2);
 720            state.splice(4..4, 1);
 721            assert_eq!(
 722                state.0.borrow().items.summary().clone(),
 723                ListItemSummary {
 724                    count: 5,
 725                    rendered_count: 2,
 726                    unrendered_count: 3,
 727                    height: 120.
 728                }
 729            );
 730
 731            let mut layout_cx = LayoutContext::new(
 732                cx,
 733                &mut new_parents,
 734                &mut notify_views_if_parents_change,
 735                false,
 736            );
 737            let (size, logical_scroll_top) = list.layout(constraint, &mut view, &mut layout_cx);
 738            assert_eq!(size, vec2f(100., 40.));
 739            assert_eq!(
 740                state.0.borrow().items.summary().clone(),
 741                ListItemSummary {
 742                    count: 5,
 743                    rendered_count: 5,
 744                    unrendered_count: 0,
 745                    height: 270.
 746                }
 747            );
 748            assert_eq!(
 749                logical_scroll_top,
 750                ListOffset {
 751                    item_ix: 3,
 752                    offset_in_item: 4.
 753                }
 754            );
 755            assert_eq!(state.0.borrow().scroll_top(&logical_scroll_top), 114.);
 756
 757            view
 758        });
 759    }
 760
 761    #[crate::test(self, iterations = 10)]
 762    fn test_random(cx: &mut crate::AppContext, mut rng: StdRng) {
 763        let operations = env::var("OPERATIONS")
 764            .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
 765            .unwrap_or(10);
 766
 767        cx.add_window(Default::default(), |cx| {
 768            let mut view = TestView;
 769
 770            let mut next_id = 0;
 771            let elements = Rc::new(RefCell::new(
 772                (0..rng.gen_range(0..=20))
 773                    .map(|_| {
 774                        let id = next_id;
 775                        next_id += 1;
 776                        (id, rng.gen_range(0..=200) as f32 / 2.0)
 777                    })
 778                    .collect::<Vec<_>>(),
 779            ));
 780            let orientation = *[Orientation::Top, Orientation::Bottom]
 781                .choose(&mut rng)
 782                .unwrap();
 783            let overdraw = rng.gen_range(1..=100) as f32;
 784
 785            let state = ListState::new(elements.borrow().len(), orientation, overdraw, {
 786                let elements = elements.clone();
 787                move |_, ix, _| {
 788                    let (id, height) = elements.borrow()[ix];
 789                    TestElement::new(id, height).into_any()
 790                }
 791            });
 792
 793            let mut width = rng.gen_range(0..=2000) as f32 / 2.;
 794            let mut height = rng.gen_range(0..=2000) as f32 / 2.;
 795            log::info!("orientation: {:?}", orientation);
 796            log::info!("overdraw: {}", overdraw);
 797            log::info!("elements: {:?}", elements.borrow());
 798            log::info!("size: ({:?}, {:?})", width, height);
 799            log::info!("==================");
 800
 801            let mut last_logical_scroll_top = None;
 802            for _ in 0..operations {
 803                match rng.gen_range(0..=100) {
 804                    0..=29 if last_logical_scroll_top.is_some() => {
 805                        let delta = vec2f(0., rng.gen_range(-overdraw..=overdraw));
 806                        log::info!(
 807                            "Scrolling by {:?}, previous scroll top: {:?}",
 808                            delta,
 809                            last_logical_scroll_top.unwrap()
 810                        );
 811                        state.0.borrow_mut().scroll(
 812                            last_logical_scroll_top.as_ref().unwrap(),
 813                            height,
 814                            delta,
 815                            true,
 816                            &mut view,
 817                            cx,
 818                        );
 819                    }
 820                    30..=34 => {
 821                        width = rng.gen_range(0..=2000) as f32 / 2.;
 822                        log::info!("changing width: {:?}", width);
 823                    }
 824                    35..=54 => {
 825                        height = rng.gen_range(0..=1000) as f32 / 2.;
 826                        log::info!("changing height: {:?}", height);
 827                    }
 828                    _ => {
 829                        let mut elements = elements.borrow_mut();
 830                        let end_ix = rng.gen_range(0..=elements.len());
 831                        let start_ix = rng.gen_range(0..=end_ix);
 832                        let new_elements = (0..rng.gen_range(0..10))
 833                            .map(|_| {
 834                                let id = next_id;
 835                                next_id += 1;
 836                                (id, rng.gen_range(0..=200) as f32 / 2.)
 837                            })
 838                            .collect::<Vec<_>>();
 839                        log::info!("splice({:?}, {:?})", start_ix..end_ix, new_elements);
 840                        state.splice(start_ix..end_ix, new_elements.len());
 841                        elements.splice(start_ix..end_ix, new_elements);
 842                        for (ix, item) in state.0.borrow().items.cursor::<()>().enumerate() {
 843                            if let ListItem::Rendered(element) = item {
 844                                let (expected_id, _) = elements[ix];
 845                                element.borrow().with_metadata(|metadata: Option<&usize>| {
 846                                    assert_eq!(*metadata.unwrap(), expected_id);
 847                                });
 848                            }
 849                        }
 850                    }
 851                }
 852
 853                let mut list = List::new(state.clone());
 854                let window_size = vec2f(width, height);
 855                let mut new_parents = Default::default();
 856                let mut notify_views_if_parents_change = Default::default();
 857                let mut layout_cx = LayoutContext::new(
 858                    cx,
 859                    &mut new_parents,
 860                    &mut notify_views_if_parents_change,
 861                    false,
 862                );
 863                let (size, logical_scroll_top) = list.layout(
 864                    SizeConstraint::new(vec2f(0., 0.), window_size),
 865                    &mut view,
 866                    &mut layout_cx,
 867                );
 868                assert_eq!(size, window_size);
 869                last_logical_scroll_top = Some(logical_scroll_top);
 870
 871                let state = state.0.borrow();
 872                log::info!("items {:?}", state.items.items(&()));
 873
 874                let scroll_top = state.scroll_top(&logical_scroll_top);
 875                let rendered_top = (scroll_top - overdraw).max(0.);
 876                let rendered_bottom = scroll_top + height + overdraw;
 877                let mut item_top = 0.;
 878
 879                log::info!(
 880                    "rendered top {:?}, rendered bottom {:?}, scroll top {:?}",
 881                    rendered_top,
 882                    rendered_bottom,
 883                    scroll_top,
 884                );
 885
 886                let mut first_rendered_element_top = None;
 887                let mut last_rendered_element_bottom = None;
 888                assert_eq!(state.items.summary().count, elements.borrow().len());
 889                for (ix, item) in state.items.cursor::<()>().enumerate() {
 890                    match item {
 891                        ListItem::Unrendered => {
 892                            let item_bottom = item_top;
 893                            assert!(item_bottom <= rendered_top || item_top >= rendered_bottom);
 894                            item_top = item_bottom;
 895                        }
 896                        ListItem::Removed(height) => {
 897                            let (id, expected_height) = elements.borrow()[ix];
 898                            assert_eq!(
 899                                *height, expected_height,
 900                                "element {} height didn't match",
 901                                id
 902                            );
 903                            let item_bottom = item_top + height;
 904                            assert!(item_bottom <= rendered_top || item_top >= rendered_bottom);
 905                            item_top = item_bottom;
 906                        }
 907                        ListItem::Rendered(element) => {
 908                            let (expected_id, expected_height) = elements.borrow()[ix];
 909                            let element = element.borrow();
 910                            element.with_metadata(|metadata: Option<&usize>| {
 911                                assert_eq!(*metadata.unwrap(), expected_id);
 912                            });
 913                            assert_eq!(element.size().y(), expected_height);
 914                            let item_bottom = item_top + element.size().y();
 915                            first_rendered_element_top.get_or_insert(item_top);
 916                            last_rendered_element_bottom = Some(item_bottom);
 917                            assert!(item_bottom > rendered_top || item_top < rendered_bottom);
 918                            item_top = item_bottom;
 919                        }
 920                    }
 921                }
 922
 923                match orientation {
 924                    Orientation::Top => {
 925                        if let Some(first_rendered_element_top) = first_rendered_element_top {
 926                            assert!(first_rendered_element_top <= scroll_top);
 927                        }
 928                    }
 929                    Orientation::Bottom => {
 930                        if let Some(last_rendered_element_bottom) = last_rendered_element_bottom {
 931                            assert!(last_rendered_element_bottom >= scroll_top + height);
 932                        }
 933                    }
 934                }
 935            }
 936
 937            view
 938        });
 939    }
 940
 941    struct TestView;
 942
 943    impl Entity for TestView {
 944        type Event = ();
 945    }
 946
 947    impl View for TestView {
 948        fn ui_name() -> &'static str {
 949            "TestView"
 950        }
 951
 952        fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
 953            Empty::new().into_any()
 954        }
 955    }
 956
 957    struct TestElement {
 958        id: usize,
 959        size: Vector2F,
 960    }
 961
 962    impl TestElement {
 963        fn new(id: usize, height: f32) -> Self {
 964            Self {
 965                id,
 966                size: vec2f(100., height),
 967            }
 968        }
 969    }
 970
 971    impl<V: View> Element<V> for TestElement {
 972        type LayoutState = ();
 973        type PaintState = ();
 974
 975        fn layout(
 976            &mut self,
 977            _: SizeConstraint,
 978            _: &mut V,
 979            _: &mut LayoutContext<V>,
 980        ) -> (Vector2F, ()) {
 981            (self.size, ())
 982        }
 983
 984        fn paint(
 985            &mut self,
 986            _: &mut SceneBuilder,
 987            _: RectF,
 988            _: RectF,
 989            _: &mut (),
 990            _: &mut V,
 991            _: &mut PaintContext<V>,
 992        ) {
 993            unimplemented!()
 994        }
 995
 996        fn rect_for_text_range(
 997            &self,
 998            _: Range<usize>,
 999            _: RectF,
1000            _: RectF,
1001            _: &Self::LayoutState,
1002            _: &Self::PaintState,
1003            _: &V,
1004            _: &ViewContext<V>,
1005        ) -> Option<RectF> {
1006            unimplemented!()
1007        }
1008
1009        fn debug(&self, _: RectF, _: &(), _: &(), _: &V, _: &ViewContext<V>) -> serde_json::Value {
1010            self.id.into()
1011        }
1012
1013        fn metadata(&self) -> Option<&dyn std::any::Any> {
1014            Some(&self.id)
1015        }
1016    }
1017}