relm4/typed_view/
grid.rs

1//! Idiomatic and high-level abstraction over [`gtk::GridView`].
2
3use super::{
4    Filter, OrdFn, RelmSelectionExt, TypedListItem, get_mut_value, get_value,
5    iterator::TypedIterator,
6};
7use gtk::{
8    gio, glib,
9    prelude::{Cast, CastNone, FilterExt, IsA, ListItemExt, ListModelExt, ObjectExt},
10};
11use std::{any::Any, cmp::Ordering, marker::PhantomData};
12
13/// An item of a [`TypedGridView`].
14pub trait RelmGridItem: Any {
15    /// The top-level widget for the grid item.
16    type Root: IsA<gtk::Widget>;
17
18    /// The widgets created for the grid item.
19    type Widgets;
20
21    /// Construct the widgets.
22    fn setup(grid_item: &gtk::ListItem) -> (Self::Root, Self::Widgets);
23
24    /// Bind the widgets to match the data of the grid item.
25    fn bind(&mut self, _widgets: &mut Self::Widgets, _root: &mut Self::Root) {}
26
27    /// Undo the steps of [`RelmGridItem::bind()`] if necessary.
28    fn unbind(&mut self, _widgets: &mut Self::Widgets, _root: &mut Self::Root) {}
29
30    /// Undo the steps of [`RelmGridItem::setup()`] if necessary.
31    fn teardown(_grid_item: &gtk::ListItem) {}
32}
33
34/// A high-level wrapper around [`gio::ListStore`],
35/// [`gtk::SignalListItemFactory`] and [`gtk::GridView`].
36///
37/// [`TypedGridView`] aims at keeping nearly the same functionality and
38/// flexibility of the raw bindings while introducing a more idiomatic
39/// and type-safe interface.
40pub struct TypedGridView<T, S> {
41    /// The internal grid view.
42    pub view: gtk::GridView,
43    /// The internal selection model.
44    pub selection_model: S,
45    store: gio::ListStore,
46    filters: Vec<Filter>,
47    active_model: gio::ListModel,
48    base_model: gio::ListModel,
49    _ty: PhantomData<*const T>,
50}
51
52impl<T: std::fmt::Debug, S: std::fmt::Debug> std::fmt::Debug for TypedGridView<T, S> {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        f.debug_struct("TypedGridView")
55            .field("store", &self.store)
56            .field("view", &self.view)
57            .field("filters", &"<Vec<gtk::Filter>>")
58            .field("active_model", &self.active_model)
59            .field("base_model", &self.base_model)
60            .field("selection_model", &self.selection_model)
61            .finish()
62    }
63}
64
65impl<T, S> TypedGridView<T, S>
66where
67    T: RelmGridItem + Ord,
68    S: RelmSelectionExt,
69{
70    /// Create a new [`TypedGridView`] that sorts the items
71    /// based on the [`Ord`] trait.
72    #[must_use]
73    pub fn with_sorting() -> Self {
74        Self::init(Some(Box::new(T::cmp)))
75    }
76}
77
78impl<T, S> Default for TypedGridView<T, S>
79where
80    T: RelmGridItem,
81    S: RelmSelectionExt,
82{
83    fn default() -> Self {
84        Self::new()
85    }
86}
87
88impl<T, S> TypedGridView<T, S>
89where
90    T: RelmGridItem,
91    S: RelmSelectionExt,
92{
93    /// Create a new, empty [`TypedGridView`].
94    #[must_use]
95    pub fn new() -> Self {
96        Self::init(None)
97    }
98
99    fn init(sort_fn: OrdFn<T>) -> Self {
100        let store = gio::ListStore::new::<glib::BoxedAnyObject>();
101
102        let factory = gtk::SignalListItemFactory::new();
103        factory.connect_setup(move |_, list_item| {
104            let list_item = list_item
105                .downcast_ref::<gtk::ListItem>()
106                .expect("Needs to be ListItem");
107
108            let (root, widgets) = T::setup(list_item);
109            unsafe { root.set_data("widgets", widgets) };
110            list_item.set_child(Some(&root));
111        });
112
113        factory.connect_bind(move |_, list_item| {
114            let list_item = list_item
115                .downcast_ref::<gtk::ListItem>()
116                .expect("Needs to be ListItem");
117
118            let widget = list_item
119                .downcast_ref::<gtk::ListItem>()
120                .expect("Needs to be ListItem")
121                .child();
122
123            let obj = list_item.item().unwrap();
124            let mut obj = get_mut_value::<T>(&obj);
125
126            let mut root = widget.and_downcast::<T::Root>().unwrap();
127
128            let mut widgets = unsafe { root.steal_data("widgets") }.unwrap();
129            obj.bind(&mut widgets, &mut root);
130            unsafe { root.set_data("widgets", widgets) };
131        });
132
133        factory.connect_unbind(move |_, list_item| {
134            let list_item = list_item
135                .downcast_ref::<gtk::ListItem>()
136                .expect("Needs to be ListItem");
137
138            let widget = list_item
139                .downcast_ref::<gtk::ListItem>()
140                .expect("Needs to be ListItem")
141                .child();
142
143            let obj = list_item.item().unwrap();
144            let mut obj = get_mut_value::<T>(&obj);
145
146            let mut root = widget.and_downcast::<T::Root>().unwrap();
147
148            let mut widgets = unsafe { root.steal_data("widgets") }.unwrap();
149            obj.unbind(&mut widgets, &mut root);
150            unsafe { root.set_data("widgets", widgets) };
151        });
152
153        factory.connect_teardown(move |_, list_item| {
154            let list_item = list_item
155                .downcast_ref::<gtk::ListItem>()
156                .expect("Needs to be ListItem");
157
158            T::teardown(list_item);
159        });
160
161        let model: gio::ListModel = store.clone().upcast();
162
163        let base_model = if let Some(sort_fn) = sort_fn {
164            let sorter = gtk::CustomSorter::new(move |first, second| {
165                let first = get_value::<T>(first);
166                let second = get_value::<T>(second);
167                match sort_fn(&first, &second) {
168                    Ordering::Less => gtk::Ordering::Smaller,
169                    Ordering::Equal => gtk::Ordering::Equal,
170                    Ordering::Greater => gtk::Ordering::Larger,
171                }
172            });
173
174            gtk::SortListModel::new(Some(model), Some(sorter)).upcast()
175        } else {
176            model
177        };
178
179        let selection_model = S::new_model(base_model.clone());
180        let view = gtk::GridView::new(Some(selection_model.clone()), Some(factory));
181
182        Self {
183            store,
184            view,
185            filters: Vec::new(),
186            active_model: base_model.clone(),
187            base_model,
188            _ty: PhantomData,
189            selection_model,
190        }
191    }
192
193    /// Add a function to filter the stored items.
194    /// Returning `false` will simply hide the item.
195    ///
196    /// Note that several filters can be added on top of each other.
197    pub fn add_filter<F: Fn(&T) -> bool + 'static>(&mut self, f: F) {
198        let filter = gtk::CustomFilter::new(move |obj| {
199            let value = get_value::<T>(obj);
200            f(&value)
201        });
202        let filter_model =
203            gtk::FilterListModel::new(Some(self.active_model.clone()), Some(filter.clone()));
204        self.active_model = filter_model.clone().upcast();
205        self.selection_model.set_list_model(&self.active_model);
206        self.filters.push(Filter {
207            filter,
208            model: filter_model,
209        });
210    }
211
212    /// Returns the amount of filters that were added.
213    pub fn filters_len(&self) -> usize {
214        self.filters.len()
215    }
216
217    /// Set a certain filter as active or inactive.
218    pub fn set_filter_status(&mut self, idx: usize, active: bool) -> bool {
219        if let Some(filter) = self.filters.get(idx) {
220            if active {
221                filter.model.set_filter(Some(&filter.filter));
222            } else {
223                filter.model.set_filter(None::<&gtk::CustomFilter>);
224            }
225            true
226        } else {
227            false
228        }
229    }
230
231    /// Notify that a certain filter has changed.
232    /// This causes the filter expression to be re-evaluated.
233    ///
234    /// Returns true if a filter was notified.
235    pub fn notify_filter_changed(&self, idx: usize) -> bool {
236        if let Some(filter) = self.filters.get(idx) {
237            filter.filter.changed(gtk::FilterChange::Different);
238            true
239        } else {
240            false
241        }
242    }
243
244    /// Remove the last filter.
245    pub fn pop_filter(&mut self) {
246        let filter = self.filters.pop();
247        if let Some(filter) = filter {
248            self.active_model = filter.model.model().unwrap();
249            self.selection_model.set_list_model(&self.active_model);
250        }
251    }
252
253    /// Remove all filters.
254    pub fn clear_filters(&mut self) {
255        self.filters.clear();
256        self.active_model = self.base_model.clone();
257        self.selection_model.set_list_model(&self.active_model);
258    }
259
260    /// Add a new item at the end of the list.
261    pub fn append(&mut self, value: T) {
262        self.store.append(&glib::BoxedAnyObject::new(value));
263    }
264
265    /// Add new items from an iterator the the end of the list.
266    pub fn extend_from_iter<I: IntoIterator<Item = T>>(&mut self, init: I) {
267        let objects: Vec<glib::BoxedAnyObject> =
268            init.into_iter().map(glib::BoxedAnyObject::new).collect();
269        self.store.extend_from_slice(&objects);
270    }
271
272    #[cfg(feature = "gnome_43")]
273    #[cfg_attr(docsrs, doc(cfg(feature = "gnome_43")))]
274    /// Find the index of the first item that matches a certain function.
275    pub fn find<F: FnMut(&T) -> bool>(&self, mut equal_func: F) -> Option<u32> {
276        self.store.find_with_equal_func(move |obj| {
277            let value = get_value::<T>(obj);
278            equal_func(&value)
279        })
280    }
281
282    /// Returns true if the list is empty.
283    pub fn is_empty(&self) -> bool {
284        self.len() == 0
285    }
286
287    /// Returns the length of the list (without filters).
288    pub fn len(&self) -> u32 {
289        self.store.n_items()
290    }
291
292    /// Get the [`TypedListItem`] at the specified position.
293    ///
294    /// Returns [`None`] if the position is invalid.
295    pub fn get(&self, position: u32) -> Option<TypedListItem<T>> {
296        if let Some(obj) = self.store.item(position) {
297            let wrapper = obj.downcast::<glib::BoxedAnyObject>().unwrap();
298            Some(TypedListItem::new(wrapper))
299        } else {
300            None
301        }
302    }
303
304    /// Get the visible [`TypedListItem`] at the specified position,
305    /// (the item at the given position after filtering and sorting).
306    ///
307    /// Returns [`None`] if the position is invalid.
308    pub fn get_visible(&self, position: u32) -> Option<TypedListItem<T>> {
309        if let Some(obj) = self.active_model.item(position) {
310            let wrapper = obj.downcast::<glib::BoxedAnyObject>().unwrap();
311            Some(TypedListItem::new(wrapper))
312        } else {
313            None
314        }
315    }
316
317    /// Insert an item at a specific position.
318    pub fn insert(&mut self, position: u32, value: T) {
319        self.store
320            .insert(position, &glib::BoxedAnyObject::new(value));
321    }
322
323    /// Insert an item into the list and calculate its position from
324    /// a sorting function.
325    pub fn insert_sorted<F: FnMut(&T, &T) -> Ordering>(
326        &self,
327        value: T,
328        mut compare_func: F,
329    ) -> u32 {
330        let item = glib::BoxedAnyObject::new(value);
331
332        let compare = move |first: &glib::Object, second: &glib::Object| -> Ordering {
333            let first = get_value::<T>(first);
334            let second = get_value::<T>(second);
335            compare_func(&first, &second)
336        };
337
338        self.store.insert_sorted(&item, compare)
339    }
340
341    /// Remove an item at a specific position.
342    pub fn remove(&mut self, position: u32) {
343        self.store.remove(position);
344    }
345
346    /// Remove all items.
347    pub fn clear(&mut self) {
348        self.store.remove_all();
349    }
350
351    /// Returns an iterator that allows modifying each [`TypedListItem`].
352    pub fn iter(&self) -> TypedIterator<'_, TypedGridView<T, S>> {
353        TypedIterator {
354            list: self,
355            index: 0,
356        }
357    }
358}