mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-27 05:44:08 +00:00

Used these commands and some manual searching ``` cargo clippy --fix --all-targets --workspace --exclude gstreamer-player --exclude i-slint-backend-linuxkms --exclude uefi-demo --exclude ffmpeg -- -A clippy::all -W clippy::uninlined_format_args cargo clippy --all-targets -- -A clippy::all -W clippy::uninlined_format_args cargo clippy --fix -- -A clippy::all -W clippy::uninlined_format_args ```
1427 lines
47 KiB
Rust
1427 lines
47 KiB
Rust
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
|
|
|
//! This module contains adapter models.
|
|
|
|
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: Pin<&Self>, row: usize) {
|
|
self.changed_rows.borrow_mut().push(row);
|
|
}
|
|
|
|
fn row_added(self: Pin<&Self>, index: usize, count: usize) {
|
|
self.added_rows.borrow_mut().push((index, count));
|
|
}
|
|
|
|
fn row_removed(self: Pin<&Self>, index: usize, count: usize) {
|
|
self.removed_rows.borrow_mut().push((index, count));
|
|
}
|
|
fn reset(self: Pin<&Self>) {
|
|
*self.reset.borrow_mut() += 1;
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
struct ModelChecker<Data: PartialEq + core::fmt::Debug + 'static> {
|
|
model: Rc<dyn Model<Data = Data>>,
|
|
rows_copy: RefCell<Vec<Data>>,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
impl<Data: PartialEq + core::fmt::Debug + 'static> ModelChangeListener for ModelChecker<Data> {
|
|
fn row_changed(self: Pin<&Self>, row: usize) {
|
|
self.rows_copy.borrow_mut()[row] = self.model.row_data(row).unwrap();
|
|
}
|
|
|
|
fn row_added(self: Pin<&Self>, index: usize, count: usize) {
|
|
let mut copy = self.rows_copy.borrow_mut();
|
|
for row in index..index + count {
|
|
copy.insert(row, self.model.row_data(row).unwrap());
|
|
}
|
|
}
|
|
|
|
fn row_removed(self: Pin<&Self>, index: usize, count: usize) {
|
|
self.rows_copy.borrow_mut().drain(index..index + count);
|
|
}
|
|
fn reset(self: Pin<&Self>) {
|
|
*self.rows_copy.borrow_mut() = ModelRc::from(self.model.clone()).iter().collect()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
impl<Data: PartialEq + core::fmt::Debug + 'static> ModelChecker<Data> {
|
|
pub fn new(
|
|
model: Rc<impl Model<Data = Data> + 'static>,
|
|
) -> Pin<Box<ModelChangeListenerContainer<Self>>> {
|
|
let s = Self { rows_copy: RefCell::new(model.iter().collect()), model: model.clone() };
|
|
let s = Box::pin(ModelChangeListenerContainer::new(s));
|
|
model.model_tracker().attach_peer(s.as_ref().model_peer());
|
|
s
|
|
}
|
|
|
|
#[track_caller]
|
|
pub fn check(&self) {
|
|
assert_eq!(
|
|
*self.rows_copy.borrow(),
|
|
ModelRc::from(self.model.clone()).iter().collect::<Vec<_>>()
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
impl<Data: PartialEq + core::fmt::Debug + 'static> Drop for ModelChecker<Data> {
|
|
fn drop(&mut self) {
|
|
self.check();
|
|
}
|
|
}
|
|
|
|
/// 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.
|
|
///
|
|
/// Generic parameters:
|
|
/// * `M` the type of the wrapped `Model`.
|
|
/// * `F` the map function.
|
|
///
|
|
/// ## Example
|
|
///
|
|
/// Here we have a [`VecModel`] holding rows of a custom type `Name`.
|
|
/// It is then mapped into a `MapModel` of [`SharedString`]s
|
|
///
|
|
/// ```
|
|
/// # use slint::{Model, VecModel, SharedString, MapModel};
|
|
/// #[derive(Clone)]
|
|
/// struct Name {
|
|
/// first: String,
|
|
/// last: String,
|
|
/// }
|
|
///
|
|
/// let model = VecModel::from(vec![
|
|
/// Name { first: "Hans".to_string(), last: "Emil".to_string() },
|
|
/// Name { first: "Max".to_string(), last: "Mustermann".to_string() },
|
|
/// Name { first: "Roman".to_string(), last: "Tisch".to_string() },
|
|
/// ]);
|
|
///
|
|
/// let mapped_model = MapModel::new(model, |n|
|
|
/// slint::format!("{}, {}", n.last, n.first)
|
|
/// );
|
|
///
|
|
/// assert_eq!(mapped_model.row_data(0).unwrap(), SharedString::from("Emil, Hans"));
|
|
/// assert_eq!(mapped_model.row_data(1).unwrap(), SharedString::from("Mustermann, Max"));
|
|
/// assert_eq!(mapped_model.row_data(2).unwrap(), SharedString::from("Tisch, Roman"));
|
|
///
|
|
/// ```
|
|
///
|
|
/// Alternatively you can use the shortcut [`ModelExt::map`].
|
|
/// ```
|
|
/// # use slint::{Model, ModelExt, VecModel, SharedString, MapModel};
|
|
/// # #[derive(Clone)]
|
|
/// # struct Name {
|
|
/// # first: String,
|
|
/// # last: String,
|
|
/// # }
|
|
/// let mapped_model = VecModel::from(vec![
|
|
/// Name { first: "Hans".to_string(), last: "Emil".to_string() },
|
|
/// Name { first: "Max".to_string(), last: "Mustermann".to_string() },
|
|
/// Name { first: "Roman".to_string(), last: "Tisch".to_string() },
|
|
/// ])
|
|
/// .map(|n| slint::format!("{}, {}", n.last, n.first));
|
|
/// # assert_eq!(mapped_model.row_data(0).unwrap(), SharedString::from("Emil, Hans"));
|
|
/// # assert_eq!(mapped_model.row_data(1).unwrap(), SharedString::from("Mustermann, Max"));
|
|
/// # assert_eq!(mapped_model.row_data(2).unwrap(), SharedString::from("Tisch, Roman"));
|
|
/// ```
|
|
///
|
|
/// If you want to modify the underlying [`VecModel`] you can give it a [`Rc`] of the MapModel:
|
|
/// ```
|
|
/// # use std::rc::Rc;
|
|
/// # use slint::{Model, VecModel, SharedString, MapModel};
|
|
/// # #[derive(Clone)]
|
|
/// # struct Name {
|
|
/// # first: String,
|
|
/// # last: String,
|
|
/// # }
|
|
/// let model = Rc::new(VecModel::from(vec![
|
|
/// Name { first: "Hans".to_string(), last: "Emil".to_string() },
|
|
/// Name { first: "Max".to_string(), last: "Mustermann".to_string() },
|
|
/// Name { first: "Roman".to_string(), last: "Tisch".to_string() },
|
|
/// ]));
|
|
///
|
|
/// let mapped_model = MapModel::new(model.clone(), |n|
|
|
/// slint::format!("{}, {}", n.last, n.first)
|
|
/// );
|
|
///
|
|
/// model.set_row_data(1, Name { first: "Minnie".to_string(), last: "Musterfrau".to_string() });
|
|
///
|
|
/// assert_eq!(mapped_model.row_data(0).unwrap(), SharedString::from("Emil, Hans"));
|
|
/// assert_eq!(mapped_model.row_data(1).unwrap(), SharedString::from("Musterfrau, Minnie"));
|
|
/// assert_eq!(mapped_model.row_data(2).unwrap(), SharedString::from("Tisch, Roman"));
|
|
///
|
|
/// ```
|
|
pub struct MapModel<M, F> {
|
|
wrapped_model: M,
|
|
map_function: F,
|
|
}
|
|
|
|
impl<M, F, T, U> Model for MapModel<M, F>
|
|
where
|
|
M: 'static,
|
|
F: 'static,
|
|
F: Fn(T) -> U,
|
|
M: Model<Data = T>,
|
|
{
|
|
type Data = U;
|
|
|
|
fn row_count(&self) -> usize {
|
|
self.wrapped_model.row_count()
|
|
}
|
|
|
|
fn row_data(&self, row: usize) -> Option<Self::Data> {
|
|
self.wrapped_model.row_data(row).map(|x| (self.map_function)(x))
|
|
}
|
|
|
|
fn model_tracker(&self) -> &dyn ModelTracker {
|
|
self.wrapped_model.model_tracker()
|
|
}
|
|
|
|
fn as_any(&self) -> &dyn core::any::Any {
|
|
self
|
|
}
|
|
}
|
|
|
|
impl<M, F, T, U> MapModel<M, F>
|
|
where
|
|
M: 'static,
|
|
F: 'static,
|
|
F: Fn(T) -> U,
|
|
M: Model<Data = T>,
|
|
{
|
|
/// Creates a new MapModel based on the given `wrapped_model` and `map_function`.
|
|
/// Alternatively you can use [`ModelExt::map`] on your Model.
|
|
pub fn new(wrapped_model: M, map_function: F) -> Self {
|
|
Self { wrapped_model, map_function }
|
|
}
|
|
|
|
/// Returns a reference to the inner model
|
|
pub fn source_model(&self) -> &M {
|
|
&self.wrapped_model
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_map_model() {
|
|
use alloc::string::ToString;
|
|
let wrapped_rc = Rc::new(VecModel::from(std::vec![1, 2, 3]));
|
|
let map = MapModel::new(wrapped_rc.clone(), |x| x.to_string());
|
|
|
|
wrapped_rc.set_row_data(2, 42);
|
|
wrapped_rc.push(4);
|
|
|
|
assert_eq!(map.row_data(2).unwrap(), "42");
|
|
assert_eq!(map.row_data(3).unwrap(), "4");
|
|
assert_eq!(map.row_data(1).unwrap(), "2");
|
|
}
|
|
|
|
struct FilterModelInner<M, F>
|
|
where
|
|
M: Model + 'static,
|
|
F: Fn(&M::Data) -> bool + 'static,
|
|
{
|
|
wrapped_model: M,
|
|
filter_function: F,
|
|
// This vector saves the indices of the elements that are not filtered out
|
|
mapping: RefCell<Vec<usize>>,
|
|
notify: ModelNotify,
|
|
}
|
|
|
|
impl<M, F> FilterModelInner<M, F>
|
|
where
|
|
M: Model + 'static,
|
|
F: Fn(&M::Data) -> bool + 'static,
|
|
{
|
|
fn build_mapping_vec(&self) {
|
|
let mut mapping = self.mapping.borrow_mut();
|
|
*mapping = self
|
|
.wrapped_model
|
|
.iter()
|
|
.enumerate()
|
|
.filter_map(|(i, e)| (self.filter_function)(&e).then_some(i))
|
|
.collect();
|
|
}
|
|
}
|
|
|
|
impl<M, F> ModelChangeListener for FilterModelInner<M, F>
|
|
where
|
|
M: Model + 'static,
|
|
F: Fn(&M::Data) -> bool + 'static,
|
|
{
|
|
fn row_changed(self: Pin<&Self>, row: usize) {
|
|
let mut mapping = self.mapping.borrow_mut();
|
|
|
|
let (index, is_contained) = match mapping.binary_search(&row) {
|
|
Ok(index) => (index, true),
|
|
Err(index) => (index, false),
|
|
};
|
|
|
|
let should_be_contained =
|
|
(self.filter_function)(&self.wrapped_model.row_data(row).unwrap());
|
|
|
|
if is_contained && should_be_contained {
|
|
drop(mapping);
|
|
self.notify.row_changed(index);
|
|
} else if !is_contained && should_be_contained {
|
|
mapping.insert(index, row);
|
|
drop(mapping);
|
|
self.notify.row_added(index, 1);
|
|
} else if is_contained && !should_be_contained {
|
|
mapping.remove(index);
|
|
drop(mapping);
|
|
self.notify.row_removed(index, 1);
|
|
}
|
|
}
|
|
|
|
fn row_added(self: Pin<&Self>, index: usize, count: usize) {
|
|
if count == 0 {
|
|
return;
|
|
}
|
|
|
|
let insertion: Vec<usize> = self
|
|
.wrapped_model
|
|
.iter()
|
|
.enumerate()
|
|
.skip(index)
|
|
.take(count)
|
|
.filter_map(|(i, e)| (self.filter_function)(&e).then_some(i))
|
|
.collect();
|
|
|
|
let mut mapping = self.mapping.borrow_mut();
|
|
let insertion_point = mapping.binary_search(&index).unwrap_or_else(|ip| ip);
|
|
mapping[insertion_point..].iter_mut().for_each(|i| *i += count);
|
|
|
|
if !insertion.is_empty() {
|
|
let insertion_len = insertion.len();
|
|
mapping.splice(insertion_point..insertion_point, insertion);
|
|
|
|
drop(mapping);
|
|
self.notify.row_added(insertion_point, insertion_len);
|
|
}
|
|
}
|
|
|
|
fn row_removed(self: Pin<&Self>, index: usize, count: usize) {
|
|
if count == 0 {
|
|
return;
|
|
}
|
|
let mut mapping = self.mapping.borrow_mut();
|
|
|
|
let start = mapping.binary_search(&index).unwrap_or_else(|s| s);
|
|
let end = mapping.binary_search(&(index + count)).unwrap_or_else(|e| e);
|
|
let range = start..end;
|
|
|
|
mapping[end..].iter_mut().for_each(|i| *i -= count);
|
|
|
|
if !range.is_empty() {
|
|
mapping.drain(range.clone());
|
|
drop(mapping);
|
|
self.notify.row_removed(start, range.len());
|
|
}
|
|
}
|
|
|
|
fn reset(self: Pin<&Self>) {
|
|
self.build_mapping_vec();
|
|
self.notify.reset();
|
|
}
|
|
}
|
|
|
|
/// Provides a filtered subset of rows by another [`Model`].
|
|
///
|
|
/// When the other Model is updated, the `FilterModel` is updated accordingly.
|
|
///
|
|
/// Generic parameters:
|
|
/// * `M` the type of the wrapped `Model`.
|
|
/// * `F` the filter function.
|
|
///
|
|
/// ## Example
|
|
///
|
|
/// Here we have a [`VecModel`] holding [`crate::SharedString`]s.
|
|
/// It is then filtered into a `FilterModel`.
|
|
///
|
|
/// ```
|
|
/// # use slint::{Model, VecModel, SharedString, FilterModel};
|
|
/// let model = VecModel::from(vec![
|
|
/// SharedString::from("Lorem"),
|
|
/// SharedString::from("ipsum"),
|
|
/// SharedString::from("dolor"),
|
|
/// ]);
|
|
///
|
|
/// let filtered_model = FilterModel::new(model, |s| s.contains('o'));
|
|
///
|
|
/// assert_eq!(filtered_model.row_data(0).unwrap(), SharedString::from("Lorem"));
|
|
/// assert_eq!(filtered_model.row_data(1).unwrap(), SharedString::from("dolor"));
|
|
/// ```
|
|
///
|
|
/// Alternatively you can use the shortcut [`ModelExt::filter`].
|
|
/// ```
|
|
/// # use slint::{Model, ModelExt, VecModel, SharedString, FilterModel};
|
|
/// let filtered_model = VecModel::from(vec![
|
|
/// SharedString::from("Lorem"),
|
|
/// SharedString::from("ipsum"),
|
|
/// SharedString::from("dolor"),
|
|
/// ]).filter(|s| s.contains('o'));
|
|
/// # assert_eq!(filtered_model.row_data(0).unwrap(), SharedString::from("Lorem"));
|
|
/// # assert_eq!(filtered_model.row_data(1).unwrap(), SharedString::from("dolor"));
|
|
/// ```
|
|
///
|
|
/// If you want to modify the underlying [`VecModel`] you can give it a [`Rc`] of the FilterModel:
|
|
/// ```
|
|
/// # use std::rc::Rc;
|
|
/// # use slint::{Model, VecModel, SharedString, FilterModel};
|
|
/// let model = Rc::new(VecModel::from(vec![
|
|
/// SharedString::from("Lorem"),
|
|
/// SharedString::from("ipsum"),
|
|
/// SharedString::from("dolor"),
|
|
/// ]));
|
|
///
|
|
/// let filtered_model = FilterModel::new(model.clone(), |s| s.contains('o'));
|
|
///
|
|
/// assert_eq!(filtered_model.row_data(0).unwrap(), SharedString::from("Lorem"));
|
|
/// assert_eq!(filtered_model.row_data(1).unwrap(), SharedString::from("dolor"));
|
|
///
|
|
/// model.set_row_data(1, SharedString::from("opsom"));
|
|
///
|
|
/// assert_eq!(filtered_model.row_data(0).unwrap(), SharedString::from("Lorem"));
|
|
/// assert_eq!(filtered_model.row_data(1).unwrap(), SharedString::from("opsom"));
|
|
/// assert_eq!(filtered_model.row_data(2).unwrap(), SharedString::from("dolor"));
|
|
/// ```
|
|
pub struct FilterModel<M, F>(Pin<Box<ModelChangeListenerContainer<FilterModelInner<M, F>>>>)
|
|
where
|
|
M: Model + 'static,
|
|
F: Fn(&M::Data) -> bool + 'static;
|
|
|
|
impl<M, F> FilterModel<M, F>
|
|
where
|
|
M: Model + 'static,
|
|
F: Fn(&M::Data) -> bool + 'static,
|
|
{
|
|
/// Creates a new FilterModel based on the given `wrapped_model` and filtered by `filter_function`.
|
|
/// Alternatively you can use [`ModelExt::filter`] on your Model.
|
|
pub fn new(wrapped_model: M, filter_function: F) -> Self {
|
|
let filter_model_inner = FilterModelInner {
|
|
wrapped_model,
|
|
filter_function,
|
|
mapping: RefCell::new(Vec::new()),
|
|
notify: Default::default(),
|
|
};
|
|
|
|
filter_model_inner.build_mapping_vec();
|
|
|
|
let container = Box::pin(ModelChangeListenerContainer::new(filter_model_inner));
|
|
|
|
container.wrapped_model.model_tracker().attach_peer(container.as_ref().model_peer());
|
|
|
|
Self(container)
|
|
}
|
|
|
|
/// Manually reapply the filter. You need to run this e.g. if the filtering function depends on
|
|
/// mutable state and it has changed.
|
|
pub fn reset(&self) {
|
|
self.0.as_ref().get().reset();
|
|
}
|
|
|
|
/// Gets the row index of the underlying unfiltered model for a given filtered row index.
|
|
pub fn unfiltered_row(&self, filtered_row: usize) -> usize {
|
|
self.0.mapping.borrow()[filtered_row]
|
|
}
|
|
|
|
/// Returns a reference to the inner model
|
|
pub fn source_model(&self) -> &M {
|
|
&self.0.as_ref().get().get_ref().wrapped_model
|
|
}
|
|
}
|
|
|
|
impl<M, F> Model for FilterModel<M, F>
|
|
where
|
|
M: Model + 'static,
|
|
F: Fn(&M::Data) -> bool + 'static,
|
|
{
|
|
type Data = M::Data;
|
|
|
|
fn row_count(&self) -> usize {
|
|
self.0.mapping.borrow().len()
|
|
}
|
|
|
|
fn row_data(&self, row: usize) -> Option<Self::Data> {
|
|
self.0
|
|
.mapping
|
|
.borrow()
|
|
.get(row)
|
|
.and_then(|&wrapped_row| self.0.wrapped_model.row_data(wrapped_row))
|
|
}
|
|
|
|
fn set_row_data(&self, row: usize, data: Self::Data) {
|
|
let wrapped_row = self.0.mapping.borrow()[row];
|
|
self.0.wrapped_model.set_row_data(wrapped_row, data);
|
|
}
|
|
|
|
fn model_tracker(&self) -> &dyn ModelTracker {
|
|
&self.0.notify
|
|
}
|
|
|
|
fn as_any(&self) -> &dyn core::any::Any {
|
|
self
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_filter_model() {
|
|
let wrapped_rc = Rc::new(VecModel::from(std::vec![1, 2, 3, 4, 5, 6]));
|
|
let filter = Rc::new(FilterModel::new(wrapped_rc.clone(), |x| x % 2 == 0));
|
|
|
|
let _checker = ModelChecker::new(filter.clone());
|
|
|
|
assert_eq!(filter.row_data(0).unwrap(), 2);
|
|
assert_eq!(filter.row_data(1).unwrap(), 4);
|
|
assert_eq!(filter.row_data(2).unwrap(), 6);
|
|
assert_eq!(filter.row_count(), 3);
|
|
|
|
wrapped_rc.remove(1);
|
|
assert_eq!(filter.row_data(0).unwrap(), 4);
|
|
assert_eq!(filter.row_data(1).unwrap(), 6);
|
|
assert_eq!(filter.row_count(), 2);
|
|
|
|
wrapped_rc.push(8);
|
|
wrapped_rc.push(7);
|
|
assert_eq!(filter.row_data(0).unwrap(), 4);
|
|
assert_eq!(filter.row_data(1).unwrap(), 6);
|
|
assert_eq!(filter.row_data(2).unwrap(), 8);
|
|
assert_eq!(filter.row_count(), 3);
|
|
|
|
wrapped_rc.set_row_data(1, 2);
|
|
assert_eq!(filter.row_data(0).unwrap(), 2);
|
|
assert_eq!(filter.row_data(1).unwrap(), 4);
|
|
assert_eq!(filter.row_data(2).unwrap(), 6);
|
|
assert_eq!(filter.row_data(3).unwrap(), 8);
|
|
assert_eq!(filter.row_count(), 4);
|
|
|
|
wrapped_rc.insert(2, 12);
|
|
assert_eq!(filter.row_data(0).unwrap(), 2);
|
|
assert_eq!(filter.row_data(1).unwrap(), 12);
|
|
assert_eq!(filter.row_data(2).unwrap(), 4);
|
|
assert_eq!(filter.row_data(3).unwrap(), 6);
|
|
assert_eq!(filter.row_data(4).unwrap(), 8);
|
|
assert_eq!(filter.row_count(), 5);
|
|
}
|
|
|
|
#[test]
|
|
fn test_filter_model_source_model() {
|
|
let wrapped_rc = Rc::new(VecModel::from(std::vec![1, 2, 3, 4]));
|
|
let model = Rc::new(FilterModel::new(wrapped_rc.clone(), |x| x % 2 == 0));
|
|
|
|
let observer = Box::pin(ModelChangeListenerContainer::<TestView>::default());
|
|
model.model_tracker().attach_peer(Pin::as_ref(&observer).model_peer());
|
|
|
|
let _checker = ModelChecker::new(model.clone());
|
|
|
|
model.source_model().push(5);
|
|
model.source_model().push(6);
|
|
|
|
let expected = &[2, 4, 6];
|
|
assert_eq!(model.row_count(), expected.len());
|
|
for (i, v) in expected.iter().enumerate() {
|
|
assert_eq!(model.row_data(i), Some(*v), "Expected {v} at index {i}");
|
|
}
|
|
}
|
|
|
|
pub trait SortHelper<D> {
|
|
fn cmp(&mut self, lhs: &D, rhs: &D) -> core::cmp::Ordering;
|
|
}
|
|
|
|
pub struct AscendingSortHelper;
|
|
|
|
impl<D> SortHelper<D> for AscendingSortHelper
|
|
where
|
|
D: core::cmp::Ord,
|
|
{
|
|
fn cmp(&mut self, lhs: &D, rhs: &D) -> core::cmp::Ordering {
|
|
lhs.cmp(rhs)
|
|
}
|
|
}
|
|
|
|
impl<F, D> SortHelper<D> for F
|
|
where
|
|
F: FnMut(&D, &D) -> core::cmp::Ordering + 'static,
|
|
{
|
|
fn cmp(&mut self, lhs: &D, rhs: &D) -> core::cmp::Ordering {
|
|
(self)(lhs, rhs)
|
|
}
|
|
}
|
|
|
|
struct SortModelInner<M, S>
|
|
where
|
|
M: Model + 'static,
|
|
S: SortHelper<M::Data> + 'static,
|
|
{
|
|
wrapped_model: M,
|
|
sort_helper: RefCell<S>,
|
|
// This vector saves the indices of the elements in sorted order.
|
|
mapping: RefCell<Vec<usize>>,
|
|
notify: ModelNotify,
|
|
sorted_rows_dirty: Cell<bool>,
|
|
}
|
|
|
|
impl<M, S> SortModelInner<M, S>
|
|
where
|
|
M: Model + 'static,
|
|
S: SortHelper<M::Data>,
|
|
{
|
|
fn build_mapping_vec(&self) {
|
|
if !self.sorted_rows_dirty.get() {
|
|
return;
|
|
}
|
|
|
|
let mut mapping = self.mapping.borrow_mut();
|
|
|
|
mapping.clear();
|
|
mapping.extend(0..self.wrapped_model.row_count());
|
|
mapping.sort_by(|lhs, rhs| {
|
|
self.sort_helper.borrow_mut().cmp(
|
|
&self.wrapped_model.row_data(*lhs).unwrap(),
|
|
&self.wrapped_model.row_data(*rhs).unwrap(),
|
|
)
|
|
});
|
|
|
|
self.sorted_rows_dirty.set(false);
|
|
}
|
|
}
|
|
|
|
impl<M, S> ModelChangeListener for SortModelInner<M, S>
|
|
where
|
|
M: Model + 'static,
|
|
S: SortHelper<M::Data> + 'static,
|
|
{
|
|
fn row_changed(self: Pin<&Self>, row: usize) {
|
|
if self.sorted_rows_dirty.get() {
|
|
self.reset();
|
|
return;
|
|
}
|
|
|
|
let mut mapping = self.mapping.borrow_mut();
|
|
let removed_index = mapping.iter().position(|r| *r == row).unwrap();
|
|
mapping.remove(removed_index);
|
|
|
|
let changed_data = self.wrapped_model.row_data(row).unwrap();
|
|
let insertion_index = mapping.partition_point(|existing_row| {
|
|
self.sort_helper
|
|
.borrow_mut()
|
|
.cmp(&self.wrapped_model.row_data(*existing_row).unwrap(), &changed_data)
|
|
== core::cmp::Ordering::Less
|
|
});
|
|
|
|
mapping.insert(insertion_index, row);
|
|
|
|
drop(mapping);
|
|
|
|
if insertion_index == removed_index {
|
|
self.notify.row_changed(removed_index);
|
|
} else {
|
|
self.notify.row_removed(removed_index, 1);
|
|
self.notify.row_added(insertion_index, 1);
|
|
}
|
|
}
|
|
|
|
fn row_added(self: Pin<&Self>, index: usize, count: usize) {
|
|
if count == 0 {
|
|
return;
|
|
}
|
|
|
|
if self.sorted_rows_dirty.get() {
|
|
self.reset();
|
|
return;
|
|
}
|
|
|
|
// Adjust the existing sorted row indices to match the updated source model
|
|
for row in self.mapping.borrow_mut().iter_mut() {
|
|
if *row >= index {
|
|
*row += count;
|
|
}
|
|
}
|
|
|
|
for row in index..(index + count) {
|
|
let added_data = self.wrapped_model.row_data(row).unwrap();
|
|
let insertion_index = self.mapping.borrow().partition_point(|existing_row| {
|
|
self.sort_helper
|
|
.borrow_mut()
|
|
.cmp(&self.wrapped_model.row_data(*existing_row).unwrap(), &added_data)
|
|
== core::cmp::Ordering::Less
|
|
});
|
|
|
|
self.mapping.borrow_mut().insert(insertion_index, row);
|
|
self.notify.row_added(insertion_index, 1)
|
|
}
|
|
}
|
|
|
|
fn row_removed(self: Pin<&Self>, index: usize, count: usize) {
|
|
if count == 0 {
|
|
return;
|
|
}
|
|
|
|
if self.sorted_rows_dirty.get() {
|
|
self.reset();
|
|
return;
|
|
}
|
|
|
|
let mut removed_rows = Vec::new();
|
|
|
|
let mut i = 0;
|
|
|
|
loop {
|
|
if i >= self.mapping.borrow().len() {
|
|
break;
|
|
}
|
|
|
|
let sort_index = self.mapping.borrow()[i];
|
|
|
|
if sort_index >= index {
|
|
if sort_index < index + count {
|
|
removed_rows.push(i);
|
|
self.mapping.borrow_mut().remove(i);
|
|
continue;
|
|
} else {
|
|
self.mapping.borrow_mut()[i] -= count;
|
|
}
|
|
}
|
|
|
|
i += 1;
|
|
}
|
|
|
|
for removed_row in removed_rows {
|
|
self.notify.row_removed(removed_row, 1);
|
|
}
|
|
}
|
|
|
|
fn reset(self: Pin<&Self>) {
|
|
self.sorted_rows_dirty.set(true);
|
|
self.notify.reset();
|
|
}
|
|
}
|
|
|
|
/// Provides a sorted view of rows by another [`Model`].
|
|
///
|
|
/// When the other Model is updated, the `Sorted` is updated accordingly.
|
|
///
|
|
/// Generic parameters:
|
|
/// * `M` the type of the wrapped `Model`.
|
|
/// * `F` a type that provides an order to model rows. It is constrained by the internal trait `SortHelper`, which is used to sort the model in ascending order if the model data supports it, or by a given sort function.
|
|
///
|
|
/// ## Example
|
|
///
|
|
/// Here we have a [`VecModel`] holding [`crate::SharedString`]s.
|
|
/// It is then sorted into a `SortModel`.
|
|
///
|
|
/// ```
|
|
/// # use slint::{Model, VecModel, SharedString, SortModel};
|
|
/// let model = VecModel::from(vec![
|
|
/// SharedString::from("Lorem"),
|
|
/// SharedString::from("ipsum"),
|
|
/// SharedString::from("dolor"),
|
|
/// ]);
|
|
///
|
|
/// let sorted_model = SortModel::new(model, |lhs, rhs| lhs.to_lowercase().cmp(&rhs.to_lowercase()));
|
|
///
|
|
/// assert_eq!(sorted_model.row_data(0).unwrap(), SharedString::from("dolor"));
|
|
/// assert_eq!(sorted_model.row_data(1).unwrap(), SharedString::from("ipsum"));
|
|
/// assert_eq!(sorted_model.row_data(2).unwrap(), SharedString::from("Lorem"));
|
|
/// ```
|
|
///
|
|
/// Alternatively you can use the shortcut [`ModelExt::sort_by`].
|
|
/// ```
|
|
/// # use slint::{Model, ModelExt, VecModel, SharedString, SortModel};
|
|
/// let sorted_model = VecModel::from(vec![
|
|
/// SharedString::from("Lorem"),
|
|
/// SharedString::from("ipsum"),
|
|
/// SharedString::from("dolor"),
|
|
/// ]).sort_by(|lhs, rhs| lhs.to_lowercase().cmp(&rhs.to_lowercase()));
|
|
/// # assert_eq!(sorted_model.row_data(0).unwrap(), SharedString::from("dolor"));
|
|
/// # assert_eq!(sorted_model.row_data(1).unwrap(), SharedString::from("ipsum"));
|
|
/// # assert_eq!(sorted_model.row_data(2).unwrap(), SharedString::from("Lorem"));
|
|
/// ```
|
|
///
|
|
/// It is also possible to get a ascending sorted `SortModel` order for `core::cmp::Ord` type items.
|
|
///
|
|
/// ```
|
|
/// # use slint::{Model, VecModel, SortModel};
|
|
/// let model = VecModel::from(vec![
|
|
/// 5,
|
|
/// 1,
|
|
/// 3,
|
|
/// ]);
|
|
///
|
|
/// let sorted_model = SortModel::new_ascending(model);
|
|
///
|
|
/// assert_eq!(sorted_model.row_data(0).unwrap(), 1);
|
|
/// assert_eq!(sorted_model.row_data(1).unwrap(), 3);
|
|
/// assert_eq!(sorted_model.row_data(2).unwrap(), 5);
|
|
/// ```
|
|
///
|
|
/// Alternatively you can use the shortcut [`ModelExt::sort`].
|
|
/// ```
|
|
/// # use slint::{Model, ModelExt, VecModel, SharedString, SortModel};
|
|
/// let sorted_model = VecModel::from(vec![
|
|
/// 5,
|
|
/// 1,
|
|
/// 3,
|
|
/// ]).sort();
|
|
/// # assert_eq!(sorted_model.row_data(0).unwrap(), 1);
|
|
/// # assert_eq!(sorted_model.row_data(1).unwrap(), 3);
|
|
/// # assert_eq!(sorted_model.row_data(2).unwrap(), 5);
|
|
/// ```
|
|
///
|
|
/// If you want to modify the underlying [`VecModel`] you can give it a [`Rc`] of the SortModel:
|
|
/// ```
|
|
/// # use std::rc::Rc;
|
|
/// # use slint::{Model, VecModel, SharedString, SortModel};
|
|
/// let model = Rc::new(VecModel::from(vec![
|
|
/// SharedString::from("Lorem"),
|
|
/// SharedString::from("ipsum"),
|
|
/// SharedString::from("dolor"),
|
|
/// ]));
|
|
///
|
|
/// let sorted_model = SortModel::new(model.clone(), |lhs, rhs| lhs.to_lowercase().cmp(&rhs.to_lowercase()));
|
|
///
|
|
/// assert_eq!(sorted_model.row_data(0).unwrap(), SharedString::from("dolor"));
|
|
/// assert_eq!(sorted_model.row_data(1).unwrap(), SharedString::from("ipsum"));
|
|
/// assert_eq!(sorted_model.row_data(2).unwrap(), SharedString::from("Lorem"));
|
|
///
|
|
/// model.set_row_data(1, SharedString::from("opsom"));
|
|
///
|
|
/// assert_eq!(sorted_model.row_data(0).unwrap(), SharedString::from("dolor"));
|
|
/// assert_eq!(sorted_model.row_data(1).unwrap(), SharedString::from("Lorem"));
|
|
/// assert_eq!(sorted_model.row_data(2).unwrap(), SharedString::from("opsom"));
|
|
/// ```
|
|
pub struct SortModel<M, F>(Pin<Box<ModelChangeListenerContainer<SortModelInner<M, F>>>>)
|
|
where
|
|
M: Model + 'static,
|
|
F: SortHelper<M::Data> + 'static;
|
|
|
|
impl<M, F> SortModel<M, F>
|
|
where
|
|
M: Model + 'static,
|
|
F: FnMut(&M::Data, &M::Data) -> core::cmp::Ordering + 'static,
|
|
{
|
|
/// Creates a new SortModel based on the given `wrapped_model` and sorted by `sort_function`.
|
|
/// Alternatively you can use [`ModelExt::sort_by`] on your Model.
|
|
pub fn new(wrapped_model: M, sort_function: F) -> Self
|
|
where
|
|
F: FnMut(&M::Data, &M::Data) -> core::cmp::Ordering + 'static,
|
|
{
|
|
let sorted_model_inner = SortModelInner {
|
|
wrapped_model,
|
|
sort_helper: RefCell::new(sort_function),
|
|
mapping: RefCell::new(Vec::new()),
|
|
notify: Default::default(),
|
|
sorted_rows_dirty: Cell::new(true),
|
|
};
|
|
|
|
let container = Box::pin(ModelChangeListenerContainer::new(sorted_model_inner));
|
|
|
|
container.wrapped_model.model_tracker().attach_peer(container.as_ref().model_peer());
|
|
|
|
Self(container)
|
|
}
|
|
|
|
/// Returns a reference to the inner model
|
|
pub fn source_model(&self) -> &M {
|
|
&self.0.as_ref().get().get_ref().wrapped_model
|
|
}
|
|
}
|
|
|
|
impl<M> SortModel<M, AscendingSortHelper>
|
|
where
|
|
M: Model + 'static,
|
|
M::Data: core::cmp::Ord,
|
|
{
|
|
/// Creates a new SortModel based on the given `wrapped_model` and sorted in ascending order.
|
|
/// Alternatively you can use [`ModelExt::sort`] on your Model.
|
|
pub fn new_ascending(wrapped_model: M) -> Self
|
|
where
|
|
M::Data: core::cmp::Ord,
|
|
{
|
|
let sorted_model_inner = SortModelInner {
|
|
wrapped_model,
|
|
sort_helper: RefCell::new(AscendingSortHelper),
|
|
mapping: RefCell::new(Vec::new()),
|
|
notify: Default::default(),
|
|
sorted_rows_dirty: Cell::new(true),
|
|
};
|
|
|
|
let container = Box::pin(ModelChangeListenerContainer::new(sorted_model_inner));
|
|
|
|
container.wrapped_model.model_tracker().attach_peer(container.as_ref().model_peer());
|
|
|
|
Self(container)
|
|
}
|
|
|
|
/// Manually reapply the sorting. You need to run this e.g. if the sort function depends
|
|
/// on mutable state and it has changed.
|
|
pub fn reset(&self) {
|
|
self.0.as_ref().get().reset();
|
|
}
|
|
|
|
/// Gets the row index of the underlying unsorted model for a given sorted row index.
|
|
pub fn unsorted_row(&self, sorted_row: usize) -> usize {
|
|
self.0.build_mapping_vec();
|
|
self.0.mapping.borrow()[sorted_row]
|
|
}
|
|
}
|
|
|
|
impl<M, S> Model for SortModel<M, S>
|
|
where
|
|
M: Model + 'static,
|
|
S: SortHelper<M::Data>,
|
|
{
|
|
type Data = M::Data;
|
|
|
|
fn row_count(&self) -> usize {
|
|
self.0.wrapped_model.row_count()
|
|
}
|
|
|
|
fn row_data(&self, row: usize) -> Option<Self::Data> {
|
|
self.0.build_mapping_vec();
|
|
|
|
self.0
|
|
.mapping
|
|
.borrow()
|
|
.get(row)
|
|
.and_then(|&wrapped_row| self.0.wrapped_model.row_data(wrapped_row))
|
|
}
|
|
|
|
fn set_row_data(&self, row: usize, data: Self::Data) {
|
|
let wrapped_row = self.0.mapping.borrow()[row];
|
|
self.0.wrapped_model.set_row_data(wrapped_row, data);
|
|
}
|
|
|
|
fn model_tracker(&self) -> &dyn ModelTracker {
|
|
&self.0.notify
|
|
}
|
|
|
|
fn as_any(&self) -> &dyn core::any::Any {
|
|
self
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod sort_tests {
|
|
use super::*;
|
|
use std::vec;
|
|
|
|
#[test]
|
|
fn test_sorted_model_insert() {
|
|
let wrapped_rc = Rc::new(VecModel::from(std::vec![3, 4, 1, 2]));
|
|
let sorted_model = Rc::new(SortModel::new(wrapped_rc.clone(), |lhs, rhs| lhs.cmp(rhs)));
|
|
|
|
let _checker = ModelChecker::new(sorted_model.clone());
|
|
|
|
let observer = Box::pin(ModelChangeListenerContainer::<TestView>::default());
|
|
sorted_model.model_tracker().attach_peer(Pin::as_ref(&observer).model_peer());
|
|
|
|
assert_eq!(sorted_model.row_count(), 4);
|
|
assert_eq!(sorted_model.row_data(0).unwrap(), 1);
|
|
assert_eq!(sorted_model.row_data(1).unwrap(), 2);
|
|
assert_eq!(sorted_model.row_data(2).unwrap(), 3);
|
|
assert_eq!(sorted_model.row_data(3).unwrap(), 4);
|
|
|
|
wrapped_rc.insert(0, 10);
|
|
|
|
assert_eq!(observer.added_rows.borrow().len(), 1);
|
|
assert!(observer.added_rows.borrow().eq(&[(4, 1)]));
|
|
assert!(observer.changed_rows.borrow().is_empty());
|
|
assert!(observer.removed_rows.borrow().is_empty());
|
|
assert_eq!(*observer.reset.borrow(), 0);
|
|
observer.clear();
|
|
|
|
assert_eq!(sorted_model.row_count(), 5);
|
|
assert_eq!(sorted_model.row_data(0).unwrap(), 1);
|
|
assert_eq!(sorted_model.row_data(1).unwrap(), 2);
|
|
assert_eq!(sorted_model.row_data(2).unwrap(), 3);
|
|
assert_eq!(sorted_model.row_data(3).unwrap(), 4);
|
|
assert_eq!(sorted_model.row_data(4).unwrap(), 10);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sorted_model_remove() {
|
|
let wrapped_rc = Rc::new(VecModel::from(vec![3, 4, 1, 2]));
|
|
let sorted_model = Rc::new(SortModel::new(wrapped_rc.clone(), |lhs, rhs| lhs.cmp(rhs)));
|
|
|
|
let _checker = ModelChecker::new(sorted_model.clone());
|
|
|
|
let observer = Box::pin(ModelChangeListenerContainer::<TestView>::default());
|
|
sorted_model.model_tracker().attach_peer(Pin::as_ref(&observer).model_peer());
|
|
|
|
assert_eq!(sorted_model.row_count(), 4);
|
|
assert_eq!(sorted_model.row_data(0).unwrap(), 1);
|
|
assert_eq!(sorted_model.row_data(1).unwrap(), 2);
|
|
assert_eq!(sorted_model.row_data(2).unwrap(), 3);
|
|
assert_eq!(sorted_model.row_data(3).unwrap(), 4);
|
|
|
|
// Remove the entry with the value 4
|
|
wrapped_rc.remove(1);
|
|
|
|
assert!(observer.added_rows.borrow().is_empty());
|
|
assert!(observer.changed_rows.borrow().is_empty());
|
|
assert_eq!(observer.removed_rows.borrow().len(), 1);
|
|
assert!(observer.removed_rows.borrow().eq(&[(3, 1)]));
|
|
assert_eq!(*observer.reset.borrow(), 0);
|
|
observer.clear();
|
|
|
|
assert_eq!(sorted_model.row_count(), 3);
|
|
assert_eq!(sorted_model.row_data(0).unwrap(), 1);
|
|
assert_eq!(sorted_model.row_data(1).unwrap(), 2);
|
|
assert_eq!(sorted_model.row_data(2).unwrap(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sorted_model_changed() {
|
|
let wrapped_rc = Rc::new(VecModel::from(vec![3, 4, 1, 2]));
|
|
let sorted_model = Rc::new(SortModel::new(wrapped_rc.clone(), |lhs, rhs| lhs.cmp(rhs)));
|
|
|
|
let _checker = ModelChecker::new(sorted_model.clone());
|
|
|
|
let observer = Box::pin(ModelChangeListenerContainer::<TestView>::default());
|
|
sorted_model.model_tracker().attach_peer(Pin::as_ref(&observer).model_peer());
|
|
|
|
assert_eq!(sorted_model.row_count(), 4);
|
|
assert_eq!(sorted_model.row_data(0).unwrap(), 1);
|
|
assert_eq!(sorted_model.row_data(1).unwrap(), 2);
|
|
assert_eq!(sorted_model.row_data(2).unwrap(), 3);
|
|
assert_eq!(sorted_model.row_data(3).unwrap(), 4);
|
|
|
|
// Change the entry with the value 4 to 10 -> maintain order
|
|
wrapped_rc.set_row_data(1, 10);
|
|
|
|
assert!(observer.added_rows.borrow().is_empty());
|
|
assert_eq!(observer.changed_rows.borrow().len(), 1);
|
|
assert_eq!(*observer.changed_rows.borrow().first().unwrap(), 3);
|
|
assert!(observer.removed_rows.borrow().is_empty());
|
|
assert_eq!(*observer.reset.borrow(), 0);
|
|
observer.clear();
|
|
|
|
assert_eq!(sorted_model.row_count(), 4);
|
|
assert_eq!(sorted_model.row_data(0).unwrap(), 1);
|
|
assert_eq!(sorted_model.row_data(1).unwrap(), 2);
|
|
assert_eq!(sorted_model.row_data(2).unwrap(), 3);
|
|
assert_eq!(sorted_model.row_data(3).unwrap(), 10);
|
|
|
|
// Change the entry with the value 10 to 0 -> new order with remove and insert
|
|
wrapped_rc.set_row_data(1, 0);
|
|
|
|
assert_eq!(observer.added_rows.borrow().len(), 1);
|
|
assert!(observer.added_rows.borrow().first().unwrap().eq(&(0, 1)));
|
|
assert!(observer.changed_rows.borrow().is_empty());
|
|
assert_eq!(observer.removed_rows.borrow().len(), 1);
|
|
assert!(observer.removed_rows.borrow().first().unwrap().eq(&(3, 1)));
|
|
assert_eq!(*observer.reset.borrow(), 0);
|
|
observer.clear();
|
|
|
|
assert_eq!(sorted_model.row_count(), 4);
|
|
assert_eq!(sorted_model.row_data(0).unwrap(), 0);
|
|
assert_eq!(sorted_model.row_data(1).unwrap(), 1);
|
|
assert_eq!(sorted_model.row_data(2).unwrap(), 2);
|
|
assert_eq!(sorted_model.row_data(3).unwrap(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sorted_model_source_model() {
|
|
let wrapped_rc = Rc::new(VecModel::from(vec![3, 4, 1, 2]));
|
|
let model = Rc::new(SortModel::new(wrapped_rc.clone(), |lhs, rhs| lhs.cmp(rhs)));
|
|
let _checker = ModelChecker::new(model.clone());
|
|
|
|
let observer = Box::pin(ModelChangeListenerContainer::<TestView>::default());
|
|
model.model_tracker().attach_peer(Pin::as_ref(&observer).model_peer());
|
|
|
|
model.source_model().push(6);
|
|
model.source_model().push(5);
|
|
|
|
let expected = &[1, 2, 3, 4, 5, 6];
|
|
assert_eq!(model.row_count(), expected.len());
|
|
for (i, v) in expected.iter().enumerate() {
|
|
assert_eq!(model.row_data(i), Some(*v), "Expected {v} at index {i}");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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: Pin<&Self>, row: usize) {
|
|
self.notify.row_changed(self.wrapped_model.row_count() - 1 - row);
|
|
}
|
|
|
|
fn row_added(self: Pin<&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: Pin<&Self>, index: usize, count: usize) {
|
|
let row_count = self.wrapped_model.row_count();
|
|
self.notify.row_removed(row_count - index, count);
|
|
}
|
|
|
|
fn reset(self: Pin<&Self>) {
|
|
self.notify.reset()
|
|
}
|
|
}
|
|
|
|
impl<M> ReverseModel<M>
|
|
where
|
|
M: Model + 'static,
|
|
{
|
|
/// Creates a new ReverseModel based on the given `wrapped_model`.
|
|
/// Alternatively you can use [`ModelExt::reverse`] on your Model.
|
|
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)
|
|
}
|
|
|
|
/// Returns a reference to the inner model
|
|
pub fn source_model(&self) -> &M {
|
|
&self.0.as_ref().get().get_ref().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.checked_sub(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
|
|
}
|
|
|
|
fn as_any(&self) -> &dyn core::any::Any {
|
|
self
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod reversed_tests {
|
|
use super::*;
|
|
use std::vec;
|
|
|
|
#[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 {v} at index {i}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_reversed_model() {
|
|
let wrapped_rc = Rc::new(VecModel::from(vec![1, 2, 3, 4]));
|
|
let model = Rc::new(ReverseModel::new(wrapped_rc.clone()));
|
|
let _checker = ModelChecker::new(model.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)] {
|
|
std::println!("Inserting at {idx} expecting mapped to {mapped_idx}");
|
|
let wrapped_rc = Rc::new(VecModel::from(vec![1, 2, 3, 4]));
|
|
let model = Rc::new(ReverseModel::new(wrapped_rc.clone()));
|
|
let _checker = ModelChecker::new(model.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)] {
|
|
std::println!("Removing at {idx} expecting mapped to {mapped_idx}");
|
|
let wrapped_rc = Rc::new(VecModel::from(vec![1, 2, 3, 4]));
|
|
let model = Rc::new(ReverseModel::new(wrapped_rc.clone()));
|
|
let _checker = ModelChecker::new(model.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)] {
|
|
std::println!("Changing at {idx} expecting mapped to {mapped_idx}");
|
|
let wrapped_rc = Rc::new(VecModel::from(std::vec![1, 2, 3, 4]));
|
|
let model = Rc::new(ReverseModel::new(wrapped_rc.clone()));
|
|
let _checker = ModelChecker::new(model.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));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_reversed_model_source_model() {
|
|
let wrapped_rc = Rc::new(VecModel::from(std::vec![1, 2, 3, 4]));
|
|
let model = Rc::new(ReverseModel::new(wrapped_rc.clone()));
|
|
let _checker = ModelChecker::new(model.clone());
|
|
|
|
let observer = Box::pin(ModelChangeListenerContainer::<TestView>::default());
|
|
model.model_tracker().attach_peer(Pin::as_ref(&observer).model_peer());
|
|
|
|
model.source_model().push(5);
|
|
|
|
check_content(&model, &[5, 4, 3, 2, 1]);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_long_chain_integrity() {
|
|
use alloc::string::ToString;
|
|
let origin_model = Rc::new(VecModel::from((0..100).collect::<Vec<_>>()));
|
|
let checker1 = ModelChecker::new(origin_model.clone());
|
|
let fizzbuzz = Rc::new(MapModel::new(origin_model.clone(), |number| {
|
|
if (number % 3) == 0 && (number % 5) == 0 {
|
|
"FizzBuzz".to_string()
|
|
} else if (number % 3) == 0 {
|
|
"Fizz".to_string()
|
|
} else if (number % 5) == 0 {
|
|
"Buzz".to_string()
|
|
} else {
|
|
number.to_string()
|
|
}
|
|
}));
|
|
let checker2 = ModelChecker::new(fizzbuzz.clone());
|
|
let filter = Rc::new(FilterModel::new(fizzbuzz, |s| s != "FizzBuzz"));
|
|
let checker3 = ModelChecker::new(filter.clone());
|
|
let reverse = Rc::new(ReverseModel::new(filter));
|
|
let checker4 = ModelChecker::new(reverse.clone());
|
|
let sorted = Rc::new(SortModel::new_ascending(reverse));
|
|
let checker5 = ModelChecker::new(sorted.clone());
|
|
let filter2 = Rc::new(FilterModel::new(sorted, |s| s != "Fizz"));
|
|
let checker6 = ModelChecker::new(filter2.clone());
|
|
|
|
let check_all = || {
|
|
checker1.check();
|
|
checker2.check();
|
|
checker3.check();
|
|
checker4.check();
|
|
checker5.check();
|
|
checker6.check();
|
|
};
|
|
|
|
origin_model.extend(50..150);
|
|
check_all();
|
|
origin_model.insert(8, 1000);
|
|
check_all();
|
|
origin_model.remove(9);
|
|
check_all();
|
|
origin_model.remove(10);
|
|
origin_model.remove(11);
|
|
origin_model.set_row_data(55, 10001);
|
|
check_all();
|
|
origin_model.set_row_data(58, 10002);
|
|
origin_model.set_row_data(59, 10003);
|
|
origin_model.remove(28);
|
|
origin_model.remove(29);
|
|
origin_model.insert(100, 8888);
|
|
origin_model.remove(30);
|
|
origin_model.set_row_data(60, 10004);
|
|
origin_model.remove(130);
|
|
origin_model.set_row_data(61, 10005);
|
|
origin_model.remove(131);
|
|
check_all();
|
|
origin_model.remove(12);
|
|
origin_model.remove(13);
|
|
origin_model.remove(14);
|
|
origin_model.set_row_data(62, 10006);
|
|
origin_model.set_row_data(63, 10007);
|
|
origin_model.set_row_data(64, 10008);
|
|
origin_model.set_row_data(65, 10009);
|
|
check_all();
|
|
|
|
// Since VecModel don't have this as public API, just add some function that use row_removed on a wider range.
|
|
trait RemoveRange {
|
|
fn remove_range(&self, range: core::ops::Range<usize>);
|
|
}
|
|
impl<T> RemoveRange for VecModel<T> {
|
|
fn remove_range(&self, range: core::ops::Range<usize>) {
|
|
self.array.borrow_mut().drain(range.clone());
|
|
self.notify.row_removed(range.start, range.len())
|
|
}
|
|
}
|
|
|
|
origin_model.remove_range(25..110);
|
|
check_all();
|
|
|
|
origin_model.extend(900..910);
|
|
origin_model.set_row_data(45, 44444);
|
|
origin_model.remove_range(10..30);
|
|
origin_model.insert(45, 3000);
|
|
origin_model.insert(45, 3001);
|
|
origin_model.insert(45, 3002);
|
|
origin_model.insert(45, 3003);
|
|
origin_model.insert(45, 3004);
|
|
origin_model.insert(45, 3006);
|
|
origin_model.insert(45, 3007);
|
|
check_all();
|
|
}
|