mirror of
https://github.com/slint-ui/slint.git
synced 2025-11-02 04:48:27 +00:00
Add ReverseModel
This commit is contained in:
parent
70978f88d2
commit
49b3e15bc8
4 changed files with 292 additions and 39 deletions
|
|
@ -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++
|
||||
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue