Skip to content

Commit

Permalink
Merge pull request #166 from AnthonyMichaelTDM/163-feattui-support-re…
Browse files Browse the repository at this point in the history
…turning-to-previous-view

feat(tui): right click to return to previous view
  • Loading branch information
AnthonyMichaelTDM authored Oct 15, 2024
2 parents 960f022 + 08384bc commit d64a210
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))
.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 d64a210

Please sign in to comment.