Skip to main content

rsynko_ui/
menu.rs

1use crate::*;
2use alux_ext::ext;
3use rsynko_manager::*;
4
5/// States one offered menu entry: what reaches it, what it does, and whether it means anything.
6pub struct MenuItem {
7    /// Names the action the entry performs.
8    pub action: ManagerAction,
9    /// Names the keys reaching the entry, the one a reader tries first at the front.
10    pub keys: Vec<Keystroke>,
11    /// Names what activating the entry does here.
12    pub verb: String,
13    /// States whether the entry means anything in the current state.
14    pub availability: ActionAvailability,
15}
16
17impl MenuItem {
18    /// States the entry as a reader reads it: the keys, then what they do.
19    #[must_use]
20    pub fn label(&self) -> String {
21        let keys = self.keys.iter().map(|key| key.label()).collect::<String>();
22        format!("[{keys}] {}", self.verb)
23    }
24}
25
26/// Derives the menu one page offers from the keys it binds and the actions it enables.
27#[ext(name = MenuPresentationExt)]
28pub impl<This> This
29where
30    This: NavigationStateAlg
31        + QueueCatalogAlg
32        + DraftStateAlg
33        + OutputDraftAlg
34        + InputDraftAlg
35        + TextEditorStateAlg
36        + DetailSelectionAlg,
37    This::Entry: QueueEntryAlg + RequestOptionsAlg,
38    This::Id: Copy + Eq,
39{
40    /// States every entry the current page offers, in the order a reader scans them.
41    ///
42    /// Exit closes every menu, because it is the one action every page offers.
43    fn menu_items(&self) -> impl Iterator<Item = MenuItem> {
44        let page = self.page();
45        let items = match page {
46            ManagerPage::Collection => vec![
47                self.cursor_item("Select"),
48                self.menu_item(ManagerAction::Activate, "Details"),
49                self.menu_item(ManagerAction::AddSources, "Add"),
50                self.menu_item(ManagerAction::Space, self.space_verb()),
51                self.menu_item(ManagerAction::Remove, "Remove"),
52            ],
53            ManagerPage::Details(_) => vec![
54                self.cursor_item("Select field or action"),
55                self.menu_item(
56                    ManagerAction::Activate,
57                    if self.selected_detail_control().is_none() { "Close details" } else { "Activate" },
58                ),
59                self.menu_item(ManagerAction::Space, self.space_verb()),
60                self.menu_item(ManagerAction::Back, COLLECTION),
61            ],
62            ManagerPage::Formats(id) => vec![
63                self.cursor_item(self.choice_verb(id)),
64                self.menu_item(ManagerAction::Activate, "Accept"),
65                self.menu_item(ManagerAction::Back, "Details"),
66            ],
67            ManagerPage::Log(_) | ManagerPage::Report(_) | ManagerPage::Command(_) => {
68                vec![self.menu_item(ManagerAction::Back, "Details")]
69            }
70            ManagerPage::AddSources => vec![
71                self.menu_item(ManagerAction::Previous, "Move"),
72                self.menu_item(ManagerAction::Next, "Move"),
73                self.menu_item(ManagerAction::Activate, "Add sources"),
74                self.menu_item(ManagerAction::Back, COLLECTION),
75            ],
76            ManagerPage::Output(_) | ManagerPage::Input(_) => vec![
77                self.menu_item(ManagerAction::Previous, "Move"),
78                self.menu_item(ManagerAction::Next, "Move"),
79                self.menu_item(ManagerAction::Activate, "Save"),
80                self.menu_item(ManagerAction::Back, "Details"),
81            ],
82        };
83        items.into_iter().chain([self.menu_item(ManagerAction::Exit, "Quit")])
84    }
85
86    /// States one entry naming one action, with the keys the page binds to it.
87    fn menu_item(&self, action: ManagerAction, verb: impl Into<String>) -> MenuItem {
88        MenuItem {
89            action,
90            keys: self.action_keys(action).take(1).collect(),
91            verb: verb.into(),
92            availability: self.action_availability(action),
93        }
94    }
95
96    /// States the one entry walking a list, which both cursor directions reach.
97    fn cursor_item(&self, verb: impl Into<String>) -> MenuItem {
98        let keys = self
99            .action_keys(ManagerAction::Previous)
100            .take(1)
101            .chain(self.action_keys(ManagerAction::Next).take(1))
102            .collect();
103        MenuItem {
104            action: ManagerAction::Next,
105            keys,
106            verb: verb.into(),
107            availability: self.action_availability(ManagerAction::Next),
108        }
109    }
110
111    /// Names what the chooser page chooses between for one request.
112    fn choice_verb(&self, id: This::Id) -> &'static str {
113        let chooses_media = self.queue_entry(id).is_some_and(|entry| entry.performer() == Performer::Retrieval);
114        if chooses_media { "Choose format" } else { "Choose transfer" }
115    }
116
117    /// Names what Space would do to the selected entry, whether or not it is offered.
118    fn space_verb(&self) -> String {
119        let entry = self.selected_queue_id().and_then(|id| self.queue_entry(id));
120        let Some(entry) = entry else {
121            return UNSTATED.to_owned();
122        };
123        entry
124            .space_action()
125            .map_or_else(
126                || match entry.phase() {
127                    TransferPhase::Extracting | TransferPhase::Downloading => "Pause",
128                    TransferPhase::Paused => "Resume",
129                    TransferPhase::Ready => "Start",
130                    TransferPhase::Waiting
131                    | TransferPhase::Rehearsing
132                    | TransferPhase::Publishing
133                    | TransferPhase::Complete
134                    | TransferPhase::Failed => UNSTATED,
135                },
136                SpaceAction::space_label,
137            )
138            .to_owned()
139    }
140}