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() {
58                        "Close details"
59                    } else {
60                        "Activate"
61                    },
62                ),
63                self.menu_item(ManagerAction::Space, self.space_verb()),
64                self.menu_item(ManagerAction::Back, COLLECTION),
65            ],
66            ManagerPage::Formats(id) => vec![
67                self.cursor_item(self.choice_verb(id)),
68                self.menu_item(ManagerAction::Activate, "Accept"),
69                self.menu_item(ManagerAction::Back, "Details"),
70            ],
71            ManagerPage::Log(_) | ManagerPage::Report(_) | ManagerPage::Command(_) => {
72                vec![self.menu_item(ManagerAction::Back, "Details")]
73            }
74            ManagerPage::AddSources => vec![
75                self.menu_item(ManagerAction::Previous, "Move"),
76                self.menu_item(ManagerAction::Next, "Move"),
77                self.menu_item(ManagerAction::Activate, "Add sources"),
78                self.menu_item(ManagerAction::Back, COLLECTION),
79            ],
80            ManagerPage::Output(_) | ManagerPage::Input(_) => vec![
81                self.menu_item(ManagerAction::Previous, "Move"),
82                self.menu_item(ManagerAction::Next, "Move"),
83                self.menu_item(ManagerAction::Activate, "Save"),
84                self.menu_item(ManagerAction::Back, "Details"),
85            ],
86        };
87        items
88            .into_iter()
89            .chain([self.menu_item(ManagerAction::Exit, "Quit")])
90    }
91
92    /// States one entry naming one action, with the keys the page binds to it.
93    fn menu_item(&self, action: ManagerAction, verb: impl Into<String>) -> MenuItem {
94        MenuItem {
95            action,
96            keys: self.action_keys(action).take(1).collect(),
97            verb: verb.into(),
98            availability: self.action_availability(action),
99        }
100    }
101
102    /// States the one entry walking a list, which both cursor directions reach.
103    fn cursor_item(&self, verb: impl Into<String>) -> MenuItem {
104        let keys = self
105            .action_keys(ManagerAction::Previous)
106            .take(1)
107            .chain(self.action_keys(ManagerAction::Next).take(1))
108            .collect();
109        MenuItem {
110            action: ManagerAction::Next,
111            keys,
112            verb: verb.into(),
113            availability: self.action_availability(ManagerAction::Next),
114        }
115    }
116
117    /// Names what the chooser page chooses between for one request.
118    fn choice_verb(&self, id: This::Id) -> &'static str {
119        let chooses_media = self
120            .queue_entry(id)
121            .is_some_and(|entry| entry.performer() == Performer::Retrieval);
122        if chooses_media {
123            "Choose format"
124        } else {
125            "Choose transfer"
126        }
127    }
128
129    /// Names what Space would do to the selected entry, whether or not it is offered.
130    fn space_verb(&self) -> String {
131        let entry = self.selected_queue_id().and_then(|id| self.queue_entry(id));
132        let Some(entry) = entry else {
133            return UNSTATED.to_owned();
134        };
135        entry
136            .space_action()
137            .map_or_else(
138                || match entry.phase() {
139                    TransferPhase::Extracting | TransferPhase::Downloading => "Pause",
140                    TransferPhase::Paused => "Resume",
141                    TransferPhase::Ready => "Start",
142                    TransferPhase::Waiting
143                    | TransferPhase::Rehearsing
144                    | TransferPhase::Publishing
145                    | TransferPhase::Complete
146                    | TransferPhase::Failed => UNSTATED,
147                },
148                SpaceAction::space_label,
149            )
150            .to_owned()
151    }
152}