use crate::core::Rect;
use crate::render::RenderContext;
use crate::signal::{ConnectionScope, GenericSignal, Signal1};
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
use std::sync::Arc;
pub trait ListModel: Send + Sync {
fn row_count(&self) -> usize;
fn data(&self, row: usize) -> Option<String>;
fn data_changed_signal(&self) -> Option<&GenericSignal> {
None
}
}
pub struct VecListModel {
items: Vec<String>,
data_changed: GenericSignal,
}
impl VecListModel {
pub fn new(items: Vec<String>) -> Self {
Self { items, data_changed: GenericSignal::new() }
}
pub fn data_changed_signal(&self) -> &GenericSignal {
&self.data_changed
}
pub fn append(&mut self, item: String) {
self.items.push(item);
self.data_changed.emit();
}
pub fn remove(&mut self, index: usize) -> Option<String> {
if index < self.items.len() {
let item = self.items.remove(index);
self.data_changed.emit();
Some(item)
} else {
None
}
}
pub fn clear(&mut self) {
self.items.clear();
self.data_changed.emit();
}
}
impl ListModel for VecListModel {
fn row_count(&self) -> usize {
self.items.len()
}
fn data(&self, row: usize) -> Option<String> {
self.items.get(row).cloned()
}
fn data_changed_signal(&self) -> Option<&GenericSignal> {
Some(&self.data_changed)
}
}
pub struct SelectionModel {
mode: SelectionMode,
selected_rows: Vec<usize>,
current_row: Option<usize>,
}
impl Default for SelectionModel {
fn default() -> Self {
Self::new()
}
}
impl SelectionModel {
pub fn new() -> Self {
Self { mode: SelectionMode::Single, selected_rows: Vec::new(), current_row: None }
}
pub fn set_mode(&mut self, mode: SelectionMode) {
self.mode = mode;
self.normalize();
}
pub fn mode(&self) -> SelectionMode {
self.mode
}
pub fn select_row(&mut self, row: usize) {
match self.mode {
SelectionMode::Single => {
self.selected_rows.clear();
self.selected_rows.push(row);
self.current_row = Some(row);
}
SelectionMode::Multi => {
if !self.selected_rows.contains(&row) {
self.selected_rows.push(row);
}
self.current_row = Some(row);
}
SelectionMode::Extended => {
self.selected_rows.push(row);
self.current_row = Some(row);
}
}
}
pub fn clear(&mut self) {
self.selected_rows.clear();
self.current_row = None;
}
pub fn current_row(&self) -> Option<usize> {
self.current_row
}
pub fn rows(&self) -> Vec<usize> {
self.selected_rows.clone()
}
fn normalize(&mut self) {
if self.mode == SelectionMode::Single && self.selected_rows.len() > 1 {
if let Some(&last) = self.selected_rows.last() {
self.selected_rows = vec![last];
} else {
self.selected_rows.clear();
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SelectionMode {
Single,
Multi,
Extended,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ViewMode {
#[default]
List,
Icon,
Details,
Thumbnails,
}
pub struct ListView {
base: BaseWidget,
model: Option<Arc<dyn ListModel>>,
model_connection_scope: ConnectionScope,
selection: SelectionModel,
focused_row: Option<usize>,
view_mode: ViewMode,
pub selection_changed: Signal1<usize>,
pub focused_row_changed: Signal1<Option<usize>>,
}
impl ListView {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::ListView, geometry, "ListView"),
model: None,
model_connection_scope: ConnectionScope::new(),
selection: SelectionModel::new(),
focused_row: None,
view_mode: ViewMode::default(),
selection_changed: Signal1::new(),
focused_row_changed: Signal1::new(),
}
}
pub fn set_model(&mut self, model: Arc<dyn ListModel>) {
self.model_connection_scope = ConnectionScope::new();
if let Some(data_changed) = model.data_changed_signal() {
let redraw = self.base.redraw_requested_signal().clone();
let layout = self.base.layout_requested_signal().clone();
data_changed.connect_scoped(&self.model_connection_scope, move || {
redraw.emit();
layout.emit();
});
}
self.model = Some(model);
self.normalize_projection_state();
self.base.request_layout();
self.base.request_redraw();
}
pub fn has_model(&self) -> bool {
self.model.is_some()
}
pub fn model_ref(&self) -> Option<&Arc<dyn ListModel>> {
self.model.as_ref()
}
pub fn row_count(&self) -> usize {
self.model.as_ref().map(|m| m.row_count()).unwrap_or(0)
}
pub fn item(&self, row: usize) -> Option<String> {
self.model.as_ref().and_then(|m| m.data(row))
}
pub fn select_row(&mut self, row: usize) -> bool {
if row < self.row_count() {
self.selection.select_row(row);
self.selection_changed.emit(row);
self.set_focused_row(row);
true
} else {
false
}
}
pub fn clear_selection(&mut self) {
self.selection.clear();
}
pub fn set_focused_row(&mut self, row: usize) -> bool {
if row >= self.row_count() {
return false;
}
if self.focused_row == Some(row) {
return true;
}
self.focused_row = Some(row);
self.focused_row_changed.emit(self.focused_row);
true
}
pub fn clear_focused_row(&mut self) {
if self.focused_row.is_none() {
return;
}
self.focused_row = None;
self.focused_row_changed.emit(None);
}
pub fn focused_row(&self) -> Option<usize> {
self.focused_row.filter(|row| *row < self.row_count())
}
pub fn selected_row(&self) -> Option<usize> {
self.selection.current_row().filter(|row| *row < self.row_count())
}
pub fn selected_rows(&self) -> Vec<usize> {
self.selection.rows().into_iter().filter(|row| *row < self.row_count()).collect()
}
pub fn set_selection_mode(&mut self, mode: SelectionMode) {
self.selection.set_mode(mode);
}
pub fn selection_mode(&self) -> SelectionMode {
self.selection.mode()
}
pub fn view_mode(&self) -> ViewMode {
self.view_mode
}
pub fn set_view_mode(&mut self, mode: ViewMode) {
self.view_mode = mode;
self.base.request_redraw();
}
fn normalize_projection_state(&mut self) {
let row_count = self.row_count();
self.selection.selected_rows.retain(|row| *row < row_count);
self.selection.current_row = self.selection.current_row.filter(|row| *row < row_count);
self.focused_row = self.focused_row.filter(|row| *row < row_count);
}
}
impl Widget for ListView {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
}
impl Draw for ListView {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.base.geometry();
use crate::core::Color;
context.fill_rect(rect, Color::from_rgb(255, 255, 255));
context.draw_rect(rect, Color::from_rgb(200, 200, 200));
if let Some(ref model) = self.model {
let item_height = 20;
let row_count = model.row_count();
let current_row = self.focused_row;
for i in 0..row_count {
let y = rect.y + item_height * i as i32;
if y + item_height > rect.y + rect.height as i32 {
break;
}
if Some(i) == current_row {
context.fill_rect(
crate::core::Rect::new(rect.x, y, rect.width, item_height as u32),
Color::from_rgb(200, 220, 255),
);
}
if let Some(text) = model.data(i) {
context.draw_text(
crate::core::Point::new(rect.x + 2, y + item_height / 2),
&text,
&crate::core::Font::default(),
Color::from_rgb(0, 0, 0),
);
}
}
}
}
}
impl crate::event::EventHandler for ListView {
fn handle_event(&mut self, event: &crate::event::Event) {
if !self.base.is_enabled() {
return;
}
match event {
crate::event::Event::MousePress { pos, button } if *button == 1 => {
let rect = self.base.geometry();
let item_height = 20;
if pos.y >= rect.y {
let index = ((pos.y - rect.y) / item_height) as usize;
let row_count = self.row_count();
if index < row_count {
self.focused_row = Some(index);
self.selection.select_row(index);
if let Some(row) = self.focused_row {
self.selection_changed.emit(row);
self.focused_row_changed.emit(Some(row));
}
}
}
}
#[cfg(feature = "touch")]
crate::event::Event::Tap { pos } => {
let rect = self.base.geometry();
let item_height = 20;
if pos.y >= rect.y {
let index = ((pos.y - rect.y) / item_height) as usize;
let row_count = self.row_count();
if index < row_count {
self.focused_row = Some(index);
self.selection.select_row(index);
if let Some(row) = self.focused_row {
self.selection_changed.emit(row);
self.focused_row_changed.emit(Some(row));
}
}
}
}
_ => { }
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
struct StaticListModel;
impl ListModel for StaticListModel {
fn row_count(&self) -> usize {
2
}
fn data(&self, row: usize) -> Option<String> {
match row {
0 => Some("A".to_string()),
1 => Some("B".to_string()),
_ => None,
}
}
}
#[test]
fn list_view_model_binding_roundtrip() {
let mut view = ListView::new(Rect::new(0, 0, 100, 80));
assert!(!view.has_model());
assert!(view.model_ref().is_none());
view.set_model(Arc::new(StaticListModel));
assert!(view.has_model());
assert!(view.model_ref().is_some());
assert_eq!(view.row_count(), 2);
assert_eq!(view.item(99), None);
}
}