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
12pub 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}