Skip to content

Commit

Permalink
feat(tui): right click to return to previous view
Browse files Browse the repository at this point in the history
right click with the mouse over the ContentView to return to the previous ActiveView.

there is currently no keybind to perform this action

uses a stack (implemented as a vec) to track previous Views so they can be returned to.
  • Loading branch information
AnthonyMichaelTDM committed Oct 15, 2024
1 parent 960f022 commit 08384bc
Show file tree
Hide file tree
Showing 14 changed files with 201 additions and 111 deletions.
10 changes: 9 additions & 1 deletion tui/src/state/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub enum Action {
/// Actions that effect the library state store.
Library(LibraryAction),
/// Actions that effect the current view.
SetCurrentView(ActiveView),
ActiveView(ViewAction),
/// Actions regarding popups
Popup(PopupAction),
/// Actions that change the active component
Expand Down Expand Up @@ -108,6 +108,14 @@ pub enum LibraryAction {
CreatePlaylistAndAddThings(String, Vec<Thing>),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ViewAction {
/// Set the active view
Set(ActiveView),
/// Return to a previous view
Back,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PopupAction {
/// Open a popup
Expand Down
6 changes: 3 additions & 3 deletions tui/src/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ struct Senders {
pub audio: UnboundedSender<action::AudioAction>,
pub search: UnboundedSender<String>,
pub library: UnboundedSender<action::LibraryAction>,
pub view: UnboundedSender<ActiveView>,
pub view: UnboundedSender<action::ViewAction>,
pub popup: UnboundedSender<action::PopupAction>,
pub component: UnboundedSender<action::ComponentAction>,
}
Expand Down Expand Up @@ -172,8 +172,8 @@ impl Dispatcher {
Action::Library(action) => {
senders.library.send(action)?;
}
Action::SetCurrentView(view) => {
senders.view.send(view)?;
Action::ActiveView(action) => {
senders.view.send(action)?;
}
Action::Popup(popup) => senders.popup.send(popup)?,
Action::ActiveComponent(action) => {
Expand Down
70 changes: 66 additions & 4 deletions tui/src/state/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use tokio::sync::{

use crate::{termination::Interrupted, ui::components::content_view::ActiveView};

use super::action::ViewAction;

/// The `ViewStore` is responsible for managing the `CurrentView` to be displayed.
#[allow(clippy::module_name_repetitions)]
pub struct ViewState {
Expand All @@ -28,21 +30,23 @@ impl ViewState {
/// Fails if the state cannot be sent
pub async fn main_loop(
&self,
mut action_rx: UnboundedReceiver<ActiveView>,
mut action_rx: UnboundedReceiver<ViewAction>,
mut interrupt_rx: broadcast::Receiver<Interrupted>,
) -> anyhow::Result<Interrupted> {
let mut state = ActiveView::default();
// a stack to keep track of previous views
let mut view_stack = Vec::new();

// the initial state once
self.state_tx.send(state)?;
self.state_tx.send(state.clone())?;

let result = loop {
tokio::select! {
// Handle the actions coming from the UI
// and process them to do async operations
Some(action) = action_rx.recv() => {
state = action;
self.state_tx.send(state)?;
state = self.handle_action(&state, &mut view_stack, action);
self.state_tx.send(state.clone())?;
},
// Catch and handle interrupt signal to gracefully shutdown
Ok(interrupted) = interrupt_rx.recv() => {
Expand All @@ -53,4 +57,62 @@ impl ViewState {

Ok(result)
}

/// Handle the action, returning the new state
pub fn handle_action(
&self,
state: &ActiveView,
view_stack: &mut Vec<ActiveView>,
action: ViewAction,
) -> ActiveView {
match action {
ViewAction::Set(view) => {
view_stack.push(state.clone());
view
}
ViewAction::Back => view_stack.pop().unwrap_or_default(),
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;

#[test]
fn test_handle_action() {
let (view, _) = ViewState::new();

let mut view_stack = Vec::new();

let mut state = view.handle_action(
&ActiveView::default(),
&mut view_stack,
ViewAction::Set(ActiveView::Search),
);
assert_eq!(state, ActiveView::Search);

state = view.handle_action(&state, &mut view_stack, ViewAction::Set(ActiveView::Songs));
assert_eq!(state, ActiveView::Songs);

state = view.handle_action(
&state,
&mut view_stack,
ViewAction::Set(ActiveView::Artists),
);
assert_eq!(state, ActiveView::Artists);

state = view.handle_action(&state, &mut view_stack, ViewAction::Back);
assert_eq!(state, ActiveView::Songs);

state = view.handle_action(&state, &mut view_stack, ViewAction::Back);
assert_eq!(state, ActiveView::Search);

state = view.handle_action(&state, &mut view_stack, ViewAction::Back);
assert_eq!(state, ActiveView::default());

state = view.handle_action(&state, &mut view_stack, ViewAction::Back);
assert_eq!(state, ActiveView::default());
}
}
28 changes: 19 additions & 9 deletions tui/src/ui/components/content_view/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use views::{

use crate::{
state::{
action::{Action, ComponentAction},
action::{Action, ComponentAction, ViewAction},
component::ActiveComponent,
},
ui::AppState,
Expand Down Expand Up @@ -204,14 +204,24 @@ impl Component for ContentView {
mouse: crossterm::event::MouseEvent,
area: ratatui::prelude::Rect,
) {
if mouse.kind == MouseEventKind::Down(MouseButton::Left)
&& area.contains(Position::new(mouse.column, mouse.row))
{
self.action_tx
.send(Action::ActiveComponent(ComponentAction::Set(
ActiveComponent::ContentView,
)))
.unwrap();
let mouse_position = Position::new(mouse.column, mouse.row);
match mouse.kind {
// this doesn't return because the active view may want to do something as well
MouseEventKind::Down(MouseButton::Left) if area.contains(mouse_position) => {
self.action_tx
.send(Action::ActiveComponent(ComponentAction::Set(
ActiveComponent::ContentView,
)))
.unwrap();
}
// this returns because the active view should handle the event (since it changes the active view)
MouseEventKind::Down(MouseButton::Right) if area.contains(mouse_position) => {
self.action_tx
.send(Action::ActiveView(ViewAction::Back))

Check warning on line 220 in tui/src/ui/components/content_view/mod.rs

View check run for this annotation

Codecov / codecov/patch

tui/src/ui/components/content_view/mod.rs#L218-L220

Added lines #L218 - L220 were not covered by tests
.unwrap();
return;
}
_ => {}
}

// defer to active view
Expand Down
31 changes: 17 additions & 14 deletions tui/src/ui/components/content_view/views/album.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use ratatui::{
use tokio::sync::mpsc::UnboundedSender;

use crate::{
state::action::{Action, AudioAction, PopupAction, QueueAction},
state::action::{Action, AudioAction, PopupAction, QueueAction, ViewAction},
ui::{
colors::{BORDER_FOCUSED, BORDER_UNFOCUSED, TEXT_HIGHLIGHT},
components::{content_view::ActiveView, Component, ComponentRender, RenderProps},
Expand Down Expand Up @@ -124,7 +124,7 @@ impl Component for LibraryAlbumsView {

if let Some(thing) = things {
self.action_tx
.send(Action::SetCurrentView(thing.into()))
.send(Action::ActiveView(ViewAction::Set(thing.into())))
.unwrap();
}
}
Expand All @@ -143,9 +143,9 @@ impl Component for LibraryAlbumsView {
let things = self.tree_state.lock().unwrap().get_checked_things();
if !things.is_empty() {
self.action_tx
.send(Action::SetCurrentView(ActiveView::Radio(
.send(Action::ActiveView(ViewAction::Set(ActiveView::Radio(
things, RADIO_SIZE,
)))
))))
.unwrap();
}
}
Expand Down Expand Up @@ -520,10 +520,10 @@ mod item_view_tests {
view.handle_key_event(KeyEvent::from(KeyCode::Char('r')));
assert_eq!(
rx.blocking_recv().unwrap(),
Action::SetCurrentView(ActiveView::Radio(
Action::ActiveView(ViewAction::Set(ActiveView::Radio(
vec![("album", item_id()).into()],
RADIO_SIZE
))
)))
);
view.handle_key_event(KeyEvent::from(KeyCode::Char('p')));
assert_eq!(
Expand All @@ -547,7 +547,7 @@ mod item_view_tests {
view.handle_key_event(KeyEvent::from(KeyCode::Enter));
assert_eq!(
rx.blocking_recv().unwrap(),
Action::SetCurrentView(ActiveView::Song(item_id()))
Action::ActiveView(ViewAction::Set(ActiveView::Song(item_id())))
);

// check the item
Expand All @@ -568,10 +568,10 @@ mod item_view_tests {
view.handle_key_event(KeyEvent::from(KeyCode::Char('r')));
assert_eq!(
rx.blocking_recv().unwrap(),
Action::SetCurrentView(ActiveView::Radio(
Action::ActiveView(ViewAction::Set(ActiveView::Radio(
vec![("song", item_id()).into()],
RADIO_SIZE
))
)))
);

// add to playlist
Expand Down Expand Up @@ -672,7 +672,7 @@ mod item_view_tests {
);
assert_eq!(
rx.blocking_recv().unwrap(),
Action::SetCurrentView(ActiveView::Artist(item_id()))
Action::ActiveView(ViewAction::Set(ActiveView::Artist(item_id())))
);
let buffer = terminal
.draw(|frame| view.render(frame, props))
Expand Down Expand Up @@ -875,7 +875,10 @@ mod library_view_tests {
// open
view.handle_key_event(KeyEvent::from(KeyCode::Enter));
let action = rx.blocking_recv().unwrap();
assert_eq!(action, Action::SetCurrentView(ActiveView::Album(item_id())));
assert_eq!(
action,
Action::ActiveView(ViewAction::Set(ActiveView::Album(item_id())))
);

// there are checked items
view.handle_key_event(KeyEvent::from(KeyCode::Char(' ')));
Expand All @@ -897,10 +900,10 @@ mod library_view_tests {
let action = rx.blocking_recv().unwrap();
assert_eq!(
action,
Action::SetCurrentView(ActiveView::Radio(
Action::ActiveView(ViewAction::Set(ActiveView::Radio(
vec![("album", item_id()).into()],
RADIO_SIZE
))
)))
);

// add to playlist
Expand Down Expand Up @@ -1013,7 +1016,7 @@ mod library_view_tests {
);
assert_eq!(
rx.blocking_recv().unwrap(),
Action::SetCurrentView(ActiveView::Album(item_id()))
Action::ActiveView(ViewAction::Set(ActiveView::Album(item_id())))
);
}
}
28 changes: 14 additions & 14 deletions tui/src/ui/components/content_view/views/artist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use ratatui::{
use tokio::sync::mpsc::UnboundedSender;

use crate::{
state::action::{Action, AudioAction, PopupAction, QueueAction},
state::action::{Action, AudioAction, PopupAction, QueueAction, ViewAction},
ui::{
colors::{BORDER_FOCUSED, BORDER_UNFOCUSED, TEXT_HIGHLIGHT},
components::{content_view::ActiveView, Component, ComponentRender, RenderProps},
Expand Down Expand Up @@ -123,7 +123,7 @@ impl Component for LibraryArtistsView {

if let Some(thing) = things {
self.action_tx
.send(Action::SetCurrentView(thing.into()))
.send(Action::ActiveView(ViewAction::Set(thing.into())))
.unwrap();
}
}
Expand All @@ -142,9 +142,9 @@ impl Component for LibraryArtistsView {
let things = self.tree_state.lock().unwrap().get_checked_things();
if !things.is_empty() {
self.action_tx
.send(Action::SetCurrentView(ActiveView::Radio(
.send(Action::ActiveView(ViewAction::Set(ActiveView::Radio(
things, RADIO_SIZE,
)))
))))
.unwrap();
}
}
Expand Down Expand Up @@ -498,10 +498,10 @@ mod item_view_tests {
view.handle_key_event(KeyEvent::from(KeyCode::Char('r')));
assert_eq!(
rx.blocking_recv().unwrap(),
Action::SetCurrentView(ActiveView::Radio(
Action::ActiveView(ViewAction::Set(ActiveView::Radio(
vec![("artist", item_id()).into()],
RADIO_SIZE
))
)))
);
view.handle_key_event(KeyEvent::from(KeyCode::Char('p')));
assert_eq!(
Expand All @@ -525,7 +525,7 @@ mod item_view_tests {
view.handle_key_event(KeyEvent::from(KeyCode::Enter));
assert_eq!(
rx.blocking_recv().unwrap(),
Action::SetCurrentView(ActiveView::Song(item_id()))
Action::ActiveView(ViewAction::Set(ActiveView::Song(item_id())))
);

// check the item
Expand All @@ -546,10 +546,10 @@ mod item_view_tests {
view.handle_key_event(KeyEvent::from(KeyCode::Char('r')));
assert_eq!(
rx.blocking_recv().unwrap(),
Action::SetCurrentView(ActiveView::Radio(
Action::ActiveView(ViewAction::Set(ActiveView::Radio(
vec![("song", item_id()).into()],
RADIO_SIZE
))
)))
);

// add to playlist
Expand Down Expand Up @@ -650,7 +650,7 @@ mod item_view_tests {
);
assert_eq!(
rx.blocking_recv().unwrap(),
Action::SetCurrentView(ActiveView::Album(item_id()))
Action::ActiveView(ViewAction::Set(ActiveView::Album(item_id())))
);
let buffer = terminal
.draw(|frame| view.render(frame, props))
Expand Down Expand Up @@ -847,7 +847,7 @@ mod library_view_tests {
let action = rx.blocking_recv().unwrap();
assert_eq!(
action,
Action::SetCurrentView(ActiveView::Artist(item_id()))
Action::ActiveView(ViewAction::Set(ActiveView::Artist(item_id())))
);

// there are checked items
Expand All @@ -870,10 +870,10 @@ mod library_view_tests {
let action = rx.blocking_recv().unwrap();
assert_eq!(
action,
Action::SetCurrentView(ActiveView::Radio(
Action::ActiveView(ViewAction::Set(ActiveView::Radio(
vec![("artist", item_id()).into()],
RADIO_SIZE
))
)))
);

// add to playlist
Expand Down Expand Up @@ -986,7 +986,7 @@ mod library_view_tests {
);
assert_eq!(
rx.blocking_recv().unwrap(),
Action::SetCurrentView(ActiveView::Artist(item_id()))
Action::ActiveView(ViewAction::Set(ActiveView::Artist(item_id())))
);
}
}
Loading

0 comments on commit 08384bc

Please sign in to comment.