Add ReverseModel

This commit is contained in:
Guilhem Vallat 2023-07-20 14:33:43 +02:00 committed by Olivier Goffart
parent 70978f88d2
commit 49b3e15bc8
4 changed files with 292 additions and 39 deletions

View file

@ -19,7 +19,8 @@ All notable changes to this project are documented in this file.
### Rust API
- Implementede `Default` for `slint::Weak`
- Implemented `Default` for `slint::Weak`
- Added `ReverseModel` and `ModelExt::reverse`
### C++

View file

@ -273,7 +273,7 @@ pub use i_slint_core::graphics::{
};
pub use i_slint_core::model::{
FilterModel, MapModel, Model, ModelExt, ModelNotify, ModelPeer, ModelRc, ModelTracker,
SortModel, StandardListViewItem, TableColumn, VecModel,
ReverseModel, SortModel, StandardListViewItem, TableColumn, VecModel,
};
pub use i_slint_core::sharedvector::SharedVector;
pub use i_slint_core::timers::{Timer, TimerMode};

View file

@ -11,7 +11,7 @@ use crate::items::{ItemRef, SortOrder};
use crate::layout::Orientation;
use crate::lengths::{LogicalLength, RectLengths};
use crate::{Coord, Property, SharedString, SharedVector};
pub use adapters::{FilterModel, MapModel, SortModel};
pub use adapters::{FilterModel, MapModel, ReverseModel, SortModel};
use alloc::boxed::Box;
use alloc::rc::Rc;
use alloc::vec::Vec;
@ -236,6 +236,15 @@ pub trait ModelExt: Model {
{
SortModel::new(self, sort_function)
}
/// Returns a new Model where the elements are reversed.
/// This is a shortcut for [`ReverseModel::new()`].
fn reverse(self) -> ReverseModel<Self>
where
Self: Sized + 'static,
{
ReverseModel::new(self)
}
}
impl<T: Model> ModelExt for T {}

View file

