mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-05 14:35:20 -04:00
feat(multiverse): Settings view
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -2,4 +2,5 @@ pub mod help;
|
||||
pub mod recovery;
|
||||
pub mod room_list;
|
||||
pub mod room_view;
|
||||
pub mod settings;
|
||||
pub mod status;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
104
labs/multiverse/src/widgets/settings/developer.rs
Normal file
104
labs/multiverse/src/widgets/settings/developer.rs
Normal file
@@ -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<usize> 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<SyncService>,
|
||||
state: ListState,
|
||||
}
|
||||
|
||||
impl DeveloperSettingsView {
|
||||
pub fn new(client: Client, sync_service: Arc<SyncService>) -> 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);
|
||||
}
|
||||
}
|
||||
205
labs/multiverse/src/widgets/settings/mod.rs
Normal file
205
labs/multiverse/src/widgets/settings/mod.rs
Normal file
@@ -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<SyncService>) -> 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);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user