Skip to main content

modde_ui/
action_button.rs

1use std::sync::atomic::{AtomicU64, Ordering};
2
3use crate::app::{Message, ReorderDirection, SidebarGroup, View, WabbajackTab};
4use crate::views::browse_nexus::BrowseTab;
5use iced::Element;
6use iced::widget::{Button, mouse_area};
7use modde_core::NexusModId;
8use modde_core::filter::FilterKind;
9
10static NEXT_BUTTON_HOVER_ID: AtomicU64 = AtomicU64::new(1);
11
12/// Trait-enforced hover copy for GUI button actions.
13pub trait ButtonActionDescription {
14    fn button_description(&self) -> &'static str;
15}
16
17#[derive(Debug, Clone)]
18pub enum ButtonAction {
19    SwitchView(View),
20    ToggleSidebarGroup(SidebarGroup),
21    DeleteProfile(String),
22    OpenNewProfileDialog,
23    ForkProfile {
24        source: String,
25        new_name: String,
26    },
27    RollbackExperiment,
28    CommitExperiment,
29    TryProfile,
30    OpenModPage,
31    ModGalleryNext,
32    ModEndorseToggle,
33    ModTrackToggle,
34    RestoreSaveSnapshot(String),
35    AddMod,
36    RemoveMod(usize),
37    Deploy,
38    ToggleFilterMode,
39    CycleFilter(FilterKind),
40    ClearFilters,
41    ToggleCompactModList,
42    ToggleSeparator(Option<i64>),
43    ReorderMod {
44        mod_id: String,
45        direction: ReorderDirection,
46    },
47    SelectMod(usize),
48    SearchCollections(String),
49    InstallCollection {
50        slug: String,
51        version: String,
52    },
53    BrowseTabSwitched(BrowseTab),
54    BrowseInstallMod {
55        game_domain: String,
56        mod_id: NexusModId,
57    },
58    LoadWabbajackCatalog,
59    WabbajackTabChanged(WabbajackTab),
60    OpenWabbajackFile,
61    WabbajackDownloadSelected,
62    WabbajackStartInstall,
63    WabbajackSelectEntry(usize),
64    WabbajackOpenUrl(String),
65    WabbajackGenerateHmSnippet,
66    WabbajackCopyHmSnippet,
67    WabbajackSaveHmSnippet,
68    FomodCancel,
69    FomodUndo,
70    FomodBack,
71    FomodNext,
72    PauseDownload(usize),
73    ResumeDownload(usize),
74    CancelDownload(usize),
75    RunDiagnostics,
76    ClearOverwrite,
77    MoveOverwriteToMod(String),
78    LoadSaveHistory,
79    ValidateNexusKey,
80    ToggleNexusApiKeyVisibility,
81    ReplaceNexusApiKey,
82    RemoveNexusConfigKey,
83    BrowseGamePath,
84    BrowseDownloadDir,
85    CreateStockSnapshot,
86    VerifyStockSnapshot,
87    RefreshTools,
88    SelectToolTab(String),
89    UpdateToolSetting {
90        tool_id: String,
91        key: String,
92        value: serde_json::Value,
93    },
94    ToggleToolAdvancedSettings,
95    ApplyTool(String),
96    RevertTool(String),
97    ActivateOptiScaler,
98    DeactivateOptiScaler,
99    AdoptOptiScaler,
100    RestoreOptiScalerBackup,
101    ResetOptiScalerConfig,
102    RestoreToolSettings {
103        tool_id: String,
104        node_id: String,
105    },
106    RefreshOptiScalerReleases,
107    InstallOptiScalerRelease,
108    RefreshProtonVersions,
109    InstallProtonVersion,
110    OpenExecutableEditor,
111    RefreshExecutables,
112    ClearExecutableDraft,
113    EditExecutable(String),
114    SaveExecutable,
115    RemoveExecutable(String),
116    RunExecutable(String),
117    BrowseExecutablePath,
118    BrowseExecutableWorkingDir,
119    WindowMinimize,
120    WindowToggleMaximize,
121    WindowClose,
122    CancelNewProfileDialog,
123    SubmitNewProfileDialog,
124    GamePathDialogBrowse,
125    CancelGamePathDialog,
126    OpenAddCustomGame,
127    BrowseAddCustomGameInstallPath,
128    AddCustomGameSubmit,
129    AddCustomGameCancel,
130    OpenManageCustomGames,
131    CloseManageCustomGames,
132    RemoveCustomGame(String),
133    OpenUpdateReleasePage,
134    DismissUpdateBanner,
135}
136
137impl ButtonActionDescription for ButtonAction {
138    fn button_description(&self) -> &'static str {
139        match self {
140            ButtonAction::SwitchView(_) => "Switch the main workspace to this section.",
141            ButtonAction::ToggleSidebarGroup(_) => {
142                "Expand or collapse this sidebar navigation group."
143            }
144            ButtonAction::DeleteProfile(_) => {
145                "Delete the active profile and remove it from the profile list."
146            }
147            ButtonAction::OpenNewProfileDialog => {
148                "Open a dialog for creating a new profile for the selected game."
149            }
150            ButtonAction::ForkProfile { .. } => {
151                "Create a copy of the active profile that can be changed independently."
152            }
153            ButtonAction::RollbackExperiment => {
154                "Discard the current profile experiment and return to the previous state."
155            }
156            ButtonAction::CommitExperiment => {
157                "Keep the current experiment changes and make them the active profile state."
158            }
159            ButtonAction::TryProfile => {
160                "Start an experimental profile layer so changes can be tested before committing."
161            }
162            ButtonAction::OpenModPage => "Open the selected mod's Nexus Mods page in a browser.",
163            ButtonAction::ModGalleryNext => {
164                "Show the next image from the selected mod's Nexus gallery."
165            }
166            ButtonAction::ModEndorseToggle => "Toggle your Nexus endorsement for the selected mod.",
167            ButtonAction::ModTrackToggle => {
168                "Toggle whether Nexus tracks updates for the selected mod."
169            }
170            ButtonAction::RestoreSaveSnapshot(_) => {
171                "Restore this save snapshot and its captured save files."
172            }
173            ButtonAction::AddMod => "Choose a mod archive or folder to add to the active profile.",
174            ButtonAction::RemoveMod(_) => "Remove the selected mod from the active profile.",
175            ButtonAction::Deploy => "Deploy the active profile's enabled mods to the game folder.",
176            ButtonAction::ToggleFilterMode => {
177                "Switch whether mod list filters must all match or any one can match."
178            }
179            ButtonAction::CycleFilter(_) => {
180                "Cycle this filter between ignored, required, and excluded."
181            }
182            ButtonAction::ClearFilters => "Clear all active mod list filters.",
183            ButtonAction::ToggleCompactModList => {
184                "Toggle between compact and normal spacing in the mod list."
185            }
186            ButtonAction::ToggleSeparator(_) => {
187                "Expand or collapse this category group in the mod list."
188            }
189            ButtonAction::ReorderMod { .. } => {
190                "Move this mod one position in the active profile load order."
191            }
192            ButtonAction::SelectMod(_) => "Select this mod and show its details in the sidebar.",
193            ButtonAction::SearchCollections(_) => {
194                "Run a Nexus Collections search using the current search text."
195            }
196            ButtonAction::InstallCollection { .. } => {
197                "Start downloading and installing this Nexus collection."
198            }
199            ButtonAction::BrowseTabSwitched(_) => {
200                "Switch the Nexus browser to this feed and load its results if needed."
201            }
202            ButtonAction::BrowseInstallMod { .. } => {
203                "Install this Nexus mod into the active profile."
204            }
205            ButtonAction::LoadWabbajackCatalog => {
206                "Refresh the Wabbajack catalog and authored file lists."
207            }
208            ButtonAction::WabbajackTabChanged(_) => "Switch the Wabbajack explorer to this tab.",
209            ButtonAction::OpenWabbajackFile => "Choose a local .wabbajack file from disk.",
210            ButtonAction::WabbajackDownloadSelected => {
211                "Download the selected or entered Wabbajack modlist file."
212            }
213            ButtonAction::WabbajackStartInstall => {
214                "Install the currently selected local Wabbajack file."
215            }
216            ButtonAction::WabbajackSelectEntry(_) => {
217                "Select this Wabbajack entry and show its details."
218            }
219            ButtonAction::WabbajackOpenUrl(_) => "Open this Wabbajack page in your browser.",
220            ButtonAction::WabbajackGenerateHmSnippet => {
221                "Generate a Home Manager configuration snippet for this Wabbajack setup."
222            }
223            ButtonAction::WabbajackCopyHmSnippet => {
224                "Copy the generated Home Manager snippet to the clipboard."
225            }
226            ButtonAction::WabbajackSaveHmSnippet => {
227                "Save the generated Home Manager snippet to disk."
228            }
229            ButtonAction::FomodCancel => "Cancel the FOMOD installer and close the wizard.",
230            ButtonAction::FomodUndo => "Undo the most recent FOMOD wizard selection change.",
231            ButtonAction::FomodBack => "Return to the previous FOMOD installer step.",
232            ButtonAction::FomodNext => {
233                "Continue to the next FOMOD step or install when all required choices are ready."
234            }
235            ButtonAction::PauseDownload(_) => "Pause this active download.",
236            ButtonAction::ResumeDownload(_) => "Resume or retry this download.",
237            ButtonAction::CancelDownload(_) => "Cancel this queued or active download.",
238            ButtonAction::RunDiagnostics => {
239                "Scan the active profile for game-specific modding and integrity issues."
240            }
241            ButtonAction::ClearOverwrite => {
242                "Delete all files currently stored in the profile override area."
243            }
244            ButtonAction::MoveOverwriteToMod(_) => {
245                "Create a regular mod from the files currently in the override area."
246            }
247            ButtonAction::LoadSaveHistory => {
248                "Refresh the list of captured save snapshots for the active profile."
249            }
250            ButtonAction::ValidateNexusKey => {
251                "Validate the configured Nexus Mods API key and show account status."
252            }
253            ButtonAction::ToggleNexusApiKeyVisibility => {
254                "Show or hide the Nexus Mods API key in the settings field."
255            }
256            ButtonAction::ReplaceNexusApiKey => {
257                "Save this key to modde's own Nexus API key config file."
258            }
259            ButtonAction::RemoveNexusConfigKey => {
260                "Remove only modde's own Nexus API key config file."
261            }
262            ButtonAction::BrowseGamePath => "Choose the game installation directory.",
263            ButtonAction::BrowseDownloadDir => "Choose where downloaded mod archives are stored.",
264            ButtonAction::CreateStockSnapshot => {
265                "Capture a clean stock game snapshot for later deployment checks."
266            }
267            ButtonAction::VerifyStockSnapshot => {
268                "Compare the current game installation with the saved stock snapshot."
269            }
270            ButtonAction::RefreshTools => {
271                "Refresh detected gaming tools and overlay integration status."
272            }
273            ButtonAction::SelectToolTab(_) => "Switch to this tool's game-specific settings tab.",
274            ButtonAction::UpdateToolSetting { .. } => {
275                "Apply this value to the selected tool setting."
276            }
277            ButtonAction::ToggleToolAdvancedSettings => {
278                "Show or hide advanced tool settings for the active tool."
279            }
280            ButtonAction::ApplyTool(_) => {
281                "Apply this tool's required files or configuration to the game directory."
282            }
283            ButtonAction::RevertTool(_) => {
284                "Remove this tool's applied files from the game directory."
285            }
286            ButtonAction::ActivateOptiScaler => {
287                "Apply OptiScaler files and enable its launch integration."
288            }
289            ButtonAction::DeactivateOptiScaler => {
290                "Revert OptiScaler files and disable its launch integration."
291            }
292            ButtonAction::AdoptOptiScaler => {
293                "Record the detected OptiScaler files as managed for this game."
294            }
295            ButtonAction::RestoreOptiScalerBackup => {
296                "Restore the latest backed-up OptiScaler files for this game."
297            }
298            ButtonAction::ResetOptiScalerConfig => {
299                "Clear OptiScaler INI overrides so the selected release defaults are used."
300            }
301            ButtonAction::RestoreToolSettings { .. } => {
302                "Restore this settings version without applying or reverting game files."
303            }
304            ButtonAction::RefreshOptiScalerReleases => {
305                "Load OptiScaler release tags and assets from the official GitHub repository."
306            }
307            ButtonAction::InstallOptiScalerRelease => {
308                "Download and cache the selected OptiScaler release for this game."
309            }
310            ButtonAction::RefreshProtonVersions => {
311                "Load GE-Proton release versions from the official GitHub repository."
312            }
313            ButtonAction::InstallProtonVersion => {
314                "Install the selected GEProton version through protonup-rs."
315            }
316            ButtonAction::OpenExecutableEditor => {
317                "Open the editor for adding a new executable launch target."
318            }
319            ButtonAction::RefreshExecutables => {
320                "Refresh executable launch targets for the selected game."
321            }
322            ButtonAction::ClearExecutableDraft => {
323                "Close the executable editor and discard unsaved field values."
324            }
325            ButtonAction::EditExecutable(_) => {
326                "Load this executable into the editor so its settings can be updated."
327            }
328            ButtonAction::SaveExecutable => {
329                "Save the executable launch target for the selected game."
330            }
331            ButtonAction::RemoveExecutable(_) => {
332                "Remove this executable launch target from the selected game."
333            }
334            ButtonAction::RunExecutable(_) => {
335                "Run this executable through the active profile with overwrite capture."
336            }
337            ButtonAction::BrowseExecutablePath => "Choose the executable file to launch.",
338            ButtonAction::BrowseExecutableWorkingDir => {
339                "Choose the working directory for this executable."
340            }
341            ButtonAction::WindowMinimize => "Minimize the modde window.",
342            ButtonAction::WindowToggleMaximize => {
343                "Toggle the modde window between maximized and restored size."
344            }
345            ButtonAction::WindowClose => "Close the modde window.",
346            ButtonAction::CancelNewProfileDialog => {
347                "Close the new profile dialog without creating a profile."
348            }
349            ButtonAction::SubmitNewProfileDialog => {
350                "Create the profile using the entered name and selected game."
351            }
352            ButtonAction::GamePathDialogBrowse => {
353                "Choose the selected game's installation directory."
354            }
355            ButtonAction::CancelGamePathDialog => {
356                "Cancel setting the game path and return to the previous game selection."
357            }
358            ButtonAction::OpenAddCustomGame => "Open a dialog to register a new custom game.",
359            ButtonAction::BrowseAddCustomGameInstallPath => {
360                "Choose the custom game's install directory and scan it for executables."
361            }
362            ButtonAction::AddCustomGameSubmit => {
363                "Save the custom game, reload the registry, and select it."
364            }
365            ButtonAction::AddCustomGameCancel => "Close the custom game dialog without saving.",
366            ButtonAction::OpenManageCustomGames => {
367                "Open the list of user-defined games and remove existing entries."
368            }
369            ButtonAction::CloseManageCustomGames => "Close the custom game manager.",
370            ButtonAction::RemoveCustomGame(_) => {
371                "Remove this user-defined game from the runtime registry."
372            }
373            ButtonAction::OpenUpdateReleasePage => {
374                "Open the latest modde release page in your browser."
375            }
376            ButtonAction::DismissUpdateBanner => {
377                "Hide this update notification for the current session."
378            }
379        }
380    }
381}
382
383impl From<ButtonAction> for Message {
384    fn from(action: ButtonAction) -> Self {
385        match action {
386            ButtonAction::SwitchView(view) => Message::SwitchView(view),
387            ButtonAction::ToggleSidebarGroup(group) => Message::ToggleSidebarGroup(group),
388            ButtonAction::DeleteProfile(name) => Message::DeleteProfile(name),
389            ButtonAction::OpenNewProfileDialog => Message::OpenNewProfileDialog,
390            ButtonAction::ForkProfile { source, new_name } => {
391                Message::ForkProfile { source, new_name }
392            }
393            ButtonAction::RollbackExperiment => Message::RollbackExperiment,
394            ButtonAction::CommitExperiment => Message::CommitExperiment,
395            ButtonAction::TryProfile => Message::TryProfile,
396            ButtonAction::OpenModPage => Message::OpenModPage,
397            ButtonAction::ModGalleryNext => Message::ModGalleryNext,
398            ButtonAction::ModEndorseToggle => Message::ModEndorseToggle,
399            ButtonAction::ModTrackToggle => Message::ModTrackToggle,
400            ButtonAction::RestoreSaveSnapshot(id) => Message::RestoreSaveSnapshot(id),
401            ButtonAction::AddMod => Message::AddMod,
402            ButtonAction::RemoveMod(index) => Message::RemoveMod(index),
403            ButtonAction::Deploy => Message::Deploy,
404            ButtonAction::ToggleFilterMode => Message::ToggleFilterMode,
405            ButtonAction::CycleFilter(kind) => Message::CycleFilter(kind),
406            ButtonAction::ClearFilters => Message::ClearFilters,
407            ButtonAction::ToggleCompactModList => Message::ToggleCompactModList,
408            ButtonAction::ToggleSeparator(cat_id) => Message::ToggleSeparator(cat_id),
409            ButtonAction::ReorderMod { mod_id, direction } => {
410                Message::ReorderMod { mod_id, direction }
411            }
412            ButtonAction::SelectMod(index) => Message::SelectMod(index),
413            ButtonAction::SearchCollections(query) => Message::SearchCollections(query),
414            ButtonAction::InstallCollection { slug, version } => {
415                Message::InstallCollection { slug, version }
416            }
417            ButtonAction::BrowseTabSwitched(tab) => Message::BrowseTabSwitched(tab),
418            ButtonAction::BrowseInstallMod {
419                game_domain,
420                mod_id,
421            } => Message::BrowseInstallMod {
422                game_domain,
423                mod_id,
424            },
425            ButtonAction::LoadWabbajackCatalog => Message::LoadWabbajackCatalog,
426            ButtonAction::WabbajackTabChanged(tab) => Message::WabbajackTabChanged(tab),
427            ButtonAction::OpenWabbajackFile => Message::OpenWabbajackFile,
428            ButtonAction::WabbajackDownloadSelected => Message::WabbajackDownloadSelected,
429            ButtonAction::WabbajackStartInstall => Message::WabbajackStartInstall,
430            ButtonAction::WabbajackSelectEntry(index) => Message::WabbajackSelectEntry(index),
431            ButtonAction::WabbajackOpenUrl(url) => Message::WabbajackOpenUrl(url),
432            ButtonAction::WabbajackGenerateHmSnippet => Message::WabbajackGenerateHmSnippet,
433            ButtonAction::WabbajackCopyHmSnippet => Message::WabbajackCopyHmSnippet,
434            ButtonAction::WabbajackSaveHmSnippet => Message::WabbajackSaveHmSnippet,
435            ButtonAction::FomodCancel => Message::FOMODCancel,
436            ButtonAction::FomodUndo => Message::FOMODUndo,
437            ButtonAction::FomodBack => Message::FOMODBack,
438            ButtonAction::FomodNext => Message::FOMODNext,
439            ButtonAction::PauseDownload(id) => Message::PauseDownload(id),
440            ButtonAction::ResumeDownload(id) => Message::ResumeDownload(id),
441            ButtonAction::CancelDownload(id) => Message::CancelDownload(id),
442            ButtonAction::RunDiagnostics => Message::RunDiagnostics,
443            ButtonAction::ClearOverwrite => Message::ClearOverwrite,
444            ButtonAction::MoveOverwriteToMod(mod_id) => Message::MoveOverwriteToMod(mod_id),
445            ButtonAction::LoadSaveHistory => Message::LoadSaveHistory,
446            ButtonAction::ValidateNexusKey => Message::ValidateNexusKey,
447            ButtonAction::ToggleNexusApiKeyVisibility => Message::ToggleNexusApiKeyVisibility,
448            ButtonAction::ReplaceNexusApiKey => Message::ReplaceNexusApiKey,
449            ButtonAction::RemoveNexusConfigKey => Message::RemoveNexusConfigKey,
450            ButtonAction::BrowseGamePath => Message::BrowseGamePath,
451            ButtonAction::BrowseDownloadDir => Message::BrowseDownloadDir,
452            ButtonAction::CreateStockSnapshot => Message::CreateStockSnapshot,
453            ButtonAction::VerifyStockSnapshot => Message::VerifyStockSnapshot,
454            ButtonAction::RefreshTools => Message::RefreshTools,
455            ButtonAction::SelectToolTab(tool_id) => Message::SelectToolTab(tool_id),
456            ButtonAction::UpdateToolSetting {
457                tool_id,
458                key,
459                value,
460            } => Message::UpdateToolSetting {
461                tool_id,
462                key,
463                value,
464            },
465            ButtonAction::ToggleToolAdvancedSettings => Message::ToggleToolAdvancedSettings,
466            ButtonAction::ApplyTool(tool_id) => Message::ApplyTool(tool_id),
467            ButtonAction::RevertTool(tool_id) => Message::RevertTool(tool_id),
468            ButtonAction::ActivateOptiScaler => Message::ActivateOptiScaler,
469            ButtonAction::DeactivateOptiScaler => Message::DeactivateOptiScaler,
470            ButtonAction::AdoptOptiScaler => Message::AdoptOptiScaler,
471            ButtonAction::RestoreOptiScalerBackup => Message::RestoreOptiScalerBackup,
472            ButtonAction::ResetOptiScalerConfig => Message::ResetOptiScalerConfig,
473            ButtonAction::RestoreToolSettings { tool_id, node_id } => {
474                Message::RestoreToolSettings { tool_id, node_id }
475            }
476            ButtonAction::RefreshOptiScalerReleases => Message::RefreshOptiScalerReleases,
477            ButtonAction::InstallOptiScalerRelease => Message::InstallOptiScalerRelease,
478            ButtonAction::RefreshProtonVersions => Message::RefreshProtonVersions,
479            ButtonAction::InstallProtonVersion => Message::InstallProtonVersion,
480            ButtonAction::OpenExecutableEditor => Message::OpenExecutableEditor,
481            ButtonAction::RefreshExecutables => Message::RefreshExecutables,
482            ButtonAction::ClearExecutableDraft => Message::ClearExecutableDraft,
483            ButtonAction::EditExecutable(name) => Message::EditExecutable(name),
484            ButtonAction::SaveExecutable => Message::SaveExecutable,
485            ButtonAction::RemoveExecutable(name) => Message::RemoveExecutable(name),
486            ButtonAction::RunExecutable(name) => Message::RunExecutable(name),
487            ButtonAction::BrowseExecutablePath => Message::BrowseExecutablePath,
488            ButtonAction::BrowseExecutableWorkingDir => Message::BrowseExecutableWorkingDir,
489            ButtonAction::WindowMinimize => Message::WindowMinimize,
490            ButtonAction::WindowToggleMaximize => Message::WindowToggleMaximize,
491            ButtonAction::WindowClose => Message::WindowClose,
492            ButtonAction::CancelNewProfileDialog => Message::CancelNewProfileDialog,
493            ButtonAction::SubmitNewProfileDialog => Message::SubmitNewProfileDialog,
494            ButtonAction::GamePathDialogBrowse => Message::GamePathDialogBrowse,
495            ButtonAction::CancelGamePathDialog => Message::CancelGamePathDialog,
496            ButtonAction::OpenAddCustomGame => Message::OpenAddCustomGame,
497            ButtonAction::BrowseAddCustomGameInstallPath => Message::BrowseAddCustomGameInstallPath,
498            ButtonAction::AddCustomGameSubmit => Message::AddCustomGameSubmit,
499            ButtonAction::AddCustomGameCancel => Message::AddCustomGameCancel,
500            ButtonAction::OpenManageCustomGames => Message::OpenManageCustomGames,
501            ButtonAction::CloseManageCustomGames => Message::CloseManageCustomGames,
502            ButtonAction::RemoveCustomGame(id) => Message::RemoveCustomGame(id),
503            ButtonAction::OpenUpdateReleasePage => Message::OpenUpdateReleasePage,
504            ButtonAction::DismissUpdateBanner => Message::DismissUpdateBanner,
505        }
506    }
507}
508
509pub trait DescribedButtonExt<'a> {
510    fn on_action(self, action: ButtonAction) -> Element<'a, Message>;
511    fn on_action_maybe(
512        self,
513        action: Option<ButtonAction>,
514        disabled_description: &'static str,
515    ) -> Element<'a, Message>;
516    fn described_disabled(self, description: &'static str) -> Element<'a, Message>;
517}
518
519impl<'a> DescribedButtonExt<'a> for Button<'a, Message> {
520    fn on_action(self, action: ButtonAction) -> Element<'a, Message> {
521        let description = action.button_description();
522        described(self.on_press(action.into()), description)
523    }
524
525    fn on_action_maybe(
526        self,
527        action: Option<ButtonAction>,
528        disabled_description: &'static str,
529    ) -> Element<'a, Message> {
530        match action {
531            Some(action) => self.on_action(action),
532            None => self.described_disabled(disabled_description),
533        }
534    }
535
536    fn described_disabled(self, description: &'static str) -> Element<'a, Message> {
537        described(self, description)
538    }
539}
540
541fn described<'a>(button: Button<'a, Message>, description: &'static str) -> Element<'a, Message> {
542    let id = NEXT_BUTTON_HOVER_ID.fetch_add(1, Ordering::Relaxed);
543
544    mouse_area(button)
545        .on_enter(Message::ButtonHoverStarted { id, description })
546        .on_exit(Message::ButtonHoverEnded { id })
547        .into()
548}
549
550#[cfg(test)]
551mod tests {
552    use super::*;
553
554    fn sample_actions() -> Vec<(&'static str, ButtonAction)> {
555        vec![
556            ("Mod List", ButtonAction::SwitchView(View::ModList)),
557            ("Game", ButtonAction::ToggleSidebarGroup(SidebarGroup::Game)),
558            ("Del", ButtonAction::DeleteProfile("Default".to_string())),
559            ("New", ButtonAction::OpenNewProfileDialog),
560            (
561                "Fork",
562                ButtonAction::ForkProfile {
563                    source: "Default".to_string(),
564                    new_name: "Default-fork".to_string(),
565                },
566            ),
567            ("Rollback", ButtonAction::RollbackExperiment),
568            ("Commit", ButtonAction::CommitExperiment),
569            ("Try Profile", ButtonAction::TryProfile),
570            ("Open in Nexus", ButtonAction::OpenModPage),
571            ("Next image", ButtonAction::ModGalleryNext),
572            ("Endorse", ButtonAction::ModEndorseToggle),
573            ("Track", ButtonAction::ModTrackToggle),
574            (
575                "Restore",
576                ButtonAction::RestoreSaveSnapshot("abc123".to_string()),
577            ),
578            ("Add Mod", ButtonAction::AddMod),
579            ("Remove", ButtonAction::RemoveMod(0)),
580            ("Deploy", ButtonAction::Deploy),
581            ("AND", ButtonAction::ToggleFilterMode),
582            ("Enabled", ButtonAction::CycleFilter(FilterKind::Enabled)),
583            ("Clear", ButtonAction::ClearFilters),
584            ("Compact", ButtonAction::ToggleCompactModList),
585            ("Category", ButtonAction::ToggleSeparator(None)),
586            (
587                "^",
588                ButtonAction::ReorderMod {
589                    mod_id: "mod".to_string(),
590                    direction: ReorderDirection::Up,
591                },
592            ),
593            ("Mod", ButtonAction::SelectMod(0)),
594            (
595                "Search",
596                ButtonAction::SearchCollections("query".to_string()),
597            ),
598            (
599                "Install",
600                ButtonAction::InstallCollection {
601                    slug: "collection".to_string(),
602                    version: "1.0".to_string(),
603                },
604            ),
605            ("Top", ButtonAction::BrowseTabSwitched(BrowseTab::Top)),
606            (
607                "Install",
608                ButtonAction::BrowseInstallMod {
609                    game_domain: "skyrimspecialedition".to_string(),
610                    mod_id: 1.into(),
611                },
612            ),
613            ("Refresh", ButtonAction::LoadWabbajackCatalog),
614            (
615                "Catalog",
616                ButtonAction::WabbajackTabChanged(WabbajackTab::Catalog),
617            ),
618            ("Select File", ButtonAction::OpenWabbajackFile),
619            ("Download", ButtonAction::WabbajackDownloadSelected),
620            ("Install", ButtonAction::WabbajackStartInstall),
621            ("Entry", ButtonAction::WabbajackSelectEntry(0)),
622            (
623                "Open Readme",
624                ButtonAction::WabbajackOpenUrl("https://example.test".to_string()),
625            ),
626            ("Generate", ButtonAction::WabbajackGenerateHmSnippet),
627            ("Copy", ButtonAction::WabbajackCopyHmSnippet),
628            ("Save", ButtonAction::WabbajackSaveHmSnippet),
629            ("Cancel", ButtonAction::FomodCancel),
630            ("Undo", ButtonAction::FomodUndo),
631            ("Back", ButtonAction::FomodBack),
632            ("Next", ButtonAction::FomodNext),
633            ("Pause", ButtonAction::PauseDownload(0)),
634            ("Resume", ButtonAction::ResumeDownload(0)),
635            ("Cancel", ButtonAction::CancelDownload(0)),
636            ("Run Diagnostics", ButtonAction::RunDiagnostics),
637            ("Clear All", ButtonAction::ClearOverwrite),
638            (
639                "Create Mod",
640                ButtonAction::MoveOverwriteToMod("__from_overrides__".to_string()),
641            ),
642            ("Refresh", ButtonAction::LoadSaveHistory),
643            ("Validate", ButtonAction::ValidateNexusKey),
644            ("Show", ButtonAction::ToggleNexusApiKeyVisibility),
645            ("Replace", ButtonAction::ReplaceNexusApiKey),
646            ("Remove modde config", ButtonAction::RemoveNexusConfigKey),
647            ("Browse", ButtonAction::BrowseGamePath),
648            ("Browse", ButtonAction::BrowseDownloadDir),
649            ("Create Snapshot", ButtonAction::CreateStockSnapshot),
650            ("Verify Snapshot", ButtonAction::VerifyStockSnapshot),
651            ("Refresh", ButtonAction::RefreshTools),
652            ("Tool", ButtonAction::SelectToolTab("mangohud".to_string())),
653            (
654                "Tool Setting",
655                ButtonAction::UpdateToolSetting {
656                    tool_id: "tool".to_string(),
657                    key: "setting".to_string(),
658                    value: serde_json::json!(true),
659                },
660            ),
661            (
662                "Advanced Settings",
663                ButtonAction::ToggleToolAdvancedSettings,
664            ),
665            ("Apply", ButtonAction::ApplyTool("tool".to_string())),
666            ("Revert", ButtonAction::RevertTool("tool".to_string())),
667            ("Adopt OptiScaler", ButtonAction::AdoptOptiScaler),
668            ("Restore OptiScaler", ButtonAction::RestoreOptiScalerBackup),
669            ("Reset OptiScaler", ButtonAction::ResetOptiScalerConfig),
670            ("Releases", ButtonAction::RefreshOptiScalerReleases),
671            ("Install OptiScaler", ButtonAction::InstallOptiScalerRelease),
672            ("Proton Versions", ButtonAction::RefreshProtonVersions),
673            ("Install Proton", ButtonAction::InstallProtonVersion),
674            ("Add executable", ButtonAction::OpenExecutableEditor),
675            ("Refresh", ButtonAction::RefreshExecutables),
676            ("Clear", ButtonAction::ClearExecutableDraft),
677            ("Edit", ButtonAction::EditExecutable("xEdit".to_string())),
678            ("Save", ButtonAction::SaveExecutable),
679            (
680                "Remove",
681                ButtonAction::RemoveExecutable("xEdit".to_string()),
682            ),
683            ("Run", ButtonAction::RunExecutable("xEdit".to_string())),
684            ("Browse", ButtonAction::BrowseExecutablePath),
685            ("Browse", ButtonAction::BrowseExecutableWorkingDir),
686            ("-", ButtonAction::WindowMinimize),
687            ("Maximize", ButtonAction::WindowToggleMaximize),
688            ("Close", ButtonAction::WindowClose),
689            ("Cancel", ButtonAction::CancelNewProfileDialog),
690            ("Create", ButtonAction::SubmitNewProfileDialog),
691            ("Browse", ButtonAction::GamePathDialogBrowse),
692            ("Cancel", ButtonAction::CancelGamePathDialog),
693        ]
694    }
695
696    #[test]
697    fn action_descriptions_are_present_and_more_specific_than_labels() {
698        for (label, action) in sample_actions() {
699            let description = action.button_description();
700            assert!(
701                !description.trim().is_empty(),
702                "missing description for {action:?}"
703            );
704            assert!(
705                description.len() > label.len(),
706                "description for {action:?} is not more detailed than {label:?}"
707            );
708        }
709    }
710
711    #[test]
712    fn optiscaler_state_actions_have_descriptions() {
713        for action in [
714            ButtonAction::AdoptOptiScaler,
715            ButtonAction::RestoreOptiScalerBackup,
716            ButtonAction::ResetOptiScalerConfig,
717        ] {
718            assert!(
719                !action.button_description().trim().is_empty(),
720                "missing OptiScaler description for {action:?}"
721            );
722        }
723    }
724}