@ -5,6 +5,45 @@
use super::*;
#[cfg(test)]
#[derive(Default)]
struct TestView {
// Track the parameters reported by the model (row counts, indices, etc.).
// The last field in the tuple is the row size the model reports at the time
// of callback
changed_rows: RefCell<Vec<usize>>,
added_rows: RefCell<Vec<(usize, usize)>>,
removed_rows: RefCell<Vec<(usize, usize)>>,
reset: RefCell<usize>,
}
#[cfg(test)]
impl TestView {
fn clear(&self) {
self.changed_rows.borrow_mut().clear();
self.added_rows.borrow_mut().clear();
self.removed_rows.borrow_mut().clear();
}
}
#[cfg(test)]
impl ModelChangeListener for TestView {
fn row_changed(&self, row: usize) {
self.changed_rows.borrow_mut().push(row);
}
fn row_added(&self, index: usize, count: usize) {
self.added_rows.borrow_mut().push((index, count));
}
fn row_removed(&self, index: usize, count: usize) {
self.removed_rows.borrow_mut().push((index, count));
}
fn reset(&self) {
*self.reset.borrow_mut() += 1;
}
}
/// Provides rows that are generated by a map function based on the rows of another Model
///
/// When the other Model is updated, the `MapModel` is updated accordingly.
@ -800,42 +839,6 @@ where
mod sort_tests {
use super::*;
#[derive(Default)]
struct TestView {
// Track the parameters reported by the model (row counts, indices, etc.).
// The last field in the tuple is the row size the model reports at the time
// of callback
changed_rows: RefCell<Vec<usize>>,
added_rows: RefCell<Vec<(usize, usize)>>,
removed_rows: RefCell<Vec<(usize, usize)>>,
reset: RefCell<usize>,
}
impl TestView {
fn clear(&self) {
self.changed_rows.borrow_mut().clear();
self.added_rows.borrow_mut().clear();
self.removed_rows.borrow_mut().clear();
}
}
impl ModelChangeListener for TestView {
fn row_changed(&self, row: usize) {
self.changed_rows.borrow_mut().push(row);
}
fn row_added(&self, index: usize, count: usize) {
self.added_rows.borrow_mut().push((index, count));
}
fn row_removed(&self, index: usize, count: usize) {
self.removed_rows.borrow_mut().push((index, count));
}
fn reset(&self) {
*self.reset.borrow_mut() += 1;
}
}
#[test]
fn test_sorted_model_insert() {
let wrapped_rc = Rc::new(VecModel::from(vec![3, 4, 1, 2]));
@ -945,3 +948,243 @@ mod sort_tests {
assert_eq!(sorted_model.row_data(3).unwrap(), 3);
}
}
/// Provides a reversed view of another [`Model`].
///
/// When the other Model is updated, the `ReverseModel` is updated accordingly.
///
/// Generic parameters:
/// * `M` the type of the wrapped `Model`.
///
/// ## Example
///
/// Here we have a [`VecModel`] holding [`crate::SharedString`]s.
/// It is then reversed into a `ReverseModel`.
///
/// ```
/// # use slint::{Model, VecModel, SharedString, ReverseModel};
/// let model = VecModel::from(vec![
/// SharedString::from("Lorem"),
/// SharedString::from("ipsum"),
/// SharedString::from("dolor"),
/// ]);
///
/// let reverse_model = ReverseModel::new(model);
///
/// assert_eq!(reverse_model.row_data(0).unwrap(), SharedString::from("dolor"));
/// assert_eq!(reverse_model.row_data(1).unwrap(), SharedString::from("ipsum"));
/// assert_eq!(reverse_model.row_data(2).unwrap(), SharedString::from("Lorem"));
/// ```
///
/// Alternatively you can use the shortcut [`ModelExt::reverse`].
/// ```
/// # use slint::{Model, ModelExt, VecModel, SharedString};
/// let reverse_model = VecModel::from(vec![
/// SharedString::from("Lorem"),
/// SharedString::from("ipsum"),
/// SharedString::from("dolor"),
/// ]).reverse();
/// assert_eq!(reverse_model.row_data(0).unwrap(), SharedString::from("dolor"));
/// assert_eq!(reverse_model.row_data(1).unwrap(), SharedString::from("ipsum"));
/// assert_eq!(reverse_model.row_data(2).unwrap(), SharedString::from("Lorem"));
/// ```
///
/// If you want to modify the underlying [`VecModel`] you can give the ReverseModel a [`Rc`] of it:
/// ```
/// # use std::rc::Rc;
/// # use slint::{Model, VecModel, SharedString, ReverseModel};
/// let model = Rc::new(VecModel::from(vec![
/// SharedString::from("Lorem"),
/// SharedString::from("ipsum"),
/// SharedString::from("dolor"),
/// ]));
///
/// let reverse_model = ReverseModel::new(model.clone());
///
/// assert_eq!(reverse_model.row_data(0).unwrap(), SharedString::from("dolor"));
/// assert_eq!(reverse_model.row_data(1).unwrap(), SharedString::from("ipsum"));
/// assert_eq!(reverse_model.row_data(2).unwrap(), SharedString::from("Lorem"));
///
/// model.push(SharedString::from("opsom"));
///
/// assert_eq!(reverse_model.row_data(0).unwrap(), SharedString::from("opsom"));
/// assert_eq!(reverse_model.row_data(1).unwrap(), SharedString::from("dolor"));
/// assert_eq!(reverse_model.row_data(2).unwrap(), SharedString::from("ipsum"));
/// assert_eq!(reverse_model.row_data(3).unwrap(), SharedString::from("Lorem"));
/// ```
pub struct ReverseModel<M>(Pin<Box<ModelChangeListenerContainer<ReverseModelInner<M>>>>)
where
M: Model + 'static;
struct ReverseModelInner<M>
where
M: Model + 'static,
{
wrapped_model: M,
notify: ModelNotify,
}
impl<M> ModelChangeListener for ReverseModelInner<M>
where
M: Model + 'static,
{
fn row_changed(&self, row: usize) {
self.notify.row_changed(self.wrapped_model.row_count() - 1 - row);
}
fn row_added(&self, index: usize, count: usize) {
let row_count = self.wrapped_model.row_count();
let old_row_count = row_count - count;
let index = old_row_count - index;
self.notify.row_added(index, count);
}
fn row_removed(&self, index: usize, count: usize) {
let row_count = self.wrapped_model.row_count();
let old_row_count = row_count + count;
let index = old_row_count - index - 1;
self.notify.row_removed(index, count);
}
fn reset(&self) {
self.notify.reset()
}
}
impl<M> ReverseModel<M>
where
M: Model + 'static,
{
pub fn new(wrapped_model: M) -> Self {
let inner = ReverseModelInner { wrapped_model, notify: Default::default() };
let container = Box::pin(ModelChangeListenerContainer::new(inner));
container.wrapped_model.model_tracker().attach_peer(container.as_ref().model_peer());
Self(container)
}
pub fn inner_model(&self) -> &M {
&self.0.wrapped_model
}
}
impl<M> Model for ReverseModel<M>
where
M: Model + 'static,
{
type Data = M::Data;
fn row_count(&self) -> usize {
self.0.wrapped_model.row_count()
}
fn row_data(&self, row: usize) -> Option<Self::Data> {
let count = self.0.wrapped_model.row_count();
self.0.wrapped_model.row_data(count - row - 1)
}
fn set_row_data(&self, row: usize, data: Self::Data) {
let count = self.0.as_ref().wrapped_model.row_count();
self.0.wrapped_model.set_row_data(count - row - 1, data);
}
fn model_tracker(&self) -> &dyn ModelTracker {
&self.0.notify
}
}
#[cfg(test)]
mod reversed_tests {
use super::*;
#[track_caller]
fn check_content(model: &ReverseModel<Rc<VecModel<i32>>>, expected: &[i32]) {
assert_eq!(model.row_count(), expected.len());
for (i, v) in expected.iter().enumerate() {
assert_eq!(model.row_data(i), Some(*v), "Expected {} at index {}", v, i);
}
}
#[test]
fn test_reversed_model() {
let wrapped_rc = Rc::new(VecModel::from(vec![1, 2, 3, 4]));
let model = ReverseModel::new(wrapped_rc.clone());
let observer = Box::pin(ModelChangeListenerContainer::<TestView>::default());
model.model_tracker().attach_peer(Pin::as_ref(&observer).model_peer());
check_content(&model, &[4, 3, 2, 1]);
}
#[test]
fn test_reversed_model_insert() {
for (idx, mapped_idx) in [(0, 4), (1, 3), (2, 2), (3, 1), (4, 0)] {
println!("Inserting at {} expecting mapped to {}", idx, mapped_idx);
let wrapped_rc = Rc::new(VecModel::from(vec![1, 2, 3, 4]));
let model = ReverseModel::new(wrapped_rc.clone());
let observer = Box::pin(ModelChangeListenerContainer::<TestView>::default());
model.model_tracker().attach_peer(Pin::as_ref(&observer).model_peer());
wrapped_rc.insert(idx, 10);
assert_eq!(observer.added_rows.borrow().len(), 1);
assert!(
observer.added_rows.borrow().eq(&[(mapped_idx, 1)]),
"Added rows: {:?}",
observer.added_rows.borrow()
);
assert!(observer.changed_rows.borrow().is_empty());
assert!(observer.removed_rows.borrow().is_empty());
assert_eq!(*observer.reset.borrow(), 0);
assert_eq!(model.row_data(mapped_idx), Some(10));
}
}
#[test]
fn test_reversed_model_remove() {
for (idx, mapped_idx) in [(0, 3), (1, 2), (2, 1), (3, 0)] {
println!("Removing at {} expecting mapped to {}", idx, mapped_idx);
let wrapped_rc = Rc::new(VecModel::from(vec![1, 2, 3, 4]));
let model = ReverseModel::new(wrapped_rc.clone());
let observer = Box::pin(ModelChangeListenerContainer::<TestView>::default());
model.model_tracker().attach_peer(Pin::as_ref(&observer).model_peer());
wrapped_rc.remove(idx);
assert_eq!(observer.removed_rows.borrow().len(), 1);
assert!(
observer.removed_rows.borrow().eq(&[(mapped_idx, 1)]),
"Remove rows: {:?}",
observer.removed_rows.borrow()
);
assert!(observer.added_rows.borrow().is_empty());
assert!(observer.changed_rows.borrow().is_empty());
assert_eq!(*observer.reset.borrow(), 0);
}
}
#[test]
fn test_reversed_model_changed() {
for (idx, mapped_idx) in [(0, 3), (1, 2), (2, 1), (3, 0)] {
println!("Changing at {} expecting mapped to {}", idx, mapped_idx);
let wrapped_rc = Rc::new(VecModel::from(vec![1, 2, 3, 4]));
let model = ReverseModel::new(wrapped_rc.clone());
let observer = Box::pin(ModelChangeListenerContainer::<TestView>::default());
model.model_tracker().attach_peer(Pin::as_ref(&observer).model_peer());
wrapped_rc.set_row_data(idx, 10);
assert_eq!(observer.changed_rows.borrow().len(), 1);
assert!(
observer.changed_rows.borrow().eq(&[mapped_idx]),
"Changed rows: {:?}",
observer.changed_rows.borrow()
);
assert!(observer.added_rows.borrow().is_empty());
assert!(observer.removed_rows.borrow().is_empty());
assert_eq!(*observer.reset.borrow(), 0);
assert_eq!(model.row_data(mapped_idx), Some(10));
}
}
}