makepad_widgets/
portal_list.rs

1use crate::{
2    widget::*,
3    makepad_derive_widget::*,
4    makepad_draw::*,
5    scroll_bar::{ScrollBar, ScrollAxis, ScrollBarAction}
6};
7
8live_design!{
9    link widgets;
10    use link::theme::*;
11    use link::shaders::*;
12    use crate::scroll_bar::ScrollBar;
13    
14    pub PortalListBase = {{PortalList}} {}
15        
16    pub PortalList = <PortalListBase> {
17        width: Fill, height: Fill,
18        capture_overload: true
19        scroll_bar: <ScrollBar> {}
20        flow: Down
21    }
22}
23
24/// The maximum number of items that will be shown as part of a smooth scroll animation.
25const SMOOTH_SCROLL_MAXIMUM_WINDOW: usize = 20;
26
27#[derive(Clone,Copy)]
28struct ScrollSample{
29    abs: f64,
30    time: f64,
31}
32
33enum ScrollState {
34    Stopped,
35    Drag{samples:Vec<ScrollSample>},
36    Flick {delta: f64, next_frame: NextFrame},
37    Pulldown {next_frame: NextFrame},
38    ScrollingTo {target_id: usize, delta: f64, next_frame: NextFrame},
39}
40
41#[derive(Clone)]
42enum ListDrawState {
43    Begin,
44    Down {index: usize, pos: f64, viewport: Rect},
45    Up {index: usize, pos: f64, hit_bottom: bool, viewport: Rect},
46    DownAgain {index: usize, pos: f64, viewport: Rect},
47    End {viewport: Rect}
48}
49
50#[derive(Clone, Debug, DefaultNone)]
51pub enum PortalListAction {
52    Scroll,
53    SmoothScrollReached,
54    None
55}
56impl ListDrawState {
57    fn is_down_again(&self) -> bool {
58        match self {
59            Self::DownAgain {..} => true,
60            _ => false
61        }
62    }
63}
64#[derive(Live, Widget)]
65pub struct PortalList {
66    #[redraw] #[rust] area: Area,
67    #[walk] walk: Walk,
68    #[layout] layout: Layout,
69    
70    #[rust] range_start: usize,
71    #[rust(usize::MAX)] range_end: usize,
72    #[rust(0usize)] view_window: usize,
73    #[rust(0usize)] visible_items: usize,
74    #[live(0.2)] flick_scroll_minimum: f64,
75    #[live(80.0)] flick_scroll_maximum: f64,
76    #[live(0.005)] flick_scroll_scaling: f64,
77    #[live(0.97)] flick_scroll_decay: f64,
78    #[live(80.0)] max_pull_down: f64,
79    #[live(true)] align_top_when_empty: bool,
80    #[live(false)] grab_key_focus: bool,
81    #[live(true)] drag_scrolling: bool,
82    #[rust] first_id: usize,
83    #[rust] first_scroll: f64,
84    #[rust(Vec2Index::X)] vec_index: Vec2Index,
85    #[live] scroll_bar: ScrollBar,
86    #[live] capture_overload: bool,
87    #[live(false)] keep_invisible: bool,
88    #[rust] draw_state: DrawStateWrap<ListDrawState>,
89    #[rust] draw_align_list: Vec<AlignItem>,
90    #[rust] detect_tail_in_draw: bool,
91    #[live(false)] auto_tail: bool,
92    #[live(false)] draw_caching: bool,
93    
94    #[rust(false)] tail_range: bool,
95    #[rust(false)] at_end: bool,
96    #[rust(true)] not_filling_viewport: bool,
97    #[live(false)] reuse_items: bool,
98    
99    #[rust] templates: ComponentMap<LiveId, LivePtr>,
100    #[rust] items: ComponentMap<usize, WidgetItem>,
101    #[rust] reusable_items: Vec<WidgetItem>,
102    #[rust] _draw_list_cache: Vec<DrawList>,
103    
104    //#[rust(DragState::None)] drag_state: DragState,
105    #[rust(ScrollState::Stopped)] scroll_state: ScrollState,
106    /// Whether the PortalList was actively scrolling during the most recent finger down hit.
107    #[rust] was_scrolling: bool,
108}
109
110
111#[derive(Default)]
112struct WidgetItem{
113    widget: WidgetRef,
114    template: LiveId,
115}
116
117struct AlignItem {
118    align_range: TurtleAlignRange,
119    size: DVec2,
120    shift: f64,
121    index: usize
122}
123
124impl LiveHook for PortalList {
125    fn before_apply(&mut self, _cx: &mut Cx, apply: &mut Apply, _index: usize, _nodes: &[LiveNode]) {
126        if let ApplyFrom::UpdateFromDoc {..} = apply.from {
127            self.templates.clear();
128        }
129    }
130    
131    // hook the apply flow to collect our templates and apply to instanced childnodes
132    fn apply_value_instance(&mut self, cx: &mut Cx, apply: &mut Apply, index: usize, nodes: &[LiveNode]) -> usize {
133        if nodes[index].is_instance_prop() {
134            if let Some(live_ptr) = apply.from.to_live_ptr(cx, index){
135                let id = nodes[index].id;
136                self.templates.insert(id, live_ptr);
137                // lets apply this thing over all our childnodes with that template
138                for (_, item) in self.items.iter_mut() {
139                    if item.template == id {
140                        item.widget.apply(cx, apply, index, nodes);
141                    }
142                }
143            }
144        }
145        else {
146            cx.apply_error_no_matching_field(live_error_origin!(), index, nodes);
147        }
148        nodes.skip_node(index)
149    }
150    
151    fn after_apply(&mut self, _cx: &mut Cx, _applyl: &mut Apply, _index: usize, _nodes: &[LiveNode]) {
152        if let Flow::Down = self.layout.flow {
153            self.vec_index = Vec2Index::Y
154        }
155        else {
156            self.vec_index = Vec2Index::X
157        }
158        if self.auto_tail{
159            self.tail_range = true;
160        }
161    }
162}
163
164impl PortalList {
165    
166    fn begin(&mut self, cx: &mut Cx2d, walk: Walk) {
167        cx.begin_turtle(walk, self.layout);
168        self.draw_align_list.clear();
169    }
170    
171    fn end(&mut self, cx: &mut Cx2d) {
172        // in this code we position all the drawn items 
173
174        self.at_end = false;
175        self.not_filling_viewport = false;
176
177        let vi = self.vec_index;
178        let mut visible_items = 0;
179
180        if let Some(ListDrawState::End {viewport}) = self.draw_state.get() {
181            let list = &mut self.draw_align_list;
182            if list.len() > 0 {
183                list.sort_by( | a, b | a.index.cmp(&b.index));
184                let first_index = list.iter().position( | v | v.index == self.first_id).unwrap();
185                
186                // find the position of the first item in our set
187                
188                let mut first_pos = self.first_scroll;
189                for i in (0..first_index).rev() {
190                    let item = &list[i];
191                    first_pos -= item.size.index(vi);
192                }
193                
194                // find the position of the last item in the range
195                // note that the listview requests items beyond the range so you can pad out the listview 
196                // when there is not enough data
197
198                let mut last_pos = self.first_scroll;
199                let mut last_item_pos = None;
200                for i in first_index..list.len() {
201                    let item = &list[i];
202                    last_pos += item.size.index(vi);
203                    if item.index < self.range_end {
204                        last_item_pos = Some(last_pos);
205                    }
206                    else {
207                        break;
208                    }
209                }
210                
211                // compute if we are filling the viewport
212                // if not we have to trigger a stick-to-top
213                if list[0].index == self.range_start {
214                    let mut total = 0.0;
215                    for item in list.iter() {
216                        if item.index >= self.range_end {
217                            break;
218                        }
219                        total += item.size.index(vi);
220                    }
221                    self.not_filling_viewport = total < viewport.size.index(vi);
222                }
223                
224                // in this case we manage the 'pull down' situation when we are at the top
225                if list.first().unwrap().index == self.range_start && first_pos > 0.0 {
226                    let min = if let ScrollState::Stopped = self.scroll_state {
227                        0.0
228                    }
229                    else {
230                        self.max_pull_down
231                    };
232                    
233                    let mut pos = first_pos.min(min); // lets do a maximum for first scroll
234                    for item in list {
235                        let shift = DVec2::from_index_pair(vi, pos, 0.0);
236                        cx.shift_align_range(&item.align_range, shift - DVec2::from_index_pair(vi, item.shift, 0.0));
237                        pos += item.size.index(vi);
238                        visible_items += 1;
239                    }
240                    self.first_scroll = first_pos.min(min);
241                    self.first_id = self.range_start;
242                }
243                else {
244                    // this is the normal case, however we have to here compute
245                    // the 'stick to bottom' case 
246                    let shift = if let Some(last_item_pos) = last_item_pos {
247                        if self.align_top_when_empty && self.not_filling_viewport {
248                            -first_pos
249                        }
250                        else {
251                            let ret = viewport.size.index(vi) - last_item_pos;
252                            if ret >= 0.0 {
253                                self.at_end = true;
254                            }
255                            ret.max(0.0)
256                        }
257                    }
258                    else {
259                        0.0
260                    };
261                    // first we scan upwards and move items in place
262                    let mut first_id_changed = false;
263                    let start_pos = self.first_scroll + shift;
264                    let mut pos = start_pos;
265                    for i in (0..first_index).rev() {
266                        let item = &list[i];
267                        let visible = pos > 0.0;
268                        pos -= item.size.index(vi);
269                        let shift = DVec2::from_index_pair(vi, pos, 0.0);
270                        cx.shift_align_range(&item.align_range, shift - DVec2::from_index_pair(vi, item.shift, 0.0));
271                        if visible { // move up
272                            self.first_scroll = pos;
273                            self.first_id = item.index;
274                            first_id_changed = true;
275                            if item.index < self.range_end {
276                                visible_items += 1;
277                            }
278                        }
279                    }
280                    // then we scan downwards
281                    let mut pos = start_pos;
282                    for i in first_index..list.len() {
283                        let item = &list[i];
284                        let shift = DVec2::from_index_pair(vi, pos, 0.0);
285                        cx.shift_align_range(&item.align_range, shift - DVec2::from_index_pair(vi, item.shift, 0.0));
286                        pos += item.size.index(vi);
287                        let invisible = pos < 0.0;
288                        if invisible { // move down
289                            self.first_scroll = pos - item.size.index(vi);
290                            self.first_id = item.index;
291                            first_id_changed = true;
292                        }
293                        else if item.index < self.range_end {
294                            visible_items += 1;
295                        }
296                    }
297                    // overwrite first scroll for top/bottom aligns if we havent updated already
298                    if !first_id_changed {
299                        self.first_scroll = start_pos;
300                    }
301                }
302                if !self.scroll_bar.animator_in_state(cx, id!(hover.pressed)){
303                    self.update_scroll_bar(cx);
304                }
305            }
306        }
307        else {
308            //log!("Draw state not at end in listview, please review your next_visible_item loop")
309        }
310        let rect = cx.turtle().rect();
311        if self.at_end || self.view_window == 0 || self.view_window > visible_items{
312            self.view_window = visible_items.max(4) - 3;
313        }
314        if self.detect_tail_in_draw{
315            self.detect_tail_in_draw = false;
316            if self.auto_tail && self.at_end{
317                self.tail_range = true;
318            }
319        }
320        let total_views = (self.range_end - self.range_start) as f64 / self.view_window as f64;
321        match self.vec_index {
322            Vec2Index::Y => {
323                self.scroll_bar.draw_scroll_bar(cx, ScrollAxis::Vertical, rect, dvec2(100.0, rect.size.y * total_views));
324            }
325            Vec2Index::X => {
326                self.scroll_bar.draw_scroll_bar(cx, ScrollAxis::Horizontal, rect, dvec2(rect.size.x * total_views, 100.0));
327            }
328        }        
329        
330        if !self.keep_invisible{
331            if self.reuse_items{
332                let reusable_items = &mut self.reusable_items;
333                self.items.retain_visible_with(|v|{
334                    reusable_items.push(v);
335                });
336            }
337            else{
338                self.items.retain_visible();
339            }
340        }
341
342        cx.end_turtle_with_area(&mut self.area);
343        self.visible_items = visible_items;
344    }
345    
346    
347    /// Returns the index of the next visible item that will be drawn by this PortalList.
348    pub fn next_visible_item(&mut self, cx: &mut Cx2d) -> Option<usize> {
349        let vi = self.vec_index;
350        let layout = if vi == Vec2Index::Y { Layout::flow_down() } else { Layout::flow_right() };
351        if let Some(draw_state) = self.draw_state.get() {
352            match draw_state {
353                ListDrawState::Begin => {
354                    // Sanity check: warn on the first item ID being outside of the previously-set item range.
355                    // This check is done here rather than in `begin()`, as most PortalList usage doesn't set
356                    // the item range properly until right before looping over `next_visible_items()`.
357                    #[cfg(debug_assertions)]
358                    if self.fails_sanity_check_first_id_within_item_range() {
359                        warning!("PortalList: first_id {} is greater than range_end {}.\n\
360                            --> Check that you have set the correct item range and first item ID!",
361                            self.first_id, self.range_end,
362                        );
363                    }
364
365                    let viewport = cx.turtle().padded_rect();
366                    self.draw_state.set(ListDrawState::Down {
367                        index: self.first_id,
368                        pos: self.first_scroll,
369                        viewport,
370                    });
371                    match vi {
372                        Vec2Index::Y => {
373                            cx.begin_turtle(Walk {
374                                abs_pos: Some(dvec2(viewport.pos.x, viewport.pos.y + self.first_scroll)),
375                                margin: Default::default(),
376                                width: Size::Fill,
377                                height: Size::Fit
378                            }, layout);
379                        }
380                        Vec2Index::X => {
381                            cx.begin_turtle(Walk {
382                                abs_pos: Some(dvec2(viewport.pos.x + self.first_scroll, viewport.pos.y)),
383                                margin: Default::default(),
384                                width: Size::Fit,
385                                height: Size::Fill
386                            }, layout);
387                        }
388                    }
389                    return Some(self.first_id);
390                }
391                ListDrawState::Down {index, pos, viewport} | ListDrawState::DownAgain {index, pos, viewport} => {
392                    let is_down_again = draw_state.is_down_again();
393                    let did_draw = cx.turtle_has_align_items();
394                    let align_range = cx.get_turtle_align_range();
395                    let rect = cx.end_turtle();
396                    self.draw_align_list.push(AlignItem {
397                        align_range,
398                        shift: pos, 
399                        size: rect.size,
400                        index
401                    });
402                    
403                    if !did_draw || pos + rect.size.index(vi) > viewport.size.index(vi) {
404                        // lets scan upwards
405                        if self.first_id>0 && !is_down_again {
406                            self.draw_state.set(ListDrawState::Up {
407                                index: self.first_id - 1,
408                                pos: self.first_scroll,
409                                hit_bottom: index >= self.range_end,
410                                viewport
411                            });
412                            match vi {
413                                Vec2Index::Y => {
414                                    cx.begin_turtle(Walk {
415                                        abs_pos: Some(dvec2(viewport.pos.x, viewport.pos.y)),
416                                        margin: Default::default(),
417                                        width: Size::Fill,
418                                        height: Size::Fit
419                                    }, layout);
420                                }
421                                Vec2Index::X => {
422                                    cx.begin_turtle(Walk {
423                                        abs_pos: Some(dvec2(viewport.pos.x, viewport.pos.y)),
424                                        margin: Default::default(),
425                                        width: Size::Fit,
426                                        height: Size::Fill
427                                    }, layout);
428                                }
429                            }
430                            return Some(self.first_id - 1);
431                        }
432                        else {
433                            self.draw_state.set(ListDrawState::End {viewport});
434                            return None
435                        }
436                    }
437                    if is_down_again {
438                        self.draw_state.set(ListDrawState::DownAgain {
439                            index: index + 1,
440                            pos: pos + rect.size.index(vi),
441                            viewport
442                        });
443                    }
444                    else {
445                        self.draw_state.set(ListDrawState::Down {
446                            index: index + 1,
447                            pos: pos + rect.size.index(vi),
448                            viewport
449                        });
450                    }
451                    match vi {
452                        Vec2Index::Y => {
453                            cx.begin_turtle(Walk {
454                                abs_pos: Some(dvec2(viewport.pos.x, viewport.pos.y + pos + rect.size.index(vi))),
455                                margin: Default::default(),
456                                width: Size::Fill,
457                                height: Size::Fit
458                            }, layout);
459                        }
460                        Vec2Index::X => {
461                            cx.begin_turtle(Walk {
462                                abs_pos: Some(dvec2(viewport.pos.x + pos + rect.size.index(vi), viewport.pos.y)),
463                                margin: Default::default(),
464                                width: Size::Fit,
465                                height: Size::Fill
466                            }, layout);
467                        }
468                    }
469                    return Some(index + 1);
470                }
471                ListDrawState::Up {index, pos, hit_bottom, viewport} => {
472                    let did_draw = cx.turtle_has_align_items();
473                    let align_range = cx.get_turtle_align_range();
474                    let rect = cx.end_turtle();
475                    self.draw_align_list.push(AlignItem {
476                        align_range,
477                        size: rect.size,
478                        shift: 0.0,
479                        index
480                    });
481                    if index == self.range_start {
482                        // we are at range start, but if we snap to top, we might need to walk further down as well
483                        // therefore we now go 'down again' to make sure we have enough visible items
484                        // if we snap to the top 
485                        if pos - rect.size.index(vi) > 0.0 {
486                            // scan the tail
487                            if let Some(last_index) = self.draw_align_list.iter().map( | v | v.index).max() {
488                                // lets sum up all the items
489                                let total_height: f64 = self.draw_align_list.iter().map( | v | v.size.index(vi)).sum();
490                                self.draw_state.set(ListDrawState::DownAgain {
491                                    index: last_index + 1,
492                                    pos: total_height,
493                                    viewport
494                                });
495                                cx.begin_turtle(Walk {
496                                    abs_pos: Some(dvec2(viewport.pos.x, viewport.pos.y + total_height)),
497                                    margin: Default::default(),
498                                    width: Size::Fill,
499                                    height: Size::Fit
500                                }, Layout::flow_down());
501                                return Some(last_index + 1);
502                            }
503                        }
504                        self.draw_state.set(ListDrawState::End {viewport});
505                        return None
506                    }
507                    
508                    if !did_draw || pos < if hit_bottom {-viewport.size.index(vi)} else {0.0} {
509                        self.draw_state.set(ListDrawState::End {viewport});
510                        return None
511                    }
512                    
513                    self.draw_state.set(ListDrawState::Up {
514                        index: index - 1,
515                        hit_bottom,
516                        pos: pos - rect.size.index(vi),
517                        viewport
518                    });
519                    
520                    cx.begin_turtle(Walk {
521                        abs_pos: Some(dvec2(viewport.pos.x, viewport.pos.y)),
522                        margin: Default::default(),
523                        width: Size::Fill,
524                        height: Size::Fit
525                    }, Layout::flow_down());
526                    
527                    return Some(index - 1);
528                }
529                _ => ()
530            }
531        }
532        None
533    }
534    
535    /// Creates a new widget from the given `template` or returns an existing widget,
536    /// if one already exists with the same `entry_id`.
537    ///
538    /// If you care whether the widget already existed or not, use [`PortalList::item_with_existed()`] instead.
539    ///
540    /// ## Return
541    /// * If a widget already existed for the given `entry_id` and `template`,
542    ///   this returns a reference to that widget.
543    /// * If a new widget was created successfully, this returns a reference to that new widget.
544    /// * If the given `template` could not be found, this returns `None`.
545    pub fn item(&mut self, cx: &mut Cx, entry_id: usize, template: LiveId) -> WidgetRef {
546        self.item_with_existed(cx, entry_id, template).0
547    }
548
549    /// Creates a new widget from the given `template` or returns an existing widget,
550    /// if one already exists with the same `entry_id` and `template`.
551    ///
552    /// * If you only want to check whether the item already existed without creating one,
553    ///   use [`PortalList::get_item()`] instead.
554    /// * If you don't care whether the widget already existed or not, use [`PortalList::item()`] instead.
555    ///
556    /// ## Return
557    /// * If a widget of the same `template` already existed for the given `entry_id`,
558    ///   this returns a tuple of that widget and `true`.
559    /// * If a new widget was created successfully, either because an item with the given `entry_id`
560    ///   did not exist or because the existing item with the given `entry_id` did not use the given `template`,
561    ///   this returns a tuple of that widget and `false`.
562    /// * If the given `template` could not be found, this returns `None`.
563    pub fn item_with_existed(&mut self, cx: &mut Cx, entry_id: usize, template: LiveId) -> (WidgetRef, bool) {
564        use std::collections::hash_map::Entry;
565        if let Some(ptr) = self.templates.get(&template) {
566            match self.items.entry(entry_id) {
567                Entry::Occupied(mut occ) => {
568                    if occ.get().template == template {
569                        (occ.get().widget.clone(), true)
570                    } else {
571                        let widget_ref =  if let Some(pos) = self.reusable_items.iter().position(|v| v.template == template){
572                            self.reusable_items.remove(pos).widget
573                        }
574                        else{
575                            WidgetRef::new_from_ptr(cx, Some(*ptr))
576                        };
577                        occ.insert(WidgetItem{
578                            template, 
579                            widget:widget_ref.clone(),
580                            ..Default::default()
581                        });
582                        (widget_ref, false)
583                    }
584                }
585                Entry::Vacant(vac) => {
586                    let widget_ref =  if let Some(pos) = self.reusable_items.iter().position(|v| v.template == template){
587                        self.reusable_items.remove(pos).widget
588                    }
589                    else{
590                        WidgetRef::new_from_ptr(cx, Some(*ptr))
591                    };
592                    vac.insert(WidgetItem{
593                        template, 
594                        widget: widget_ref.clone(),
595                        ..Default::default()
596                    });
597                    (widget_ref, false)
598                }
599            }
600        } else {
601            warning!("Template not found: {template}. Did you add it to the <PortalList> instance in `live_design!{{}}`?");
602            (WidgetRef::empty(), false)
603        }
604    }
605
606    /// Returns the "start" position of the item with the given `entry_id`
607    /// relative to the "start" position of the PortalList.
608    ///
609    /// * For vertical lists, the start position is the top of the item
610    ///   relative to the top of the PortalList.
611    /// * For horizontal lists, the start position is the left side of the item
612    ///   relative to the left side of the PortalList.
613    ///
614    /// Returns `None` if the item with the given `entry_id` does not exist
615    /// or if the item's area rectangle is zero.
616    ///
617    /// TODO: FIXME: this may not properly handle bottom-up lists
618    ///              or lists that go from right to left.
619    pub fn position_of_item(&self, cx: &Cx, entry_id: usize) -> Option<f64> {
620        const ZEROED: Rect = Rect { pos: DVec2 { x: 0.0, y: 0.0 }, size: DVec2 { x: 0.0, y: 0.0 } };
621
622        if let Some(item) = self.items.get(&entry_id) {
623            let item_rect = item.widget.area().rect(cx);
624            if item_rect == ZEROED {
625                return None;
626            }
627            let self_rect = self.area.rect(cx);
628            if self_rect == ZEROED {
629                return None;
630            }
631            let vi = self.vec_index;
632            Some(item_rect.pos.index(vi) - self_rect.pos.index(vi))
633        } else {
634            None
635        }
636    }
637    
638    /// Returns a reference to the template and widget for the given `entry_id`.
639    pub fn get_item(&self, entry_id: usize) -> Option<(LiveId,WidgetRef)> {
640        if let Some(item) = self.items.get(&entry_id){
641            Some((item.template.clone(), item.widget.clone()))
642        }
643        else{
644            None
645        }
646    }
647    
648    pub fn set_item_range(&mut self, cx: &mut Cx, range_start: usize, range_end: usize) {
649        self.range_start = range_start;
650        if self.range_end != range_end {
651            self.range_end = range_end;
652            if self.tail_range{
653                self.first_id = self.range_end.max(1) - 1;
654                self.first_scroll = 0.0;
655            }
656            self.update_scroll_bar(cx);
657        }
658    }
659    
660    pub fn update_scroll_bar(&mut self, cx: &mut Cx) {
661        let scroll_pos = ((self.first_id - self.range_start) as f64 / ((self.range_end - self.range_start).max(self.view_window + 1) - self.view_window) as f64) * self.scroll_bar.get_scroll_view_total();
662        // move the scrollbar to the right 'top' position
663        self.scroll_bar.set_scroll_pos_no_action(cx, scroll_pos);
664    }
665
666    /// Sets the current scroll offset (`first_scroll`) based on the given `delta` value.
667    ///
668    /// ## Arguments
669    /// * `delta`: The amount to scroll by.
670    ///    A positive value scrolls upwards towards the top of the list,
671    ///    while a negative value scrolls downwards towards the bottom of the list.
672    /// * `clip_top`: If `true`, the scroll offset will be clamped to `0.0`, meaning that
673    ///    no pulldown "bounce" animation will occur when you scroll upwards beyond the top of the list.
674    /// * `transition_to_pulldown`: Whether to transition an existing scroll action to the `Pulldown`
675    ///    scroll state when an upwards scroll action reaches the top of the list.
676    fn delta_top_scroll(
677        &mut self,
678        cx: &mut Cx,
679        delta: f64,
680        clip_top: bool,
681        transition_to_pulldown: bool,
682    ) {
683        if self.range_start == self.range_end {
684            self.first_scroll = 0.0
685        }
686        else {
687            self.first_scroll += delta;
688        }
689
690        if self.first_id == self.range_start {
691            self.first_scroll = self.first_scroll.min(self.max_pull_down);
692            if transition_to_pulldown && self.first_scroll > 0.0 {
693                self.scroll_state = ScrollState::Pulldown {next_frame: cx.new_next_frame()};
694            }
695        }
696        if clip_top && self.first_id == self.range_start && self.first_scroll > 0.0 {
697            self.first_scroll = 0.0;
698        }
699        // Stop a downwards scroll when we reach the end of the list.
700        if self.at_end && delta < 0.0 {
701            self.was_scrolling = false;
702            self.scroll_state = ScrollState::Stopped;
703        }
704        self.update_scroll_bar(cx);
705    }
706
707    /// Returns `true` if currently at the end of the list, meaning that the lasat item
708    /// is visible in the viewport.
709    pub fn is_at_end(&self) -> bool {
710        self.at_end
711    }
712
713    /// Returns the number of items that are currently visible in the viewport,
714    /// including partially visible items.
715    pub fn visible_items(&self) -> usize {
716        self.visible_items
717    }
718
719    /// Returns `true` if this sanity check fails: the first item ID is within the item range.
720    ///
721    /// Returns `false` if the sanity check passes as expected.
722    pub fn fails_sanity_check_first_id_within_item_range(&self) -> bool {
723        !self.tail_range
724            && (self.first_id > self.range_end)
725    }
726
727    /// Initiates a smooth scrolling animation to the specified target item in the list.
728    ///
729    /// ## Arguments
730    /// * `target_id`: The ID (index) of the item to scroll to.
731    /// * `speed`: A positive floating-point value that controls the speed of the animation.
732    ///    The `speed` will always be treated as an absolute value, with the direction of the scroll
733    ///    (up or down) determined by whether `target_id` is above or below the current item.
734    /// * `max_items_to_show`: The maximum number of items to show during the scrolling animation.
735    ///    If `None`, the default value of 20 is used.
736    ///
737    /// ## Example
738    /// ```rust,ignore
739    /// // Scrolls to item 42 at speed 100.0, including at most 30 items in the scroll animation.
740    /// smooth_scroll_to(&mut cx, 42, 100.0, Some(30));
741    /// ```
742    pub fn smooth_scroll_to(&mut self, cx: &mut Cx, target_id: usize, speed: f64, max_items_to_show: Option<usize>) {
743        if self.items.is_empty() { return };
744        if target_id < self.range_start || target_id > self.range_end { return };
745
746        let max_items_to_show = max_items_to_show.unwrap_or(SMOOTH_SCROLL_MAXIMUM_WINDOW);
747        let scroll_direction: f64;
748        let starting_id: Option<usize>;
749        if target_id > self.first_id {
750            // Scrolling down to a larger item index
751            scroll_direction = -1.0;
752            starting_id = ((target_id.saturating_sub(self.first_id)) > max_items_to_show)
753                .then_some(target_id.saturating_sub(max_items_to_show));
754        } else {
755            // Scrolling up to a smaller item index
756            scroll_direction = 1.0;
757            starting_id = ((self.first_id.saturating_sub(target_id)) > max_items_to_show)
758                .then_some(target_id + max_items_to_show);
759        };
760
761        // First, if the target_id was too far away, jump directly to a closer starting_id.
762        if let Some(start) = starting_id {
763            self.first_id = start;
764        }
765        // Then, we kick off the actual smooth scroll process.
766        self.scroll_state = ScrollState::ScrollingTo {
767            target_id,
768            delta: speed.abs() * scroll_direction as f64,
769            next_frame: cx.new_next_frame()
770        };
771    }
772
773    /// Trigger an scrolling animation to the end of the list
774    ///
775    /// ## Arguments
776    /// * `speed`: This value controls how fast the scrolling animation is.
777    ///    Note: This number should be large enough to reach the end, so it is important to
778    ///    test the passed number. TODO provide a better implementation to ensure that the end
779    ///    is always reached, no matter the speed value.
780    /// * `max_items_to_show`: The maximum number of items to show during the scrolling animation.
781    ///    If `None`, the default value of 20 is used.
782    pub fn smooth_scroll_to_end(&mut self, cx: &mut Cx, speed: f64, max_items_to_show: Option<usize>) {
783        if self.items.is_empty() { return };
784
785	let speed = speed * self.range_end as f64;
786	self.smooth_scroll_to(cx, self.range_end, speed, max_items_to_show);
787    }
788}
789
790
791impl Widget for PortalList {
792
793    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
794        let uid = self.widget_uid();
795        
796        let mut scroll_to = None;
797        self.scroll_bar.handle_event_with(cx, event, &mut | _cx, action | {
798            // snap the scrollbar to a top-index with scroll_pos 0
799            if let ScrollBarAction::Scroll {scroll_pos, view_total, view_visible} = action {
800                scroll_to = Some((scroll_pos, scroll_pos+0.5 >= view_total - view_visible))
801            }
802        });
803        if let Some((scroll_to, at_end)) = scroll_to {
804            if at_end && self.auto_tail{
805                self.first_id = self.range_end.max(1) - 1;
806                self.first_scroll = 0.0;
807                self.tail_range = true;
808            } else {
809                self.tail_range = false;
810            }
811
812            self.first_id = ((scroll_to / self.scroll_bar.get_scroll_view_visible()) * self.view_window as f64) as usize;
813            self.first_scroll = 0.0;
814            cx.widget_action(uid, &scope.path, PortalListAction::Scroll);
815            self.was_scrolling = false;
816            self.area.redraw(cx);
817        }
818
819        for item in self.items.values_mut() {
820            let item_uid = item.widget.widget_uid();
821            cx.group_widget_actions(uid, item_uid, |cx|{
822                item.widget.handle_event(cx, event, scope)
823            });
824        }
825        
826        match &mut self.scroll_state {
827            ScrollState::ScrollingTo {target_id, delta, next_frame} => {
828                if let Some(_) = next_frame.is_event(event) {
829                    let target_id = *target_id;
830
831                    let distance_to_target = target_id as isize - self.first_id as isize;
832                    let target_passed = distance_to_target.signum() == delta.signum() as isize;
833                    // check to see if we passed the target and fix it. this may happen if the delta is too high,
834                    // so we can just correct the first id, since the animation isn't being smooth anyways.
835                    if target_passed {
836                        self.first_id = target_id;
837                        self.area.redraw(cx);
838                    }
839
840                    let distance_to_target = target_id as isize - self.first_id as isize;
841
842                    // If the target is under first_id (its bigger than it), and end is reached,
843                    // first_id would never be the target, so we just take it as reached.
844                    let target_visible_at_end = self.at_end && target_id > self.first_id;
845                    let target_reached = distance_to_target == 0 || target_visible_at_end;
846
847                    if !target_reached {
848                        *next_frame = cx.new_next_frame();
849                        let delta = *delta;
850                        // Don't enable the pulldown animation when scrolling to a specific item.
851                        self.delta_top_scroll(cx, delta, true, false);
852                        cx.widget_action(uid, &scope.path, PortalListAction::Scroll);
853                        self.area.redraw(cx);
854                    } else {
855                        self.was_scrolling = false;
856                        self.scroll_state = ScrollState::Stopped;
857                        cx.widget_action(uid, &scope.path, PortalListAction::SmoothScrollReached);
858                    }
859                }
860            }
861            ScrollState::Flick {delta, next_frame} => {
862                if let Some(_) = next_frame.is_event(event) {
863                    *delta = *delta * self.flick_scroll_decay;
864                    if delta.abs()>self.flick_scroll_minimum {
865                        *next_frame = cx.new_next_frame();
866                        let delta = *delta;
867                        self.delta_top_scroll(cx, delta, false, true);
868                        cx.widget_action(uid, &scope.path, PortalListAction::Scroll);
869                        self.area.redraw(cx);
870                    } else {
871                        self.was_scrolling = false;
872                        self.scroll_state = ScrollState::Stopped;
873                    }
874                }
875            }
876            ScrollState::Pulldown {next_frame} => {
877                if let Some(_) = next_frame.is_event(event) {
878                    // we have to bounce back
879                    if self.first_id == self.range_start && self.first_scroll > 0.0 {
880                        self.first_scroll *= 0.85;
881                        if self.first_scroll < 1.0 {
882                            self.first_scroll = 0.0;
883                            // the pulldown animation is finished
884                            self.was_scrolling = false;
885                            self.scroll_state = ScrollState::Stopped;
886                        }
887                        else {
888                            *next_frame = cx.new_next_frame();
889                            cx.widget_action(uid, &scope.path, PortalListAction::Scroll);
890                        }
891                        self.area.redraw(cx);
892                    }
893                    else {
894                        self.was_scrolling = false;
895                        self.scroll_state = ScrollState::Stopped;
896                    }
897                }
898            }
899            _=>()
900        }
901        let vi = self.vec_index;
902        let is_scroll = if let Event::Scroll(_) = event {true} else {false};
903        if self.scroll_bar.is_area_captured(cx){
904            self.scroll_state = ScrollState::Stopped;
905        }
906        if !self.scroll_bar.is_area_captured(cx) || is_scroll{ 
907            match event.hits_with_capture_overload(cx, self.area, self.capture_overload) {
908                Hit::FingerScroll(e) => {
909                    self.tail_range = false;
910                    self.detect_tail_in_draw = true;
911                    self.was_scrolling = false;
912                    self.scroll_state = ScrollState::Stopped;
913                    self.delta_top_scroll(cx, -e.scroll.index(vi), false, true);
914                    cx.widget_action(uid, &scope.path, PortalListAction::Scroll);
915                    self.area.redraw(cx);
916                },
917                
918                Hit::KeyDown(ke) => match ke.key_code {
919                    KeyCode::Home => {
920                        self.first_id = 0;
921                        self.first_scroll = 0.0;
922                        self.tail_range = false;
923                        self.update_scroll_bar(cx);
924                        self.area.redraw(cx);
925                    },
926                    KeyCode::End => {
927                        self.first_id = self.range_end.max(1) - 1;
928                        self.first_scroll = 0.0;
929                        if self.auto_tail {
930                            self.tail_range = true;
931                        }
932                        self.update_scroll_bar(cx);
933                        self.area.redraw(cx);
934                    },
935                    KeyCode::PageUp => {
936                        self.first_id = self.first_id.max(self.view_window) - self.view_window;
937                        self.first_scroll = 0.0;
938                        self.tail_range = false;
939                        self.update_scroll_bar(cx);
940                        self.area.redraw(cx);
941                    },
942                    KeyCode::PageDown => {
943                        self.first_id += self.view_window;
944                        self.first_scroll = 0.0;
945                        if self.first_id >= self.range_end.max(1) {
946                            self.first_id = self.range_end.max(1) - 1;
947                        }
948                        self.detect_tail_in_draw = true;
949                        self.update_scroll_bar(cx);
950                        self.area.redraw(cx);
951                    },
952                    KeyCode::ArrowDown => {
953                        self.first_id += 1;
954                        if self.first_id >= self.range_end.max(1) {
955                            self.first_id = self.range_end.max(1) - 1;
956                        }
957                        self.detect_tail_in_draw = true;
958                        self.first_scroll = 0.0;
959                        self.update_scroll_bar(cx);
960                        self.area.redraw(cx);
961                    },
962                    KeyCode::ArrowUp => {
963                        if self.first_id > 0 {
964                            self.first_id -= 1;
965                            if self.first_id < self.range_start {
966                                self.first_id = self.range_start;
967                            }
968                            self.first_scroll = 0.0;
969                            self.area.redraw(cx);
970                            self.tail_range = false;
971                            self.update_scroll_bar(cx);
972                        }
973                    },
974                    _ => ()
975                }
976                Hit::FingerDown(fe) => {
977                    // We allow other mouse buttons to grab key focus and stop the tail range behavior,
978                    // but we only want the primary button (or touch) to actually scroll via dragging.
979
980                    //log!("Finger down {} {}", e.time, e.abs);
981                    if self.grab_key_focus {
982                        cx.set_key_focus(self.area);
983                    }
984                    self.tail_range = false;
985                    self.was_scrolling = match &self.scroll_state {
986                        ScrollState::Drag { samples } => samples.len() > 1,
987                        ScrollState::Stopped => false,
988                        _ => true,
989                    };
990                    if self.drag_scrolling && fe.is_primary_hit() {
991                        self.scroll_state = ScrollState::Drag {
992                            samples: vec![ScrollSample{abs: fe.abs.index(vi), time: fe.time}]
993                        };
994                    }
995                }
996                Hit::FingerMove(e) => {
997                    //log!("Finger move {} {}", e.time, e.abs);
998                    cx.set_cursor(MouseCursor::Default);
999                    match &mut self.scroll_state {
1000                        ScrollState::Drag {samples}=>{
1001                            let new_abs = e.abs.index(vi);
1002                            let old_sample = *samples.last().unwrap();
1003                            samples.push(ScrollSample{abs:new_abs, time:e.time});
1004                            if samples.len()>4{
1005                                samples.remove(0);
1006                            }
1007                            self.delta_top_scroll(cx, new_abs - old_sample.abs, false, false);
1008                            self.area.redraw(cx);
1009                        }
1010                        _=>()
1011                    }
1012                }
1013                Hit::FingerUp(fe) if fe.is_primary_hit() => {
1014                    //log!("Finger up {} {}", e.time, e.abs);
1015                    match &mut self.scroll_state {
1016                        ScrollState::Drag {samples}=>{
1017                            // alright so we need to see if in the last couple of samples
1018                            // we have a certain distance per time
1019                            let mut last = None;
1020                            let mut scaled_delta = 0.0;
1021                            let mut total_delta = 0.0;
1022                            for sample in samples.iter().rev(){
1023                                if last.is_none(){
1024                                    last = Some(sample);
1025                                }
1026                                else{
1027                                    total_delta += last.unwrap().abs - sample.abs;
1028                                    scaled_delta += (last.unwrap().abs - sample.abs)/ (last.unwrap().time - sample.time)
1029                                }
1030                            }
1031                            scaled_delta *= self.flick_scroll_scaling;
1032                            if self.first_id == self.range_start && self.first_scroll > 0.0 {
1033                                self.scroll_state = ScrollState::Pulldown {next_frame: cx.new_next_frame()};
1034                            }
1035                            else if total_delta.abs() > 10.0 && scaled_delta.abs() > self.flick_scroll_minimum{
1036                                
1037                                self.scroll_state = ScrollState::Flick {
1038                                    delta: scaled_delta.min(self.flick_scroll_maximum).max(-self.flick_scroll_maximum),
1039                                    next_frame: cx.new_next_frame()
1040                                };
1041                            }
1042                            else {
1043                                self.was_scrolling = false;
1044                                self.scroll_state = ScrollState::Stopped;
1045                            }
1046                        }
1047                        _=>()
1048                    }
1049                    // ok so. lets check our gap from 'drag'
1050                    // here we kinda have to take our last delta and animate it
1051                }
1052                Hit::KeyFocus(_) => {
1053                }
1054                Hit::KeyFocusLost(_) => {
1055                }
1056                _ => ()
1057            }
1058        }
1059    }
1060    
1061    fn draw_walk(&mut self, cx: &mut Cx2d, _scope:&mut Scope, walk: Walk) -> DrawStep {
1062        if self.draw_state.begin(cx, ListDrawState::Begin) {
1063            self.begin(cx, walk);
1064            return DrawStep::make_step()
1065        }
1066        // ok so if we are
1067        if let Some(_) = self.draw_state.get() {
1068            self.end(cx);
1069            self.draw_state.end();
1070        }
1071        DrawStep::done()
1072    }
1073}
1074
1075impl PortalListRef {
1076    /// Sets the first item to be shown and its scroll offset.
1077    ///
1078    /// On the next draw pass, this PortalList will draw the item with the given `id`
1079    /// as the first item in the list, and will set the *scroll offset*
1080    /// (from the top of the viewport to the beginning of the first item)
1081    /// to the given value `s`.
1082    pub fn set_first_id_and_scroll(&self, id: usize, s: f64) {
1083        if let Some(mut inner) = self.borrow_mut() {
1084            inner.first_id = id;
1085            inner.first_scroll = s;
1086        }
1087    }
1088    
1089    /// Sets the first item to be shown by this PortalList to the item with the given `id`.
1090    pub fn set_first_id(&self, id: usize) {
1091        if let Some(mut inner) = self.borrow_mut() {
1092            inner.first_id = id;
1093        }
1094    }
1095    
1096    /// Returns the ID of the item currently shown as the first item in this PortalList.
1097    pub fn first_id(&self) -> usize {
1098        if let Some(inner) = self.borrow() {
1099            inner.first_id
1100        }
1101        else {
1102            0
1103        }
1104    }
1105    
1106    /// Enables whether the PortalList auto-tracks the last item in the list.
1107    ///
1108    /// If `true`, the PortalList will continually scroll to the last item in the list
1109    /// automatically, as new items are added.
1110    /// If `false`, the PortalList will not auto-scroll to the last item.
1111    pub fn set_tail_range(&self, tail_range: bool) {
1112        if let Some(mut inner) = self.borrow_mut() {
1113            inner.tail_range = tail_range
1114        }
1115    }
1116
1117    /// See [`PortalList::is_at_end()`].
1118    pub fn is_at_end(&self) -> bool {
1119        let Some(inner) = self.borrow() else { return false };
1120        inner.is_at_end()
1121    }
1122
1123    /// See [`PortalList::visible_items()`].
1124    pub fn visible_items(&self) -> usize {
1125        let Some(inner) = self.borrow() else { return 0 };
1126        inner.visible_items()
1127    }
1128
1129    /// Returns whether this PortalList was scrolling when the most recent finger hit occurred.
1130    pub fn was_scrolling(&self) -> bool {
1131        self.borrow().is_some_and(|inner| inner.was_scrolling)
1132    }
1133
1134    /// Returns whether the given `actions` contain an action indicating that this PortalList was scrolled.
1135    pub fn scrolled(&self, actions: &Actions) -> bool {
1136        if let PortalListAction::Scroll = actions.find_widget_action(self.widget_uid()).cast() {
1137            return true;
1138        }
1139        false
1140    }
1141
1142    /// Returns the current scroll offset of this PortalList.
1143    ///
1144    /// See [`PortalListRef::set_first_id_and_scroll()`] for more information.
1145    pub fn scroll_position(&self) -> f64 {
1146        let Some(inner) = self.borrow_mut() else { return 0.0 };
1147        inner.first_scroll
1148    }
1149    
1150    /// See [`PortalList::item()`].
1151    pub fn item(&self, cx: &mut Cx, entry_id: usize, template: LiveId) -> WidgetRef {
1152        if let Some(mut inner) = self.borrow_mut(){
1153            inner.item(cx, entry_id, template)
1154        }
1155        else{
1156            WidgetRef::empty()
1157        }
1158    }
1159
1160    /// See [`PortalList::item_with_existed()`].
1161    pub fn item_with_existed(&self, cx: &mut Cx, entry_id: usize, template: LiveId) -> (WidgetRef, bool) {
1162        if let Some(mut inner) = self.borrow_mut(){
1163            inner.item_with_existed(cx, entry_id, template)
1164        }
1165        else{
1166            (WidgetRef::empty(), false)
1167        }
1168    }
1169
1170    /// See [`PortalList::get_item()`].
1171    pub fn get_item(&self, entry_id: usize) -> Option<(LiveId, WidgetRef)> {
1172        let Some(inner) = self.borrow() else { return None };
1173        inner.get_item(entry_id)
1174    }
1175    
1176    pub fn position_of_item(&self, cx:&Cx, entry_id: usize) -> Option<f64>{
1177        let Some(inner) = self.borrow() else { return None };
1178        inner.position_of_item(cx, entry_id)
1179    }
1180    
1181    pub fn items_with_actions(&self, actions: &Actions) -> ItemsWithActions {
1182        let mut set = Vec::new();
1183        self.items_with_actions_vec(actions, &mut set);
1184        set
1185    }
1186    
1187    fn items_with_actions_vec(&self, actions: &Actions, set: &mut ItemsWithActions) {
1188        let uid = self.widget_uid();
1189        if let Some(inner) = self.borrow() {
1190            for action in actions {
1191                if let Some(action) = action.as_widget_action(){
1192                    if let Some(group) = &action.group{
1193                        if group.group_uid == uid{
1194                            for (item_id, item) in inner.items.iter() {
1195                                if group.item_uid == item.widget.widget_uid(){
1196                                    set.push((*item_id, item.widget.clone()))
1197                                }
1198                            }
1199                        }
1200                    }
1201                }
1202            }
1203        }
1204    }
1205    
1206    pub fn any_items_with_actions(&self, actions: &Actions)->bool {
1207        let uid = self.widget_uid();
1208        for action in actions {
1209            if let Some(action) = action.as_widget_action(){
1210                if let Some(group) = &action.group{
1211                    if group.group_uid == uid{
1212                        return true
1213                    }
1214                }
1215            }
1216        }
1217        false
1218    }
1219
1220    /// Initiates a smooth scrolling animation to the specified target item in the list.
1221    ///
1222    /// ## Arguments
1223    /// * `target_id`: The ID (index) of the item to scroll to.
1224    /// * `speed`: A positive floating-point value that controls the speed of the animation.
1225    ///    The `speed` will always be treated as an absolute value, with the direction of the scroll
1226    ///    (up or down) determined by whether `target_id` is above or below the current item.
1227    /// * `max_items_to_show`: The maximum number of items to show during the scrolling animation.
1228    ///    If `None`, the default value of 20 is used.
1229    ///
1230    /// ## Example
1231    /// ```rust,ignore
1232    /// // Scrolls to item 42 at speed 100.0, including at most 30 items in the scroll animation.
1233    /// smooth_scroll_to(&mut cx, 42, 100.0, Some(30));
1234    /// ```
1235    pub fn smooth_scroll_to(&self, cx: &mut Cx, target_id: usize, speed: f64, max_items_to_show: Option<usize>) {
1236        let Some(mut inner) = self.borrow_mut() else { return };
1237	inner.smooth_scroll_to(cx, target_id, speed, max_items_to_show);
1238    }
1239
1240    /// Returns the ID of the item that is currently being smoothly scrolled to, if any.
1241    pub fn is_smooth_scrolling(&self) -> Option<usize> {
1242        let Some(inner) = self.borrow_mut() else { return None };
1243        if let ScrollState::ScrollingTo { target_id, .. } = inner.scroll_state {
1244            Some(target_id)
1245        } else {
1246            None
1247        }
1248    }
1249
1250    /// Returns whether the given `actions` contain an action indicating that this PortalList completed
1251    /// a smooth scroll, reaching the target.
1252    pub fn smooth_scroll_reached(&self, actions: &Actions) -> bool {
1253        if let PortalListAction::SmoothScrollReached = actions.find_widget_action(self.widget_uid()).cast() {
1254            return true;
1255        }
1256        false
1257    }
1258
1259    /// Trigger an scrolling animation to the end of the list
1260    ///
1261    /// ## Arguments
1262    /// * `speed`: This value controls how fast the scrolling animation is.
1263    ///    Note: This number should be large enough to reach the end, so it is important to
1264    ///    test the passed number. TODO provide a better implementation to ensure that the end
1265    ///    is always reached, no matter the speed value.
1266    /// * `max_items_to_show`: The maximum number of items to show during the scrolling animation.
1267    ///    If `None`, the default value of 20 is used.
1268    pub fn smooth_scroll_to_end(&self, cx: &mut Cx, speed: f64, max_items_to_show: Option<usize>) {
1269        let Some(mut inner) = self.borrow_mut() else { return };
1270
1271	inner.smooth_scroll_to_end(cx, speed, max_items_to_show);
1272    }
1273
1274    /// It indicates if we have items not displayed towards the end of the list (below)
1275    /// For instance, it is useful to show or hide a "jump to the most recent" button
1276    /// on a chat messages list
1277    pub fn further_items_bellow_exist(&self) -> bool {
1278        let Some(inner) = self.borrow() else { return false };
1279        !(inner.at_end || inner.not_filling_viewport)
1280    }
1281}
1282
1283type ItemsWithActions = Vec<(usize, WidgetRef)>;
1284
1285impl PortalListSet {
1286    pub fn set_first_id(&self, id: usize) {
1287        for list in self.iter() {
1288            list.set_first_id(id)
1289        }
1290    }
1291    
1292    
1293    pub fn items_with_actions(&self, actions: &Actions) -> ItemsWithActions {
1294        let mut set = Vec::new();
1295        for list in self.iter() {
1296            list.items_with_actions_vec(actions, &mut set)
1297        }
1298        set
1299    }
1300}