From 58e48e8ca9f9758c3e3681a03af7dc2e405353dc Mon Sep 17 00:00:00 2001 From: heyrict Date: Mon, 16 Mar 2020 12:12:31 +0800 Subject: [PATCH] feat: Add modal to display assets in an item --- src/app.rs | 46 ++++++- src/event.rs | 21 ++- src/reducer.rs | 110 +++++++++++++-- src/ui.rs | 364 ++++++++++++++++++++++++++++++++++++++----------- 4 files changed, 441 insertions(+), 100 deletions(-) diff --git a/src/app.rs b/src/app.rs index 9b903ff..354b23a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,4 +1,4 @@ -use crate::event::ModalState; +use crate::event::SaveModalState; use dirs::config_dir; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -49,6 +49,8 @@ pub struct Question { pub answer: Option, #[serde(default, with = "selection_flags_serde")] pub user_selection: SelectionFlags, + #[serde(default)] + pub assets: Vec, #[serde(flatten)] pub extra: HashMap, } @@ -82,8 +84,9 @@ impl HasQuestionResult for Question { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Card { - question: String, - answer: String, + pub question: String, + pub answer: String, + pub assets: Vec, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -207,14 +210,21 @@ impl Home { } } +pub enum AssetsModalState { + Hidden, + Show(ListState), +} + pub struct Modal { - pub save_modal_state: ModalState, + pub save_modal_state: SaveModalState, + pub assets_modal_state: AssetsModalState, } impl Default for Modal { fn default() -> Modal { Modal { - save_modal_state: ModalState::Hidden, + save_modal_state: SaveModalState::Hidden, + assets_modal_state: AssetsModalState::Hidden, } } } @@ -227,6 +237,8 @@ pub struct Config { pub show_usage: bool, #[serde(default = "Config::_default_pretty_printing")] pub pretty_printing: bool, + #[serde(default = "Config::_default_launcher")] + pub launcher: String, } impl Config { @@ -239,6 +251,20 @@ impl Config { const fn _default_pretty_printing() -> bool { false } + fn _default_launcher() -> String { + #[cfg(target_os = "macos")] + { + "open".to_owned() + } + #[cfg(target_os = "linux")] + { + "xdg-open".to_owned() + } + #[cfg(target_os = "windows")] + { + "explorer".to_owned() + } + } } impl Default for Config { @@ -247,6 +273,7 @@ impl Default for Config { items_per_line: Self::_default_items_per_line(), show_usage: Self::_default_show_usage(), pretty_printing: Self::_default_pretty_printing(), + launcher: Self::_default_launcher(), } } } @@ -306,6 +333,15 @@ impl Exam { } } +impl Item { + pub fn get_assets(&self) -> &Vec { + match self { + Item::Question(question) => &question.assets, + Item::Card(card) => &card.assets, + } + } +} + impl Question { pub fn num_selections(&self) -> usize { self.selections.len() diff --git a/src/event.rs b/src/event.rs index 6f4d8f0..6a253e8 100644 --- a/src/event.rs +++ b/src/event.rs @@ -14,7 +14,7 @@ pub enum UpdateQuestionIndexEvent { } #[derive(Debug)] -pub enum UpdateHomeSelectedEvent { +pub enum UpdateListSelectedEvent { Next, Prev, Home, @@ -28,20 +28,28 @@ pub enum QuitAction { } #[derive(Debug)] -pub enum ModalState { +pub enum SaveModalState { Hidden, ShowSave, ShowQuit(QuitAction), } #[derive(Debug)] -pub enum ModalActions { - Open(ModalState), +pub enum SaveModalActions { + Open(SaveModalState), Quit(QuitAction), Okay, Cancel, } +#[derive(Debug)] +pub enum AssetsModalActions { + Open, + Select(UpdateListSelectedEvent), + OpenFile, + Close, +} + #[derive(Debug)] pub enum Messages { Input(KeyEvent), @@ -49,11 +57,12 @@ pub enum Messages { ChangeRoute(AppRoute), UpdateQuestionIndex(UpdateQuestionIndexEvent), ScrollQuestion(u16), - UpdateHomeSelected(UpdateHomeSelectedEvent), + UpdateHomeSelected(UpdateListSelectedEvent), UpdateJumpboxValue(u16), ToggleSelection(SelectionFlags), LoadFile, - ModalAction(ModalActions), + SaveModalAction(SaveModalActions), + AssetsModalAction(AssetsModalActions), UnsavedChanges(bool), FileLoaded(Exam), SetOpenMode(OpenMode), diff --git a/src/reducer.rs b/src/reducer.rs index f364dde..866a5e3 100644 --- a/src/reducer.rs +++ b/src/reducer.rs @@ -3,9 +3,11 @@ use crate::event::*; use libflate::gzip::{Decoder, Encoder}; use std::fs::File; use std::io::Read; +use std::process::Command; use std::sync::mpsc; use std::thread; use std::thread::JoinHandle; +use tui::widgets::ListState; pub fn reduce(state: &mut App, event: Messages, tx: mpsc::Sender) -> Option { // Route handler @@ -123,7 +125,7 @@ pub fn reduce(state: &mut App, event: Messages, tx: mpsc::Sender) -> O }; let selected = state.home.list_state.selected(); let next_index = match &evt { - UpdateHomeSelectedEvent::Next => selected + UpdateListSelectedEvent::Next => selected .map(|selected| { if selected < max_index { selected + 1 @@ -132,7 +134,7 @@ pub fn reduce(state: &mut App, event: Messages, tx: mpsc::Sender) -> O } }) .unwrap_or(0), - UpdateHomeSelectedEvent::Prev => selected + UpdateListSelectedEvent::Prev => selected .map(|selected| { if selected > 0 { selected - 1 @@ -141,8 +143,8 @@ pub fn reduce(state: &mut App, event: Messages, tx: mpsc::Sender) -> O } }) .unwrap_or(0), - UpdateHomeSelectedEvent::Home => 0, - UpdateHomeSelectedEvent::End => max_index, + UpdateListSelectedEvent::Home => 0, + UpdateListSelectedEvent::End => max_index, }; state.home.list_state.select(Some(next_index)); None @@ -211,13 +213,13 @@ pub fn reduce(state: &mut App, event: Messages, tx: mpsc::Sender) -> O state.exam = Some(exam); None } - Messages::ModalAction(action) => match action { - ModalActions::Open(modal_state) => { + Messages::SaveModalAction(action) => match action { + SaveModalActions::Open(modal_state) => { state.modal.save_modal_state = modal_state; None } - ModalActions::Quit(quit_action) => { - state.modal.save_modal_state = ModalState::Hidden; + SaveModalActions::Quit(quit_action) => { + state.modal.save_modal_state = SaveModalState::Hidden; match quit_action { QuitAction::BackHome => { @@ -229,13 +231,97 @@ pub fn reduce(state: &mut App, event: Messages, tx: mpsc::Sender) -> O }; None } - ModalActions::Okay => { + SaveModalActions::Okay => { save_state(&state, tx.clone()); - state.modal.save_modal_state = ModalState::Hidden; + state.modal.save_modal_state = SaveModalState::Hidden; None } - ModalActions::Cancel => { - state.modal.save_modal_state = ModalState::Hidden; + SaveModalActions::Cancel => { + state.modal.save_modal_state = SaveModalState::Hidden; + None + } + }, + Messages::AssetsModalAction(action) => match action { + AssetsModalActions::Open => { + let exam = state.exam.as_ref().unwrap(); + let current_item = exam.question_at(exam.display.question_index).unwrap(); + let num_assets = current_item.get_assets().len(); + + if num_assets > 0 { + state.modal.assets_modal_state = AssetsModalState::Show(ListState::default()); + } + None + } + AssetsModalActions::Close => { + state.modal.assets_modal_state = AssetsModalState::Hidden; + None + } + AssetsModalActions::Select(evt) => { + let exam = state.exam.as_ref().unwrap(); + let current_item = exam.question_at(exam.display.question_index).unwrap(); + let assets = current_item.get_assets(); + + let max_index = if assets.len() > 0 { + assets.len() - 1 + } else { + return None; + }; + + let next_index = { + let selected = match &state.modal.assets_modal_state { + AssetsModalState::Show(list_state) => list_state.selected(), + AssetsModalState::Hidden => return None, + }; + + match &evt { + UpdateListSelectedEvent::Next => selected + .map(|selected| { + if selected < max_index { + selected + 1 + } else { + 0 + } + }) + .unwrap_or(0), + UpdateListSelectedEvent::Prev => selected + .map(|selected| { + if selected > 0 { + selected - 1 + } else { + max_index + } + }) + .unwrap_or(0), + UpdateListSelectedEvent::Home => 0, + UpdateListSelectedEvent::End => max_index, + } + }; + + if let AssetsModalState::Show(list_state) = &mut state.modal.assets_modal_state { + list_state.select(Some(next_index)); + }; + None + } + AssetsModalActions::OpenFile => { + let launcher = &state.config.launcher; + let current_path = &state.home.current_path; + + let assets_list_state = match &state.modal.assets_modal_state { + AssetsModalState::Show(list_state) => list_state, + AssetsModalState::Hidden => unreachable!(), + }; + let exam = state.exam.as_ref().unwrap(); + exam.question_at(exam.display.question_index) + .map(|current_item| current_item.get_assets()) + .and_then(|assets| { + let selected = assets_list_state.selected()?; + let current_file = assets.get(selected)?; + Command::new(launcher) + .arg(current_path.join(current_file)) + .spawn() + .ok() + }); + None } }, diff --git a/src/ui.rs b/src/ui.rs index 3c158c2..d48c091 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -24,6 +24,61 @@ use crate::app::*; use crate::event::*; use crate::toggle_buttons::*; +const BG_STYLE: Style = Style { + fg: Color::Reset, + bg: Color::Gray, + modifier: Modifier::empty(), +}; +const BUTTON_STYLE: Style = Style { + fg: Color::White, + bg: Color::Magenta, + modifier: Modifier::empty(), +}; +const BTN_WIDTH: u16 = 8; + +struct ModalRect { + content: Rect, + px: u16, + py: u16, +} + +impl Into for ModalRect { + fn into(self) -> Rect { + Rect { + x: self.content.x + self.px, + y: self.content.y + self.py, + width: self.content.width - self.px * 2, + height: self.content.height - self.py * 2, + } + } +} + +impl ModalRect { + fn new(content: Rect) -> Self { + // When the terminal is at least 30x10 large, set paddings + // around the modal. + let px = if content.width > 30 { + (content.width - 30) / 5 + } else { + 0 + }; + let py = if content.height > 10 { + (content.height - 10) / 3 + } else { + 0 + }; + + Self { content, px, py } + } + + fn btn_pad(&self, num_btns: u16) -> u16 { + let inner_width = self.content.width - self.px * 2 - 2; + + // pad-around + (inner_width - BTN_WIDTH * num_btns) / (num_btns + 1) + } +} + pub struct AppWidget<'a> { app: &'a mut App, } @@ -42,24 +97,35 @@ impl<'a> AppWidget<'a> { // Overlay modals match self.app.modal.save_modal_state { - ModalState::ShowSave => { + SaveModalState::ShowSave => { SaveModalWidget::new(self.app).draw(frame, content); } - ModalState::ShowQuit(_) => { + SaveModalState::ShowQuit(_) => { SaveModalWidget::new(self.app).draw(frame, content); } _ => {} } + match self.app.modal.assets_modal_state { + AssetsModalState::Show(_) => { + AssetsModalWidget::new(self.app).draw(frame, content); + } + _ => {} + } } pub fn propagate(state: &App, event: Messages, tx: mpsc::Sender) -> Option { // Propagation match &event { Messages::Input(key!('Q')) => { + // Do not ask when quit from home + if let AppRoute::Home = state.route { + return None; + }; + state.exam.as_ref().map(|exam| match exam.unsaved_changes { true => tx - .send(Messages::ModalAction(ModalActions::Open( - ModalState::ShowQuit(QuitAction::QuitProgram), + .send(Messages::SaveModalAction(SaveModalActions::Open( + SaveModalState::ShowQuit(QuitAction::QuitProgram), ))) .unwrap(), false => tx.send(Messages::Quit).unwrap(), @@ -69,9 +135,13 @@ impl<'a> AppWidget<'a> { _ => Some(event), } .and_then(|event| match &state.modal.save_modal_state { - ModalState::Hidden => Some(event), + SaveModalState::Hidden => Some(event), _ => SaveModalWidget::propagate(state, event, tx.clone()), }) + .and_then(|event| match &state.modal.assets_modal_state { + AssetsModalState::Hidden => Some(event), + _ => AssetsModalWidget::propagate(state, event, tx.clone()), + }) .and_then(|event| match state.route { AppRoute::Home => HomeWidget::propagate(state, event, tx), AppRoute::DoExam => ExamWidget::propagate(state, event, tx), @@ -89,52 +159,28 @@ impl<'a> SaveModalWidget<'a> { } pub fn draw(&mut self, frame: &mut Frame, content: Rect) { - const BG_STYLE: Style = Style { - fg: Color::Reset, - bg: Color::Gray, - modifier: Modifier::empty(), - }; - const BUTTON_STYLE: Style = Style { - fg: Color::White, - bg: Color::Magenta, - modifier: Modifier::empty(), - }; - const BTN_WIDTH: u16 = 8; - let modal_state = &self.app.modal.save_modal_state; let num_btns = match modal_state { - ModalState::ShowQuit(_) => 3, - ModalState::ShowSave => 2, + SaveModalState::ShowQuit(_) => 3, + SaveModalState::ShowSave => 2, _ => unreachable!(), }; - // When the terminal is at least 30x10 large, set paddings - // around the modal. - let padding_x = if content.width > 30 { - (content.width - 30) / 5 - } else { - 0 - }; - let padding_y = if content.height > 10 { - (content.height - 10) / 3 - } else { - 0 - }; - let inner_width = content.width - padding_x * 2 - 2; - let btn_pad_around = (inner_width - BTN_WIDTH * 2) / (num_btns + 1); + let content = ModalRect::new(content); + let btn_pad = content.btn_pad(num_btns); let layout = Layout::default() - .vertical_margin(padding_y) - .horizontal_margin(padding_x) .direction(Direction::Vertical) .constraints([Constraint::Min(6), Constraint::Length(2)].as_ref()) - .split(content); + .split(content.into()); let filename = self.app.home.get_selected_path().unwrap(); let description_text = match modal_state { - ModalState::ShowSave => format!("Save changes to \"{}\"?", filename.to_str().unwrap()), - ModalState::ShowQuit(_) => format!( + SaveModalState::ShowSave => { + format!("Save changes to \"{}\"?", filename.to_str().unwrap()) + } + SaveModalState::ShowQuit(_) => format!( "Save changes to \"{}\" before quit?", filename.to_str().unwrap() ), @@ -149,17 +195,17 @@ impl<'a> SaveModalWidget<'a> { .block( Block::default() .clean(true) - .borders(Borders::LEFT | Borders::RIGHT | Borders::TOP) + .borders(Borders::ALL - Borders::BOTTOM) .border_style(BG_STYLE), ) .style(BG_STYLE), layout[0], ); - let pad_text = || Text::raw(" ".repeat(btn_pad_around as usize)); + let pad_text = || Text::raw(" ".repeat(btn_pad as usize)); let btn_group = match modal_state { - ModalState::ShowSave => vec![ + SaveModalState::ShowSave => vec![ pad_text(), Text::styled(" ", BUTTON_STYLE), Text::styled("O", BUTTON_STYLE.modifier(Modifier::UNDERLINED)), @@ -170,7 +216,7 @@ impl<'a> SaveModalWidget<'a> { Text::styled("ANCEL ", BUTTON_STYLE), pad_text(), ], - ModalState::ShowQuit(_) => vec![ + SaveModalState::ShowQuit(_) => vec![ pad_text(), Text::styled(" ", BUTTON_STYLE), Text::styled("Q", BUTTON_STYLE.modifier(Modifier::UNDERLINED)), @@ -193,7 +239,7 @@ impl<'a> SaveModalWidget<'a> { .block( Block::default() .clean(true) - .borders(Borders::LEFT | Borders::RIGHT | Borders::BOTTOM) + .borders(Borders::ALL - Borders::TOP) .border_style(BG_STYLE), ) .style(BG_STYLE), @@ -204,42 +250,177 @@ impl<'a> SaveModalWidget<'a> { pub fn propagate(state: &App, event: Messages, tx: mpsc::Sender) -> Option { let modal_state = &state.modal.save_modal_state; match modal_state { - ModalState::ShowQuit(quit_action) => match event { + SaveModalState::ShowQuit(quit_action) => match event { Messages::Input(keyevent) => match keyevent { key!('q') | key!('Q') => { - let action = ModalActions::Quit(quit_action.clone()); - tx.send(Messages::ModalAction(action)).unwrap(); + let action = SaveModalActions::Quit(quit_action.clone()); + tx.send(Messages::SaveModalAction(action)).unwrap(); } key!('o') | key!('O') => { - tx.send(Messages::ModalAction(ModalActions::Okay)) + tx.send(Messages::SaveModalAction(SaveModalActions::Okay)) .and_then(|_| { - let action = ModalActions::Quit(quit_action.clone()); - tx.send(Messages::ModalAction(action)) + let action = SaveModalActions::Quit(quit_action.clone()); + tx.send(Messages::SaveModalAction(action)) }) .unwrap(); } key!('c') | key!('C') | key!(Esc) => { - tx.send(Messages::ModalAction(ModalActions::Cancel)) + tx.send(Messages::SaveModalAction(SaveModalActions::Cancel)) .unwrap(); } _ => {} }, _ => {} }, - ModalState::ShowSave => match event { + SaveModalState::ShowSave => match event { Messages::Input(keyevent) => match keyevent { key!('o') | key!('O') => { - tx.send(Messages::ModalAction(ModalActions::Okay)).unwrap(); + tx.send(Messages::SaveModalAction(SaveModalActions::Okay)) + .unwrap(); } key!('c') | key!('C') | key!(Esc) => { - tx.send(Messages::ModalAction(ModalActions::Cancel)) + tx.send(Messages::SaveModalAction(SaveModalActions::Cancel)) .unwrap(); } _ => {} }, _ => {} }, - ModalState::Hidden => unreachable!(), + SaveModalState::Hidden => unreachable!(), + }; + None // Blocks all other inputs + } +} + +pub struct AssetsModalWidget<'a> { + app: &'a mut App, +} + +impl<'a> AssetsModalWidget<'a> { + pub fn new(app: &'a mut App) -> Self { + AssetsModalWidget { app } + } + + pub fn draw(&mut self, frame: &mut Frame, content: Rect) { + let content = ModalRect::new(content); + let num_btns = 1; + let btn_pad = content.btn_pad(num_btns); + + let assets_list_state = match &mut self.app.modal.assets_modal_state { + AssetsModalState::Show(list_state) => list_state, + AssetsModalState::Hidden => unreachable!(), + }; + let exam = self.app.exam.as_ref().unwrap(); + let current_item = exam.question_at(exam.display.question_index).unwrap(); + let assets = current_item.get_assets(); + + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Length(2), + Constraint::Min(4), + Constraint::Length(2), + ] + .as_ref(), + ) + .split(content.into()); + + // Title + let title_texts = [Text::styled("Assets", BG_STYLE.modifier(Modifier::BOLD))]; + frame.render_widget( + Paragraph::new(title_texts.iter()) + .alignment(Alignment::Center) + .block( + Block::default() + .clean(true) + .borders(Borders::ALL - Borders::BOTTOM) + .border_style(BG_STYLE), + ) + .style(BG_STYLE), + layout[0], + ); + + // Assets list + frame.render_stateful_widget( + List::new(assets.iter().map(|text| Text::raw(text))) + .highlight_symbol(">") + .highlight_style(BG_STYLE) + .block( + Block::default() + .clean(true) + .borders(Borders::LEFT | Borders::RIGHT) + .border_style(BG_STYLE), + ) + .style(BG_STYLE), + layout[1], + assets_list_state, + ); + + // The buttons + let pad_text = || Text::raw(" ".repeat(btn_pad as usize)); + let btn_group = vec![ + pad_text(), + Text::styled(" ", BUTTON_STYLE), + Text::styled("C", BUTTON_STYLE.modifier(Modifier::UNDERLINED)), + Text::styled("LOSE ", BUTTON_STYLE), + pad_text(), + ]; + frame.render_widget( + Paragraph::new(btn_group.iter()) + .block( + Block::default() + .clean(true) + .borders(Borders::ALL - Borders::TOP) + .border_style(BG_STYLE), + ) + .style(BG_STYLE), + layout[2], + ); + } + + pub fn propagate( + _state: &App, + event: Messages, + tx: mpsc::Sender, + ) -> Option { + match event { + Messages::Input(keyevent) => match keyevent { + key!(Enter) => { + tx.send(Messages::AssetsModalAction(AssetsModalActions::OpenFile)) + .unwrap(); + } + key!('c') | key!('C') | key!(Esc) => { + tx.send(Messages::AssetsModalAction(AssetsModalActions::Close)) + .unwrap(); + } + key!('j') | key!(Down) => { + tx.send(Messages::AssetsModalAction(AssetsModalActions::Select( + UpdateListSelectedEvent::Next, + ))) + .unwrap(); + } + key!('k') | key!(Up) => { + tx.send(Messages::AssetsModalAction(AssetsModalActions::Select( + UpdateListSelectedEvent::Prev, + ))) + .unwrap(); + } + key!('g') => { + tx.send(Messages::AssetsModalAction(AssetsModalActions::Select( + UpdateListSelectedEvent::Home, + ))) + .unwrap(); + } + key!('G') => { + tx.send(Messages::AssetsModalAction(AssetsModalActions::Select( + UpdateListSelectedEvent::End, + ))) + .unwrap(); + } + _ => {} + }, + _ => {} }; None // Blocks all other inputs } @@ -347,22 +528,22 @@ impl<'a> HomeWidget<'a> { None } key!('j') | key!(Down) => { - tx.send(Messages::UpdateHomeSelected(UpdateHomeSelectedEvent::Next)) + tx.send(Messages::UpdateHomeSelected(UpdateListSelectedEvent::Next)) .unwrap(); None } key!('k') | key!(Up) => { - tx.send(Messages::UpdateHomeSelected(UpdateHomeSelectedEvent::Prev)) + tx.send(Messages::UpdateHomeSelected(UpdateListSelectedEvent::Prev)) .unwrap(); None } key!('g') => { - tx.send(Messages::UpdateHomeSelected(UpdateHomeSelectedEvent::Home)) + tx.send(Messages::UpdateHomeSelected(UpdateListSelectedEvent::Home)) .unwrap(); None } key!('G') => { - tx.send(Messages::UpdateHomeSelected(UpdateHomeSelectedEvent::End)) + tx.send(Messages::UpdateHomeSelected(UpdateListSelectedEvent::End)) .unwrap(); None } @@ -379,13 +560,6 @@ impl<'a> HomeWidget<'a> { tx.send(Messages::Quit).unwrap(); None } - key!(^'s') => { - tx.send(Messages::ModalAction(ModalActions::Open( - ModalState::ShowSave, - ))) - .unwrap(); - None - } _ => Some(event), }, _ => Some(event), @@ -528,18 +702,25 @@ impl<'a> ExamWidget<'a> { let exam = state.exam.as_ref().unwrap(); match exam.unsaved_changes { true => tx - .send(Messages::ModalAction(ModalActions::Open( - ModalState::ShowQuit(QuitAction::BackHome), + .send(Messages::SaveModalAction(SaveModalActions::Open( + SaveModalState::ShowQuit(QuitAction::BackHome), ))) .unwrap(), false => tx.send(Messages::ChangeRoute(AppRoute::Home)).unwrap(), } return None; } + Messages::Input(key!(^'s')) => { + tx.send(Messages::SaveModalAction(SaveModalActions::Open( + SaveModalState::ShowSave, + ))) + .unwrap(); + return None; + } _ => {} }; - ExamItemsWidget::propagate(state, event, tx.clone()) - .and_then(|event| JumpBarWidget::propagate(state, event, tx.clone())) + JumpBarWidget::propagate(state, event, tx.clone()) + .and_then(|event| ExamItemsWidget::propagate(state, event, tx.clone())) .and_then(|event| ItemWidget::propagate(state, event, tx)) } } @@ -569,13 +750,23 @@ impl<'a> ItemWidget<'a> { pub fn propagate(state: &App, event: Messages, tx: mpsc::Sender) -> Option { let exam = state.exam.as_ref().unwrap(); - match exam.question_at(exam.display.question_index) { - Some(item) => match item { - Item::Question(_) => QuestionWidget::propagate(state, event, tx), - _ => Some(event), - }, + match event { + Messages::Input(key!('t')) => { + tx.send(Messages::AssetsModalAction(AssetsModalActions::Open)) + .unwrap(); + None + } _ => Some(event), } + .and_then( + |event| match exam.question_at(exam.display.question_index) { + Some(item) => match item { + Item::Question(_) => QuestionWidget::propagate(state, event, tx), + _ => Some(event), + }, + _ => Some(event), + }, + ) } } @@ -595,10 +786,16 @@ impl<'a> QuestionWidget<'a> { } pub fn draw(&mut self, frame: &mut Frame, content: Rect) { + const ASSETS_STYLE: Style = Style { + fg: Color::Red, + bg: Color::Reset, + modifier: Modifier::empty(), + }; + let exam = &self.app.exam.as_ref().unwrap(); let question_title = format!( "Question ({}/{})", &self.display.question_index + 1, - &self.app.exam.as_ref().unwrap().num_questions() + exam.num_questions() ); const WRAPPER_SELECT: [&str; 2] = ["(", ")"]; const WRAPPER_MULTSEL: [&str; 2] = ["[", "]"]; @@ -608,6 +805,19 @@ impl<'a> QuestionWidget<'a> { WRAPPER_MULTSEL }; + // Adds indicator for assets + let mut question_display = vec![Text::raw(&self.question.question)]; + let current_item = exam.question_at(exam.display.question_index).unwrap(); + let num_assets = current_item.get_assets().len(); + if num_assets > 0 { + question_display.push(Text::styled("\n\n[Asse", ASSETS_STYLE)); + question_display.push(Text::styled( + "t", + ASSETS_STYLE.modifier(Modifier::UNDERLINED), + )); + question_display.push(Text::styled(format!("s: {}]", num_assets), ASSETS_STYLE)); + } + // Question + Selections let two_chunks = Layout::default() .direction(Direction::Vertical) @@ -626,11 +836,11 @@ impl<'a> QuestionWidget<'a> { ) .split(content); - match self.app.exam.as_ref().unwrap().display.display_answer { + match exam.display.display_answer { false => { // Question frame.render_widget( - Paragraph::new([Text::raw(&self.question.question)].iter()) + Paragraph::new(question_display.iter()) .block( Block::default() .borders(Borders::ALL)