use std::cmp::Ordering;
use std::collections::HashSet;
use std::rc::Rc;
use async_trait::async_trait;
use gui::derive::Widget;
use gui::Cap;
use gui::Handleable;
use gui::Id;
use gui::MutCap;
use gui::Widget;
use crate::tags::Tag;
use crate::tasks::Task;
use super::event::Event;
use super::event::Key;
use super::message::Message;
use super::message::MessageExt;
use super::modal::Modal;
use super::selectable::Selectable;
#[derive(Clone, Debug, PartialEq)]
pub enum SetUnsetTag {
Set(Tag),
Unset(Tag),
}
impl SetUnsetTag {
pub fn name(&self) -> &str {
match self {
Self::Unset(template) | Self::Set(template) => template.name(),
}
}
pub fn is_set(&self) -> bool {
match self {
Self::Set(_) => true,
Self::Unset(_) => false,
}
}
fn toggle(&mut self) {
*self = match self {
Self::Set(tag) => Self::Unset(tag.clone()),
Self::Unset(tag) => Self::Set(tag.clone()),
};
}
}
fn cmp_template(lhs: &Tag, rhs: &Tag) -> Ordering {
lhs.name().to_lowercase().cmp(&rhs.name().to_lowercase())
}
fn prepare_tags(task: &Task) -> Vec<SetUnsetTag> {
let set = task.tags(|iter| iter.map(Tag::template).collect::<HashSet<_>>());
let mut unset = task
.templates()
.iter()
.filter(|template| !set.contains(template))
.map(Tag::new)
.collect::<Vec<_>>();
unset.sort_by(cmp_template);
let mut set = task.tags(|iter| iter.cloned().collect::<Vec<_>>());
set.sort_by(cmp_template);
set
.into_iter()
.map(SetUnsetTag::Set)
.chain(unset.into_iter().map(SetUnsetTag::Unset))
.collect::<Vec<_>>()
}
#[derive(Copy, Clone, Debug, PartialEq)]
enum Direction {
Forward,
Backward,
}
#[derive(Debug)]
struct Data {
prev_focused: Option<Id>,
task: Rc<Task>,
to_edit: Task,
tags: Vec<SetUnsetTag>,
selection: isize,
jump_to: Option<Direction>,
}
impl Data {
fn new(task: Rc<Task>, to_edit: Task) -> Self {
let tags = prepare_tags(&to_edit);
Self {
prev_focused: None,
task,
to_edit,
tags,
selection: 0,
jump_to: None,
}
}
fn select_task_beginning_with(&mut self, c: char, direction: Direction) -> bool {
let pattern = &c.to_lowercase().to_string();
let new_selection = match direction {
Direction::Forward => self
.tags
.iter()
.enumerate()
.skip(self.selection(1))
.find(|(_, tag)| tag.name().to_lowercase().starts_with(pattern)),
Direction::Backward => self
.tags
.iter()
.enumerate()
.rev()
.skip(self.count() - self.selection(0))
.find(|(_, tag)| tag.name().to_lowercase().starts_with(pattern)),
};
if let Some((new_selection, _)) = new_selection {
self.set_selection_index(new_selection as isize);
true
} else {
false
}
}
fn into_task(mut self) -> (Rc<Task>, Task) {
let tags = self.tags.into_iter().filter_map(|tag| match tag {
SetUnsetTag::Set(tag) => Some(tag),
SetUnsetTag::Unset(_) => None,
});
self.to_edit.set_tags(tags);
(self.task, self.to_edit)
}
}
impl Selectable for Data {
fn selection_index(&self) -> isize {
self.selection
}
fn set_selection_index(&mut self, selection: isize) {
self.selection = selection
}
fn count(&self) -> usize {
self.tags.len()
}
}
#[derive(Debug)]
pub struct DialogData {
data: Option<Data>,
}
impl DialogData {
pub fn new() -> Self {
Self { data: None }
}
fn selected_tag(&mut self) -> Option<&mut SetUnsetTag> {
let selection = self.selection(0);
self
.data
.as_mut()
.map(|data| data.tags.get_mut(selection))
.expect("dialog has no data set")
}
fn toggle_tag(&mut self) -> bool {
self
.selected_tag()
.map(|tag| {
tag.toggle();
true
})
.unwrap_or(false)
}
}
impl Selectable for DialogData {
fn selection_index(&self) -> isize {
self
.data
.as_ref()
.map(Selectable::selection_index)
.expect("dialog has no data set")
}
fn set_selection_index(&mut self, selection: isize) {
self
.data
.as_mut()
.map(|data| data.set_selection_index(selection))
.expect("dialog has no data set")
}
fn count(&self) -> usize {
self
.data
.as_ref()
.map(Selectable::count)
.expect("dialog has no data set")
}
}
#[derive(Debug, Widget)]
#[gui(Event = Event, Message = Message)]
pub struct Dialog {
id: Id,
}
impl Dialog {
pub fn new(id: Id) -> Self {
Self { id }
}
#[allow(clippy::option_map_unit_fn)]
async fn handle_key(&self, cap: &mut dyn MutCap<Event, Message>, key: Key) -> Option<Message> {
if let Some(result) = self.handle_jump_to(cap, key) {
return result
}
let data = self.data_mut::<DialogData>(cap);
match key {
Key::Esc | Key::Char('\n') | Key::Char('q') => {
let widget = self.restore_focus(cap);
cap.hide(self.id);
let data = self.data_mut::<DialogData>(cap);
let data = data.data.take();
if key == Key::Char('\n') {
let (task, updated) = data.map(Data::into_task).expect("dialog has no data set");
cap.send(widget, Message::UpdateTask(task, updated)).await;
}
Some(Message::Updated)
},
Key::Char(' ') => MessageExt::maybe_update(None, data.toggle_tag()),
Key::Char('f') => {
data
.data
.as_mut()
.map(|data| data.jump_to = Some(Direction::Forward));
None
},
Key::Char('F') => {
data
.data
.as_mut()
.map(|data| data.jump_to = Some(Direction::Backward));
None
},
Key::Char('g') => MessageExt::maybe_update(None, data.select(0)),
Key::Char('G') => MessageExt::maybe_update(None, data.select(isize::MAX)),
Key::Char('j') => MessageExt::maybe_update(None, data.change_selection(1)),
Key::Char('k') => MessageExt::maybe_update(None, data.change_selection(-1)),
_ => None,
}
}
fn handle_jump_to(
&self,
cap: &mut dyn MutCap<Event, Message>,
key: Key,
) -> Option<Option<Message>> {
let data = self
.data_mut::<DialogData>(cap)
.data
.as_mut()
.expect("dialog has no data set");
match data.jump_to {
Some(direction) => {
data.jump_to = None;
match key {
Key::Char(c) => {
let updated = data.select_task_beginning_with(c, direction);
Some(MessageExt::maybe_update(None, updated))
},
_ => None,
}
},
None => None,
}
}
pub fn tags<'cap>(&self, cap: &'cap dyn Cap) -> &'cap [SetUnsetTag] {
let data = self.data::<DialogData>(cap);
data
.data
.as_ref()
.map(|data| &data.tags)
.expect("dialog has no data set")
}
pub fn selection(&self, cap: &dyn Cap) -> usize {
let data = self.data::<DialogData>(cap);
data.selection(0)
}
}
impl Modal for Dialog {
fn prev_focused(&self, cap: &dyn Cap) -> Option<Id> {
let data = self.data::<DialogData>(cap);
data
.data
.as_ref()
.map(|data| data.prev_focused)
.expect("dialog has no data set")
}
fn set_prev_focused(&self, cap: &mut dyn MutCap<Event, Message>, focused: Option<Id>) {
let data = self.data_mut::<DialogData>(cap);
data
.data
.as_mut()
.map(|mut data| data.prev_focused = focused)
.expect("dialog has no data set")
}
}
#[async_trait(?Send)]
impl Handleable<Event, Message> for Dialog {
async fn handle(&self, cap: &mut dyn MutCap<Event, Message>, event: Event) -> Option<Event> {
match event {
Event::Key(key, _raw) => self.handle_key(cap, key).await.into_event(),
_ => Some(event),
}
}
async fn react(&self, message: Message, cap: &mut dyn MutCap<Event, Message>) -> Option<Message> {
match message {
Message::EditTags(task, edited) => {
let data = self.data_mut::<DialogData>(cap);
debug_assert!(data.data.is_none());
data.data = Some(Data::new(task, edited));
self.make_focused(cap);
Some(Message::Updated)
},
m => panic!("Received unexpected message: {:?}", m),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::ops::Deref as _;
use std::rc::Rc;
use crate::db::Db;
use crate::tags::Templates;
use crate::test::COMPLETE_TAG;
#[test]
fn tag_preparation() {
let template_list = vec![COMPLETE_TAG, "foobaz", "Z", "a", "foobar"];
let mut templates = Templates::new();
templates.extend(template_list);
let templates = Rc::new(templates);
let tags = vec![
templates.instantiate_from_name("foobaz"),
templates.instantiate_from_name("foobar"),
];
let task = Task::with_summary_and_tags("do something, mate", tags, templates.clone());
let tags = prepare_tags(&task);
let expected = vec![
SetUnsetTag::Set(templates.instantiate_from_name("foobar")),
SetUnsetTag::Set(templates.instantiate_from_name("foobaz")),
SetUnsetTag::Unset(templates.instantiate_from_name("a")),
SetUnsetTag::Unset(templates.instantiate_from_name(COMPLETE_TAG)),
SetUnsetTag::Unset(templates.instantiate_from_name("Z")),
];
assert_eq!(tags, expected);
}
#[test]
fn data_tag_selection() {
let template_list = vec![COMPLETE_TAG, "a", "b", "c", "c1", "d", "h", "z"];
let mut templates = Templates::new();
templates.extend(template_list);
let templates = Rc::new(templates);
let tags = vec![
templates.instantiate_from_name("a"),
templates.instantiate_from_name("h"),
templates.instantiate_from_name("d"),
];
let iter = [Task::with_summary_and_tags("task", tags, templates)];
let db = Db::from_iter(iter);
let entry = db.get(0).unwrap();
let task = entry.deref().clone();
let clone = task.deref().clone();
let mut data = Data::new(task, clone);
assert_eq!(data.selection, 0);
assert!(!data.select_task_beginning_with('h', Direction::Backward));
assert_eq!(data.selection, 0);
assert!(data.select_task_beginning_with('h', Direction::Forward));
assert_eq!(data.selection, 2);
assert!(data.select_task_beginning_with('z', Direction::Forward));
assert_eq!(data.selection, 7);
assert!(data.select_task_beginning_with('c', Direction::Backward));
assert_eq!(data.selection, 6);
assert!(data.select_task_beginning_with('c', Direction::Backward));
assert_eq!(data.selection, 5);
assert!(data.select_task_beginning_with('c', Direction::Backward));
assert_eq!(data.selection, 4);
assert!(!data.select_task_beginning_with('c', Direction::Backward));
assert_eq!(data.selection, 4);
assert!(data.select_task_beginning_with('c', Direction::Forward));
assert_eq!(data.selection, 5);
assert!(data.select_task_beginning_with('c', Direction::Forward));
assert_eq!(data.selection, 6);
assert!(!data.select_task_beginning_with('c', Direction::Forward));
assert_eq!(data.selection, 6);
}
}