diff --git a/labs/multiverse/src/main.rs b/labs/multiverse/src/main.rs index a69c3ca29..97d8be462 100644 --- a/labs/multiverse/src/main.rs +++ b/labs/multiverse/src/main.rs @@ -31,10 +31,7 @@ use ratatui::{prelude::*, style::palette::tailwind, widgets::*}; use tokio::{spawn, task::JoinHandle}; use tracing::{error, warn}; use tracing_subscriber::EnvFilter; -use widgets::{ - recovery::{RecoveryView, RecoveryViewState}, - room_view::RoomView, -}; +use widgets::{room_view::RoomView, settings::SettingsView}; use crate::widgets::{ help::HelpView, @@ -74,8 +71,8 @@ pub enum GlobalMode { Default, /// Mode where we have opened the help screen. Help, - /// Mode where we have opened the recovery screen. - Recovery { state: RecoveryViewState }, + /// Mode where we have opened the settings screen. + Settings { view: SettingsView }, } /// Helper function to create a centered rect using up certain percentage of the @@ -316,8 +313,8 @@ impl App { match (key.modifiers, key.code) { (KeyModifiers::NONE, F(1)) => self.set_global_mode(GlobalMode::Help), - (KeyModifiers::NONE, F(10)) => self.set_global_mode(GlobalMode::Recovery { - state: RecoveryViewState::new(self.client.clone()), + (KeyModifiers::NONE, F(10)) => self.set_global_mode(GlobalMode::Settings { + view: SettingsView::new(self.client.clone(), self.sync_service.clone()), }), (KeyModifiers::CONTROL, Char('j') | Down) => { @@ -343,32 +340,14 @@ impl App { _ => self.room_view.handle_key_press(key).await, } - // TODO: Remove the remaining keys. - if key.kind == KeyEventKind::Press && key.modifiers == KeyModifiers::NONE { - match key.code { - Char('s') => self.sync_service.start().await, - - Char('S') => self.sync_service.stop().await, - - Char('Q') => { - let q = self.client.send_queue(); - let enabled = q.is_enabled(); - q.set_enabled(!enabled).await; - } - - _ => {} - } - } - Ok(false) } fn on_tick(&mut self) { match &mut self.state.global_mode { GlobalMode::Help | GlobalMode::Default => {} - - GlobalMode::Recovery { state } => { - state.on_tick(); + GlobalMode::Settings { view } => { + view.on_tick(); } } } @@ -393,8 +372,8 @@ impl App { } _ => (), }, - GlobalMode::Recovery { state } => { - if state.handle_key_press(key) { + GlobalMode::Settings { view } => { + if view.handle_key_press(key).await { self.set_global_mode(GlobalMode::Default); } } @@ -453,9 +432,8 @@ impl Widget for &mut App { match &mut self.state.global_mode { GlobalMode::Default => {} - GlobalMode::Recovery { state } => { - let mut recovery_view = RecoveryView::new(); - recovery_view.render(area, buf, state); + GlobalMode::Settings { view } => { + view.render(area, buf); } GlobalMode::Help => { let mut help_view = HelpView::new(); diff --git a/labs/multiverse/src/widgets/mod.rs b/labs/multiverse/src/widgets/mod.rs index fe1a005af..882f7cda7 100644 --- a/labs/multiverse/src/widgets/mod.rs +++ b/labs/multiverse/src/widgets/mod.rs @@ -2,4 +2,5 @@ pub mod help; pub mod recovery; pub mod room_list; pub mod room_view; +pub mod settings; pub mod status; diff --git a/labs/multiverse/src/widgets/recovery/default.rs b/labs/multiverse/src/widgets/recovery/default.rs index 1d3182015..94acc5029 100644 --- a/labs/multiverse/src/widgets/recovery/default.rs +++ b/labs/multiverse/src/widgets/recovery/default.rs @@ -80,7 +80,7 @@ impl DefaultRecoveryView { match self.mode { Mode::Default => match key.code { - KeyCode::Esc => Yes, + KeyCode::Esc | KeyCode::Char('q') => Yes, KeyCode::Char('j') | KeyCode::Down => { self.state.select_next(); No diff --git a/labs/multiverse/src/widgets/recovery/mod.rs b/labs/multiverse/src/widgets/recovery/mod.rs index ad3f9707e..9e3a022b9 100644 --- a/labs/multiverse/src/widgets/recovery/mod.rs +++ b/labs/multiverse/src/widgets/recovery/mod.rs @@ -1,9 +1,8 @@ use crossterm::event::{KeyCode, KeyEvent}; -use layout::Flex; use matrix_sdk::{encryption::recovery::RecoveryState, Client}; use ratatui::{ prelude::*, - widgets::{Block, Borders, Clear, Padding}, + widgets::{Block, Borders, Padding}, }; use recovering::RecoveringView; use throbber_widgets_tui::{Throbber, ThrobberState}; @@ -104,7 +103,7 @@ impl RecoveryViewState { match &mut self.mode { Mode::Unknown => match (key.modifiers, key.code) { - (_, Esc) => true, + (_, Esc | Char('q')) => true, _ => false, }, Mode::Incomplete { view } => match view.handle_key(key) { @@ -163,23 +162,6 @@ impl StatefulWidget for &mut RecoveryView { fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { state.update_state(); - // Create a centered popout with 8 lines and 70 rows. This size is picked so the - // recovery view looks a bit like a settings screen. - let vertical = - Layout::vertical([Constraint::Fill(1), Constraint::Length(10), Constraint::Fill(1)]) - .flex(Flex::Center); - - let horizontal = - Layout::horizontal([Constraint::Fill(1), Constraint::Min(50), Constraint::Fill(1)]) - .flex(Flex::Center); - - let [_, area, _] = vertical.areas(area); - let [_, area, _] = horizontal.areas(area); - - // Clear that part of the screen so we can draw our bounding block and other - // widgets inside the block. - Clear.render(area, buf); - // Render our block, mainly for the border. let block = Block::bordered() .title(Line::from("Encryption").centered()) diff --git a/labs/multiverse/src/widgets/settings/developer.rs b/labs/multiverse/src/widgets/settings/developer.rs new file mode 100644 index 000000000..360edb6c2 --- /dev/null +++ b/labs/multiverse/src/widgets/settings/developer.rs @@ -0,0 +1,104 @@ +use std::sync::Arc; + +use crossterm::event::{KeyCode, KeyEvent}; +use matrix_sdk::Client; +use matrix_sdk_ui::sync_service::{self, SyncService}; +use ratatui::{ + prelude::*, + widgets::{HighlightSpacing, *}, +}; + +// TODO: This replicates a lot of the logic the details view has, we should make +// a generic tab popout widget to share a bit of logic here. + +enum MenuEntries { + Sync = 0, + SendQueue = 1, +} + +impl From for MenuEntries { + fn from(value: usize) -> Self { + match value { + 0 => MenuEntries::Sync, + 1 => MenuEntries::SendQueue, + _ => unreachable!("The developer settings view has only 2 options"), + } + } +} + +pub struct DeveloperSettingsView { + client: Client, + sync_service: Arc, + state: ListState, +} + +impl DeveloperSettingsView { + pub fn new(client: Client, sync_service: Arc) -> Self { + Self { client, sync_service, state: ListState::default() } + } + + pub async fn handle_key_press(&mut self, key: KeyEvent) { + use MenuEntries::*; + + match key.code { + KeyCode::Char('j') | KeyCode::Down => { + self.state.select_next(); + } + KeyCode::Char('k') | KeyCode::Up => { + self.state.select_previous(); + } + + KeyCode::Enter | KeyCode::Char(' ') => { + if let Some(selected) = self.state.selected() { + match selected.into() { + Sync => { + let sync_service = &self.sync_service; + + match sync_service.state().get() { + sync_service::State::Running => sync_service.stop().await, + sync_service::State::Idle + | sync_service::State::Terminated + | sync_service::State::Error + | sync_service::State::Offline => sync_service.start().await, + } + } + SendQueue => { + let send_queue = self.client.send_queue(); + let enabled = send_queue.is_enabled(); + send_queue.set_enabled(!enabled).await + } + } + } + } + + _ => (), + } + } +} + +impl Widget for &mut DeveloperSettingsView { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + let sync_item = match self.sync_service.state().get() { + sync_service::State::Running => ListItem::new("Sync [x]"), + sync_service::State::Idle + | sync_service::State::Terminated + | sync_service::State::Error + | sync_service::State::Offline => ListItem::new("Sync [ ]"), + }; + + let send_queue_item = if self.client.send_queue().is_enabled() { + ListItem::new("Send Queue [x]") + } else { + ListItem::new("Send Queue [ ]") + }; + + let list = List::new(vec![sync_item, send_queue_item]) + .highlight_symbol("> ") + .highlight_spacing(HighlightSpacing::Always); + + StatefulWidget::render(list, area, buf, &mut self.state); + } +} diff --git a/labs/multiverse/src/widgets/settings/mod.rs b/labs/multiverse/src/widgets/settings/mod.rs new file mode 100644 index 000000000..25628614e --- /dev/null +++ b/labs/multiverse/src/widgets/settings/mod.rs @@ -0,0 +1,205 @@ +use std::sync::Arc; + +use crossterm::event::{KeyCode, KeyEvent}; +use developer::DeveloperSettingsView; +use matrix_sdk::Client; +use matrix_sdk_ui::sync_service::SyncService; +use ratatui::{prelude::*, widgets::*}; +use strum::{Display, EnumIter, FromRepr, IntoEnumIterator}; +use style::palette::tailwind; + +use super::recovery::{RecoveryView, RecoveryViewState}; +use crate::popup_area; + +mod developer; + +// TODO: This replicates a lot of the logic the details view has, we should make +// a generic tab popout widget to share a bit of logic here. + +#[derive(Clone, Copy, Default, Display, FromRepr, EnumIter)] +enum SelectedTab { + /// Show the developer settings we have. + #[default] + Developer, + + /// Show the encryption settings + Encryption, +} + +impl SelectedTab { + /// Get the previous tab, if there is no previous tab return the current + /// tab. + fn previous(self) -> Self { + let current_index: usize = self as usize; + let previous_index = current_index.saturating_sub(1); + Self::from_repr(previous_index).unwrap_or(self) + } + + /// Get the next tab, if there is no next tab return the current tab. + fn next(self) -> Self { + let current_index = self as usize; + let next_index = current_index.saturating_add(1); + Self::from_repr(next_index).unwrap_or(self) + } + + /// Cycle to the next tab, if we're at the last tab we return the first and + /// default tab. + fn cycle_next(self) -> Self { + let current_index = self as usize; + let next_index = current_index.saturating_add(1); + Self::from_repr(next_index).unwrap_or(SelectedTab::default()) + } + + /// Cycle to the previous tab, if we're at the first tab we return the last + /// tab. + fn cycle_prev(self) -> Self { + let current_index = self as usize; + + if current_index == 0 { + Self::iter().last().expect("We should always have a last element in our enum") + } else { + let previous_index = current_index.saturating_sub(1); + Self::from_repr(previous_index).unwrap_or(self) + } + } + + /// Return tab's name as a styled `Line` + fn title(self) -> Line<'static> { + format!(" {self} ").fg(tailwind::SLATE.c200).bg(self.palette().c900).into() + } + + const fn palette(&self) -> tailwind::Palette { + match self { + Self::Developer => tailwind::BLUE, + Self::Encryption => tailwind::EMERALD, + } + } +} + +pub struct SettingsView { + selected_tab: SelectedTab, + + developer_settings_view: DeveloperSettingsView, + recovery_view_state: RecoveryViewState, +} + +impl SettingsView { + pub fn new(client: Client, sync_service: Arc) -> Self { + let recovery_view_state = RecoveryViewState::new(client.clone()); + let developer_settings_view = DeveloperSettingsView::new(client, sync_service); + + Self { selected_tab: SelectedTab::default(), recovery_view_state, developer_settings_view } + } + + pub async fn handle_key_press(&mut self, event: KeyEvent) -> bool { + use KeyCode::*; + + match event.code { + Char('l') | Right => { + self.next_tab(); + false + } + + Tab => { + self.cycle_next_tab(); + false + } + + BackTab => { + self.cycle_prev_tab(); + false + } + + Char('h') | Left => { + self.previous_tab(); + false + } + + Char('q') | Esc => match self.selected_tab { + SelectedTab::Developer => true, + SelectedTab::Encryption => self.recovery_view_state.handle_key_press(event), + }, + + _ => match self.selected_tab { + SelectedTab::Developer => { + self.developer_settings_view.handle_key_press(event).await; + false + } + SelectedTab::Encryption => self.recovery_view_state.handle_key_press(event), + }, + } + } + + pub fn on_tick(&mut self) { + self.recovery_view_state.on_tick(); + } + + fn cycle_next_tab(&mut self) { + self.selected_tab = self.selected_tab.cycle_next(); + } + + fn cycle_prev_tab(&mut self) { + self.selected_tab = self.selected_tab.cycle_prev(); + } + + fn next_tab(&mut self) { + self.selected_tab = self.selected_tab.next(); + } + + fn previous_tab(&mut self) { + self.selected_tab = self.selected_tab.previous(); + } +} + +impl<'a> Widget for &'a mut SettingsView { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + use Constraint::{Length, Min}; + + let area = popup_area(area, 40, 30); + Clear.render(area, buf); + + let vertical = Layout::vertical([Length(1), Min(0), Length(1)]); + let [header_area, inner_area, footer_area] = vertical.areas(area); + + let horizontal = Layout::horizontal([Min(0), Length(20)]); + let [tab_title_area, title_area] = horizontal.areas(header_area); + + "Settings".bold().render(title_area, buf); + + Block::bordered() + .border_set(symbols::border::PROPORTIONAL_TALL) + .padding(Padding::horizontal(1)) + .border_style(tailwind::BLUE.c700) + .render(inner_area, buf); + + let titles = SelectedTab::iter().map(SelectedTab::title); + let highlight_style = (Color::default(), self.selected_tab.palette().c700); + let selected_tab_index = self.selected_tab as usize; + + let tabs_area = inner_area.inner(Margin::new(1, 1)); + + Tabs::new(titles) + .highlight_style(highlight_style) + .select(selected_tab_index) + .padding("", "") + .divider(" ") + .render(tab_title_area, buf); + + match self.selected_tab { + SelectedTab::Developer => { + self.developer_settings_view.render(tabs_area, buf); + } + SelectedTab::Encryption => { + let mut view = RecoveryView::new(); + view.render(tabs_area, buf, &mut self.recovery_view_state); + } + } + + Line::raw("◄ ► to change tab | Press q to exit the settings screen") + .centered() + .render(footer_area, buf); + } +} diff --git a/labs/multiverse/src/widgets/status.rs b/labs/multiverse/src/widgets/status.rs index 420faf2b9..0ef1175b7 100644 --- a/labs/multiverse/src/widgets/status.rs +++ b/labs/multiverse/src/widgets/status.rs @@ -122,7 +122,7 @@ impl StatefulWidget for &mut Status { match global_mode { GlobalMode::Help => "Press q to exit the help screen", - GlobalMode::Recovery { .. } => "Press ESC to exit the recovery screen", + GlobalMode::Settings { .. } => "Press ESC to exit the settings screen", GlobalMode::Default => "Press F1 to show the help screen", } };