diff --git a/src/main.rs b/src/main.rs index f96f3df..9b24b53 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,7 @@ #![feature(iter_map_windows)] +use iced::keyboard::key::Named; +use iced::keyboard::{Key, Modifiers}; +use iced::{event, task, Event, Subscription}; use iced::widget::scrollable; use iced::{ Element, Length, Task, Theme, clipboard, color, @@ -43,32 +46,14 @@ fn main() -> iced::Result { } } - // ALL BUILTIN THEMES - // Light - // Dark - // Dracula - // Nord - // SolarizedLight - // SolarizedDark - // GruvboxLight - // GruvboxDark - // CatppuccinLatte - // CatppuccinFrappe - // CatppuccinMacchiato - // CatppuccinMocha - // TokyoNight - // TokyoNightStorm - // TokyoNightLight - // KanagawaWave - // KanagawaDragon - // KanagawaLotus - // Moonfly - // Nightfly - // Oxocarbon - // Ferra + // THEMES + // Dark - KanagawaDragon + // Default - Nord + // Light - TokyoNightLight // GUI iced::application("Bible Study", State::update, State::view) + .subscription(State::subscription) .theme(|_| Theme::Nord) .run() } @@ -85,6 +70,10 @@ enum Message { QuickSearch(usize, String, usize), AddColRight, DeleteColumn(usize), + ErrorSelectingBible(usize), + RecievedEvent(Event), + ColSelected(usize), + ColUnselected, } struct ColState { @@ -96,6 +85,8 @@ struct ColState { word_search: String, word_search_results: Option>, + + error: Option, } impl Default for ColState { @@ -109,6 +100,41 @@ impl Default for ColState { word_search: String::new(), word_search_results: None, + + error: None, + } + } +} + +#[derive(PartialEq, Eq)] +enum SelectedInputBox { + Bible, + VerseSearch, + WordSearch, +} +impl SelectedInputBox { + fn in_order() -> Vec { + vec![ + SelectedInputBox::Bible, + SelectedInputBox::VerseSearch, + SelectedInputBox::WordSearch + ] + } + fn next(current: &Option) -> Self { + if let Some(input) = current { + if let Some(found) = Self::in_order().into_iter().find(|a| a==input) { + return found + } + } + // I don't see why this would panic + Self::in_order().into_iter().next().unwrap() + } + fn as_id_prefix(&self) -> &str { + // magic values that relate to id() as setup in the GUI + match self { + SelectedInputBox::Bible => "", + SelectedInputBox::VerseSearch => "verse-", + SelectedInputBox::WordSearch => "word-", } } } @@ -117,6 +143,9 @@ struct State { files: combo_box::State, states: Vec, cols: usize, + /// Incase an event needs a best guess column + last_selected_column: Option, + last_selected_input: Option } impl Default for State { @@ -147,13 +176,74 @@ impl Default for State { files: combo_box::State::new(files), states: vec![ColState::default()], cols: 1, + last_selected_column: None, + last_selected_input: None, } } } impl State { + + fn subscription(&self) -> Subscription { + event::listen().map(Message::RecievedEvent) + } + fn update(&mut self, msg: Message) -> Task { + // handle tab ordering match msg { + Message::WordSearchInput(_, _) => self.last_selected_input = Some(SelectedInputBox::WordSearch), + Message::BibleSearchInput(_, _) => self.last_selected_input = Some(SelectedInputBox::VerseSearch), + Message::DeleteColumn(_) => self.last_selected_input = None, + _ => {} + } + + // try to set last_selected_column for messages with side effects + match msg { + // All events that give column index + Message::ErrorSelectingBible(i) | + Message::BibleSelected(i, _) | + Message::QuickSearch(i, _, _) | + Message::BibleSearchInput(i,_) | + Message::WordSearchInput(i, _) | + Message::BibleSearchSubmit(i) | + Message::SubmitWordSearch(i) | + Message::CopyText(i) | + Message::Clear(i) + => self.last_selected_column = Some(i), + Message::DeleteColumn(_) => self.last_selected_column = None, + _ => {} + } + + // match normal messages + match msg { + Message::ColSelected(i) => self.last_selected_column = Some(i), + Message::ColUnselected => self.last_selected_column = None, + Message::RecievedEvent(event) => { + if let Event::Keyboard(kbd) =event { + if let iced::keyboard::Event::KeyReleased {key, modifiers, ..} = kbd { + if let Key::Named(n) = key { + if let Named::Tab = n { + if let Some(i) = self.last_selected_column { + if modifiers == Modifiers::SHIFT { + // Shirft + Tab + let next = SelectedInputBox::next(&self.last_selected_input); + let id = next.as_id_prefix(); + return text_input::focus(format!("{id}{}", i)); + } else { + // Tab + + // TODO prev + return text_input::focus(format!("verse-{}", i)); + } + } + } + } + } + } + } + Message::ErrorSelectingBible(i) => { + self.states[i].error = Some(String::new()); + } Message::DeleteColumn(idx) => { self.cols -= 1; self.states.remove(idx); @@ -165,14 +255,26 @@ impl State { self.states.push(ColState::default()) } Message::BibleSelected(i, file) => { - if let Ok(contents) = fs::read_to_string(&file) { - if let Ok(bible) = quick_xml::de::from_str::(&contents) { - // State is held technically in this variable, although - // the Bible is held in memory in it's own variable. - self.states[i].selected_file = Some(format!("{bible}")); - self.states[i].bible_selected = Some(bible); - // automatically focus the search bar when done here - // return text_input::focus("bible-search"); + match fs::read_to_string(&file) { + Ok(contents) => { + match quick_xml::de::from_str::(&contents) { + Ok(bible) => { + // State is held technically in this variable, although + // the Bible is held in memory in it's own variable. + self.states[i].selected_file = Some(format!("{bible}")); + self.states[i].bible_selected = Some(bible); + // automatically focus the search bar when done here + // return text_input::focus("bible-search"); + } + Err(err) => { + eprintln!("{}", err); + return self.update(Message::ErrorSelectingBible(i)); + }, + } + }, + Err(err) => { + eprintln!("{}", err); + return self.update(Message::ErrorSelectingBible(i)); } } } @@ -184,8 +286,12 @@ impl State { } return self.update(Message::BibleSearchSubmit(i)); } - Message::BibleSearchInput(i, query) => self.states[i].bible_search = query, - Message::WordSearchInput(i, query) => self.states[i].word_search = query, + Message::BibleSearchInput(i, query) => { + self.states[i].bible_search = query + }, + Message::WordSearchInput(i, query) => { + self.states[i].word_search = query + }, Message::BibleSearchSubmit(i) => { let mut splits = self.states[i].bible_search.split_whitespace(); let book = splits.next(); @@ -218,13 +324,13 @@ impl State { self.states[i].word_search_results = None; self.states[i].bible_search = String::new(); self.states[i].word_search = String::new(); - // return text_input::focus("bible-search"); } }; Task::none() } fn view(&self) -> Element { + //TODO error message scrollable( row![ row((0..self.cols).map(|col_index| { @@ -239,14 +345,17 @@ impl State { "Select Bible", self.states[col_index].selected_file.as_ref(), move |s| Message::BibleSelected(col_index, s) - ), + ) + .on_open(Message::ColSelected(col_index)), text_input( "Search query, ie: Gen 1:1", &self.states[col_index].bible_search ) + .id(format!("verse-{}", col_index)) .on_input(move |s| Message::BibleSearchInput(col_index, s)) .on_submit(Message::BibleSearchSubmit(col_index)), widget::text_input("Word Search", &self.states[col_index].word_search) + .id(format!("word-{}", col_index)) .on_input(move |s| Message::WordSearchInput(col_index, s)) .on_submit(Message::SubmitWordSearch(col_index)), row![