Skip to main content

ferroui_controls/
table_view.rs

1use crate::generators::RecycleKey;
2use crate::presenters::table_view_layout_helper::TableViewLayoutHelper;
3use crate::presenters::TableViewColumnHeadersPresenter;
4use crate::primitives::{SelectingItemsControlImpl, TemplatedControlImpl};
5use crate::templates::IDataTemplate;
6use crate::{
7    AssignedBinding, Control, ControlImpl, ItemsControl, ItemsControlImpl, ItemsControlImplExt, ListBox, TableViewColumn, TableViewRow,
8};
9use ferroui_base::collections::{FerroList, NotifyCollectionChangedAction, ResetBehavior};
10use ferroui_base::input::InputElementImpl;
11use ferroui_base::interactivity::InteractiveImpl;
12use ferroui_base::layout::LayoutableImpl;
13use ferroui_base::reactive::{Disposable, IDisposable};
14use ferroui_base::{
15    ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, instantiate, BoxedValue, DirectProperty,
16    FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Ref, StyledElement,
17    StyledElementImpl, StyledElementImplExt, StyledProperty, VisualImpl, WeakRef,
18};
19use std::cell::{Cell, RefCell};
20use std::rc::Rc;
21
22/// A read-only tabular control that presents items in configurable columns.
23#[repr(C)]
24pub struct TableView {
25    base: ListBox,
26    columns_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
27    columns: RefCell<Option<FerroList<Ref<TableViewColumn>>>>,
28    /// The presenter of the column headers of the template; it is part of
29    /// the tree below the control and held weakly.
30    headers_presenter: RefCell<Option<WeakRef<TableViewColumnHeadersPresenter>>>,
31}
32
33ferro_class!(TableView: ListBox);
34ferro_class_info!(TableView { new: TableView::new });
35ferro_impl_classes!(
36    TableView: VisualImpl,
37    LayoutableImpl,
38    InteractiveImpl,
39    InputElementImpl,
40    ControlImpl,
41    TemplatedControlImpl,
42    SelectingItemsControlImpl
43);
44
45impl FerroObjectImpl for TableView {
46    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
47        Self::parent_on_property_changed(this, change);
48
49        if change.property() == Self::can_user_resize_columns_property().as_property() {
50            let columns = this.columns.borrow().clone();
51            if let Some(columns) = columns {
52                for column in columns.snapshot().iter() {
53                    column.update_can_user_effectively_resize();
54                }
55            }
56        }
57    }
58}
59
60impl StyledElementImpl for TableView {
61    fn on_initialized(this: &Self) {
62        Self::parent_on_initialized(this);
63
64        let columns = this.columns.borrow().clone();
65        if let Some(columns) = columns {
66            this.subscribe_to_columns(&columns);
67        }
68    }
69}
70
71impl ItemsControlImpl for TableView {
72    fn create_container_for_item_override(
73        _this: &Self,
74        _item: &Option<BoxedValue>,
75        _index: i32,
76        _recycle_key: Option<RecycleKey>,
77    ) -> Ref<Control> {
78        TableViewRow::new().upcast()
79    }
80
81    fn prepare_container_for_item_override(
82        this: &Self,
83        container: &Ref<Control>,
84        item: &Option<BoxedValue>,
85        index: i32,
86    ) {
87        Self::parent_prepare_container_for_item_override(this, container, item, index);
88
89        if let Some(row) = container.downcast_ref::<TableViewRow>() {
90            row.set_columns(Some(this.columns()));
91            row.rebuild_cells();
92        }
93    }
94
95    fn needs_container_override(this: &Self, item: &Option<BoxedValue>, _index: i32) -> (bool, Option<RecycleKey>) {
96        this.needs_container::<TableViewRow>(item)
97    }
98
99    fn clear_container_for_item_override(this: &Self, element: &Ref<Control>) {
100        Self::parent_clear_container_for_item_override(this, element);
101
102        if let Some(row) = element.downcast_ref::<TableViewRow>() {
103            row.set_columns(None);
104            row.clear_cells();
105        }
106    }
107}
108
109ferro_properties! {
110    impl TableView {
111        /// Defines the `Columns` property.
112        pub fn columns_property() -> DirectProperty<TableView, Option<FerroList<Ref<TableViewColumn>>>> {
113            FerroProperty::register_direct::<TableView, _>(
114                "Columns",
115                |o| Some(o.columns()),
116                Some(|o, v| o.set_columns(v)),
117                None,
118            )
119        }
120
121        /// Defines the `CanUserResizeColumns` property.
122        pub fn can_user_resize_columns_property() -> StyledProperty<bool> {
123            FerroProperty::register::<TableView, _>("CanUserResizeColumns", true)
124        }
125    }
126}
127
128impl TableView {
129    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
130    pub fn construct() -> Self {
131        Self {
132            base: ListBox::construct(),
133            columns_subscription: RefCell::new(None),
134            columns: RefCell::new(None),
135            headers_presenter: RefCell::new(None),
136        }
137    }
138
139    pub fn new() -> Ref<Self> {
140        instantiate(Self::construct())
141    }
142
143    /// The binding of the display member of the items control: it has no
144    /// effect on a table view.
145    #[deprecated(note = "DisplayMemberBinding has no effect on a TableView. Use TableViewColumn.Binding instead.")]
146    pub fn display_member_binding(&self) -> Option<AssignedBinding> {
147        ItemsControl::display_member_binding(self)
148    }
149
150    #[deprecated(note = "DisplayMemberBinding has no effect on a TableView. Use TableViewColumn.Binding instead.")]
151    pub fn set_display_member_binding(&self, value: Option<AssignedBinding>) {
152        ItemsControl::set_display_member_binding(self, value)
153    }
154
155    /// The item template of the items control: it has no effect on a table
156    /// view.
157    #[deprecated(note = "ItemTemplate has no effect on a TableView. Use TableViewColumn.CellTemplate instead.")]
158    pub fn item_template(&self) -> Option<Rc<dyn IDataTemplate>> {
159        ItemsControl::item_template(self)
160    }
161
162    #[deprecated(note = "ItemTemplate has no effect on a TableView. Use TableViewColumn.CellTemplate instead.")]
163    pub fn set_item_template(&self, value: Option<Rc<dyn IDataTemplate>>) {
164        ItemsControl::set_item_template(self, value)
165    }
166
167    /// Gets or sets the collection of columns displayed by this
168    /// [`TableView`]. The collection is created when it is first read.
169    pub fn columns(&self) -> FerroList<Ref<TableViewColumn>> {
170        if let Some(columns) = self.columns.borrow().clone() {
171            return columns;
172        }
173
174        let columns = FerroList::new();
175        columns.set_reset_behavior(ResetBehavior::Remove);
176        *self.columns.borrow_mut() = Some(columns.clone());
177        if self.is_initialized() {
178            self.subscribe_to_columns(&columns);
179        }
180
181        columns
182    }
183
184    pub fn set_columns(&self, value: Option<FerroList<Ref<TableViewColumn>>>) {
185        let old_value = self.columns.borrow().clone();
186        if old_value == value {
187            return;
188        }
189
190        self.unsubscribe_from_columns();
191        if let Some(value) = &value {
192            value.set_reset_behavior(ResetBehavior::Remove);
193        }
194        self.set_and_raise(Self::columns_property(), &self.columns, value.clone());
195
196        if self.is_initialized() {
197            if let Some(value) = &value {
198                self.subscribe_to_columns(value);
199            }
200
201            self.rebuild_headers();
202            self.rebuild_cells();
203        }
204    }
205
206    /// Gets or sets a value indicating whether the user can resize columns
207    /// by dragging the separator between column headers.
208    pub fn can_user_resize_columns(&self) -> bool {
209        self.get_value(Self::can_user_resize_columns_property())
210    }
211
212    pub fn set_can_user_resize_columns(&self, value: bool) {
213        self.set_value(Self::can_user_resize_columns_property(), value)
214    }
215
216    pub(crate) fn headers_presenter(&self) -> Option<Ref<TableViewColumnHeadersPresenter>> {
217        self.headers_presenter.borrow().as_ref().and_then(WeakRef::upgrade)
218    }
219
220    pub(crate) fn set_headers_presenter(&self, value: Option<&Ref<TableViewColumnHeadersPresenter>>) {
221        *self.headers_presenter.borrow_mut() = value.map(Ref::downgrade);
222    }
223
224    fn subscribe_to_columns(&self, columns: &FerroList<Ref<TableViewColumn>>) {
225        debug_assert!(self.columns_subscription.borrow().is_none());
226
227        let is_initial_iteration = Rc::new(Cell::new(true));
228
229        let weak = self.to_ref().downgrade();
230        let added = {
231            let weak = weak.clone();
232            let is_initial_iteration = is_initial_iteration.clone();
233            move |column: &Ref<TableViewColumn>| {
234                let Some(this) = weak.upgrade() else { return };
235                this.attach_column(column);
236                if !is_initial_iteration.get() {
237                    this.on_columns_changed();
238                }
239            }
240        };
241        let removed = {
242            let weak = weak.clone();
243            move |column: &Ref<TableViewColumn>| {
244                let Some(this) = weak.upgrade() else { return };
245                this.detach_column(column);
246                this.on_columns_changed();
247            }
248        };
249        let items = move || {
250            let this = weak.upgrade()?;
251            let columns = this.columns.borrow().clone();
252            columns.map(|columns| columns.snapshot())
253        };
254
255        let subscription = for_each_item(columns, items, added, removed, || {});
256        *self.columns_subscription.borrow_mut() = Some(subscription);
257
258        is_initial_iteration.set(false);
259    }
260
261    fn attach_column(&self, column: &Ref<TableViewColumn>) {
262        let this = self.to_ref();
263
264        if column.table_view().is_some_and(|table_view| table_view != this) {
265            panic!("The column {} is already attached to a TableView.", column.debug_display());
266        }
267
268        // Resolve styles and bindings before enabling refresh notifications.
269        // The headers and cells are rebuilt after attachment to apply the
270        // column's current values.
271        column.set_parent(this.clone().upcast::<StyledElement>());
272        column.set_table_view(Some(this));
273    }
274
275    fn detach_column(&self, column: &Ref<TableViewColumn>) {
276        debug_assert!(column.table_view().is_none_or(|table_view| table_view == self.to_ref()));
277
278        column.set_table_view(None);
279        column.set_parent(None::<Ref<StyledElement>>);
280        column.set_actual_width(f64::NAN);
281    }
282
283    fn unsubscribe_from_columns(&self) {
284        let Some(subscription) = self.columns_subscription.take() else { return };
285
286        subscription.dispose();
287
288        for column in self.columns().snapshot().iter() {
289            self.detach_column(column);
290        }
291    }
292
293    fn on_columns_changed(&self) {
294        TableViewLayoutHelper::reset_actual_widths(&self.columns());
295        self.rebuild_headers();
296        self.rebuild_cells();
297    }
298
299    pub(crate) fn on_columns_size_changed(&self) {
300        TableViewLayoutHelper::reset_actual_widths(&self.columns());
301        self.invalidate_headers_measure();
302        self.invalidate_cells_measure();
303    }
304
305    fn rebuild_headers(&self) {
306        if let Some(headers_presenter) = self.headers_presenter() {
307            headers_presenter.rebuild_headers();
308        }
309    }
310
311    fn rebuild_cells(&self) {
312        let columns = self.columns();
313
314        for row in self.get_realized_containers() {
315            if let Some(table_view_row) = row.downcast_ref::<TableViewRow>() {
316                table_view_row.set_columns(Some(columns.clone()));
317                table_view_row.rebuild_cells();
318            } else {
319                row.invalidate_measure();
320            }
321        }
322    }
323
324    pub(crate) fn invalidate_headers_measure(&self) {
325        if let Some(headers_presenter) = self.headers_presenter() {
326            headers_presenter.invalidate_measure();
327        }
328    }
329
330    pub(crate) fn invalidate_cells_measure(&self) {
331        for row in self.get_realized_containers() {
332            if let Some(table_view_row) = row.downcast_ref::<TableViewRow>() {
333                table_view_row.invalidate_cells_measure();
334            } else {
335                row.invalidate_measure();
336            }
337        }
338    }
339
340    pub(crate) fn refresh_column_headers(&self, column: &Ref<TableViewColumn>) {
341        let Some(column_index) = self.columns().index_of(column) else { return };
342
343        if let Some(headers_presenter) = self.headers_presenter() {
344            headers_presenter.refresh_header(column_index);
345        }
346    }
347
348    pub(crate) fn refresh_column_cells(&self, column: &Ref<TableViewColumn>) {
349        let Some(column_index) = self.columns().index_of(column) else { return };
350
351        for row in self.get_realized_containers() {
352            if let Some(table_view_row) = row.downcast_ref::<TableViewRow>() {
353                table_view_row.refresh_cell(column_index);
354            }
355        }
356    }
357}
358
359/// Invokes `added` for each item of the list and for each item added to it
360/// later, `removed` for each item removed from it and `reset` when the list
361/// is reset; the items of the list, read with `items`, are then added again.
362/// The subscription lasts until the returned handle is disposed.
363///
364/// The handler belongs to the list, so it cannot hold the list itself:
365/// `items` reads the list through its owner.
366fn for_each_item<T: Clone + 'static>(
367    collection: &FerroList<T>,
368    items: impl Fn() -> Option<Rc<Vec<T>>> + 'static,
369    added: impl Fn(&T) + 'static,
370    removed: impl Fn(&T) + 'static,
371    reset: impl Fn() + 'static,
372) -> Rc<dyn IDisposable> {
373    let add = move |items: &[T]| {
374        for item in items {
375            added(item);
376        }
377    };
378    let remove = move |items: &[T]| {
379        for item in items.iter().rev() {
380            removed(item);
381        }
382    };
383
384    add(&collection.snapshot());
385
386    let token = collection.add_collection_changed(Rc::new(move |e| match e.action {
387        NotifyCollectionChangedAction::Add => add(e.new_items),
388        NotifyCollectionChangedAction::Move | NotifyCollectionChangedAction::Replace
389            if e.old_starting_index >= 0 =>
390        {
391            remove(e.old_items);
392            add(e.new_items);
393        }
394        NotifyCollectionChangedAction::Remove => remove(e.old_items),
395        NotifyCollectionChangedAction::Move
396        | NotifyCollectionChangedAction::Replace
397        | NotifyCollectionChangedAction::Reset => {
398            reset();
399            if let Some(items) = items() {
400                add(&items);
401            }
402        }
403    }));
404
405    let collection = collection.clone();
406    Disposable::create(move || {
407        collection.remove_collection_changed(token);
408    })
409}