Skip to main content

ferroui_controls/presenters/
items_presenter.rs

1use super::panel_container_generator::PanelContainerGenerator;
2use crate::primitives::{as_logical_scrollable, register_logical_scrollable, ILogicalScrollable};
3use crate::templates::ITemplateOf;
4use crate::{Control, ControlImpl, ItemsControl, Panel, VirtualizingPanel};
5use ferroui_base::input::{IScrollable, InputElementImpl, NavigationDirection};
6use ferroui_base::interactivity::InteractiveImpl;
7use ferroui_base::layout::{BringIntoViewRequest, Layoutable, LayoutableImpl};
8use ferroui_base::reactive::{Disposable, IDisposable};
9use ferroui_base::utilities::HandlerList;
10use ferroui_base::{
11    ferro_class, ferro_impl_classes, ferro_property, instantiate, FerroObject, FerroObjectImpl, FerroObjectImplExt,
12    FerroPropertyChangedEventArgs, Rect, Ref, Size, StyledElement, StyledElementImpl, StyledProperty, Vector,
13    VisualImpl, VisualImplExt, VisualTreeAttachmentEventArgs, WeakRef,
14};
15use std::cell::{Cell, RefCell};
16use std::rc::Rc;
17
18/// Presents items inside an [`ItemsControl`].
19#[repr(C)]
20pub struct ItemsPresenter {
21    base: Control,
22    panel: RefCell<Option<Ref<Panel>>>,
23    items_control: RefCell<Option<WeakRef<ItemsControl>>>,
24    generator: RefCell<Option<Rc<PanelContainerGenerator>>>,
25    /// The panel, when it is a logical scrollable.
26    logical_scrollable: RefCell<Option<Ref<Panel>>>,
27    logical_scroll_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
28    scroll_invalidated: Rc<HandlerList<dyn Fn()>>,
29    pending_scroll_into_view_index: Cell<i32>,
30}
31
32ferro_class!(ItemsPresenter: Control);
33ferroui_base::ferro_class_info!(ItemsPresenter { new: ItemsPresenter::new });
34ferro_impl_classes!(ItemsPresenter: StyledElementImpl, InteractiveImpl, ControlImpl);
35
36impl FerroObjectImpl for ItemsPresenter {
37    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
38        Self::parent_on_property_changed(this, change);
39
40        if change.property() == StyledElement::templated_parent_property().as_property() {
41            this.reset_state();
42            *this.items_control.borrow_mut() = None;
43
44            let new_value = change.get_new_value::<Option<Ref<FerroObject>>>();
45            if let Some(items_control) = new_value.and_then(|v| v.cast::<ItemsControl>()) {
46                *this.items_control.borrow_mut() = Some(items_control.downgrade());
47                items_control.register_items_presenter(this);
48            }
49        } else if change.property() == Self::items_panel_property().as_property() {
50            this.reset_state();
51            this.invalidate_measure();
52        }
53    }
54}
55
56impl VisualImpl for ItemsPresenter {
57    fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
58        Self::parent_on_attached_to_visual_tree(this, e);
59
60        if this.pending_scroll_into_view_index.get() >= 0 {
61            if let Some(root) = this.get_layout_root() {
62                let layout_manager = root.layout_manager();
63                if let Some(layout_manager) = layout_manager.as_bring_into_view_layout_manager() {
64                    layout_manager.enqueue_bring_into_view(this.scroll_into_view_request());
65                }
66            }
67        }
68    }
69}
70
71impl LayoutableImpl for ItemsPresenter {
72    fn apply_template(this: &Self) {
73        if this.panel.borrow().is_some() {
74            return;
75        }
76        let Some(items_control) = this.items_control() else { return };
77
78        if let Some(subscription) = this.logical_scroll_subscription.take() {
79            subscription.dispose();
80        }
81
82        let Some(panel) = this.items_panel().build_typed() else { return };
83        *this.panel.borrow_mut() = Some(panel.clone());
84
85        panel.set_templated_parent(this.templated_parent());
86        panel.set_is_items_host(true);
87        this.logical_children().add(panel.clone().upcast());
88        this.visual_children().add(panel.clone().upcast());
89
90        if let Some(v) = panel.downcast_ref::<VirtualizingPanel>() {
91            v.attach(&items_control);
92        } else {
93            this.create_simple_panel_generator();
94        }
95
96        let is_logical_scrollable = as_logical_scrollable(&panel).is_some();
97        *this.logical_scrollable.borrow_mut() = if is_logical_scrollable { Some(panel.clone()) } else { None };
98
99        if let Some(scrollable) = as_logical_scrollable(&panel) {
100            let handlers = Rc::downgrade(&this.scroll_invalidated);
101            let subscription = scrollable.scroll_invalidated(Rc::new(move || {
102                if let Some(handlers) = handlers.upgrade() {
103                    for (_, handler) in handlers.snapshot().iter() {
104                        handler();
105                    }
106                }
107            }));
108            *this.logical_scroll_subscription.borrow_mut() = Some(subscription);
109        }
110    }
111}
112
113impl InputElementImpl for ItemsPresenter {
114    fn as_scrollable(this: &Self) -> Option<&dyn IScrollable> {
115        Some(this)
116    }
117}
118
119impl IScrollable for ItemsPresenter {
120    fn extent(&self) -> Size {
121        self.with_logical_scrollable(|s| s.extent()).unwrap_or_default()
122    }
123
124    fn offset(&self) -> Vector {
125        self.with_logical_scrollable(|s| s.offset()).unwrap_or_default()
126    }
127
128    fn set_offset(&self, value: Vector) {
129        self.with_logical_scrollable(|s| s.set_offset(value));
130    }
131
132    fn viewport(&self) -> Size {
133        self.with_logical_scrollable(|s| s.viewport()).unwrap_or_default()
134    }
135
136    fn can_horizontally_scroll(&self) -> bool {
137        self.with_logical_scrollable(|s| s.can_horizontally_scroll()).unwrap_or(false)
138    }
139
140    fn can_vertically_scroll(&self) -> bool {
141        self.with_logical_scrollable(|s| s.can_vertically_scroll()).unwrap_or(false)
142    }
143}
144
145impl ILogicalScrollable for ItemsPresenter {
146    fn set_can_horizontally_scroll(&self, value: bool) {
147        self.with_logical_scrollable(|s| s.set_can_horizontally_scroll(value));
148    }
149
150    fn set_can_vertically_scroll(&self, value: bool) {
151        self.with_logical_scrollable(|s| s.set_can_vertically_scroll(value));
152    }
153
154    fn is_logical_scroll_enabled(&self) -> bool {
155        self.with_logical_scrollable(|s| s.is_logical_scroll_enabled()).unwrap_or(false)
156    }
157
158    fn scroll_size(&self) -> Size {
159        self.with_logical_scrollable(|s| s.scroll_size()).unwrap_or_default()
160    }
161
162    fn page_scroll_size(&self) -> Size {
163        self.with_logical_scrollable(|s| s.page_scroll_size()).unwrap_or_default()
164    }
165
166    fn scroll_invalidated(&self, handler: Rc<dyn Fn()>) -> Rc<dyn IDisposable> {
167        let token = self.scroll_invalidated.add(handler);
168        let handlers = Rc::downgrade(&self.scroll_invalidated);
169        Disposable::create(move || {
170            if let Some(handlers) = handlers.upgrade() {
171                handlers.remove(token);
172            }
173        })
174    }
175
176    fn bring_into_view(&self, target: &Ref<Control>, target_rect: Rect) -> bool {
177        self.with_logical_scrollable(|s| s.bring_into_view(target, target_rect)).unwrap_or(false)
178    }
179
180    fn get_control_in_direction(
181        &self,
182        direction: NavigationDirection,
183        from: Option<&Ref<Control>>,
184    ) -> Option<Ref<Control>> {
185        self.with_logical_scrollable(|s| s.get_control_in_direction(direction, from)).flatten()
186    }
187
188    fn raise_scroll_invalidated(&self) {
189        for (_, handler) in self.scroll_invalidated.snapshot().iter() {
190            handler();
191        }
192    }
193}
194
195/// The deferred scroll of an items presenter, executed by the layout
196/// manager once the layout is stable.
197struct ScrollIntoViewRequest {
198    presenter: Ref<ItemsPresenter>,
199}
200
201impl BringIntoViewRequest for ScrollIntoViewRequest {
202    fn target(&self) -> Ref<Layoutable> {
203        self.presenter.clone().upcast()
204    }
205
206    fn try_execute(&self) -> bool {
207        self.presenter.try_execute_pending_scroll_into_view()
208    }
209}
210
211ferroui_base::ferro_properties! { impl ItemsPresenter {
212    ferro_property!(
213        /// Defines the `ItemsPanel` property.
214        pub fn items_panel_property() -> StyledProperty<Rc<dyn ITemplateOf<Option<Ref<Panel>>>>> {
215            ItemsControl::items_panel_property().add_owner::<ItemsPresenter>()
216        }
217    );
218} }
219
220impl ItemsPresenter {
221    fn static_constructor() {
222        register_logical_scrollable::<ItemsPresenter>();
223    }
224
225    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
226    pub fn construct() -> Self {
227        Self {
228            base: Control::construct(),
229            panel: RefCell::new(None),
230            items_control: RefCell::new(None),
231            generator: RefCell::new(None),
232            logical_scrollable: RefCell::new(None),
233            logical_scroll_subscription: RefCell::new(None),
234            scroll_invalidated: Rc::new(HandlerList::new()),
235            pending_scroll_into_view_index: Cell::new(-1),
236        }
237    }
238
239    pub fn new() -> Ref<Self> {
240        instantiate(Self::construct())
241    }
242
243    /// Gets or sets a template which creates the [`Panel`] used to display
244    /// the items.
245    pub fn items_panel(&self) -> Rc<dyn ITemplateOf<Option<Ref<Panel>>>> {
246        self.get_value(Self::items_panel_property())
247    }
248
249    pub fn set_items_panel(&self, value: Rc<dyn ITemplateOf<Option<Ref<Panel>>>>) {
250        self.set_value(Self::items_panel_property(), value)
251    }
252
253    /// Gets the panel used to display the items.
254    pub fn panel(&self) -> Option<Ref<Panel>> {
255        self.panel.borrow().clone()
256    }
257
258    /// Gets the owner [`ItemsControl`].
259    pub(crate) fn items_control(&self) -> Option<Ref<ItemsControl>> {
260        self.items_control.borrow().as_ref().and_then(WeakRef::upgrade)
261    }
262
263    fn with_logical_scrollable<R>(&self, f: impl FnOnce(&dyn ILogicalScrollable) -> R) -> Option<R> {
264        let panel = self.logical_scrollable.borrow().clone()?;
265        let scrollable = as_logical_scrollable(&panel)?;
266        Some(f(scrollable))
267    }
268
269    fn scroll_into_view_request(&self) -> Rc<dyn BringIntoViewRequest> {
270        Rc::new(ScrollIntoViewRequest { presenter: self.to_ref() })
271    }
272
273    pub(crate) fn scroll_into_view(&self, index: i32) {
274        if index < 0 {
275            return;
276        }
277
278        // Not attached to a layout root yet: the request will be enqueued
279        // when attached to the visual tree.
280        let layout_manager = self.get_layout_root().map(|root| root.layout_manager());
281        let Some(layout_manager) = layout_manager.as_ref().and_then(|m| m.as_bring_into_view_layout_manager()) else {
282            self.pending_scroll_into_view_index.set(index);
283            return;
284        };
285
286        // Try to execute synchronously when no layout pass is running.
287        if !layout_manager.is_in_layout_pass() && self.try_scroll_into_view_now(index) {
288            self.pending_scroll_into_view_index.set(-1);
289            return;
290        }
291
292        // Defer to the end of the layout pass.
293        self.pending_scroll_into_view_index.set(index);
294        layout_manager.enqueue_bring_into_view(self.scroll_into_view_request());
295    }
296
297    pub(crate) fn cancel_scroll_into_view(&self, index: i32) {
298        if self.pending_scroll_into_view_index.get() == index {
299            self.pending_scroll_into_view_index.set(-1);
300        }
301    }
302
303    fn try_scroll_into_view_now(&self, index: i32) -> bool {
304        if !self.is_effectively_visible() {
305            return false;
306        }
307        let Some(panel) = self.panel() else { return false };
308
309        if let Some(virtualizing_panel) = panel.downcast_ref::<VirtualizingPanel>() {
310            return virtualizing_panel.scroll_into_view(index).is_some();
311        }
312
313        if (index as usize) < panel.children().count() {
314            panel.children().get(index as usize).bring_into_view();
315            return true;
316        }
317
318        false
319    }
320
321    fn try_execute_pending_scroll_into_view(&self) -> bool {
322        let index = self.pending_scroll_into_view_index.get();
323        if index < 0 {
324            return true;
325        }
326
327        let panel_valid = self.panel().is_some_and(|panel| panel.is_measure_valid() && panel.is_arrange_valid());
328        if !panel_valid || !self.is_effectively_visible() {
329            return false;
330        }
331
332        self.pending_scroll_into_view_index.set(-1);
333
334        // Try to scroll, but ignore the return value. We don't want to
335        // return false if this fails: this would requeue a request that
336        // might never been be fulfilled because items changed.
337        let _ = self.try_scroll_into_view_now(index);
338
339        // Executing the scroll may have prepared containers whose handlers
340        // requested a new scroll: in that case keep the request queued so it
341        // gets executed as well.
342        self.pending_scroll_into_view_index.get() < 0
343    }
344
345    pub(crate) fn refresh(&self) {
346        let panel = self.panel();
347        if let Some(v) = panel.as_ref().and_then(|p| p.downcast_ref::<VirtualizingPanel>()) {
348            v.refresh();
349        } else {
350            let generator = self.generator.borrow().clone();
351            if let Some(generator) = generator {
352                generator.refresh();
353            }
354        }
355    }
356
357    fn reset_state(&self) {
358        if let Some(generator) = self.generator.take() {
359            generator.dispose();
360        }
361        self.logical_children().clear();
362        self.visual_children().clear();
363        let panel = self.panel.take();
364        if let Some(v) = panel.as_ref().and_then(|p| p.downcast_ref::<VirtualizingPanel>()) {
365            v.detach();
366        }
367    }
368
369    fn create_simple_panel_generator(&self) {
370        debug_assert!(!self.panel().is_some_and(|p| p.is::<VirtualizingPanel>()));
371
372        if self.items_control().is_none() || self.panel().is_none() {
373            return;
374        }
375
376        if let Some(generator) = self.generator.take() {
377            generator.dispose();
378        }
379        let generator = PanelContainerGenerator::new(self);
380        *self.generator.borrow_mut() = Some(generator);
381    }
382
383    pub(crate) fn container_from_index(&self, index: i32) -> Option<Ref<Control>> {
384        let panel = self.panel()?;
385        if let Some(v) = panel.downcast_ref::<VirtualizingPanel>() {
386            return v.container_from_index(index);
387        }
388        if index >= 0 {
389            panel.children().try_get(index as usize)
390        } else {
391            None
392        }
393    }
394
395    pub(crate) fn get_realized_containers(&self) -> Option<Vec<Ref<Control>>> {
396        let panel = self.panel()?;
397        if let Some(v) = panel.downcast_ref::<VirtualizingPanel>() {
398            return v.get_realized_containers();
399        }
400        Some(panel.children().to_vec())
401    }
402
403    pub(crate) fn index_from_container(&self, container: &Ref<Control>) -> i32 {
404        let Some(panel) = self.panel() else { return -1 };
405        if let Some(v) = panel.downcast_ref::<VirtualizingPanel>() {
406            return v.index_from_container(container);
407        }
408        panel.children().index_of(container).map_or(-1, |index| index as i32)
409    }
410}