use std::cell::RefCell;
use std::rc::Rc;
use saudade::{
App, Button, Container, Event, EventCtx, Label, List, ListItem, Painter, Rect, TextInput,
Theme, Widget, WindowConfig,
};
const W: i32 = 420;
const H: i32 = 268;
const LIST_RECT: Rect = Rect::new(12, 46, 230, 174);
fn main() {
let model = Rc::new(RefCell::new(Model {
people: vec![
Person::new("Hans", "Emil"),
Person::new("Max", "Mustermann"),
Person::new("Roman", "Tisch"),
],
filter: String::new(),
visible: Vec::new(),
}));
let list = Rc::new(RefCell::new(List::new(LIST_RECT)));
refill(&model, &list, None);
let name = Rc::new(RefCell::new(TextInput::new(Rect::new(318, 50, 90, 22))));
let surname = Rc::new(RefCell::new(TextInput::new(Rect::new(318, 80, 90, 22))));
let filter = TextInput::new(Rect::new(104, 14, 150, 22)).on_change({
let model = model.clone();
let list = list.clone();
move |cx, text| {
let keep = {
let m = model.borrow();
m.selected_db(list.borrow().selected_index())
};
model.borrow_mut().filter = text.to_string();
refill(&model, &list, keep);
cx.request_paint();
}
});
let create = ActionButton::new(
Button::new(Rect::new(12, 232, 80, 26), "Create").on_click({
let model = model.clone();
let list = list.clone();
let name = name.clone();
let surname = surname.clone();
move |cx| {
let person = read_person(&name, &surname);
let new_db = {
let mut m = model.borrow_mut();
m.people.push(person);
m.people.len() - 1
};
refill(&model, &list, Some(new_db));
cx.request_paint();
}
}),
{
let name = name.clone();
let surname = surname.clone();
move || has_input(&name, &surname)
},
);
let update = ActionButton::new(
Button::new(Rect::new(100, 232, 80, 26), "Update").on_click({
let model = model.clone();
let list = list.clone();
let name = name.clone();
let surname = surname.clone();
move |cx| {
let target = {
let m = model.borrow();
m.selected_db(list.borrow().selected_index())
};
if let Some(db) = target {
let person = read_person(&name, &surname);
model.borrow_mut().people[db] = person;
refill(&model, &list, Some(db));
cx.request_paint();
}
}
}),
{
let list = list.clone();
let name = name.clone();
let surname = surname.clone();
move || has_selection(&list) && has_input(&name, &surname)
},
);
let delete = ActionButton::new(
Button::new(Rect::new(188, 232, 80, 26), "Delete").on_click({
let model = model.clone();
let list = list.clone();
move |cx| {
let target = {
let m = model.borrow();
m.selected_db(list.borrow().selected_index())
};
if let Some(db) = target {
model.borrow_mut().people.remove(db);
refill(&model, &list, None);
cx.request_paint();
}
}
}),
{
let list = list.clone();
move || has_selection(&list)
},
);
let root = Container::new(W, H)
.add(Label::new(Rect::new(12, 18, 90, 16), "Filter prefix:"))
.add(filter)
.add(SharedList(list.clone()))
.add(Label::new(Rect::new(254, 54, 60, 16), "Name:"))
.add(SharedTextInput(name.clone()))
.add(Label::new(Rect::new(254, 84, 60, 16), "Surname:"))
.add(SharedTextInput(surname.clone()))
.add(create)
.add(update)
.add(delete);
App::new(WindowConfig::new("CRUD", W, H), root)
.with_theme(Theme::windows_31())
.run();
}
struct Person {
name: String,
surname: String,
}
impl Person {
fn new(name: impl Into<String>, surname: impl Into<String>) -> Self {
Self {
name: name.into(),
surname: surname.into(),
}
}
fn display(&self) -> String {
format!("{}, {}", self.surname, self.name)
}
}
struct Model {
people: Vec<Person>,
filter: String,
visible: Vec<usize>,
}
impl Model {
fn recompute_visible(&mut self) {
let prefix = self.filter.to_lowercase();
self.visible = self
.people
.iter()
.enumerate()
.filter(|(_, p)| p.surname.to_lowercase().starts_with(&prefix))
.map(|(i, _)| i)
.collect();
}
fn selected_db(&self, row: Option<usize>) -> Option<usize> {
row.and_then(|i| self.visible.get(i).copied())
}
}
fn refill(model: &Rc<RefCell<Model>>, list: &Rc<RefCell<List>>, keep: Option<usize>) {
let (items, selected) = {
let mut m = model.borrow_mut();
m.recompute_visible();
let items: Vec<ListItem> = m
.visible
.iter()
.map(|&i| ListItem::new(m.people[i].display()))
.collect();
let selected = keep.and_then(|db| m.visible.iter().position(|&i| i == db));
(items, selected)
};
let mut l = list.borrow_mut();
l.set_items(items);
l.set_selected(selected);
}
fn read_person(name: &Rc<RefCell<TextInput>>, surname: &Rc<RefCell<TextInput>>) -> Person {
Person::new(name.borrow().text().trim(), surname.borrow().text().trim())
}
fn has_input(name: &Rc<RefCell<TextInput>>, surname: &Rc<RefCell<TextInput>>) -> bool {
!name.borrow().text().trim().is_empty() || !surname.borrow().text().trim().is_empty()
}
fn has_selection(list: &Rc<RefCell<List>>) -> bool {
list.borrow().selected_index().is_some()
}
struct ActionButton {
button: Button,
enabled: Box<dyn Fn() -> bool>,
}
impl ActionButton {
fn new(button: Button, enabled: impl Fn() -> bool + 'static) -> Self {
Self {
button,
enabled: Box::new(enabled),
}
}
fn sync(&mut self) {
let enabled = (self.enabled)();
self.button.set_enabled(enabled);
}
}
impl Widget for ActionButton {
fn bounds(&self) -> Rect {
self.button.bounds()
}
fn paint(&mut self, painter: &mut Painter, theme: &Theme) {
self.sync();
self.button.paint(painter, theme);
}
fn event(&mut self, event: &Event, ctx: &mut EventCtx) {
self.sync();
self.button.event(event, ctx);
}
fn captures_pointer(&self) -> bool {
self.button.captures_pointer()
}
fn focusable(&self) -> bool {
(self.enabled)()
}
fn set_focused(&mut self, focused: bool) {
self.button.set_focused(focused);
}
fn layout(&mut self, bounds: Rect) {
self.button.layout(bounds);
}
}
struct SharedList(Rc<RefCell<List>>);
impl Widget for SharedList {
fn bounds(&self) -> Rect {
self.0.borrow().bounds()
}
fn paint(&mut self, painter: &mut Painter, theme: &Theme) {
self.0.borrow_mut().paint(painter, theme);
}
fn event(&mut self, event: &Event, ctx: &mut EventCtx) {
self.0.borrow_mut().event(event, ctx);
}
fn captures_pointer(&self) -> bool {
self.0.borrow().captures_pointer()
}
fn focusable(&self) -> bool {
self.0.borrow().focusable()
}
fn set_focused(&mut self, focused: bool) {
self.0.borrow_mut().set_focused(focused);
}
fn layout(&mut self, bounds: Rect) {
self.0.borrow_mut().layout(bounds);
}
}
struct SharedTextInput(Rc<RefCell<TextInput>>);
impl Widget for SharedTextInput {
fn bounds(&self) -> Rect {
self.0.borrow().bounds()
}
fn paint(&mut self, painter: &mut Painter, theme: &Theme) {
self.0.borrow_mut().paint(painter, theme);
}
fn event(&mut self, event: &Event, ctx: &mut EventCtx) {
self.0.borrow_mut().event(event, ctx);
}
fn captures_pointer(&self) -> bool {
self.0.borrow().captures_pointer()
}
fn focusable(&self) -> bool {
self.0.borrow().focusable()
}
fn set_focused(&mut self, focused: bool) {
self.0.borrow_mut().set_focused(focused);
}
fn layout(&mut self, bounds: Rect) {
self.0.borrow_mut().layout(bounds);
}
fn wants_ticks(&self) -> bool {
self.0.borrow().wants_ticks()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn seeded() -> (Rc<RefCell<Model>>, Rc<RefCell<List>>) {
let model = Rc::new(RefCell::new(Model {
people: vec![
Person::new("Hans", "Emil"),
Person::new("Max", "Mustermann"),
Person::new("Roman", "Tisch"),
],
filter: String::new(),
visible: Vec::new(),
}));
let list = Rc::new(RefCell::new(List::new(LIST_RECT)));
refill(&model, &list, None);
(model, list)
}
fn labels(list: &Rc<RefCell<List>>) -> Vec<String> {
list.borrow()
.items()
.iter()
.map(|i| i.label.clone())
.collect()
}
fn selected_db(model: &Rc<RefCell<Model>>, list: &Rc<RefCell<List>>) -> Option<usize> {
model.borrow().selected_db(list.borrow().selected_index())
}
#[test]
fn initial_view_lists_everyone_unselected() {
let (_model, list) = seeded();
assert_eq!(
labels(&list),
["Emil, Hans", "Mustermann, Max", "Tisch, Roman"]
);
assert_eq!(list.borrow().selected_index(), None);
}
#[test]
fn filter_matches_surname_prefix_case_insensitively() {
let (model, list) = seeded();
model.borrow_mut().filter = "m".to_string();
refill(&model, &list, None);
assert_eq!(labels(&list), ["Mustermann, Max"]);
}
#[test]
fn create_appends_and_selects_the_new_row() {
let (model, list) = seeded();
let new_db = {
let mut m = model.borrow_mut();
m.people.push(Person::new("Ada", "Lovelace"));
m.people.len() - 1
};
refill(&model, &list, Some(new_db));
assert_eq!(labels(&list).last().unwrap(), "Lovelace, Ada");
assert_eq!(selected_db(&model, &list), Some(new_db));
}
#[test]
fn update_replaces_the_selected_entry_in_place() {
let (model, list) = seeded();
list.borrow_mut().set_selected(Some(1)); let db = selected_db(&model, &list).unwrap();
model.borrow_mut().people[db] = Person::new("Maria", "Musterfrau");
refill(&model, &list, Some(db));
assert_eq!(
labels(&list),
["Emil, Hans", "Musterfrau, Maria", "Tisch, Roman"]
);
assert_eq!(list.borrow().selected_index(), Some(1));
}
#[test]
fn delete_removes_the_selected_entry_and_clears_selection() {
let (model, list) = seeded();
list.borrow_mut().set_selected(Some(0)); let db = selected_db(&model, &list).unwrap();
model.borrow_mut().people.remove(db);
refill(&model, &list, None);
assert_eq!(labels(&list), ["Mustermann, Max", "Tisch, Roman"]);
assert_eq!(list.borrow().selected_index(), None);
}
#[test]
fn selection_survives_a_filter_it_still_matches() {
let (model, list) = seeded();
list.borrow_mut().set_selected(Some(1)); let keep = selected_db(&model, &list);
model.borrow_mut().filter = "Must".to_string();
refill(&model, &list, keep);
assert_eq!(labels(&list), ["Mustermann, Max"]);
assert_eq!(list.borrow().selected_index(), Some(0)); }
#[test]
fn selection_is_dropped_when_filtered_out() {
let (model, list) = seeded();
list.borrow_mut().set_selected(Some(0)); let keep = selected_db(&model, &list);
model.borrow_mut().filter = "T".to_string();
refill(&model, &list, keep);
assert_eq!(labels(&list), ["Tisch, Roman"]);
assert_eq!(list.borrow().selected_index(), None);
}
#[test]
fn input_predicates_track_field_contents() {
let name = Rc::new(RefCell::new(TextInput::new(LIST_RECT)));
let surname = Rc::new(RefCell::new(TextInput::new(LIST_RECT)));
assert!(!has_input(&name, &surname));
name.borrow_mut().set_text(" "); assert!(!has_input(&name, &surname));
surname.borrow_mut().set_text("Tisch");
assert!(has_input(&name, &surname));
assert_eq!(read_person(&name, &surname).display(), "Tisch, ");
}
}