Skip to main content

rsynko_manager/
menu.rs

1use crate::*;
2use alux_ext::ext;
3
4/// Derives action availability from renderer-neutral manager observations.
5#[ext(name = ManagerMenuExt)]
6pub impl<This> This
7where
8    This: NavigationStateAlg
9        + QueueCatalogAlg
10        + DraftStateAlg
11        + InputDraftAlg
12        + OutputDraftAlg
13        + TextEditorStateAlg
14        + DetailSelectionAlg,
15    This::Entry: QueueEntryAlg + RequestOptionsAlg,
16    This::Id: Copy + Eq,
17{
18    /// Derives whether one reusable action currently has meaning.
19    fn action_availability(&self, action: ManagerAction) -> ActionAvailability {
20        let enabled = match action {
21            ManagerAction::Exit => true,
22            ManagerAction::Back => self.page() != ManagerPage::Collection,
23            ManagerAction::AddSources => self.page() == ManagerPage::Collection,
24            ManagerAction::Space => {
25                matches!(
26                    self.page(),
27                    ManagerPage::Collection | ManagerPage::Details(_)
28                ) && self
29                    .selected_queue_id()
30                    .and_then(|id| self.queue_entry(id))
31                    .and_then(QueueEntryAlg::space_action)
32                    .is_some()
33            }
34            ManagerAction::Remove => {
35                self.page() == ManagerPage::Collection && self.selected_queue_id().is_some()
36            }
37            ManagerAction::Previous => self.cursor_can_move(false),
38            ManagerAction::Next => self.cursor_can_move(true),
39            ManagerAction::Activate => self.selection_can_activate(),
40        };
41        if enabled {
42            ActionAvailability::Enabled
43        } else {
44            ActionAvailability::Disabled
45        }
46    }
47
48    /// Derives whether the current page cursor can move in one direction.
49    fn cursor_can_move(&self, forward: bool) -> bool {
50        match self.page() {
51            ManagerPage::Collection => self.queue_ids().take(2).count() > 1,
52            ManagerPage::AddSources | ManagerPage::Output(_) | ManagerPage::Input(_) => {
53                self.active_text_editor().is_some_and(|(text, cursor)| {
54                    if forward {
55                        cursor < text.len()
56                    } else {
57                        cursor > 0
58                    }
59                })
60            }
61            ManagerPage::Details(id) => self
62                .queue_entry(id)
63                .is_some_and(|entry| !entry.detail_controls().is_empty()),
64            ManagerPage::Formats(id) => self
65                .queue_entry(id)
66                .is_some_and(|entry| entry.selectable_choices().next().is_some()),
67            // A record, a report, and a command are read, not traversed: they state no cursor.
68            ManagerPage::Log(_) | ManagerPage::Report(_) | ManagerPage::Command(_) => false,
69        }
70    }
71
72    /// Derives whether the current cursor position denotes an activatable value.
73    fn selection_can_activate(&self) -> bool {
74        match self.page() {
75            ManagerPage::Collection => self.selected_queue_id().is_some(),
76            ManagerPage::AddSources => self.draft().lines().any(|line| !line.trim().is_empty()),
77            ManagerPage::Output(id) => {
78                self.queue_entry(id).is_some_and(QueueEntryAlg::is_editable)
79                    && !self.output_draft().trim().is_empty()
80            }
81            ManagerPage::Input(id) => {
82                self.queue_entry(id).is_some_and(QueueEntryAlg::is_editable)
83                    && !self.input_draft().trim().is_empty()
84            }
85            ManagerPage::Details(id) => self.queue_entry(id).is_some_and(|entry| {
86                self.selected_detail_control()
87                    .is_none_or(|control| entry.detail_controls().contains(&control))
88            }),
89            ManagerPage::Formats(id) => self
90                .queue_entry(id)
91                .is_some_and(|entry| entry.selectable_choices().next().is_some()),
92            ManagerPage::Log(_) | ManagerPage::Report(_) | ManagerPage::Command(_) => false,
93        }
94    }
95}
96
97/// Identifies one reusable manager-menu action independently of its input binding.
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99pub enum ManagerAction {
100    /// Moves the current page's cursor to the preceding value.
101    Previous,
102    /// Moves the current page's cursor to the following value.
103    Next,
104    /// Activates the current page's selected value.
105    Activate,
106    /// Opens the add-sources editor.
107    AddSources,
108    /// Applies the selected queue entry's state-dependent Space action.
109    Space,
110    /// Removes the selected queue entry.
111    Remove,
112    /// Returns to the parent page.
113    Back,
114    /// Requests safe application exit.
115    Exit,
116}
117
118/// Denotes whether a menu action currently has meaning.
119#[derive(Clone, Copy, Debug, PartialEq, Eq)]
120pub enum ActionAvailability {
121    /// Denotes an action that may be selected and activated.
122    Enabled,
123    /// Denotes a visible action that must not be selected or activated.
124    Disabled,
125}
126
127impl ActionAvailability {
128    /// Observes whether the action may be activated.
129    #[must_use]
130    pub const fn is_enabled(self) -> bool {
131        matches!(self, Self::Enabled)
132    }
133}