diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4358566b2..2f66f50cd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,7 +20,7 @@ If you... You can: - Write code. Add features, fix bugs, improve user experience. - - Please do adhere to nice quality standards, although don't worry + - Please do adhere to nice quality standards, but don't worry about fitting in. We are welcoming! - Write documentation/guides - Contribute to the website (repo: ) diff --git a/Cargo.lock b/Cargo.lock index f83ded0d6..e019bf25f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4054,7 +4054,6 @@ dependencies = [ "owo-colors", "ql_core", "serde", - "serde_json", "tar", "thiserror 2.0.14", "tokio", @@ -4067,7 +4066,6 @@ version = "0.0.0" dependencies = [ "chrono", "futures", - "image", "owo-colors", "ql_core", "ql_java_handler", @@ -4103,11 +4101,9 @@ dependencies = [ name = "ql_servers" version = "0.0.0" dependencies = [ - "chrono", "owo-colors", "ql_core", "ql_java_handler", - "serde", "serde_json", "thiserror 2.0.14", "tokio", @@ -4128,7 +4124,6 @@ name = "quantum_launcher" version = "0.5.0" dependencies = [ "cfg-if", - "chrono", "clap", "constcat", "dark-light", @@ -4147,13 +4142,13 @@ dependencies = [ "ql_mod_manager", "ql_packager", "ql_servers", + "rand 0.9.2", "rfd", "semver", "serde", "serde_json", "terminal_size", "tokio", - "urlencoding", "windows 0.61.3", ] diff --git a/README.md b/README.md index 0b51686fe..84c9c5cc9 100644 --- a/README.md +++ b/README.md @@ -73,10 +73,8 @@ size. What about the others? Well... - The official Minecraft launcher is slow, unstable, buggy and frustrating to use, - with barely any modding features. -- Prism Launcher is a great launcher overall, but it does not support - offline accounts. Same for MultiMC. -- Legacy Launcher isn't as feature rich as this + with barely any modding features +- Legacy Launcher lacks many features - TLauncher is *suspected* to be malware # File Locations diff --git a/assets/freedesktop/quantum-launcher.metainfo.xml b/assets/freedesktop/quantum-launcher.metainfo.xml index 0207159af..ea983807d 100644 --- a/assets/freedesktop/quantum-launcher.metainfo.xml +++ b/assets/freedesktop/quantum-launcher.metainfo.xml @@ -12,9 +12,7 @@ GPL-3.0-only https://mrmayman.github.io/quantumlauncher - https://github.com/Mrmayman/quantumlauncher/issues + https://github.com/Mrmayman/quantumlauncher/issues https://mrmayman.github.io/contact https://mrmayman.github.io/quantumlauncher/faq https://github.com/Mrmayman/quantumlauncher/ @@ -76,13 +74,34 @@ - + + +
    Mod loaders: OptiFine + Forge together, plus legacy Fabric support
+
    UI and themes: major overhauls, polish, keyboard navigation and new themes
+
    Experimental: early Server Manager and MultiMC/PrismLauncher import
+
    Power users: global Java args, pre-launch commands, custom jar/main class
+
    Lots of fixes and improvements
+
+
+ + + +
    +
  • Revamped and improved many menus, plus a new Teal theme!
  • +
  • Added `ely.by` and `littleskin` account integration
  • +
  • Switched to BetterJSONs and LaunchWrapper, fixing many issues
  • +
  • Better CLI interface
  • +
  • Many, many bug-fixes and performance improvements! (for real)
  • +
+
+
+ +
  • Added shaders, resource packs and modpack stores
  • Added jar mod patching and modpack support
  • -
  • Added keyboard navigation and a new Catppuccin theme
  • +
  • Added keyboard navigation and a new Catppuccin theme
  • Overhauled many menus and improved UI/UX
  • Fixed a lot of bugs
diff --git a/changelogs/0.1.md b/changelogs/0.1.md index 718634042..e6985b940 100644 --- a/changelogs/0.1.md +++ b/changelogs/0.1.md @@ -1,10 +1,15 @@ # v0.1 + The first release of QuantumLauncher. Very buggy and janky. + ## Features: + - Make separate instances of Minecraft with one click. - Auto-installs Java. - Built in Fabric installer. + ## Doesn't have: + - Log viewer (added in v0.2) - Forge/OptiFine installer (added in v0.2) - Skin/Sound fixes for old versions (added in v0.3) diff --git a/changelogs/0.2.md b/changelogs/0.2.md index 6e2be918c..ba0eb6803 100644 --- a/changelogs/0.2.md +++ b/changelogs/0.2.md @@ -1,19 +1,28 @@ # v0.2 + ## Features + ### Forge + - Added a Forge installer (and uninstaller too) - Select your instance, go to `Mods` -> `Install Forge` and you're done! - Note: Only release 1.6.4 and above are currently supported. + ### Logs + - Now you can view game logs! - When the game launches, the logs will be available on the right side. -- Press `Copy Logs` to copy the logs (if you face any issues you can send it) -- Only a section of the log is shown for performance reasons, but you can get the whole thing by pressing the `Copy Logs` button. +- Only a section of the log is shown for performance reasons +- Press `Copy Logs` to copy the **full** logs (if you face any issues you can send it) + ### Misc -- There is now an update detector that looks for launcher updates and asks you if you want to install it. If you accept, it will automatically update the launcher and create a backup for you. + +- Added an update detector that looks for launcher updates and asks to install it. + - If you accept, it automatically updates the launcher and create a backup for you. - The UI design has been changed to be cleaner. -- Added a progress bar for Fabric installer. -- Added an uninstaller for Fabric. -- Now there are basic command line options: `--version` and `--help`. I am planning to add a headless command line mode in the future. -- Now the game's current directory will be the `.minecraft` folder. This fixes random files popping up near the launcher executable. -- Now the debug output of the launcher text incorporates colored text and dynamic progress bars. +- Added progress bar for Fabric installer. +- Added uninstaller for Fabric. +- Now there are basic command line options: `--version` and `--help` + - (Proper headless CLI mode added in v0.4.2) +- Game current directory is now the `.minecraft` folder. Fixes random files popping up near the launcher executable. +- Debug launcher logs now incorporate colored text and dynamic progress bars. diff --git a/changelogs/0.3.1.md b/changelogs/0.3.1.md index 963457adf..f6e009c10 100644 --- a/changelogs/0.3.1.md +++ b/changelogs/0.3.1.md @@ -1,15 +1,20 @@ # v0.3.1 + - Quilt support is here! - Added instance renaming! -- OmniArchive integration is here, allowing you to download old, rare versions of Minecraft. Nostalgia incoming! -- Added a brand-new style: Sky blue. Try it out in Settings -> Style! +- Omniarchive integration is here, allowing you to download old, rare versions of Minecraft. Nostalgia incoming! +- Added a brand-new style: Sky blue. Try it out in Settings → Style! - Added a cool ASCII art intro in the terminal + ## Mods: + - Added mod presets! - - Share your custom mod configurations with friends - - Download recommended mods directly from the launcher - Now you can copy mod IDs or open the mod page in the mod store + ## Misc: + - Improved command line support (try `./quantum_launcher --help` for more info) - Added a changelog viewer - Added a welcome screen for new users! Say hi! @@ -18,8 +23,10 @@ - Started work on macOS support - Added a confirmation dialog for uninstalling loaders - MASSIVE codebase cleanup, optimizations and improvements (you probably won't notice it) + ## Fixes and polish: -- Launcher logs no longer have garbled text on windows -- Launcher logs are no longer delayed on windows + +- Launcher logs no longer have garbled text on Windows +- Launcher logs are no longer delayed on Windows - Fixed a crash in some old versions of Minecraft (`useLegacyMergeSort`) -- Fixed a bug where OptiFine installer was broken on windows +- Fixed a bug where OptiFine installer was broken on Windows diff --git a/changelogs/0.3.md b/changelogs/0.3.md index 51e19c11b..765e20962 100644 --- a/changelogs/0.3.md +++ b/changelogs/0.3.md @@ -1,28 +1,44 @@ # v0.3 + TLDR; Mod manager, OptiFine support, mod store for downloading mods, performance improvements, bugfixes + ## Mods + - Select, enable, disable and delete your mods with the new mod manager! - Added OptiFine support! (Mods->Install OptiFine) + ### Store -- Added a built-in mod store, integrated with modrinth.com (Mods->Download Mods) + +- Added a built-in mod store, integrated with modrinth.com (Mods → Download Mods) - You can now search, view and download mods -- *note: this is experimental and may be buggy* +- **Note: this is experimental and may be buggy** + ## Optimization + - *DRASTICALLY* improved download times for creating an instance - Asset files are shared between instances, saving a lot of storage + ## Old Minecraft + - Fixed sound being broken in old versions (1.6.4 and below) due to incorrect asset downloading - BetaCraft proxy is now used for older versions, resulting in correct sounds and skins + ## Settings + - Added launcher themes (light/dark mode) and styles (brown/purple color scheme) - The launcher settings menu contains links to our GitHub, website and discord. - You can now edit Java and Game arguments in instance settings. -- Added a debugging option to disable log output (to see advanced crash messages correctly in the terminal window). Use this if your instance crashed without any logs. Edit Instance -> Enable logging (disable it) +- Added a debugging option to disable log output (to see advanced crash messages correctly in the terminal window). + Use this if your instance crashed without any logs. Edit Instance → Enable logging (disable it) + ## Command-Line + - `--command` argument to not make it pop up a window (headless) - `--list-instances` flag to print a list of instances (name, version, loader) - More headless functionality coming soon + ## Other + - Fixed a crash with modern versions of Minecraft and fabric/forge - Added a search bar for create instance version list - Cleaned up and improved the menus for Forge and Fabric installers diff --git a/changelogs/0.4.1.md b/changelogs/0.4.1.md index d5cad569d..1358b7d10 100644 --- a/changelogs/0.4.1.md +++ b/changelogs/0.4.1.md @@ -1,9 +1,12 @@ # v0.4.1 + ## Modding + - Added Modpacks, Shaders and Resource Packs stores - Drag-and-drop support for mod files, modpacks and presets - JarMod patching support (useful for old Minecraft versions) - OptiFine support for b1.6.6, b1.7.3, 1.2.5 and 1.5.2 + ### UX - "Open Mod Page" in mod description now shows URL in tooltip - Added bullet/numbered list support in curseforge mod descriptions @@ -19,42 +22,58 @@ Leave the file **blank** for full portable mode, or write a path inside it to se > Note: On macOS you may need to **extract the `.app` from the DMG** for portable mode. ## Menu Overhauls + - Welcome screen redesigned to guide new users more clearly - Error screen looks better, now has a "Copy Log + Error" button - Fabric Install screen is now simpler and less cluttered + ## UI/UX + - New Catppuccin color scheme - Improved aesthetics of light themes - Animated pulsating "..." dots in a few "Loading" screens - Minor tweaks to buttons and layout in many screens - Added a "Clean Java Installs" button (for reinstalling java) in launcher settings -- Old versions in the version list now have cleaner names ("beta/b1.7/b1.7.3" -> "b1.7.3") +- Old versions in the version list now have cleaner names ("beta/b1.7/b1.7.3" → "b1.7.3") - Window size now respects UI scale + ## Keyboard Navigation + - Up/Down arrow keys to select instance -- Ctrl+Enter -> launch instance -- Ctrl+Backspace -> kill instance -- Ctrl+Q -> exit launcher (only in main menu, when nothing's running) +- Ctrl+Enter → launch instance +- Ctrl+Backspace → kill instance +- Ctrl+Q → exit launcher (only in main menu, when nothing's running) - (note: on macOS it's Cmd instead of Ctrl) - More coming in the future! + ## Game Crashed Message + - There's now a "Join Discord" button in Logs tab - There's no longer a tip mentioning a "second terminal" upon crash (no longer needed) + ## Dependencies + - Replaced OpenSSL with `rustls` - Linux binary now only depends on `libgcc_s` and `libc` (glibc); OpenSSL and LZMA `.so` dependencies have been removed + +--- + ## Fixes + - Crash messages now appear in Logs tab (critical!) - Forge now works after renaming instance - Multiplayer now works in 1.16.x for offline accounts - Mod updating and preset importing are now working again! (fixed error for curseforge, index corruption for modrinth) - Fixed extreme launcher log file spam + ### UI/UX + - UI scaling now has a minimum limit preventing unusable layouts - Many mod-related operations now return you to Mods screen, instead of launch screen - Pressing Esc in mod descriptions now exits the description only (not the entire store) - Fixed mod store "forgetting" scroll position when opening description and exiting + ### Platform-specific - Fixed 1.13-1.16 crashing on linux ARM - Fixed 1.8.9 crashing on macOS ARM diff --git a/changelogs/0.4.2.md b/changelogs/0.4.2.md index 96322b3d3..1d5830a9d 100644 --- a/changelogs/0.4.2.md +++ b/changelogs/0.4.2.md @@ -50,6 +50,8 @@ The following commands have been added: - Linux ARM 32-bit is now supported! - Experimental FreeBSD support is also available +--- + # Fixes - Fixed many crashes on Linux ARM and macOS `(b)` @@ -58,7 +60,7 @@ The following commands have been added: - Fixed welcome screen not working - Fixed arrow keys to switch instances, not updating the Edit menu ---- +Tweaks: - Account session ID is now censored in game logs - Improved readability of a few errors @@ -67,26 +69,26 @@ The following commands have been added: - Last account selected is now remembered - On Windows, custom file managers are now respected when opening files ---- +UX: - Added dialog to prevent glitching upon changing UI scale - Removed Ctrl-Scroll to change UI scale due to many bugs -## Modding +Modding: - Fixed Fabric API being missing for some curseforge mods - Fixed getting stuck in an infinite loop when downloading some curseforge mods - Fixed modrinth mods repeating infinitely in the store list - Improved mod description rendering in the store -## Versions +Versions: - Fixed Minecraft Indev and early Infdev being unplayable `(b)` - Fixed broken colors in old versions on M-series Macs `(b)` - Old Minecraft versions are now in the correct order in the download list `(b)` - Snapshots of 1.0 to 1.5.2 are no longer missing for download `(b)` -## Performance +Performance: - Fixed lag spikes on some systems when selecting instances - Many autosaving features has been slowed down, and disk accesses reduced diff --git a/changelogs/0.4.md b/changelogs/0.4.md index 746134d47..fd5931705 100644 --- a/changelogs/0.4.md +++ b/changelogs/0.4.md @@ -1,30 +1,44 @@ # v0.4 + ## Redesign + - Redesigned the launcher UI! - Select instances easily with a sidebar, and enjoy the simpler navigation with tabs! -- The purple colour palette is now more vibrant and punchy +- The purple color palette is now more vibrant and punchy - Many other menus have been changed to look nicer + ## Mods + - Added CurseForge to the Mods store (alongside Modrinth). You can choose the backend - Added NeoForge installer! - Mod updating and preset importing is now nearly 2x faster! - Getting list of versions when creating instance is now nearly 2x faster! - The mod store now has infinite scrolling! Just scroll down to see more mods + ## Microsoft Account + - Added optional Microsoft login for those with a paid account - Users can still continue launching the game in offline mode like earlier + ## Game + - Added option to close launcher after opening game - Overhauled the game log viewer. There is no limit now! - You can see the launcher debug logs by clicking the bottom bar + ## Platform + - macOS support is now ready! - Improved Java installer support for Windows 32 bit, Windows ARM and macOS + ## Development + - MASSIVE code cleanup and overhaul - Redesigned the command-line experience with the "clap" library. (It's still not complete though) - Updated "iced" to 0.13.1 from 0.12.1. Also updated many other libraries + ## Fixes + - Fixed the terminal popping up on Windows - Fixed a bug where you couldn't disable local mods - Fixed a JSON error when installing some mods (like `debugify`) @@ -35,4 +49,4 @@ - Fixed many rendering bugs with the mod description viewer in the mod store - Reduced useless log file spam in "QuantumLauncher/logs/" folder - Reduced errors in the fabric installer -- Improved reliability of network operations, you should see "Request Error"s less now +- Improved reliability of network operations, you should see Request Error’s less now diff --git a/changelogs/0.5.md b/changelogs/0.5.md index b08739b9b..b31dcd6b5 100644 --- a/changelogs/0.5.md +++ b/changelogs/0.5.md @@ -26,7 +26,7 @@ Added alternate fabric implementations for versions without official Fabric supp # UX - Export mods as a shareable text list with optional links! -- Write instance-specific notes for coordinates, todo lists, etc! +- Write instance-specific notes for coordinates, todo lists, etc.! - Many small UX improvements and polish - The launcher will now remember the last-selected instance (toggleable) @@ -43,7 +43,7 @@ Overhauled the Create Instance screen, now with: - Sidebar to view versions - Filters for release/snapshot/beta/... (thanks @Sreehari425) - Search bar -- Auto-filling version and name by default +- Autofilling version and name by default ## Mod Menu @@ -126,7 +126,7 @@ You can now import `.zip` instances from MultiMC and PrismLauncher! - Fixed NeoForge 1.21.1 and Forge 1.21.5 crash (reinstall loader to apply) - Fixed forge installer error: "Processor failed, invalid outputs" - Fixed wrong link used for "Open Website" in auto-update screen -- Fixed confusing error on entering wrong password in ely.by login +- Fixed confusing error on entering wrong password in `ely.by` login ## Platform diff --git a/changelogs/unreleased.md b/changelogs/unreleased.md index 177006f0d..7007c9665 100644 --- a/changelogs/unreleased.md +++ b/changelogs/unreleased.md @@ -1,6 +1,19 @@ # unreleased changelog -- Added a Quick Uninstall button to the Mod Store +# TLDR; +- Added instance folders! +- Added shortcuts; launch the game from your taskbar in one click! +- Many UX improvements and fixes + +--- + +- Added instance folders, with drag & drop and renaming + +# UX + +- Added a quick-uninstall button to Mod Store +- Improved new-user Welcome screen with keyboard navigation, + better layout and more guidance # Shortcuts @@ -32,6 +45,8 @@ # Fixes +- Fixed context menus not closing after a click + - Fixed many CurseForge concurrent downloading issues - Fixed QMP presets added via "Add File" in Mods menu, to not install all mods - Fixed account login persistence for new users diff --git a/crates/ezshortcut/src/os/unix.rs b/crates/ezshortcut/src/os/unix.rs index ee57048da..637307789 100644 --- a/crates/ezshortcut/src/os/unix.rs +++ b/crates/ezshortcut/src/os/unix.rs @@ -76,7 +76,7 @@ Categories=Game;", write_file(&path.join(shortcut.get_filename()), content).await?; } _ => write_file(path, content).await?, - }; + } Ok(()) } diff --git a/crates/ql_core/src/file_utils.rs b/crates/ql_core/src/file_utils.rs index 37a88efea..36fa6c60e 100644 --- a/crates/ql_core/src/file_utils.rs +++ b/crates/ql_core/src/file_utils.rs @@ -604,12 +604,10 @@ pub async fn extract_zip_archive< archive.extract_unwrapped_root_dir(extract_to, zip::read::root_dir_common_filter) }) .await - .unwrap()?; } else { - tokio::task::spawn_blocking(move || archive.extract(extract_to)) - .await - .unwrap()?; + tokio::task::spawn_blocking(move || archive.extract(extract_to)).await } + .map_err(|n| zip::result::ZipError::Io(n.into()))??; Ok(()) } diff --git a/crates/ql_core/src/json/instance_config.rs b/crates/ql_core/src/json/instance_config.rs index 8967630e5..4eaa04dd9 100644 --- a/crates/ql_core/src/json/instance_config.rs +++ b/crates/ql_core/src/json/instance_config.rs @@ -1,8 +1,14 @@ -use std::path::{Path, PathBuf}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; use serde::{Deserialize, Serialize}; -use crate::{InstanceSelection, IntoIoError, IntoJsonError, JsonFileError, Loader}; +use crate::{ + InstanceSelection, IntoIoError, IntoJsonError, JsonFileError, Loader, + DEFAULT_RAM_MB_FOR_INSTANCE, +}; /// Configuration for a specific instance. /// Not to be confused with [`crate::json::VersionDetails`]. That one @@ -90,9 +96,41 @@ pub struct InstanceConfigJson { /// An override for the main class when launching the game. /// Mainly only used for debugging purposes. pub main_class_override: Option, + + #[serde(flatten)] + _extra: HashMap, } impl InstanceConfigJson { + #[must_use] + pub fn new(is_server: bool, is_classic_server: bool, version_info: VersionInfo) -> Self { + #[allow(deprecated)] + Self { + mod_type: Loader::Vanilla, + java_override_version: None, + java_override: None, + ram_in_mb: DEFAULT_RAM_MB_FOR_INSTANCE, + enable_logger: Some(true), + java_args: None, + game_args: None, + + is_server: Some(is_server), + is_classic_server: Some(is_classic_server), + + omniarchive: None, + close_on_start: None, + global_settings: None, + global_java_args_enable: None, + custom_jar: None, + pre_launch_prefix_mode: None, + mod_type_info: None, + + version_info: Some(version_info), + main_class_override: None, + _extra: HashMap::new(), + } + } + /// Returns a String containing the Java argument to /// allocate the configured amount of RAM. #[must_use] @@ -212,6 +250,7 @@ impl InstanceConfigJson { } } + #[must_use] pub fn c_global_settings(&mut self) -> &mut GlobalSettings { self.global_settings .get_or_insert_with(GlobalSettings::default) @@ -269,6 +308,31 @@ pub struct ModTypeInfo { /// was used, which one (eg: Legacy Fabric). pub backend_implementation: Option, pub optifine_jar: Option, + + #[serde(flatten)] + _extra: HashMap, +} + +impl ModTypeInfo { + #[must_use] + pub fn new_regular(version: String) -> Self { + Self { + version: Some(version), + backend_implementation: None, + optifine_jar: None, + _extra: HashMap::new(), + } + } + + #[must_use] + pub fn new_with_backend(version: String, backend: String) -> Self { + Self { + version: Some(version), + backend_implementation: Some(backend), + optifine_jar: None, + _extra: HashMap::new(), + } + } } /// Settings that can both be set on a per-instance basis @@ -287,11 +351,26 @@ pub struct GlobalSettings { /// to the launch command (e.g., "prime-run" for NVIDIA GPU usage on Linux). // Since: v0.5.0 pub pre_launch_prefix: Option>, + + #[serde(flatten)] + _extra: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VersionInfo { pub is_special_lwjgl3: bool, + #[serde(flatten)] + _extra: HashMap, +} + +impl VersionInfo { + #[must_use] + pub fn new(version: &str) -> Self { + Self { + is_special_lwjgl3: version.ends_with("-lwjgl3"), + _extra: HashMap::new(), + } + } } /// Defines how instance pre-launch prefix commands should interact with global pre-launch prefix commands @@ -347,6 +426,19 @@ impl std::fmt::Display for PreLaunchPrefixMode { pub struct CustomJarConfig { pub name: String, pub autoset_main_class: bool, + #[serde(flatten)] + _extra: HashMap, +} + +impl CustomJarConfig { + #[must_use] + pub fn new(name: String) -> Self { + Self { + name, + autoset_main_class: false, + _extra: HashMap::new(), + } + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/crates/ql_core/src/json/version.rs b/crates/ql_core/src/json/version.rs index 37d80e966..bcbe03672 100644 --- a/crates/ql_core/src/json/version.rs +++ b/crates/ql_core/src/json/version.rs @@ -319,6 +319,7 @@ impl Library { allowed = false; for rule in rules { + #[allow(clippy::unnecessary_semicolon)] // cfg_if weirdness if let Some(ref os) = rule.os { cfg_if!( if #[cfg(any( diff --git a/crates/ql_core/src/lib.rs b/crates/ql_core/src/lib.rs index 9c68d9021..75ab8c707 100644 --- a/crates/ql_core/src/lib.rs +++ b/crates/ql_core/src/lib.rs @@ -356,7 +356,7 @@ pub enum ListEntryKind { Special, } -impl std::fmt::Display for ListEntryKind { +impl Display for ListEntryKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ListEntryKind::Release => write!(f, "Release"), @@ -444,7 +444,7 @@ impl ListEntryKind { } } - /// Returns true if this is a "old" version category + /// Returns true if this is an "old" version category #[must_use] pub const fn is_old(&self) -> bool { matches!( diff --git a/crates/ql_core/src/progress.rs b/crates/ql_core/src/progress.rs index c8bc89877..31ee00cc4 100644 --- a/crates/ql_core/src/progress.rs +++ b/crates/ql_core/src/progress.rs @@ -4,8 +4,8 @@ use std::fmt::Display; /// a Minecraft instance. /// /// # Order -/// 1) Manifest Json -/// 2) Version Json +/// 1) Manifest JSON +/// 2) Version JSON /// 3) Logging config /// 4) Jar /// 5) Libraries diff --git a/crates/ql_core/src/read_log.rs b/crates/ql_core/src/read_log.rs index fc47dcbe1..3ed561ecf 100644 --- a/crates/ql_core/src/read_log.rs +++ b/crates/ql_core/src/read_log.rs @@ -125,7 +125,7 @@ async fn read_log_from_stream( LogLine::Error } else { LogLine::Message - }(remaining.replace('\t', "\n ")), + }(remaining.replace('\t', "\n\t")), ); } @@ -463,11 +463,10 @@ impl Display for LogEvent { let date = self.get_time().unwrap_or_else(|| self.timestamp.clone()); writeln!( f, - // "[{level}] [{date}:{thread}:{class}] {msg}", - "[{level}] [{date}] {msg}", + "[{date}] [{class}/{level}]: {msg}", level = self.level, // thread = self.thread, - // class = self.logger, + class = self.logger, msg = if let Some(n) = &self.message { &n } else { "" } )?; if let Some(throwable) = self.throwable.as_deref() { diff --git a/crates/ql_instances/src/download/downloader.rs b/crates/ql_instances/src/download/downloader.rs index 04fb5bca4..c669e4c10 100644 --- a/crates/ql_instances/src/download/downloader.rs +++ b/crates/ql_instances/src/download/downloader.rs @@ -6,7 +6,6 @@ use std::{ use crate::json_profiles::ProfileJson; use ql_core::{ - constants::DEFAULT_RAM_MB_FOR_INSTANCE, do_jobs, file_utils::{self, LAUNCHER_DIR}, impl_3_errs_jri, info, @@ -14,7 +13,7 @@ use ql_core::{ instance_config::VersionInfo, AssetIndex, InstanceConfigJson, Manifest, VersionDetails, }, pt, DownloadFileError, DownloadProgress, IntoIoError, IntoJsonError, IoError, JsonError, - ListEntry, Loader, RequestError, + ListEntry, RequestError, }; use thiserror::Error; use tokio::{fs, sync::Mutex}; @@ -310,29 +309,8 @@ impl GameDownloader { } pub async fn create_config_json(&self) -> Result<(), DownloadError> { - #[allow(deprecated)] - let config_json = InstanceConfigJson { - java_override: None, - java_override_version: None, - ram_in_mb: DEFAULT_RAM_MB_FOR_INSTANCE, - mod_type: Loader::Vanilla, - enable_logger: Some(true), - java_args: None, - game_args: None, - is_classic_server: None, - close_on_start: None, - is_server: Some(false), - omniarchive: None, - global_settings: None, - global_java_args_enable: None, - pre_launch_prefix_mode: None, - custom_jar: None, - mod_type_info: None, - version_info: Some(VersionInfo { - is_special_lwjgl3: self.version_json.id.ends_with("-lwjgl3"), - }), - main_class_override: None, - }; + let config_json = + InstanceConfigJson::new(false, false, VersionInfo::new(&self.version_json.id)); let config_json = serde_json::to_string(&config_json).json_to()?; let config_json_path = self.instance_dir.join("config.json"); diff --git a/crates/ql_instances/src/download/libraries.rs b/crates/ql_instances/src/download/libraries.rs index fc39625cd..62cf01de1 100644 --- a/crates/ql_instances/src/download/libraries.rs +++ b/crates/ql_instances/src/download/libraries.rs @@ -22,8 +22,8 @@ use tokio::fs; use super::{DownloadError, GameDownloader}; -const MACOS_ARM_LWJGL_294_1: &str = "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar"; -const MACOS_ARM_LWJGL_294_2: &str = "https://github.com/Dungeons-Guide/lwjgl/releases/download/2.9.4-20150209-mmachina.2-syeyoung.1/lwjgl-platform-2.9.4-nightly-20150209-natives-osx-arm64.jar"; +const MACOS_X64_LWJGL_294: &str = "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar"; +const MACOS_ARM_LWJGL_294: &str = "https://github.com/Dungeons-Guide/lwjgl/releases/download/2.9.4-20150209-mmachina.2-syeyoung.1/lwjgl-platform-2.9.4-nightly-20150209-natives-osx-arm64.jar"; impl GameDownloader { pub async fn download_libraries(&mut self) -> Result<(), DownloadError> { @@ -39,23 +39,34 @@ impl GameDownloader { .iter() .map(|lib| self.download_library_fn(lib, &num_library, total_libraries)); - // Uncomment for synchronous downloads. WAY slower, - // but easier to debug/inspect logs of, + // (a) Synchronous downloader. WAY slower, + // but easier to debug/inspect, // if you're working on the library downloader // for job in results { // job.await?; // } - // The one below is the concurrent downloader, downloading multiple - // libraries at the same time. If you uncomment the above one, make sure - // to comment this below one out. - // This is WAY faster but harder to debug/inspect + // (b) Concurrent downloader, downloads multiple libs at the same time + // WAY faster but harder to debug/inspect _ = do_jobs(results).await?; + self.cleanup_junk().await; + Ok(()) } + async fn cleanup_junk(&self) { + let natives_dir = self.instance_dir.join("libraries/natives"); + _ = fs::remove_dir_all(natives_dir.join("META-INF")).await; + _ = fs::remove_file(natives_dir.join("INDEX.LIST")).await; + _ = fs::remove_file(natives_dir.join("MANIFEST.MF")).await; + + if let Err(err) = cleanup_directory(&natives_dir).await { + err!("While cleaning up libraries/natives/: {err}"); + } + } + async fn download_library_fn( &self, library: &Library, @@ -306,8 +317,8 @@ impl GameDownloader { if url == "https://github.com/theofficialgman/lwjgl3-binaries-arm64/raw/lwjgl-3.1.6/lwjgl-jemalloc-natives-linux.jar" { "https://github.com/theofficialgman/lwjgl3-binaries-arm64/raw/lwjgl-3.1.6/lwjgl-jemalloc-patched-natives-linux-arm64.jar".clone_into(&mut url); } - if (cfg!(target_arch = "aarch64") && url == MACOS_ARM_LWJGL_294_1) || url == "https://github.com/MinecraftMachina/lwjgl/releases/download/2.9.4-20150209-mmachina.2/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar" { - MACOS_ARM_LWJGL_294_2.clone_into(&mut url); + if (cfg!(target_arch = "aarch64") && url == MACOS_X64_LWJGL_294) || url == "https://github.com/MinecraftMachina/lwjgl/releases/download/2.9.4-20150209-mmachina.2/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar" { + MACOS_ARM_LWJGL_294.clone_into(&mut url); } #[cfg(any( @@ -468,3 +479,31 @@ impl GameDownloader { Ok(()) } } + +async fn cleanup_directory(dir: &Path) -> Result<(), IoError> { + async fn is_dir_empty(dir: &Path) -> Result { + let mut entries = fs::read_dir(dir).await.path(dir)?; + Ok(entries.next_entry().await.path(dir)?.is_none()) + } + + let mut entries = fs::read_dir(dir).await.path(dir)?; + + while let Some(entry) = entries.next_entry().await.path(dir)? { + let path = entry.path(); + let file_type = entry.file_type().await.path(&path)?; + + if file_type.is_dir() { + Box::pin(cleanup_directory(&path)).await?; + + // After recursing, try to remove if empty + if is_dir_empty(&path).await? { + fs::remove_dir(&path).await.path(path)?; + } + } else if file_type.is_file() && path.extension().and_then(|e| e.to_str()) == Some("class") + { + fs::remove_file(&path).await.path(path)?; + } + } + + Ok(()) +} diff --git a/crates/ql_instances/src/launcher_update_detector.rs b/crates/ql_instances/src/launcher_update_detector.rs index 86075ab48..be7e2c593 100644 --- a/crates/ql_instances/src/launcher_update_detector.rs +++ b/crates/ql_instances/src/launcher_update_detector.rs @@ -27,16 +27,15 @@ pub enum UpdateCheckInfo { /// (url pointing to zip file containing new version executable). /// /// # Errors -/// - If current version is ahead of latest version +/// - current version is ahead of latest version /// (experimental dev build) -/// - If the release info couldn't be downloaded -/// - If the release info couldn't be parsed into JSON -/// - If no releases could be found in the GitHub repo -/// - If the new version's version number is incompatible +/// - release info couldn't be downloaded +/// - release info couldn't be parsed into JSON +/// - no releases could be found in the GitHub repo +/// - new version's version number is incompatible /// with semver (even after conversion) -/// - If user is on unsupported architecture -/// - If user is on unsupported OS -/// - If no matching download could be found for your OS +/// - user is on unsupported architecture or OS +/// - no matching download could be found for the platform pub async fn check_for_launcher_updates() -> Result { const URL: &str = "https://api.github.com/repos/Mrmayman/quantum-launcher/releases"; diff --git a/crates/ql_instances/src/lib.rs b/crates/ql_instances/src/lib.rs index fbe001d0f..f97c2d819 100644 --- a/crates/ql_instances/src/lib.rs +++ b/crates/ql_instances/src/lib.rs @@ -37,7 +37,7 @@ //! whose jar file is extracted to the `natives` folder. //! - `classifiers: *` - Once again, part of main library //! but have separate jar files for each OS. Just formatted -//! differently in the json. +//! differently in the JSON. //! //! For whatever reason, natives are arranged in an //! **extremely** messy way in the JSONs. diff --git a/crates/ql_java_handler/Cargo.toml b/crates/ql_java_handler/Cargo.toml index 417667d1e..1a6db8d51 100644 --- a/crates/ql_java_handler/Cargo.toml +++ b/crates/ql_java_handler/Cargo.toml @@ -15,7 +15,6 @@ ql_core.path = "../ql_core" thiserror.workspace = true tokio.workspace = true serde.workspace = true -serde_json.workspace = true owo-colors.workspace = true cfg-if.workspace = true diff --git a/crates/ql_java_handler/src/lib.rs b/crates/ql_java_handler/src/lib.rs index 3da8bfbab..997c0f68e 100644 --- a/crates/ql_java_handler/src/lib.rs +++ b/crates/ql_java_handler/src/lib.rs @@ -90,7 +90,7 @@ const fn which_java() -> &'static str { /// /// `javaw` on Windows, `java` on all other platforms. /// -/// On windows, `javaw` is used to avoid accidentally opening +/// On Windows, `javaw` is used to avoid accidentally opening /// secondary terminal window. This uses the Windows subsystem /// instead of the Console subsystem, so the OS treats it as /// a GUI app. diff --git a/crates/ql_mod_manager/Cargo.toml b/crates/ql_mod_manager/Cargo.toml index 869d0861b..1e3105fde 100644 --- a/crates/ql_mod_manager/Cargo.toml +++ b/crates/ql_mod_manager/Cargo.toml @@ -21,6 +21,5 @@ futures.workspace = true owo-colors.workspace = true thiserror.workspace = true -image.workspace = true chrono.workspace = true regex.workspace = true diff --git a/crates/ql_mod_manager/src/loaders/fabric/mod.rs b/crates/ql_mod_manager/src/loaders/fabric/mod.rs index c4e37b181..3d3595280 100644 --- a/crates/ql_mod_manager/src/loaders/fabric/mod.rs +++ b/crates/ql_mod_manager/src/loaders/fabric/mod.rs @@ -113,14 +113,10 @@ pub async fn install_server( } else { Loader::Fabric }, - Some(ModTypeInfo { - version: Some(loader_version), - backend_implementation: if let BackendType::Fabric | BackendType::Quilt = backend { - None - } else { - Some(backend.to_string()) - }, - optifine_jar: None, + Some(if let BackendType::Fabric | BackendType::Quilt = backend { + ModTypeInfo::new_regular(loader_version) + } else { + ModTypeInfo::new_with_backend(loader_version, backend.to_string()) }), ) .await?; @@ -217,14 +213,10 @@ pub async fn install_client( } else { Loader::Fabric }, - Some(ModTypeInfo { - version: Some(loader_version), - backend_implementation: if let BackendType::Fabric | BackendType::Quilt = backend { - None - } else { - Some(backend.to_string()) - }, - optifine_jar: None, + Some(if let BackendType::Fabric | BackendType::Quilt = backend { + ModTypeInfo::new_regular(loader_version) + } else { + ModTypeInfo::new_with_backend(loader_version, backend.to_string()) }), ) .await?; diff --git a/crates/ql_mod_manager/src/loaders/forge/mod.rs b/crates/ql_mod_manager/src/loaders/forge/mod.rs index ca2e2a82b..97b213972 100644 --- a/crates/ql_mod_manager/src/loaders/forge/mod.rs +++ b/crates/ql_mod_manager/src/loaders/forge/mod.rs @@ -224,7 +224,7 @@ impl ForgeInstaller { self.run_installer_create_garbage_files().await?; let java_version = if cfg!(target_os = "windows") { - // WTF: No clue why this is needed but it won't work without this. + // WTF: No clue why this is needed, but it won't work without this. // Hey, that's what you get for not using PrismLauncher! self.version_json .javaVersion @@ -586,11 +586,7 @@ pub async fn install_client( change_instance_type( &installer.instance_dir, Loader::Forge, - Some(ModTypeInfo { - version: Some(installer.version.clone()), - backend_implementation: None, - optifine_jar: None, - }), + Some(ModTypeInfo::new_regular(installer.version)), ) .await?; diff --git a/crates/ql_mod_manager/src/loaders/forge/server.rs b/crates/ql_mod_manager/src/loaders/forge/server.rs index 4afef2f2b..c0b096138 100644 --- a/crates/ql_mod_manager/src/loaders/forge/server.rs +++ b/crates/ql_mod_manager/src/loaders/forge/server.rs @@ -44,11 +44,7 @@ pub async fn install_server( change_instance_type( &installer.instance_dir, Loader::Forge, - Some(ModTypeInfo { - version: Some(installer.version.clone()), - backend_implementation: None, - optifine_jar: None, - }), + Some(ModTypeInfo::new_regular(installer.version)), ) .await?; diff --git a/crates/ql_mod_manager/src/loaders/neoforge.rs b/crates/ql_mod_manager/src/loaders/neoforge.rs index 315715e39..a902e121a 100644 --- a/crates/ql_mod_manager/src/loaders/neoforge.rs +++ b/crates/ql_mod_manager/src/loaders/neoforge.rs @@ -75,11 +75,7 @@ pub async fn install( change_instance_type( &instance_dir, Loader::Neoforge, - Some(ModTypeInfo { - version: Some(neoforge_version), - backend_implementation: None, - optifine_jar: None, - }), + Some(ModTypeInfo::new_regular(neoforge_version)), ) .await?; diff --git a/crates/ql_mod_manager/src/loaders/paper.rs b/crates/ql_mod_manager/src/loaders/paper.rs index dda452fa1..1f1356692 100644 --- a/crates/ql_mod_manager/src/loaders/paper.rs +++ b/crates/ql_mod_manager/src/loaders/paper.rs @@ -74,11 +74,7 @@ pub async fn install(instance_name: String, version: PaperVer) -> Result<(), Pap change_instance_type( &server_dir, Loader::Paper, - Some(ModTypeInfo { - version: Some(version.id.to_string()), - backend_implementation: None, - optifine_jar: None, - }), + Some(ModTypeInfo::new_regular(version.id.to_string())), ) .await?; diff --git a/crates/ql_mod_manager/src/presets/mod.rs b/crates/ql_mod_manager/src/presets/mod.rs index 25e8a913a..f9b924d91 100644 --- a/crates/ql_mod_manager/src/presets/mod.rs +++ b/crates/ql_mod_manager/src/presets/mod.rs @@ -16,6 +16,7 @@ use zip::ZipWriter; use crate::store::{install_modpack, ModConfig, ModError, ModIndex}; +#[must_use] #[derive(Debug, Clone, Default)] pub struct PresetOutput { pub local_files: Vec, diff --git a/crates/ql_mod_manager/src/store/mod.rs b/crates/ql_mod_manager/src/store/mod.rs index b16b4c2a7..4498e45ee 100644 --- a/crates/ql_mod_manager/src/store/mod.rs +++ b/crates/ql_mod_manager/src/store/mod.rs @@ -178,10 +178,11 @@ impl Display for QueryType { } impl QueryType { - // use this for the store since datapacks cant be installed globally, - // only pre worlds, since you need to copy the datapack file into each world - // once the launcher have support for installing datapacks properly delete this - // and use ALL in the store too. + /// Use this for the store since datapacks can't be installed globally, + /// only per worlds, since you need to copy the datapack file into each world. + /// + /// Once the launcher has support for installing datapacks properly, + /// delete this and use ALL in the store too. pub const STORE_QUERIES: &'static [Self] = &[ Self::Mods, Self::ResourcePacks, diff --git a/crates/ql_mod_manager/src/store/modrinth/download.rs b/crates/ql_mod_manager/src/store/modrinth/download.rs index 6998a8995..98a6d88d4 100644 --- a/crates/ql_mod_manager/src/store/modrinth/download.rs +++ b/crates/ql_mod_manager/src/store/modrinth/download.rs @@ -42,7 +42,7 @@ impl ModDownloader { .get_loader() .await? .not_vanilla() - .map(|n| n.to_modrinth_str()); + .map(ql_core::Loader::to_modrinth_str); let currently_installing_mods = HashSet::new(); Ok(ModDownloader { version: version_json.get_id().to_owned(), diff --git a/crates/ql_packager/src/multimc.rs b/crates/ql_packager/src/multimc.rs index 28210e8b8..379ef3cb4 100644 --- a/crates/ql_packager/src/multimc.rs +++ b/crates/ql_packager/src/multimc.rs @@ -316,13 +316,7 @@ async fn install_fabric( let version_json = VersionDetails::load(instance_selection).await?; if !version_json.is_before_or_eq(V_OFFICIAL_FABRIC_SUPPORT) { - ql_mod_manager::loaders::fabric::install( - version, - instance_selection.clone(), - sender, - backend, - ) - .await?; + fabric::install(version, instance_selection.clone(), sender, backend).await?; return Ok(()); } diff --git a/crates/ql_servers/Cargo.toml b/crates/ql_servers/Cargo.toml index 2c8d85690..563224956 100644 --- a/crates/ql_servers/Cargo.toml +++ b/crates/ql_servers/Cargo.toml @@ -8,10 +8,8 @@ license = "GPL-3.0-only" ql_core.path = "../ql_core" ql_java_handler.path = "../ql_java_handler" -serde.workspace = true serde_json.workspace = true tokio.workspace = true owo-colors.workspace = true zip.workspace = true thiserror.workspace = true -chrono.workspace = true diff --git a/crates/ql_servers/src/create.rs b/crates/ql_servers/src/create.rs index e73e829cc..cc4e9244f 100644 --- a/crates/ql_servers/src/create.rs +++ b/crates/ql_servers/src/create.rs @@ -3,8 +3,7 @@ use std::sync::mpsc::Sender; use ql_core::{ file_utils, info, json::{instance_config::VersionInfo, InstanceConfigJson, Manifest, VersionDetails}, - pt, DownloadProgress, IntoIoError, IntoJsonError, IntoStringError, ListEntry, Loader, - LAUNCHER_DIR, + pt, DownloadProgress, IntoIoError, IntoJsonError, IntoStringError, ListEntry, LAUNCHER_DIR, }; use crate::ServerError; @@ -97,32 +96,8 @@ async fn write_config( server_dir: &std::path::Path, version_json: &VersionDetails, ) -> Result<(), ServerError> { - #[allow(deprecated)] - let server_config = InstanceConfigJson { - mod_type: Loader::Vanilla, - java_override_version: None, - java_override: None, - ram_in_mb: 2048, - enable_logger: Some(true), - java_args: None, - game_args: None, - - is_server: Some(true), - is_classic_server: is_classic_server.then_some(true), - - omniarchive: None, - close_on_start: None, - global_settings: None, - global_java_args_enable: None, - custom_jar: None, - pre_launch_prefix_mode: None, - mod_type_info: None, - - version_info: Some(VersionInfo { - is_special_lwjgl3: version_json.id.ends_with("-lwjgl3"), - }), - main_class_override: None, - }; + let server_config = + InstanceConfigJson::new(true, is_classic_server, VersionInfo::new(&version_json.id)); let server_config_path = server_dir.join("config.json"); tokio::fs::write( &server_config_path, diff --git a/crates/ql_servers/src/run.rs b/crates/ql_servers/src/run.rs index baa50bf53..f609ae2ea 100644 --- a/crates/ql_servers/src/run.rs +++ b/crates/ql_servers/src/run.rs @@ -122,7 +122,7 @@ impl ServerLauncher { pub async fn get_server_jar(&self) -> Result { Ok(if let Some(custom_jar) = &self.config.custom_jar { - // Should I prioritise Fabric/Forge/Paper over a custom JAR? + // Should I prioritize Fabric/Forge/Paper over a custom JAR? PathBuf::from(&custom_jar.name) } else { let regular = self.dir.join("server.jar"); diff --git a/docs/CODEBASE.md b/docs/CODEBASE.md index 081ad216b..0b9a821e4 100644 --- a/docs/CODEBASE.md +++ b/docs/CODEBASE.md @@ -127,7 +127,7 @@ tokio::fs::write(&path, &bytes).await.path(path)?; ### `.json(original_string)` (from `ql_core::IntoJsonError`) -For adding context when parsing json **strings** into structs. Use on `serde_json` errors. +For adding context when parsing JSON **strings** into structs. Use on `serde_json` errors. ### `.json_to()` (from `ql_core::IntoJsonError`) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 3b272908b..f91551816 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -16,7 +16,7 @@ Plans for the future of QuantumLauncher. - 🟠¹: 1.12.2 and below - 🔴: Not supported -> Not all platforms are well supported due to lack of +> Not all platforms are well-supported due to lack of > development resources. If you can, consider > contributing and helping out. > diff --git a/quantum_launcher/Cargo.toml b/quantum_launcher/Cargo.toml index 26b7620e0..792cee8b4 100644 --- a/quantum_launcher/Cargo.toml +++ b/quantum_launcher/Cargo.toml @@ -55,21 +55,10 @@ frostmark = { git = "https://github.com/Mrmayman/frostmark.git", rev = "94a4e904 serde.workspace = true serde_json.workspace = true +tokio.workspace = true cfg-if.workspace = true semver.workspace = true -# For URL encoding in log uploads -urlencoding = "2" - -# For managing async tasks -tokio.workspace = true -# For managing the logging time/date -chrono.workspace = true -# For the icon widget macro -paste = "1" -# For the file picker -rfd.workspace = true - # Terminal owo-colors.workspace = true clap = { version = "4", features = ["help", "derive"] } @@ -80,7 +69,11 @@ notify = "8" open = "5.3.2" dark-light = "2" dirs.workspace = true + +paste = "1" # icon widget macro +rfd.workspace = true # file picker constcat = "0.6" +rand = "0.9" [target.'cfg(not(target_os = "macos"))'.dependencies] notify-rust = { version = "4", default-features = false, features = ["z-with-tokio"] } diff --git a/quantum_launcher/src/cli/mod.rs b/quantum_launcher/src/cli/mod.rs index 79d46fe03..6c7637bcb 100644 --- a/quantum_launcher/src/cli/mod.rs +++ b/quantum_launcher/src/cli/mod.rs @@ -138,7 +138,7 @@ enum PrintCmd { /// Prints the "intro" to the screen /// consisting of the **ASCII art logo**, as well as -/// **stylised text saying `QuantumLauncher`** +/// **stylized text saying `QuantumLauncher`** /// /// The actual data is `include_str!()`ed from /// - `assets/ascii/icon.txt` for the ASCII art diff --git a/quantum_launcher/src/config.rs b/quantum_launcher/src/config/mod.rs similarity index 82% rename from quantum_launcher/src/config.rs rename to quantum_launcher/src/config/mod.rs index c204a1c8b..96e767cdb 100644 --- a/quantum_launcher/src/config.rs +++ b/quantum_launcher/src/config/mod.rs @@ -1,3 +1,4 @@ +use crate::config::sidebar::{InstanceKind, SidebarConfig, SidebarNode, SidebarNodeKind}; use crate::stylesheet::styles::{LauncherTheme, LauncherThemeColor, LauncherThemeLightness}; use crate::{WINDOW_HEIGHT, WINDOW_WIDTH}; use ql_core::json::GlobalSettings; @@ -5,10 +6,13 @@ use ql_core::ListEntryKind; use ql_core::{ err, IntoIoError, IntoJsonError, JsonFileError, LAUNCHER_DIR, LAUNCHER_VERSION_NAME, }; +use ql_instances::auth::AccountData; use serde::{Deserialize, Serialize}; use std::collections::HashSet; use std::{collections::HashMap, path::Path}; +pub mod sidebar; + pub const SIDEBAR_WIDTH: f32 = 0.33; const OPACITY: f32 = 0.9; @@ -20,7 +24,7 @@ const OPACITY: f32 = 0.9; /// /// # Why `Option`? /// -/// Many fields are `Option`s for backwards compatibility. +/// Many fields are `Option`'s for backwards compatibility. /// If upgrading from an older version, /// `serde` will deserialize missing fields as `None`, /// which is treated as a default value. @@ -94,6 +98,12 @@ pub struct LauncherConfig { pub ui: Option, // Since: v0.5.0 pub persistent: Option, + // Since: v0.5.1 + pub sidebar: Option, + + /// Preserve fields when downgrading + #[serde(flatten)] + _extra: HashMap, } impl Default for LauncherConfig { @@ -114,6 +124,8 @@ impl Default for LauncherConfig { extra_java_args: None, ui: None, persistent: None, + sidebar: None, + _extra: HashMap::new(), } } } @@ -166,6 +178,31 @@ impl LauncherConfig { Ok(()) } + pub fn update_sidebar(&mut self, instances: &[String], is_server: bool) { + let sidebar = self.sidebar.get_or_insert_with(SidebarConfig::default); + let kind = if is_server { + InstanceKind::Server + } else { + InstanceKind::Client + }; + + // Remove nonexistent instances + sidebar.retain_instances(|node| match &node.kind { + SidebarNodeKind::Instance(instance_kind) => { + *instance_kind == kind && instances.contains(&node.name) + } + SidebarNodeKind::Folder { .. } => true, + }); + // Add new instances + for instance in instances { + if !sidebar.contains_instance(instance, kind) { + sidebar + .list + .push(SidebarNode::new_instance(instance.clone(), kind)); + } + } + } + fn create(path: &Path) -> Result { let mut config = LauncherConfig::default(); config.fix(); @@ -243,6 +280,19 @@ impl LauncherConfig { self.persistent .get_or_insert_with(PersistentSettings::default) } + + pub fn c_sidebar(&mut self) -> &mut SidebarConfig { + self.sidebar.get_or_insert_with(SidebarConfig::default) + } + + pub fn c_idle_fps(&self) -> u64 { + const IDLE_FPS: u64 = 6; + + self.ui + .as_ref() + .and_then(|n| n.idle_fps) + .unwrap_or(IDLE_FPS) + } } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -284,6 +334,22 @@ pub struct ConfigAccount { /// username while the regular "username" /// would be an email. pub username_nice: Option, + + #[serde(flatten)] + _extra: HashMap, +} + +impl ConfigAccount { + pub fn from_account(data: &AccountData) -> Self { + Self { + uuid: data.uuid.clone(), + skin: None, + account_type: Some(data.account_type.to_string()), + keyring_identifier: Some(data.username.clone()), + username_nice: Some(data.nice_username.clone()), + _extra: HashMap::new(), + } + } } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -300,6 +366,9 @@ pub struct WindowProperties { /// Used to restore the window size between launches. // Since: v0.4.2 pub height: Option, + + #[serde(flatten)] + _extra: HashMap, } impl Default for WindowProperties { @@ -308,11 +377,12 @@ impl Default for WindowProperties { save_window_size: true, width: None, height: None, + _extra: HashMap::new(), } } } -#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct UiSettings { // Since: v0.5.0 pub window_decorations: UiWindowDecorations, @@ -320,6 +390,8 @@ pub struct UiSettings { pub window_opacity: f32, // Since: v0.5.0 pub idle_fps: Option, + #[serde(flatten)] + _extra: HashMap, } impl Default for UiSettings { @@ -328,16 +400,11 @@ impl Default for UiSettings { window_decorations: UiWindowDecorations::default(), window_opacity: OPACITY, idle_fps: None, + _extra: HashMap::new(), } } } -impl UiSettings { - pub fn get_idle_fps(&self) -> u64 { - self.idle_fps.unwrap_or(6) - } -} - #[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)] pub enum UiWindowDecorations { #[serde(rename = "system")] @@ -366,6 +433,9 @@ pub struct PersistentSettings { /// Remembers version filters (eg: snapshot, release, etc) in Create Instance pub create_instance_filters: Option>, + + #[serde(flatten)] + _extra: HashMap, } impl Default for PersistentSettings { @@ -375,13 +445,14 @@ impl Default for PersistentSettings { selected_server: None, selected_remembered: true, create_instance_filters: None, + _extra: HashMap::new(), } } } impl PersistentSettings { #[must_use] - pub fn get_create_instance_filters(&self) -> std::collections::HashSet { + pub fn get_create_instance_filters(&self) -> HashSet { self.create_instance_filters .clone() .filter(|n| !n.is_empty()) diff --git a/quantum_launcher/src/config/sidebar/drag_drop.rs b/quantum_launcher/src/config/sidebar/drag_drop.rs new file mode 100644 index 000000000..068fd3b14 --- /dev/null +++ b/quantum_launcher/src/config/sidebar/drag_drop.rs @@ -0,0 +1,140 @@ +use crate::config::sidebar::{ + SDragLocation, SDragTo, SidebarConfig, SidebarNode, SidebarNodeKind, SidebarSelection, +}; + +impl SidebarConfig { + pub fn drag_drop(&mut self, selection: &SidebarSelection, location: Option) { + if self.is_illegal_location(selection, location.as_ref()) { + return; + } + let Some(yoinked) = self.remove(selection) else { + return; + }; + let Some(location) = location else { + // Dragged to empty space, push at end + self.list.push(yoinked); + return; + }; + + self.insert_at(yoinked, location); + } + + fn insert_at(&mut self, yoinked: SidebarNode, location: SDragLocation) { + if let Some((index, folder)) = self + .list + .iter_mut() + .enumerate() + .find(|(_, n)| **n == location.sel) + { + // Inserting directly in top level of list + if let SidebarNodeKind::Folder(f) = &mut folder.kind { + if location.offset == SDragTo::Inside { + f.children.push(yoinked); + f.is_expanded = true; + return; + } + } + self.list.insert(index + location.offset as usize, yoinked); + return; + } + + // Inserting inside folder + for item in &mut self.list { + if item.insert_at(&yoinked, &location) { + return; + } + } + self.list.push(yoinked); + } + + pub fn remove(&mut self, selection: &SidebarSelection) -> Option { + if let Some(index) = self.list.iter().position(|n| n == selection) { + return Some(self.list.remove(index)); + } + + for item in &mut self.list { + if let Some(found) = item.remove(selection) { + return Some(found); + } + } + + None + } + + fn is_illegal_location( + &self, + selection: &SidebarSelection, + location: Option<&SDragLocation>, + ) -> bool { + if let Some(location) = location { + if let (Some(selection), Some(location)) = + (self.get_node(selection), self.get_node(&location.sel)) + { + if location.is_contained_by(selection) { + return true; + } + } + } + false + } +} + +impl SidebarNode { + fn remove(&mut self, selection: &SidebarSelection) -> Option { + let SidebarNodeKind::Folder(f) = &mut self.kind else { + return None; + }; + if let Some(pos) = f.children.iter().position(|n| n == selection) { + return Some(f.children.remove(pos)); + } + for child in &mut f.children { + if let Some(node) = child.remove(selection) { + return Some(node); + } + } + None + } + + pub fn insert_at(&mut self, node: &SidebarNode, location: &SDragLocation) -> bool { + let offset = location.offset as usize; + let SidebarNodeKind::Folder(f) = &mut self.kind else { + return false; + }; + if let SidebarNodeKind::Folder(f2) = &node.kind { + if f2.id == f.id { + return false; + } + } + + if let Some((index, folder)) = f + .children + .iter_mut() + .enumerate() + .find(|(_, n)| **n == location.sel) + { + if location.offset == SDragTo::Inside { + if let SidebarNodeKind::Folder(f) = &mut folder.kind { + f.children.push(node.clone()); + f.is_expanded = true; + return true; + } + debug_assert!(false, "can't drop item \"inside\" an instance"); + } + f.children.insert(index + offset, node.clone()); + f.is_expanded = true; + return true; + } + + f.children.iter_mut().any(|c| c.insert_at(node, location)) + } + + pub fn is_contained_by(&self, node: &Self) -> bool { + if self == node { + return true; + } + let SidebarNodeKind::Folder(f) = &node.kind else { + return false; + }; + f.children.iter().any(|c| self.is_contained_by(c)) + } +} diff --git a/quantum_launcher/src/config/sidebar/mod.rs b/quantum_launcher/src/config/sidebar/mod.rs new file mode 100644 index 000000000..9a193a26d --- /dev/null +++ b/quantum_launcher/src/config/sidebar/mod.rs @@ -0,0 +1,268 @@ +use std::collections::{HashMap, HashSet}; + +use serde::{Deserialize, Serialize}; + +mod drag_drop; +mod types; + +pub use types::*; + +// Since: v0.5.1 +#[derive(Serialize, Deserialize, Clone, Debug, Default)] +pub struct SidebarConfig { + pub list: Vec, + #[serde(flatten)] + _extra: HashMap, +} + +impl SidebarConfig { + pub fn contains_instance(&self, name: &str, instance_kind: InstanceKind) -> bool { + for node in &self.list { + if node.contains_instance(name, instance_kind) { + return true; + } + } + false + } + + pub fn retain_instances bool>(&mut self, mut f: F) { + let f = &mut f; + self.list.retain_mut(|node| node.retain_instances(f)); + } + + pub fn new_folder_at(&mut self, selection: Option, name: &str) -> FolderId { + fn walk( + node: &mut SidebarNode, + selection: &SidebarSelection, + name: &str, + ) -> Option { + let SidebarNodeKind::Folder(f) = &mut node.kind else { + return None; + }; + let mut index = None; + for (i, child) in f.children.iter_mut().enumerate() { + if child == selection { + index = Some(i + 1); + break; + } + if let Some(f) = walk(child, selection, name) { + return Some(f); + } + } + + let index = index?; + let folder = SidebarNode::new_folder(name.to_owned()); + let id = folder + .get_folder_id() + .expect("should be folder, not instance"); + f.children.insert(index, folder); + Some(id) + } + + if let Some(selection) = selection { + for (i, child) in self.list.iter_mut().enumerate() { + if *child == selection { + let folder = SidebarNode::new_folder(name.to_owned()); + let id = folder + .get_folder_id() + .expect("should be folder, not instance"); + self.list.insert(i + 1, folder); + return id; + } + if let Some(f) = walk(child, &selection, name) { + return f; + } + } + } + + let folder = SidebarNode::new_folder(name.to_owned()); + let id = folder + .get_folder_id() + .expect("should be folder, not instance"); + self.list.push(folder); + id + } + + pub fn delete_folder(&mut self, folder_id: FolderId) { + fn walk(node: &mut SidebarNode, folder_id: FolderId, temp: &mut Vec) -> bool { + if let SidebarNodeKind::Folder(f) = &mut node.kind { + if f.id == folder_id { + temp.append(&mut f.children); + return false; + } + let mut temp = Vec::new(); + f.children.retain_mut(|n| walk(n, folder_id, &mut temp)); + f.children.extend(temp); + } + true + } + + let mut temp = Vec::new(); + self.list.retain_mut(|n| walk(n, folder_id, &mut temp)); + self.list.extend(temp); + } + + pub fn rename_folder(&mut self, id: FolderId, new_name: &str) { + fn walk(node: &mut SidebarNode, id: FolderId, new_name: &str) -> bool { + let SidebarNodeKind::Folder(f) = &mut node.kind else { + return false; + }; + + if f.id == id { + // Finally rename the thing + new_name.clone_into(&mut node.name); + return true; + } + + for child in &mut f.children { + if walk(child, id, new_name) { + return true; + } + } + false + } + + for child in &mut self.list { + if walk(child, id, new_name) { + break; + } + } + } + + pub fn toggle_visibility(&mut self, id: FolderId) { + fn walk(node: &mut SidebarNode, folder_id: FolderId) { + if let SidebarNodeKind::Folder(f) = &mut node.kind { + if folder_id == f.id { + f.is_expanded = !f.is_expanded; + } else { + for child in &mut f.children { + walk(child, folder_id); + } + } + } + } + + for child in &mut self.list { + walk(child, id); + } + } + + pub fn get_node(&self, selection: &SidebarSelection) -> Option<&SidebarNode> { + fn walk<'a>( + child: &'a SidebarNode, + selection: &SidebarSelection, + ) -> Option<&'a SidebarNode> { + if child == selection { + return Some(child); + } + if let SidebarNodeKind::Folder(f) = &child.kind { + for child in &f.children { + if let Some(sel) = walk(child, selection) { + return Some(sel); + } + } + } + None + } + + for child in &self.list { + if let Some(node) = walk(child, selection) { + return Some(node); + } + } + None + } + + // I've tried fixing an entry duplication bug + // (most likely caused by an incorrect `contains_instance` implementation). + // But never can be too safe! + pub fn fix(&mut self) { + fn visit(n: &mut SidebarNode, visited: &mut HashSet) { + if let SidebarNodeKind::Folder(f) = &mut n.kind { + f.children.retain_mut(|n| { + if !visited.insert(SidebarSelection::from_node(n)) { + return false; + } + visit(n, visited); + true + }); + } + } + + let mut visited = HashSet::new(); + self.list.retain_mut(|n| { + if !visited.insert(SidebarSelection::from_node(n)) { + return false; + } + visit(n, &mut visited); + true + }); + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct SidebarNode { + pub name: String, + // icon: Option + pub kind: SidebarNodeKind, + #[serde(flatten)] + _extra: HashMap, +} + +impl SidebarNode { + #[must_use] + fn contains_instance(&self, name: &str, instance_kind: InstanceKind) -> bool { + match &self.kind { + SidebarNodeKind::Instance(kind) => { + if *kind == instance_kind && self.name == name { + return true; + } + } + SidebarNodeKind::Folder(f) => { + for child in &f.children { + if child.contains_instance(name, instance_kind) { + return true; + } + } + } + } + false + } + + #[must_use] + fn retain_instances bool>(&mut self, f: &mut F) -> bool { + if let SidebarNodeKind::Folder(folder) = &mut self.kind { + folder.children.retain_mut(|node| node.retain_instances(f)); + } else if !f(self) { + return false; + } + true + } + + #[must_use] + pub fn new_folder(name: String) -> Self { + SidebarNode { + name, + kind: SidebarNodeKind::Folder(SidebarFolder::default()), + _extra: HashMap::new(), + } + } + + #[must_use] + pub fn new_instance(name: String, kind: InstanceKind) -> Self { + SidebarNode { + name, + kind: SidebarNodeKind::Instance(kind), + _extra: HashMap::new(), + } + } + + #[must_use] + pub fn get_folder_id(&self) -> Option { + if let SidebarNodeKind::Folder(f) = &self.kind { + Some(f.id) + } else { + None + } + } +} diff --git a/quantum_launcher/src/config/sidebar/types.rs b/quantum_launcher/src/config/sidebar/types.rs new file mode 100644 index 000000000..287ebc705 --- /dev/null +++ b/quantum_launcher/src/config/sidebar/types.rs @@ -0,0 +1,144 @@ +use std::collections::HashMap; + +use ql_core::InstanceSelection; +use serde::{Deserialize, Serialize}; + +use crate::config::sidebar::SidebarNode; + +impl PartialEq for SidebarNode { + fn eq(&self, other: &SidebarSelection) -> bool { + match other { + SidebarSelection::Instance(name, instance_kind) => { + if let SidebarNodeKind::Instance(kind) = &self.kind { + if kind == instance_kind { + return self.name == *name; + } + } + } + SidebarSelection::Folder(folder_id) => { + if let SidebarNodeKind::Folder(f) = &self.kind { + return f.id == *folder_id; + } + } + } + false + } +} + +impl PartialEq for SidebarNode { + fn eq(&self, other: &InstanceSelection) -> bool { + match &self.kind { + SidebarNodeKind::Instance(kind) => { + kind.is_server() == other.is_server() && self.name == other.get_name() + } + SidebarNodeKind::Folder(_) => false, + } + } +} + +impl PartialEq for SidebarSelection { + fn eq(&self, other: &InstanceSelection) -> bool { + match self { + SidebarSelection::Instance(name, instance_kind) => { + instance_kind.is_server() == other.is_server() && name == other.get_name() + } + SidebarSelection::Folder(_) => false, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq)] +pub struct SidebarFolder { + pub id: FolderId, + pub children: Vec, + pub is_expanded: bool, + #[serde(flatten)] + _extra: HashMap, +} + +impl Default for SidebarFolder { + fn default() -> Self { + Self { + id: FolderId::new(), + children: Vec::new(), + is_expanded: true, + _extra: HashMap::new(), + } + } +} + +impl PartialEq for SidebarFolder { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq)] +pub enum SidebarNodeKind { + Instance(InstanceKind), + Folder(SidebarFolder), +} + +impl PartialEq for SidebarNodeKind { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Instance(l0), Self::Instance(r0)) => l0 == r0, + (Self::Folder(l), Self::Folder(r)) => l.id == r.id, + _ => false, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct FolderId(u128); + +impl FolderId { + pub fn new() -> Self { + Self(rand::random()) + } +} + +// TODO: Refactor the entire launcher to use this +// instead of `is_server: bool` +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[serde(rename_all = "snake_case")] +pub enum InstanceKind { + Client, + Server, +} + +impl InstanceKind { + pub fn is_server(self) -> bool { + matches!(self, Self::Server) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum SidebarSelection { + Instance(String, InstanceKind), + Folder(FolderId), +} + +impl SidebarSelection { + pub fn from_node(node: &SidebarNode) -> Self { + match &node.kind { + SidebarNodeKind::Instance(instance_kind) => { + Self::Instance(node.name.clone(), *instance_kind) + } + SidebarNodeKind::Folder(f) => Self::Folder(f.id), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct SDragLocation { + pub sel: SidebarSelection, + pub offset: SDragTo, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum SDragTo { + Before, + After, + Inside, +} diff --git a/quantum_launcher/src/main.rs b/quantum_launcher/src/main.rs index 7036540e8..77bc9ee2e 100644 --- a/quantum_launcher/src/main.rs +++ b/quantum_launcher/src/main.rs @@ -72,7 +72,7 @@ mod message_handler; /// Handlers for "child messages". /// /// The [`Message`] enum is split into -/// categories (like `Message::Account(AccountMessage)`). +/// categories (like `Message::Account(AccountMessage::*)`). /// /// This module has functions for handling each of /// these "child messages". @@ -130,10 +130,8 @@ impl Launcher { #[allow(clippy::unused_self)] fn subscription(&self) -> iced::Subscription { - let tick = iced::time::every(Duration::from_millis( - 1000 / self.config.ui.unwrap_or_default().get_idle_fps(), - )) - .map(|_| Message::CoreTick); + let tick = iced::time::every(Duration::from_millis(1000 / self.config.c_idle_fps())) + .map(|_| Message::CoreTick); let events = iced::event::listen_with(|a, b, _| Some(Message::CoreEvent(a, b))); iced::Subscription::batch(vec![tick, events]) @@ -271,7 +269,7 @@ fn load_fonts() -> Vec> { /// - If it's opened normally from GUI, /// no terminal window pops up /// -/// Basically Linux-default behaviour. +/// Basically Linux-default behavior. #[cfg(windows)] fn attach_to_console() { use windows::Win32::System::Console::AttachConsole; diff --git a/quantum_launcher/src/mclog_upload.rs b/quantum_launcher/src/mclog_upload.rs index 2680c403b..c8fe8449a 100644 --- a/quantum_launcher/src/mclog_upload.rs +++ b/quantum_launcher/src/mclog_upload.rs @@ -1,5 +1,8 @@ use ql_core::file_utils::check_for_success; -use ql_core::{IntoJsonError, IntoStringError, CLIENT}; +use ql_core::json::VersionDetails; +use ql_core::{ + InstanceConfigJson, InstanceSelection, IntoJsonError, IntoStringError, Loader, CLIENT, +}; use serde::Deserialize; #[derive(Debug, Deserialize)] @@ -12,18 +15,45 @@ pub struct MclogsResponse { } /// Uploads log content to and returns the URL if successful -pub async fn upload_log(content: String) -> Result { +pub async fn upload_log(content: String, instance: InstanceSelection) -> Result { + #[derive(serde::Serialize)] + struct Metadata { + key: &'static str, + value: String, + label: &'static str, + } + if content.trim().is_empty() { return Err("Cannot upload empty log".to_owned()); } - // Use form encoding instead of multipart - let form_data = format!("content={}", urlencoding::encode(&content)); + let (details, config) = tokio::try_join!( + VersionDetails::load(&instance), + InstanceConfigJson::read(&instance) + ) + .strerr()?; + + let mut metadata = vec![Metadata { + key: "version", + value: details.id, + label: "Minecraft version", + }]; + + if config.mod_type != Loader::Vanilla { + metadata.push(Metadata { + key: "loader", + value: config.mod_type.to_string(), + label: "Mod Loader", + }); + } let response = CLIENT .post("https://api.mclo.gs/1/log") - .header("Content-Type", "application/x-www-form-urlencoded") - .body(form_data) + .json(&serde_json::json!({ + "content": content, + "source": "mrmayman.github.io/quantumlauncher", + "metadata": metadata, + })) .send() .await .strerr()?; diff --git a/quantum_launcher/src/menu_renderer/create.rs b/quantum_launcher/src/menu_renderer/create.rs index 7ed4d669f..7d08e3e4a 100644 --- a/quantum_launcher/src/menu_renderer/create.rs +++ b/quantum_launcher/src/menu_renderer/create.rs @@ -10,7 +10,8 @@ use crate::{ cli::EXPERIMENTAL_MMC_IMPORT, icons, menu_renderer::{ - button_with_icon, ctxbox, dots, shortcut_ctrl, sidebar_button, tooltip, tsubtitle, Element, + button_with_icon, ctxbox, dots, offset, shortcut_ctrl, sidebar_button, tooltip, tsubtitle, + Element, }, state::{CreateInstanceMessage, MenuCreateInstance, MenuCreateInstanceChoosing, Message}, stylesheet::{ @@ -21,7 +22,7 @@ use crate::{ }; impl MenuCreateInstance { - pub fn view<'a>(&'a self, existing_instances: Option<&[String]>, timer: usize) -> Element<'a> { + pub fn view(&self, existing_instances: Option<&[String]>, timer: usize) -> Element<'_> { match self { MenuCreateInstance::Choosing(menu) => menu.view(existing_instances, timer), MenuCreateInstance::DownloadingInstance(progress) => column![ @@ -51,18 +52,14 @@ impl MenuCreateInstanceChoosing { self.get_main_page(existing_instances).into() } }) - .on_resize(10, |t| { - Message::CreateInstance(CreateInstanceMessage::SidebarResize(t.ratio)) - }); + .on_resize(10, |t| CreateInstanceMessage::SidebarResize(t.ratio).into()); widget::stack!(view) - .push_maybe(self.show_category_dropdown.then_some(row![ - widget::Space::with_width(90), - column![ - widget::Space::with_height(40), - ctxbox(Self::get_category_dropdown(&self.selected_categories)) - ] - ])) + .push_maybe(self.show_category_dropdown.then_some(offset( + ctxbox(Self::get_category_dropdown(&self.selected_categories)), + 90, + 40, + ))) .into() } @@ -112,14 +109,12 @@ impl MenuCreateInstanceChoosing { n, &self.selected_version, label, - Message::CreateInstance(CreateInstanceMessage::VersionSelected(n.clone())), + CreateInstanceMessage::VersionSelected(n.clone()).into(), ) }))) .style(LauncherTheme::style_scrollable_flat_extra_dark) .height(Length::Fill) - .id(iced::widget::scrollable::Id::new( - "MenuCreateInstance:sidebar" - )) + .id(widget::scrollable::Id::new("MenuCreateInstance:sidebar")) ] .spacing(10), ) @@ -178,8 +173,8 @@ impl MenuCreateInstanceChoosing { .push( widget::text_input("Search...", &self.search_box) .size(14) - .on_input(|t| Message::CreateInstance(CreateInstanceMessage::SearchInput(t))) - .on_submit(Message::CreateInstance(CreateInstanceMessage::SearchSubmit)), + .on_input(|t| CreateInstanceMessage::SearchInput(t).into()) + .on_submit(CreateInstanceMessage::SearchSubmit.into()), ) .push_maybe( (!self.search_box.trim().is_empty()) @@ -202,7 +197,7 @@ impl MenuCreateInstanceChoosing { { let placeholder = self.selected_version.name.as_str(); widget::text_input(placeholder, &self.instance_name) - .on_input(|n| Message::CreateInstance(CreateInstanceMessage::NameInput(n))) + .on_input(|n| CreateInstanceMessage::NameInput(n).into()) } ].spacing(10).align_y(Alignment::Center), @@ -235,7 +230,7 @@ impl MenuCreateInstanceChoosing { .push_maybe( EXPERIMENTAL_MMC_IMPORT.read().unwrap().then_some(tooltip( button_with_icon(icons::upload(), "Import from MultiMC...", 16) - .on_press(Message::CreateInstance(CreateInstanceMessage::Import)), + .on_press(CreateInstanceMessage::Import.into()), widget::text("Import Instance... (VERY EXPERIMENTAL right now)").size(14), Position::Top )) @@ -265,9 +260,7 @@ impl MenuCreateInstanceChoosing { widget::checkbox(kind.to_string(), is_checked) .text_size(13) .size(13) - .on_toggle(move |_| { - Message::CreateInstance(CreateInstanceMessage::CategoryToggle(*kind)) - }), + .on_toggle(move |_| CreateInstanceMessage::CategoryToggle(*kind).into()), ); } @@ -276,9 +269,8 @@ impl MenuCreateInstanceChoosing { } fn get_create_button(already_exists: bool) -> widget::Tooltip<'static, Message, LauncherTheme> { - let create_button = button_with_icon(icons::new(), "Create", 16).on_press_maybe( - (!already_exists).then_some(Message::CreateInstance(CreateInstanceMessage::Start)), - ); + let create_button = button_with_icon(icons::new(), "Create", 16) + .on_press_maybe((!already_exists).then_some(CreateInstanceMessage::Start.into())); if already_exists { tooltip( diff --git a/quantum_launcher/src/menu_renderer/edit_instance.rs b/quantum_launcher/src/menu_renderer/edit_instance.rs index e73563023..71a58c38c 100644 --- a/quantum_launcher/src/menu_renderer/edit_instance.rs +++ b/quantum_launcher/src/menu_renderer/edit_instance.rs @@ -39,23 +39,23 @@ impl MenuEditInstance { } else { resolution_dialog( self.config.global_settings.as_ref(), - |n| Message::EditInstance(EditInstanceMessage::WindowWidthChanged(n)), - |n| Message::EditInstance(EditInstanceMessage::WindowHeightChanged(n)), + |n| EditInstanceMessage::WindowWidthChanged(n).into(), + |n| EditInstanceMessage::WindowHeightChanged(n).into(), ) }, widget::Column::new() .push_maybe((!selected_instance.is_server()).then_some(column![ widget::checkbox("Close launcher after game opens", self.config.close_on_start.unwrap_or(false)) - .on_toggle(|t| Message::EditInstance(EditInstanceMessage::CloseLauncherToggle(t))), + .on_toggle(|t| EditInstanceMessage::CloseLauncherToggle(t).into()), ].spacing(5))) .push( column![ widget::Space::with_height(5), widget::checkbox("DEBUG: Enable log system (recommended)", self.config.enable_logger.unwrap_or(true)) - .on_toggle(|t| Message::EditInstance(EditInstanceMessage::LoggingToggle(t))), + .on_toggle(|t| EditInstanceMessage::LoggingToggle(t).into()), widget::text("Once disabled, logs will be printed in launcher STDOUT.\nRun the launcher executable from the terminal/command prompt to see it").size(12).style(tsubtitle), - widget::horizontal_space(), + horizontal_space(), ].spacing(5) ) .spacing(10), @@ -83,7 +83,7 @@ impl MenuEditInstance { icons::edit_s(12).style(|t: &LauncherTheme| t.style_text(Color::Mid)) ) .style(|t: &LauncherTheme, s| t.style_button(s, StyleButton::FlatDark)) - .on_press(Message::EditInstance(EditInstanceMessage::RenameToggle)) + .on_press(EditInstanceMessage::RenameToggle.into()) ) ) .spacing(5), @@ -106,12 +106,12 @@ impl MenuEditInstance { column![ widget::Space::with_height(1), widget::text_input("Rename Instance", &self.instance_name) - .on_input(|n| Message::EditInstance(EditInstanceMessage::RenameEdit(n))), + .on_input(|n| EditInstanceMessage::RenameEdit(n).into()), row![ widget::button(widget::text("Rename").size(12)) - .on_press(Message::EditInstance(EditInstanceMessage::RenameApply)), + .on_press(EditInstanceMessage::RenameApply.into()), widget::button(widget::text("Cancel").size(12)) - .on_press(Message::EditInstance(EditInstanceMessage::RenameToggle)) + .on_press(EditInstanceMessage::RenameToggle.into()) ] .spacing(5) ] @@ -131,9 +131,7 @@ impl MenuEditInstance { "Java arguments:", widget::horizontal_space(), widget::checkbox("Use global arguments", current_mode) - .on_toggle(|t| { - Message::EditInstance(EditInstanceMessage::JavaArgsModeChanged(t)) - }) + .on_toggle(|t| EditInstanceMessage::JavaArgsModeChanged(t).into()) .style(|t: &LauncherTheme, s| t.style_checkbox(s, Some(Color::SecondLight))) .size(12) .text_size(12) @@ -162,11 +160,12 @@ impl MenuEditInstance { ) -> widget::Column<'_, Message, LauncherTheme> { let checkbox = widget::checkbox("Use global prefix", !prefix_mode.is_disabled()) .on_toggle(|t| { - Message::EditInstance(EditInstanceMessage::PreLaunchPrefixModeChanged(if t { + EditInstanceMessage::PreLaunchPrefixModeChanged(if t { PreLaunchPrefixMode::default() } else { PreLaunchPrefixMode::Disable - })) + }) + .into() }) .style(|t: &LauncherTheme, s| t.style_checkbox(s, Some(Color::SecondLight))) .size(12) @@ -179,7 +178,7 @@ impl MenuEditInstance { .global_settings .as_ref() .and_then(|n| n.pre_launch_prefix.as_deref()), - |n| Message::EditInstance(EditInstanceMessage::PreLaunchPrefix(n)), + |n| EditInstanceMessage::PreLaunchPrefix(n).into(), )] .push_maybe( (!prefix_mode.is_disabled()).then_some( @@ -234,7 +233,7 @@ Heavy modpacks / High settings: 4-8 GB+" widget::slider( MEM_256_MB_IN_TWOS_EXPONENT..=MEM_32768_MB_IN_TWOS_EXPONENT, self.slider_value, - |n| Message::EditInstance(EditInstanceMessage::MemoryChanged(n)) + |n| EditInstanceMessage::MemoryChanged(n).into() ) .step(0.1), ] @@ -243,7 +242,7 @@ Heavy modpacks / High settings: 4-8 GB+" row![ widget::text("Or enter directly:").size(12).style(tsubtitle), widget::text_input("2048", &self.memory_input) - .on_input(|n| Message::EditInstance(EditInstanceMessage::MemoryInputChanged(n))) + .on_input(|n| EditInstanceMessage::MemoryInputChanged(n).into()) .width(64) .size(12), widget::text("MB").size(12).style(tsubtitle), @@ -282,19 +281,19 @@ Heavy modpacks / High settings: 4-8 GB+" "Auto-managed by launcher", true, Some(java_override.trim().is_empty() && java_override_ver.is_none()), - || Message::EditInstance(EditInstanceMessage::JavaOverride(String::new())) + || EditInstanceMessage::JavaOverride(String::new()).into() ), row![radio( "Specific Java version", true, Some(java_override_ver.is_some()), - || Message::EditInstance(EditInstanceMessage::JavaOverrideVersion(25)) + || EditInstanceMessage::JavaOverrideVersion(25).into() )] .push_maybe(java_override_ver.map(|n| { widget::row(JavaVersion::ALL.iter().map(|v| { let v = *v as usize; widget::radio(format!("{v}"), v, Some(n), |v| { - Message::EditInstance(EditInstanceMessage::JavaOverrideVersion(v)) + EditInstanceMessage::JavaOverrideVersion(v).into() }) .text_size(13) .size(11) @@ -310,24 +309,20 @@ Heavy modpacks / High settings: 4-8 GB+" "Custom path", true, Some(!java_override.trim().is_empty()), - || Message::EditInstance(EditInstanceMessage::JavaOverride( - "path/to/java".to_owned() - )) // ugly hack + || EditInstanceMessage::JavaOverride("path/to/java".to_owned()).into() // ugly hack ), row![ widget::Space::with_width(16), widget::text_input("Leave blank if none", java_override) .size(14) .font(FONT_MONO) - .on_input(|t| Message::EditInstance(EditInstanceMessage::JavaOverride(t))) + .on_input(|t| EditInstanceMessage::JavaOverride(t).into()) ] .push_maybe( (!java_override.trim().is_empty()).then_some( button_with_icon(icons::close_s(9), "", 13) .padding([8.0, 11.0]) - .on_press(Message::EditInstance(EditInstanceMessage::JavaOverride( - String::new() - ))), + .on_press(EditInstanceMessage::JavaOverride(String::new()).into()), ) ) .push( @@ -355,7 +350,7 @@ Heavy modpacks / High settings: 4-8 GB+" .as_ref() .map_or_else(|| NONE_JAR_NAME.to_owned(), |n| n.name.clone()), ), - |t| Message::EditInstance(EditInstanceMessage::CustomJarPathChanged(t)), + |t| EditInstanceMessage::CustomJarPathChanged(t).into(), ) .into() } else { @@ -372,7 +367,7 @@ Heavy modpacks / High settings: 4-8 GB+" widget::Space::with_height(10), widget::text("Main Class:"), widget::radio("Default", None, Some(self.main_class_mode), |t| { - Message::EditInstance(EditInstanceMessage::SetMainClass(t, None)) + EditInstanceMessage::SetMainClass(t, None).into() }) .size(14) .text_size(13), @@ -380,7 +375,7 @@ Heavy modpacks / High settings: 4-8 GB+" "Safe Mode (might fix crashes?)", Some(MainClassMode::SafeFallback), Some(self.main_class_mode), - |t| Message::EditInstance(EditInstanceMessage::SetMainClass(t, None)) + |t| EditInstanceMessage::SetMainClass(t, None).into() ) .size(14) .text_size(13), @@ -388,10 +383,10 @@ Heavy modpacks / High settings: 4-8 GB+" "Custom", Some(MainClassMode::Custom), Some(self.main_class_mode), - |t| Message::EditInstance(EditInstanceMessage::SetMainClass( + |t| EditInstanceMessage::SetMainClass( t, Some(String::new()) - )) + ).into() ) .size(14) .text_size(13)] @@ -430,7 +425,7 @@ fn item_footer( )), button_with_icon(icons::version_download_s(14), "Update Assets", 13) .padding([4, 8]) - .on_press(Message::EditInstance(EditInstanceMessage::UpdateAssets)), + .on_press(EditInstanceMessage::UpdateAssets.into()), ] .spacing(5) .wrap(), @@ -462,6 +457,7 @@ pub fn resolution_dialog<'a>( .and_then(|n| n.window_width) .map_or(String::new(), |w| w.to_string()) ) + .size(14) .on_input(width) .width(100), widget::text("Height:").size(14), @@ -471,6 +467,7 @@ pub fn resolution_dialog<'a>( .and_then(|n| n.window_height) .map_or(String::new(), |h| h.to_string()) ) + .size(14) .on_input(height) .width(100), ] @@ -527,7 +524,7 @@ pub fn args_split_by_space(split: bool) -> widget::Checkbox<'static, Message, La .style(|t: &LauncherTheme, s| t.style_checkbox(s, Some(Color::SecondLight))) .size(12) .text_size(12) - .on_toggle(|t| Message::EditInstance(EditInstanceMessage::ToggleSplitArg(t))) + .on_toggle(|t| EditInstanceMessage::ToggleSplitArg(t).into()) } fn get_args_list_add_button( diff --git a/quantum_launcher/src/menu_renderer/launch.rs b/quantum_launcher/src/menu_renderer/launch.rs index cf2b071a6..e09fba34e 100644 --- a/quantum_launcher/src/menu_renderer/launch.rs +++ b/quantum_launcher/src/menu_renderer/launch.rs @@ -1,16 +1,17 @@ use cfg_if::cfg_if; use frostmark::MarkWidget; -use iced::keyboard::Modifiers; -use iced::widget::tooltip::Position; -use iced::widget::{horizontal_space, text_editor, vertical_space}; -use iced::{widget, Alignment, Length, Padding}; +use iced::widget::{column, horizontal_space, row, text_editor, tooltip::Position, vertical_space}; +use iced::{keyboard::Modifiers, widget, Alignment, Length, Padding}; use ql_core::{InstanceSelection, LAUNCHER_VERSION_NAME}; use crate::cli::EXPERIMENTAL_SERVERS; use crate::menu_renderer::onboarding::x86_warning; -use crate::menu_renderer::{ctx_button, ctxbox, tsubtitle, underline, underline_maybe, FONT_MONO}; +use crate::menu_renderer::{ + ctx_button, ctxbox, sidebar, tsubtitle, underline, CTXI_SIZE, FONT_MONO, +}; use crate::state::{ - GameLogMessage, InstanceNotes, LaunchModal, NotesMessage, ShortcutMessage, WindowMessage, + GameLogMessage, InstanceNotes, LaunchModal, MainMenuMessage, NotesMessage, ShortcutMessage, + SidebarMessage, WindowMessage, }; use crate::{ icons, @@ -48,14 +49,20 @@ impl Launcher { &'element self, menu: &'element MenuLaunch, ) -> Element<'element> { - widget::pane_grid(&menu.sidebar_grid_state, |_, is_sidebar, _| { - if *is_sidebar { - self.get_sidebar(menu).into() - } else { - self.get_tab(menu).into() - } - }) - .on_resize(10, |t| Message::MSidebarResize(t.ratio)) + widget::stack!( + widget::pane_grid(&menu.sidebar_grid_state, |_, is_sidebar, _| { + widget::mouse_area(if *is_sidebar { + self.get_sidebar(menu) + } else { + self.get_tab(menu) + }) + .on_press(Message::CoreHideModal) + .into() + }) + .on_resize(10, |t| SidebarMessage::Resize(t.ratio).into()) + ) + .push_maybe(Self::sidebar_context_menu(menu)) + .push_maybe(self.sidebar_drag_tooltip(menu)) .into() } @@ -70,11 +77,11 @@ impl Launcher { if let Some(menu) = &menu.edit_instance { menu.view(selected, self.custom_jar.as_ref()) } else { - widget::column!( + column![ "Error: This instance is corrupted/invalid!\n(Couldn't read config.json)", button_with_icon(icons::bin(), "Delete Instance", 16) .on_press(Message::DeleteInstanceMenu) - ) + ] .padding(10) .spacing(10) .into() @@ -82,13 +89,13 @@ impl Launcher { } } } else { - widget::column!(widget::text(if menu.is_viewing_server { + column![widget::text(if menu.is_viewing_server { "Select a server\n\nNote: You are trying the *early-alpha* server manager feature.\nYou need playit.gg (or port-forwarding) for others to join" } else { "Select an instance" }) .size(14) - .style(|t: &LauncherTheme| t.style_text(Color::Mid))) + .style(|t: &LauncherTheme| t.style_text(Color::Mid))] .push_maybe(cfg!(target_arch = "x86").then(x86_warning)) .push(vertical_space()) .push( @@ -102,7 +109,7 @@ impl Launcher { .into() }; - widget::stack!(widget::column![menu.get_tab_selector(decor)] + widget::stack!(column![menu.get_tab_selector(decor)] .push_maybe(view_info_message(menu)) .push( widget::container(tab_body) @@ -110,24 +117,27 @@ impl Launcher { .height(Length::Fill) .style(|t: &LauncherTheme| t.style_container_bg(0.0, None)), )) - .push_maybe(menu.modal.as_ref().map(|n| { - match n { - LaunchModal::InstanceOptions => widget::column![ - widget::vertical_space(), + .push_maybe(if let Some(LaunchModal::InstanceOptions) = &menu.modal { + Some( + column![ + vertical_space(), ctxbox( - widget::column![ - ctx_button("Export Instance").on_press(Message::ExportInstanceOpen), - ctx_button("Create Shortcut") - .on_press(Message::Shortcut(ShortcutMessage::Open)), + column![ + ctx_button(icons::file_zip_s(CTXI_SIZE), "Export Instance") + .on_press(Message::ExportInstanceOpen), + ctx_button(icons::file_gear_s(CTXI_SIZE), "Create Shortcut") + .on_press(ShortcutMessage::Open.into()), ] - .spacing(5) + .spacing(4) ) .width(150), widget::Space::with_height(30) ] .padding(12), - } - })) + ) + } else { + None + }) .into() } @@ -135,7 +145,7 @@ impl Launcher { let selected = self.instance(); let is_running = self.is_process_running(selected); - let main_buttons = widget::row![ + let main_buttons = row![ if menu.is_viewing_server { self.get_server_play_button().into() } else { @@ -153,35 +163,34 @@ impl Launcher { vertical_space().into() } Some(InstanceNotes::Viewing { mark_state, .. }) => widget::scrollable( - widget::column![MarkWidget::new(mark_state).heading_scale(0.7).text_size(14)] - .padding(5), + column![MarkWidget::new(mark_state).heading_scale(0.7).text_size(14)].padding(5), ) .width(Length::Fill) .height(Length::Fill) .into(), Some(InstanceNotes::Editing { text_editor, .. }) => { - return widget::column!( + return column![ widget::text("Editing Notes").size(20), widget::text_editor(text_editor) .size(14) .height(Length::Fill) - .on_action(|a| Message::Notes(NotesMessage::Edit(a))), - widget::row![ + .on_action(|a| NotesMessage::Edit(a).into()), + row![ button_with_icon(icons::floppydisk_s(14), "Save", 14) - .on_press(Message::Notes(NotesMessage::SaveEdit)), + .on_press(NotesMessage::SaveEdit.into()), button_with_icon(icons::close_s(14), "Cancel", 14) - .on_press(Message::Notes(NotesMessage::CancelEdit)), + .on_press(NotesMessage::CancelEdit.into()), ] .spacing(5) - ) + ] .padding(10) .spacing(10) .into(); } }; - widget::column!( - widget::row![widget::text(selected.get_name()).font(FONT_MONO).size(20)] + column![ + row![widget::text(selected.get_name()).font(FONT_MONO).size(20)] .push_maybe(is_running.then_some(icons::play_s(20))) .push_maybe( is_running.then_some( @@ -196,16 +205,16 @@ impl Launcher { main_buttons, // widget::button("Export Instance").on_press(Message::ExportInstanceOpen), notes, - widget::row![ + row![ widget::Column::new() .push_maybe(get_view_servers(menu.is_viewing_server)) .push( - widget::row![ - widget::button(icons::lines_s(10)) - .padding([5, 8]) - .on_press(Message::MModal(Some(LaunchModal::InstanceOptions))), + row![ + widget::button(icons::lines_s(10)).padding([5, 8]).on_press( + MainMenuMessage::Modal(Some(LaunchModal::InstanceOptions)).into() + ), widget::button( - widget::row![ + row![ icons::edit_s(10), widget::text("Edit Notes").size(12).style(tsubtitle) ] @@ -213,7 +222,7 @@ impl Launcher { .spacing(8), ) .padding([4, 8]) - .on_press(Message::Notes(NotesMessage::OpenEdit)), + .on_press(NotesMessage::OpenEdit.into()), ] .spacing(5) ) @@ -221,7 +230,7 @@ impl Launcher { get_footer_text(), ] .align_y(Alignment::End) - ) + ] .padding(16) .spacing(10) .into() @@ -230,9 +239,9 @@ impl Launcher { fn get_mods_button(&self) -> widget::Button<'_, Message, LauncherTheme> { button_with_icon(icons::download(), "Mods", 15) .on_press(if self.modifiers_pressed.contains(Modifiers::SHIFT) { - Message::ManageMods(ManageModsMessage::ScreenOpenWithoutUpdate) + ManageModsMessage::ScreenOpenWithoutUpdate.into() } else { - Message::ManageMods(ManageModsMessage::ScreenOpen) + ManageModsMessage::ScreenOpen.into() }) .width(98) } @@ -267,23 +276,22 @@ impl Launcher { .font(FONT_MONO) .size(TEXT_SIZE) .height(Length::Fill) - .on_action(|a| Message::GameLog(GameLogMessage::Action(a))); + .on_action(|a| GameLogMessage::Action(a).into()); let small_button = |t| widget::button(widget::text(t).size(12)).padding([4, 8]); - widget::column![ - widget::row![ - small_button("Copy Log").on_press(Message::GameLog(GameLogMessage::Copy)), + column![ + row![ + small_button("Copy Log").on_press(GameLogMessage::Copy.into()), small_button("Upload Log").on_press_maybe( (!log_data.is_empty() && !menu.is_uploading_mclogs) - .then_some(Message::GameLog(GameLogMessage::Upload)) + .then_some(GameLogMessage::Upload.into()) ), small_button("Join Discord").on_press(Message::CoreOpenLink(DISCORD.to_owned())), widget::horizontal_space(), widget::mouse_area(widget::container(icons::arrow_up_s(12))).on_press( - Message::GameLog(GameLogMessage::Action(text_editor::Action::Move( - text_editor::Motion::PageUp - ))) + GameLogMessage::Action(text_editor::Action::Move(text_editor::Motion::PageUp)) + .into() ), widget::mouse_area(widget::container(icons::arrow_down_s(12))).on_press( Message::GameLog(GameLogMessage::Action(text_editor::Action::Move( @@ -321,71 +329,47 @@ impl Launcher { } fn get_sidebar<'a>(&'a self, menu: &'a MenuLaunch) -> Element<'a> { - let list = if menu.is_viewing_server { - self.server_list.as_deref() - } else { - self.client_list.as_deref() - }; - let decor = self.config.uses_system_decorations(); - let list = if let Some(instances) = list { - widget::column(instances.iter().map(|name| { - let playing_icon = if self - .is_process_running(&InstanceSelection::new(name, menu.is_viewing_server)) - { - Some(widget::row![ - horizontal_space(), - icons::play_s(15), - widget::Space::with_width(10), - ]) - } else { - None - }; - - let text = widget::text(name).size(15).style(tsubtitle); - let is_selected = self - .selected_instance - .as_ref() - .is_some_and(|n| n.get_name() == name); - - underline_maybe( - widget::button(widget::row![text].push_maybe(playing_icon)) - .style(|n: &LauncherTheme, status| { - n.style_button(status, StyleButton::FlatExtraDark) - }) - .on_press_maybe((!is_selected).then(|| { - Message::LaunchInstanceSelected(InstanceSelection::new( - name, - menu.is_viewing_server, - )) - })) - .width(Length::Fill), - Color::Dark, - !is_selected, - ) - })) + let list = if let Some(sidebar) = &self.config.sidebar { + widget::column( + sidebar + .list + .iter() + .map(|node| self.get_node_rendered(menu, node, sidebar::NodeMode::InTree(0))), + ) + .push(widget::Space::with_height(10)) } else { let dots = ".".repeat((self.tick_timer % 3) + 1); - widget::column![widget::text!("Loading{dots}")].padding(10) + column![widget::text!("Loading{dots}")].padding(10) }; - let list = widget::column![ - widget::scrollable(list) - .height(Length::Fill) - .style(LauncherTheme::style_scrollable_flat_extra_dark) - .id(widget::scrollable::Id::new("MenuLaunch:sidebar")) - .on_scroll(|n| { - let total = n.content_bounds().height - n.bounds().height; - Message::MSidebarScroll(total) - }), + let list = column![ + widget::mouse_area( + widget::scrollable(list) + .height(Length::Fill) + .style(LauncherTheme::style_scrollable_flat_extra_dark) + .id(widget::scrollable::Id::new("MenuLaunch:sidebar")) + .on_scroll(|n| { + let total = n.content_bounds().height - n.bounds().height; + SidebarMessage::Scroll(total).into() + }) + ) + .on_right_press( + MainMenuMessage::Modal(Some(LaunchModal::SCtxMenu( + None, + self.window_state.mouse_pos + ))) + .into() + ) + .on_press(SidebarMessage::DragDrop(None).into()), widget::horizontal_rule(1).style(|t: &LauncherTheme| t.style_rule(Color::Dark, 1)), self.get_accounts_bar(menu), ] .spacing(5) .width(Length::Fill); - widget::column![ + column![ widget::mouse_area( widget::container(get_sidebar_new_button(menu, decor)) .align_y(Alignment::End) @@ -396,7 +380,7 @@ impl Launcher { Some((Color::ExtraDark, t.alpha)) )) ) - .on_press(Message::Window(WindowMessage::Dragged)), + .on_press(WindowMessage::Dragged.into()), widget::container(list) .height(Length::Fill) .style(|n| n.style_container_sharp_box(0.0, Color::ExtraDark)) @@ -404,6 +388,22 @@ impl Launcher { .into() } + pub(super) fn get_running_icon( + &self, + menu: &MenuLaunch, + name: &str, + ) -> Option> { + if self.is_process_running(&InstanceSelection::new(name, menu.is_viewing_server)) { + Some(row![ + horizontal_space(), + icons::play_s(15), + widget::Space::with_width(10), + ]) + } else { + None + } + } + fn is_process_running(&self, instance: &InstanceSelection) -> bool { self.processes.contains_key(instance) } @@ -419,7 +419,7 @@ impl Launcher { widget::pick_list( self.accounts_dropdown.as_slice(), Some(&self.account_selected), - |n| Message::Account(AccountMessage::Selected(n)), + |n| AccountMessage::Selected(n).into(), ) .width(Length::Fill) .into() @@ -430,7 +430,7 @@ impl Launcher { (self.account_selected != OFFLINE_ACCOUNT_NAME).then_some( widget::button(widget::text("Logout").size(11)) .padding(3) - .on_press(Message::Account(AccountMessage::LogoutCheck)) + .on_press(AccountMessage::LogoutCheck.into()) .style(|n: &LauncherTheme, status| n .style_button(status, StyleButton::FlatExtraDark)) ) @@ -440,7 +440,7 @@ impl Launcher { .push_maybe( (self.account_selected == OFFLINE_ACCOUNT_NAME).then_some( widget::text_input("Enter username...", &self.config.username) - .on_input(Message::LaunchUsernameSet) + .on_input(|t| MainMenuMessage::UsernameSet(t).into()) .width(Length::Fill), ), ) @@ -457,28 +457,24 @@ impl Launcher { tooltip(play_button, "Username is empty!", Position::Bottom).into() } else if self.config.username.contains(' ') && is_offline { tooltip(play_button, "Username contains spaces!", Position::Bottom).into() - } else if let Some(selected_instance) = &self.selected_instance { - if self.processes.contains_key(selected_instance) { - tooltip( - button_with_icon(icons::play(), "Kill", 16) - .on_press(Message::LaunchKill) - .width(98), - shortcut_ctrl("Backspace"), - Position::Bottom, - ) - .into() - } else if self.is_launching_game { - button_with_icon(icons::play(), "...", 16).width(98).into() - } else { - tooltip( - play_button.on_press(Message::LaunchStart), - shortcut_ctrl("Enter"), - Position::Bottom, - ) - .into() - } + } else if self.processes.contains_key(self.instance()) { + tooltip( + button_with_icon(icons::play(), "Kill", 16) + .on_press(Message::LaunchKill) + .width(98), + shortcut_ctrl("Backspace"), + Position::Bottom, + ) + .into() + } else if self.is_launching_game { + button_with_icon(icons::play(), "...", 16).width(98).into() } else { - tooltip(play_button, "Select an instance first!", Position::Bottom).into() + tooltip( + play_button.on_press(Message::LaunchStart), + shortcut_ctrl("Enter"), + Position::Bottom, + ) + .into() } } @@ -492,7 +488,7 @@ impl Launcher { .width(97) } - fn get_server_play_button<'a>(&self) -> iced::widget::Tooltip<'a, Message, LauncherTheme> { + fn get_server_play_button(&self) -> widget::Tooltip<'_, Message, LauncherTheme> { match &self.selected_instance { Some(n) if self.processes.contains_key(n) => tooltip( button_with_icon(icons::play(), "Stop", 16) @@ -504,11 +500,7 @@ impl Launcher { _ => tooltip( button_with_icon(icons::play(), "Start", 16) .width(97) - .on_press_maybe( - self.selected_instance - .is_some() - .then(|| Message::LaunchStart), - ), + .on_press(Message::LaunchStart), "By starting the server, you agree to the EULA", Position::Bottom, ), @@ -549,18 +541,18 @@ impl MenuLaunch { .wrap(); let settings_button = widget::button( - widget::row![horizontal_space(), icons::gear_s(12), horizontal_space()] + row![horizontal_space(), icons::gear_s(12), horizontal_space()] .width(tab_height(decor) + 4.0) .height(tab_height(decor) + 4.0) .align_y(Alignment::Center), ) .padding(0) .style(|n, status| n.style_button(status, StyleButton::FlatExtraDark)) - .on_press(Message::LauncherSettings(LauncherSettingsMessage::Open)); + .on_press(LauncherSettingsMessage::Open.into()); widget::mouse_area( widget::container( - widget::row!(settings_button, tab_bar, horizontal_space()) + row![settings_button, tab_bar, horizontal_space()] // .push_maybe(window_handle_buttons) .height(tab_height(decor) + decorh(decor)) .align_y(Alignment::End), @@ -573,7 +565,7 @@ impl MenuLaunch { ) }), ) - .on_press(Message::Window(WindowMessage::Dragged)) + .on_press(WindowMessage::Dragged.into()) .into() } } @@ -598,7 +590,7 @@ fn render_tab_button(tab: LaunchTab, decor: bool, menu: &'_ MenuLaunch) -> Eleme name.into() }; - let txt = widget::row!(horizontal_space(), txt, horizontal_space()); + let txt = row![horizontal_space(), txt, horizontal_space()]; if menu.tab == tab { widget::container(txt) @@ -616,7 +608,7 @@ fn render_tab_button(tab: LaunchTab, decor: bool, menu: &'_ MenuLaunch) -> Eleme .into() } else { widget::button( - widget::row![txt] + row![txt] .width(TAB_BUTTON_WIDTH) .height(tab_height(decor) + 4.0) .padding(padding) @@ -628,7 +620,7 @@ fn render_tab_button(tab: LaunchTab, decor: bool, menu: &'_ MenuLaunch) -> Eleme StyleButton::SemiExtraDark([!decor, !decor, false, false]), ) }) - .on_press(Message::MChangeTab(tab)) + .on_press(MainMenuMessage::ChangeTab(tab).into()) .padding(0) .into() } @@ -637,7 +629,7 @@ fn render_tab_button(tab: LaunchTab, decor: bool, menu: &'_ MenuLaunch) -> Eleme fn get_no_logs_message<'a>() -> widget::Column<'a, Message, LauncherTheme> { const BASE_MESSAGE: &str = "No logs found"; - widget::column!(widget::text(BASE_MESSAGE).style(|t: &LauncherTheme| t.style_text(Color::Mid))) + column![widget::text(BASE_MESSAGE).style(|t: &LauncherTheme| t.style_text(Color::Mid))] // WARN: non x86_64 .push_maybe(cfg!(not(target_arch = "x86_64")).then_some(widget::text( "This version is experimental. If you want to get help join our discord", @@ -667,20 +659,20 @@ fn get_footer_text() -> widget::Column<'static, Message, LauncherTheme> { } ); - widget::column!( - widget::row!( + column![ + row![ horizontal_space(), widget::text!("QuantumLauncher v{LAUNCHER_VERSION_NAME}") .size(12) .style(|t: &LauncherTheme| t.style_text(Color::Mid)) - ), - widget::row!( + ], + row![ horizontal_space(), widget::text(subtext) .size(10) .style(|t: &LauncherTheme| t.style_text(Color::Mid)) - ), - ) + ], + ] } fn get_sidebar_new_button( @@ -688,7 +680,7 @@ fn get_sidebar_new_button( decor: bool, ) -> widget::Button<'_, Message, LauncherTheme> { widget::button( - widget::row![icons::new(), widget::text("New").size(15)] + row![icons::new(), widget::text("New").size(15)] .align_y(Alignment::Center) .height(tab_height(decor) - 6.0) .spacing(10), @@ -714,7 +706,7 @@ fn view_info_message( ) -> Option> { (!menu.message.is_empty()).then_some( widget::container( - widget::row![ + row![ widget::button( icons::close() .style(|t: &LauncherTheme| t.style_text(Color::Mid)) diff --git a/quantum_launcher/src/menu_renderer/login.rs b/quantum_launcher/src/menu_renderer/login.rs index 7aa0380d8..839dba4cf 100644 --- a/quantum_launcher/src/menu_renderer/login.rs +++ b/quantum_launcher/src/menu_renderer/login.rs @@ -22,7 +22,7 @@ impl MenuLoginAlternate { .into() } else { widget::column![button_with_icon(icons::checkmark(), "Login", 16) - .on_press(Message::Account(AccountMessage::AltLogin))] + .on_press(AccountMessage::AltLogin.into())] .align_x(Alignment::Center) .push_maybe(self.is_littleskin.then_some( widget::button("Login with OAuth").on_press(Message::Account( @@ -42,7 +42,7 @@ impl MenuLoginAlternate { let password_input = widget::text_input("Enter Password...", &self.password) .padding(padding) - .on_input(|n| Message::Account(AccountMessage::AltPasswordInput(n))); + .on_input(|n| AccountMessage::AltPasswordInput(n).into()); let password_input = if self.password.is_empty() || self.show_password { password_input } else { @@ -53,7 +53,7 @@ impl MenuLoginAlternate { back_button().on_press(if self.is_from_welcome_screen { Message::WelcomeContinueToAuth } else { - Message::Account(AccountMessage::Selected(NEW_ACCOUNT_NAME.to_owned())) + AccountMessage::Selected(NEW_ACCOUNT_NAME.to_owned()).into() }), widget::column![ widget::text(if self.is_littleskin { @@ -67,14 +67,14 @@ impl MenuLoginAlternate { center_x( widget::text_input("Enter Username/Email...", &self.username) .padding(padding) - .on_input(|n| Message::Account(AccountMessage::AltUsernameInput(n))) + .on_input(|n| AccountMessage::AltUsernameInput(n).into()) ), widget::row![ widget::text("Password:").size(12), widget::checkbox("Show", self.show_password) .size(12) .text_size(12) - .on_toggle(|t| Message::Account(AccountMessage::AltShowPassword(t))), + .on_toggle(|t| AccountMessage::AltShowPassword(t).into()), ] .spacing(16), center_x(password_input), @@ -84,7 +84,7 @@ impl MenuLoginAlternate { widget::text("OTP:").size(12), widget::text_input("Enter Username/Email...", otp) .padding(padding) - .on_input(|n| Message::Account(AccountMessage::AltOtpInput(n))), + .on_input(|n| AccountMessage::AltOtpInput(n).into()), ] .spacing(5) })) @@ -174,7 +174,7 @@ impl MenuLoginMS { back_button().on_press(if self.is_from_welcome_screen { Message::WelcomeContinueToAuth } else { - Message::Account(AccountMessage::Selected(NEW_ACCOUNT_NAME.to_owned())) + AccountMessage::Selected(NEW_ACCOUNT_NAME.to_owned()).into() }), widget::row!( widget::horizontal_space(), diff --git a/quantum_launcher/src/menu_renderer/mod.rs b/quantum_launcher/src/menu_renderer/mod.rs index 28ff1698e..4a83c80b8 100644 --- a/quantum_launcher/src/menu_renderer/mod.rs +++ b/quantum_launcher/src/menu_renderer/mod.rs @@ -25,6 +25,7 @@ mod mods; mod onboarding; mod settings; mod shortcuts; +mod sidebar; pub use onboarding::changelog; @@ -43,11 +44,20 @@ const PADDING_NOT_BOTTOM: iced::Padding = iced::Padding { right: 10.0, }; -fn ctx_button(e: &'_ str) -> widget::Button<'_, Message, LauncherTheme> { - widget::button(widget::text(e).size(13)) - .width(Length::Fill) - .style(|t: &LauncherTheme, s| t.style_button(s, StyleButton::FlatDark)) - .padding(2) +const CTXI_SIZE: u16 = 10; + +fn ctx_button<'a>( + icon: widget::Text<'a, LauncherTheme>, + e: &'a str, +) -> widget::Button<'a, Message, LauncherTheme> { + widget::button( + row![icon, widget::text(e).size(13)] + .align_y(Alignment::Center) + .spacing(10), + ) + .width(Length::Fill) + .style(|t: &LauncherTheme, s| t.style_button(s, StyleButton::FlatDark)) + .padding(2) } pub fn checkered_list<'a, Item: Into>>( @@ -219,7 +229,7 @@ fn sidebar<'a>( widget::scrollable(widget::column(children)) .style(LauncherTheme::style_scrollable_flat_extra_dark) .height(Length::Fill) - .id(iced::widget::scrollable::Id::new(id)) + .id(widget::scrollable::Id::new(id)) ] .spacing(10), ) @@ -227,6 +237,18 @@ fn sidebar<'a>( .style(|n: &LauncherTheme| n.style_container_sharp_box(0.0, Color::ExtraDark)) } +fn offset<'a>( + e: impl Into>, + x: impl Into, + y: impl Into, +) -> Element<'a> { + row![ + widget::Space::with_width(x), + column![widget::Space::with_height(y), e.into()] + ] + .into() +} + fn dots(tick_timer: usize) -> String { ".".repeat((tick_timer % 3) + 1) } @@ -352,17 +374,15 @@ impl MenuCurseforgeManualDownload { "Warning: Ignoring this may lead to crashes!", row![ - widget::button(widget::text("+ Select above downloaded files").size(14)).on_press(Message::ManageMods(ManageModsMessage::AddFile(self.delete_mods))), + widget::button(widget::text("+ Select above downloaded files").size(14)).on_press(ManageModsMessage::AddFile(self.delete_mods).into()), widget::button(widget::text("Continue").size(14)).on_press(if self.is_store { - Message::InstallMods(InstallModsMessage::Open) + InstallModsMessage::Open.into() } else { - Message::ManageMods(ManageModsMessage::ScreenOpenWithoutUpdate) + ManageModsMessage::ScreenOpenWithoutUpdate.into() }), widget::checkbox("Delete files when done", self.delete_mods) .text_size(14) - .on_toggle(|t| - Message::ManageMods(ManageModsMessage::CurseforgeManualToggleDelete(t)) - ) + .on_toggle(|t| ManageModsMessage::CurseforgeManualToggleDelete(t).into()) ].spacing(5).align_y(Alignment::Center).wrap() ] .padding(10) diff --git a/quantum_launcher/src/menu_renderer/mods/export_mods.rs b/quantum_launcher/src/menu_renderer/mods/export_mods.rs index 8248a410b..9e89423a1 100644 --- a/quantum_launcher/src/menu_renderer/mods/export_mods.rs +++ b/quantum_launcher/src/menu_renderer/mods/export_mods.rs @@ -58,15 +58,13 @@ impl MenuExportMods { widget::row![ widget::button(widget::text("Copy").size(14)) .padding([8, 16]) - .on_press(Message::ExportMods( - ExportModsMessage::CopyPlainTextToClipboard - )), + .on_press(ExportModsMessage::CopyPlainTextToClipboard.into()), widget::button(widget::text("Save").size(14)) .padding([8, 16]) .style(|theme: &LauncherTheme, status| { theme.style_button(status, StyleButton::FlatDark) }) - .on_press(Message::ExportMods(ExportModsMessage::ExportAsPlainText)), + .on_press(ExportModsMessage::ExportAsPlainText.into()), ] .spacing(12) ] @@ -88,16 +86,14 @@ impl MenuExportMods { widget::row![ widget::button(widget::text("Copy").size(14)) .padding([8, 16]) - .on_press(Message::ExportMods( - ExportModsMessage::CopyMarkdownToClipboard, - )), + .on_press(ExportModsMessage::CopyMarkdownToClipboard.into()), widget::button(widget::text("Save").size(14)) .padding([8, 16]) .style(|theme: &LauncherTheme, status| { use crate::stylesheet::widgets::StyleButton; theme.style_button(status, StyleButton::FlatDark) }) - .on_press(Message::ExportMods(ExportModsMessage::ExportAsMarkdown)) + .on_press(ExportModsMessage::ExportAsMarkdown.into()) ] .spacing(12) ] @@ -113,9 +109,7 @@ impl MenuExportMods { widget::column![ widget::row![ - back_button().on_press(Message::ManageMods( - ManageModsMessage::ScreenOpenWithoutUpdate - )), + back_button().on_press(ManageModsMessage::ScreenOpenWithoutUpdate.into()), widget::text("Export Mods List") .size(24) .style(|theme: &LauncherTheme| { theme.style_text(Color::Light) }), diff --git a/quantum_launcher/src/menu_renderer/mods/install_loader.rs b/quantum_launcher/src/menu_renderer/mods/install_loader.rs index 8d99fee52..76bb32889 100644 --- a/quantum_launcher/src/menu_renderer/mods/install_loader.rs +++ b/quantum_launcher/src/menu_renderer/mods/install_loader.rs @@ -66,9 +66,7 @@ impl MenuInstallOptifine { delete_installer: bool, ) -> widget::Column<'a, Message, LauncherTheme, iced::Renderer> { widget::column!( - back_button().on_press(Message::ManageMods( - ManageModsMessage::ScreenOpenWithoutUpdate - )), + back_button().on_press(ManageModsMessage::ScreenOpenWithoutUpdate.into()), widget::container( widget::column!( widget::text("Install OptiFine").size(20), @@ -83,14 +81,10 @@ impl MenuInstallOptifine { widget::container( widget::column!( "Step 2: Select the installer file", - widget::checkbox("Delete installer after use", delete_installer).on_toggle( - |t| Message::InstallOptifine( - InstallOptifineMessage::DeleteInstallerToggle(t) - ) - ), - widget::button("Select File").on_press(Message::InstallOptifine( - InstallOptifineMessage::SelectInstallerStart - )) + widget::checkbox("Delete installer after use", delete_installer) + .on_toggle(|t| InstallOptifineMessage::DeleteInstallerToggle(t).into()), + widget::button("Select File") + .on_press(InstallOptifineMessage::SelectInstallerStart.into()) ) .padding(10) .spacing(10) @@ -109,9 +103,7 @@ impl MenuInstallFabric { let dots = ".".repeat((tick_timer % 3) + 1); widget::column![ - back_button().on_press(Message::ManageMods( - ManageModsMessage::ScreenOpenWithoutUpdate - )), + back_button().on_press(ManageModsMessage::ScreenOpenWithoutUpdate.into()), widget::text!("Loading {loader_name} version list{dots}").size(20) ] } @@ -127,9 +119,7 @@ impl MenuInstallFabric { } MenuInstallFabric::Unsupported(is_quilt) => { widget::column!( - back_button().on_press(Message::ManageMods( - ManageModsMessage::ScreenOpenWithoutUpdate - )), + back_button().on_press(ManageModsMessage::ScreenOpenWithoutUpdate.into()), widget::text!( "{} is unsupported for this Minecraft version.", if *is_quilt { "Quilt" } else { "Fabric" } @@ -142,9 +132,7 @@ impl MenuInstallFabric { .. } => { widget::column!( - back_button().on_press(Message::ManageMods( - ManageModsMessage::ScreenOpenWithoutUpdate - )), + back_button().on_press(ManageModsMessage::ScreenOpenWithoutUpdate.into()), widget::text!("{backend} is unsupported for this Minecraft version.") ) } @@ -182,7 +170,7 @@ impl MenuInstallFabric { fabric::BackendType::CursedLegacy ], Some(backend), - |b| Message::InstallFabric(InstallFabricMessage::ChangeBackend(b)) + |b| InstallFabricMessage::ChangeBackend(b).into() ), version_list(list, fabric_version), ] @@ -206,7 +194,7 @@ impl MenuInstallFabric { fabric::BackendType::OrnitheMCFabric, ], Some(backend), - |b| Message::InstallFabric(InstallFabricMessage::ChangeBackend(b)) + |b| InstallFabricMessage::ChangeBackend(b).into() ), version_list(list, fabric_version), ] @@ -217,14 +205,12 @@ impl MenuInstallFabric { }; widget::column![ - back_button().on_press(Message::ManageMods( - ManageModsMessage::ScreenOpenWithoutUpdate - )), + back_button().on_press(ManageModsMessage::ScreenOpenWithoutUpdate.into()), widget::text!("Install {backend} for \"{}\"", selected_instance.get_name()) .size(20), picker, button_with_icon(icons::download(), "Install", 16) - .on_press(Message::InstallFabric(InstallFabricMessage::ButtonClicked)), + .on_press(InstallFabricMessage::ButtonClicked.into()), ] } } @@ -246,7 +232,7 @@ fn version_list<'a>( widget::column![ widget::text("Version:"), widget::row![widget::pick_list(list, Some(selected.clone()), |n| { - Message::InstallFabric(InstallFabricMessage::VersionSelected(n.loader.version)) + InstallFabricMessage::VersionSelected(n.loader.version).into() })] .push_maybe( list.first() @@ -283,18 +269,14 @@ impl MenuInstallPaper { let dots = ".".repeat((tick_timer % 3) + 1); match self { MenuInstallPaper::Loading { .. } => widget::column![ - back_button().on_press(Message::ManageMods( - ManageModsMessage::ScreenOpenWithoutUpdate - )), + back_button().on_press(ManageModsMessage::ScreenOpenWithoutUpdate.into()), widget::text!("Loading{dots}").size(20), ] .padding(10) .spacing(10) .into(), MenuInstallPaper::Loaded { version, versions } => widget::column![ - back_button().on_press(Message::ManageMods( - ManageModsMessage::ScreenOpenWithoutUpdate - )), + back_button().on_press(ManageModsMessage::ScreenOpenWithoutUpdate.into()), widget::text!("Select Version").size(20), widget::row![widget::pick_list(versions.clone(), Some(version), |v| { Message::InstallPaper(InstallPaperMessage::VersionSelected(v)) diff --git a/quantum_launcher/src/menu_renderer/mods/jarmods.rs b/quantum_launcher/src/menu_renderer/mods/jarmods.rs index df3d0912a..428bdaea3 100644 --- a/quantum_launcher/src/menu_renderer/mods/jarmods.rs +++ b/quantum_launcher/src/menu_renderer/mods/jarmods.rs @@ -14,9 +14,7 @@ impl MenuEditJarMods { widget::container( widget::scrollable( widget::column!( - back_button().on_press(Message::ManageMods( - ManageModsMessage::ScreenOpenWithoutUpdate - )), + back_button().on_press(ManageModsMessage::ScreenOpenWithoutUpdate.into()), widget::column![ { let path = selected_instance.get_instance_path().join("jarmods"); @@ -25,7 +23,7 @@ impl MenuEditJarMods { .on_press(Message::CoreOpenPath(path)) }, button_with_icon(icons::new_s(14), "Add file", 14) - .on_press(Message::ManageJarMods(ManageJarModsMessage::AddFile)), + .on_press(ManageJarModsMessage::AddFile.into()), ] .spacing(5), widget::row![ @@ -86,19 +84,19 @@ impl MenuEditJarMods { widget::text("Select some JarMods to perform actions on them").size(14), widget::row![ widget::button("Delete") - .on_press(Message::ManageJarMods(ManageJarModsMessage::DeleteSelected)), + .on_press(ManageJarModsMessage::DeleteSelected.into()), widget::button("Toggle") - .on_press(Message::ManageJarMods(ManageJarModsMessage::ToggleSelected)), + .on_press(ManageJarModsMessage::ToggleSelected.into()), widget::button(if matches!(self.selected_state, SelectedState::All) { "Unselect All" } else { "Select All" }) - .on_press(Message::ManageJarMods(ManageJarModsMessage::SelectAll)), + .on_press(ManageJarModsMessage::SelectAll.into()), widget::button(icons::arrow_up()) - .on_press(Message::ManageJarMods(ManageJarModsMessage::MoveUp)), + .on_press(ManageJarModsMessage::MoveUp.into()), widget::button(icons::arrow_down()) - .on_press(Message::ManageJarMods(ManageJarModsMessage::MoveDown)), + .on_press(ManageJarModsMessage::MoveDown.into()), ] .spacing(5) .wrap() @@ -126,10 +124,7 @@ impl MenuEditJarMods { self.selected_mods.contains(&jarmod.filename), ) .on_toggle(move |t| { - Message::ManageJarMods(ManageJarModsMessage::ToggleCheckbox( - jarmod.filename.clone(), - t, - )) + ManageJarModsMessage::ToggleCheckbox(jarmod.filename.clone(), t).into() }) .into() }) diff --git a/quantum_launcher/src/menu_renderer/mods/mods_manage.rs b/quantum_launcher/src/menu_renderer/mods/mods_manage.rs index f96146677..61e1955b8 100644 --- a/quantum_launcher/src/menu_renderer/mods/mods_manage.rs +++ b/quantum_launcher/src/menu_renderer/mods/mods_manage.rs @@ -3,7 +3,8 @@ use iced::{widget, Alignment, Length}; use ql_core::{InstanceSelection, Loader, SelectedMod}; use crate::menu_renderer::{ - ctx_button, ctxbox, dots, select_box, subbutton_with_icon, tsubtitle, FONT_MONO, + ctx_button, ctxbox, dots, offset, select_box, subbutton_with_icon, tsubtitle, CTXI_SIZE, + FONT_MONO, }; use crate::message_handler::ForgeKind; use crate::state::{ImageState, InstallPaperMessage, MenuEditModsModal}; @@ -52,60 +53,52 @@ impl MenuEditMods { .into() } else if let Some(MenuEditModsModal::Submenu) = &self.modal { let submenu = widget::column![ - ctx_button("Export list as text") - .on_press(Message::ManageMods(ManageModsMessage::ExportMenuOpen)), - ctx_button("Export QMP Preset") - .on_press(Message::EditPresets(EditPresetsMessage::Open)), + ctx_button(icons::file_info_s(CTXI_SIZE), "Export list as text") + .on_press(ManageModsMessage::ExportMenuOpen.into()), + ctx_button(icons::file_zip_s(CTXI_SIZE), "Export QMP Preset") + .on_press(EditPresetsMessage::Open.into()), widget::horizontal_rule(1) .style(|t: &LauncherTheme| t.style_rule(Color::SecondDark, 1)), - ctx_button("See recommended mods").on_press(Message::RecommendedMods( - crate::state::RecommendedModMessage::Open - )), + ctx_button(icons::download_s(CTXI_SIZE), "See recommended mods").on_press( + Message::RecommendedMods(crate::state::RecommendedModMessage::Open) + ), ] .spacing(4); widget::stack!( menu_main, - widget::row![ - widget::Space::with_width(MODS_SIDEBAR_WIDTH + 30), - widget::column![widget::Space::with_height(40), ctxbox(submenu).width(200)] - ] + offset(ctxbox(submenu).width(200), MODS_SIDEBAR_WIDTH + 30, 40), ) .into() } else if let Some(MenuEditModsModal::RightClick(id, (x, y))) = &self.modal { widget::stack!( menu_main, - widget::column![ - widget::Space::with_height(y.clamp(0.0, window_height - 130.0)), - widget::row![ - widget::Space::with_width(*x), - ctxbox( - widget::column![ - ctx_button("Toggle").on_press(Message::ManageMods( - ManageModsMessage::ToggleSelected - )), - ctx_button("Delete").on_press(Message::ManageMods( - ManageModsMessage::DeleteSelected - )), - ctx_button("Mod Details").on_press_maybe( - self.mods.mods.get(&id.get_index_str()).map(|info| { + offset( + ctxbox( + widget::column![ + ctx_button(icons::toggleon_s(CTXI_SIZE), "Toggle") + .on_press(ManageModsMessage::ToggleSelected.into()), + ctx_button(icons::bin_s(CTXI_SIZE), "Delete") + .on_press(ManageModsMessage::DeleteSelected.into()), + ctx_button(icons::file_info_s(CTXI_SIZE), "Mod Details") + .on_press_maybe(self.mods.mods.get(&id.get_index_str()).map( + |info| { Message::Multiple(vec![ - Message::InstallMods(InstallModsMessage::Open), - Message::InstallMods( - InstallModsMessage::ChangeBackend(id.get_backend()), - ), - Message::InstallMods(InstallModsMessage::SearchInput( - info.name.clone(), - )), + InstallModsMessage::Open.into(), + InstallModsMessage::ChangeBackend(id.get_backend()) + .into(), + InstallModsMessage::SearchInput(info.name.clone()) + .into(), ]) - }) - ), - ] - .spacing(4) - ) - .width(200) - ] - ] + } + )), + ] + .spacing(4) + ) + .width(150), + *x, + y.clamp(0.0, window_height - 130.0) + ), ) .into() } else { @@ -139,12 +132,12 @@ impl MenuEditMods { self.get_mod_installer_buttons(selected_instance), widget::column!( button_with_icon(icons::download_s(15), "Download Content...", 14) - .on_press(Message::InstallMods(InstallModsMessage::Open)), + .on_press(InstallModsMessage::Open.into()), button_with_icon(icons::file_jar(), "Jarmod Patches", 14) - .on_press(Message::ManageJarMods(ManageJarModsMessage::Open)), + .on_press(ManageJarModsMessage::Open.into()), tooltip( button_with_icon(icons::file(), "Add File", 14) - .on_press(Message::ManageMods(ManageModsMessage::AddFile(false))), + .on_press(ManageModsMessage::AddFile(false).into()), widget::text("Includes mods and modpacks").size(12), Position::Bottom ), @@ -190,7 +183,7 @@ impl MenuEditMods { widget::checkbox(text, *is_enabled) .on_toggle(move |b| { - Message::ManageMods(ManageModsMessage::UpdateCheckToggle(i, b)) + ManageModsMessage::UpdateCheckToggle(i, b).into() }) .text_size(12) .into() @@ -198,7 +191,7 @@ impl MenuEditMods { )) .spacing(10), button_with_icon(icons::version_download(), "Update", 16) - .on_press(Message::ManageMods(ManageModsMessage::UpdateMods)), + .on_press(ManageModsMessage::UpdateMods.into()), ) .padding(10) .spacing(10) @@ -214,12 +207,10 @@ impl MenuEditMods { InstanceSelection::Instance(_) => widget::column![ "Install:", widget::row!( - install_ldr("Fabric").on_press(Message::InstallFabric( - InstallFabricMessage::ScreenOpen { is_quilt: false } - )), - install_ldr("Quilt").on_press(Message::InstallFabric( - InstallFabricMessage::ScreenOpen { is_quilt: true } - )), + install_ldr("Fabric") + .on_press(InstallFabricMessage::ScreenOpen { is_quilt: false }.into()), + install_ldr("Quilt") + .on_press(InstallFabricMessage::ScreenOpen { is_quilt: true }.into()), ) .spacing(5), widget::row!( @@ -228,20 +219,17 @@ impl MenuEditMods { .on_press(Message::InstallForge(ForgeKind::NeoForge)) ) .spacing(5), - install_ldr("OptiFine") - .on_press(Message::InstallOptifine(InstallOptifineMessage::ScreenOpen)) + install_ldr("OptiFine").on_press(InstallOptifineMessage::ScreenOpen.into()) ] .spacing(5) .into(), InstanceSelection::Server(_) => widget::column!( "Install:", widget::row!( - install_ldr("Fabric").on_press(Message::InstallFabric( - InstallFabricMessage::ScreenOpen { is_quilt: false } - )), - install_ldr("Quilt").on_press(Message::InstallFabric( - InstallFabricMessage::ScreenOpen { is_quilt: true } - )), + install_ldr("Fabric") + .on_press(InstallFabricMessage::ScreenOpen { is_quilt: false }.into()), + install_ldr("Quilt") + .on_press(InstallFabricMessage::ScreenOpen { is_quilt: true }.into()), ) .spacing(5), widget::row!( @@ -306,14 +294,12 @@ impl MenuEditMods { .padding(2), ) .on_press(Message::UninstallLoaderConfirm( - Box::new(Message::ManageMods(ManageModsMessage::DeleteOptiforge( - optifine.to_owned(), - ))), + Box::new(ManageModsMessage::DeleteOptiforge(optifine.to_owned()).into()), Loader::OptiFine, )) } else { widget::button(widget::text("Install OptiFine with Forge").size(14)) - .on_press(Message::InstallOptifine(InstallOptifineMessage::ScreenOpen)) + .on_press(InstallOptifineMessage::ScreenOpen.into()) } } @@ -337,9 +323,8 @@ impl MenuEditMods { if self.sorted_mods_list.is_empty() { return widget::column!( "Download some mods to get started", - widget::button(widget::text("View Recommended Mods").size(14)).on_press( - Message::RecommendedMods(crate::state::RecommendedModMessage::Open) - ) + widget::button(widget::text("View Recommended Mods").size(14)) + .on_press(crate::state::RecommendedModMessage::Open.into()) ) .spacing(10) .padding(10) @@ -370,9 +355,9 @@ impl MenuEditMods { .padding(1), ) .style(|t: &LauncherTheme, s| { - t.style_button(s, crate::stylesheet::widgets::StyleButton::RoundDark) + t.style_button(s, StyleButton::RoundDark) }) - .on_press(Message::ManageMods(ManageModsMessage::SetModal(self.modal.is_none().then_some(MenuEditModsModal::Submenu)))), + .on_press(ManageModsMessage::SetModal(self.modal.is_none().then_some(MenuEditModsModal::Submenu)).into()), // Search button widget::button( @@ -388,27 +373,27 @@ impl MenuEditMods { }) }).on_press( if self.search.is_some() { - Message::ManageMods(ManageModsMessage::SetSearch(None)) + ManageModsMessage::SetSearch(None).into() } else { Message::Multiple(vec![ - Message::ManageMods(ManageModsMessage::SetSearch( + ManageModsMessage::SetSearch( Some(String::new()) - )), + ).into(), Message::CoreFocusNext ]) } ), subbutton_with_icon(icons::bin_s(12), "Delete") - .on_press_maybe((!self.selected_mods.is_empty()).then_some(Message::ManageMods(ManageModsMessage::DeleteSelected))), + .on_press_maybe((!self.selected_mods.is_empty()).then_some(ManageModsMessage::DeleteSelected.into())), subbutton_with_icon(icons::toggleoff_s(12), "Toggle") - .on_press_maybe((!self.selected_mods.is_empty()).then_some(Message::ManageMods(ManageModsMessage::ToggleSelected))), + .on_press_maybe((!self.selected_mods.is_empty()).then_some(ManageModsMessage::ToggleSelected.into())), subbutton_with_icon(icons::deselectall_s(12), if matches!(self.selected_state, SelectedState::All) { "Unselect All" } else { "Select All" }) - .on_press(Message::ManageMods(ManageModsMessage::SelectAll)), + .on_press(ManageModsMessage::SelectAll.into()), ] .spacing(5) .wrap() @@ -422,7 +407,7 @@ impl MenuEditMods { ) .push_maybe(self.search.as_ref().map(|search| widget::text_input("Search...", search).size(14).on_input(|msg| - Message::ManageMods(ManageModsMessage::SetSearch(Some(msg))) + ManageModsMessage::SetSearch(Some(msg)).into() ) )) .padding(10) @@ -456,9 +441,7 @@ impl MenuEditMods { horizontal: widget::scrollable::Scrollbar::new(), }) .id(widget::scrollable::Id::new("MenuEditMods:mods")) - .on_scroll(|viewport| { - Message::ManageMods(ManageModsMessage::ListScrolled(viewport.absolute_offset())) - }) + .on_scroll(|viewport| ManageModsMessage::ListScrolled(viewport.absolute_offset()).into()) .style(LauncherTheme::style_scrollable_flat_extra_dark) .width(Length::Fill) .height(Length::Fill) @@ -548,10 +531,7 @@ impl MenuEditMods { .padding(PADDING) .spacing(SPACING), is_selected, - Message::ManageMods(ManageModsMessage::SelectMod( - config.name.clone(), - Some(id.clone()), - )), + ManageModsMessage::SelectMod(config.name.clone(), Some(id.clone())).into(), ) .padding(0); @@ -561,17 +541,18 @@ impl MenuEditMods { tooltip(checkbox, "Disabled", Position::FollowCursor).into() }; - let rightclick = Message::ManageMods(ManageModsMessage::RightClick(id.clone())); + let rightclick = ManageModsMessage::RightClick(id.clone()).into(); widget::mouse_area(checkbox) .on_right_press(if self.selected_mods.len() > 1 && self.is_selected(id) { rightclick } else { Message::Multiple(vec![ - Message::ManageMods(ManageModsMessage::SelectEnsure( + ManageModsMessage::SelectEnsure( config.name.clone(), Some(id.clone()), - )), + ) + .into(), rightclick, ]) }) @@ -618,7 +599,7 @@ impl MenuEditMods { ] .spacing(SPACING), is_selected, - Message::ManageMods(ManageModsMessage::SelectMod(file_name.clone(), None)), + ManageModsMessage::SelectMod(file_name.clone(), None).into(), ) .padding(PADDING) .width(size.width); diff --git a/quantum_launcher/src/menu_renderer/mods/mods_store.rs b/quantum_launcher/src/menu_renderer/mods/mods_store.rs index 10f6e0819..21a10e151 100644 --- a/quantum_launcher/src/menu_renderer/mods/mods_store.rs +++ b/quantum_launcher/src/menu_renderer/mods/mods_store.rs @@ -25,7 +25,7 @@ impl MenuModsDownload { widget::scrollable( column!( widget::text_input("Search...", &self.query) - .on_input(|n| Message::InstallMods(InstallModsMessage::SearchInput(n))), + .on_input(|n| InstallModsMessage::SearchInput(n).into()), self.get_side_panel(), ) .padding(10) @@ -78,9 +78,7 @@ impl MenuModsDownload { .id(widget::scrollable::Id::new("MenuModsDownload:main:mods_list")) .height(Length::Fill) .width(Length::Fill) - .on_scroll(|viewport| { - Message::InstallMods(InstallModsMessage::Scrolled(viewport)) - }), + .on_scroll(|viewport| InstallModsMessage::Scrolled(viewport).into()), ) ) .into() @@ -88,16 +86,14 @@ impl MenuModsDownload { fn get_side_panel(&'_ self) -> Element<'_> { let normal_controls = column!( - back_button().on_press(Message::ManageMods( - ManageModsMessage::ScreenOpenWithoutUpdate - )), - widget::Space::with_height(5.0), + back_button().on_press(ManageModsMessage::ScreenOpenWithoutUpdate.into()), + widget::Space::with_height(5), widget::text("Select store:").size(18), widget::radio( "Modrinth", StoreBackendType::Modrinth, Some(self.backend), - |v| { Message::InstallMods(InstallModsMessage::ChangeBackend(v)) } + |v| InstallModsMessage::ChangeBackend(v).into() ) .text_size(14) .size(14), @@ -105,7 +101,7 @@ impl MenuModsDownload { "CurseForge", StoreBackendType::Curseforge, Some(self.backend), - |v| { Message::InstallMods(InstallModsMessage::ChangeBackend(v)) } + |v| InstallModsMessage::ChangeBackend(v).into() ) .text_size(14) .size(14), @@ -113,7 +109,7 @@ impl MenuModsDownload { widget::text("Select Type:").size(18), widget::column(QueryType::STORE_QUERIES.iter().map(|n| { widget::radio(n.to_string(), *n, Some(self.query_type), |v| { - Message::InstallMods(InstallModsMessage::ChangeQueryType(v)) + InstallModsMessage::ChangeQueryType(v).into() }) .text_size(14) .size(14) @@ -205,7 +201,7 @@ impl MenuModsDownload { .style(|t: &LauncherTheme, s| { t.style_button(s, StyleButton::SemiDarkBorder([true; 4])) }) - .on_press(Message::InstallMods(InstallModsMessage::Uninstall(i))), + .on_press(InstallModsMessage::Uninstall(i).into()), "Uninstall", widget::tooltip::Position::FollowCursor, ) @@ -213,10 +209,7 @@ impl MenuModsDownload { } else { // Download button side_button(icons::download()) - .on_press_maybe( - (!is_downloading) - .then_some(Message::InstallMods(InstallModsMessage::Download(i))), - ) + .on_press_maybe((!is_downloading).then_some(InstallModsMessage::Download(i).into())) .into() }; @@ -259,7 +252,7 @@ impl MenuModsDownload { .spacing(10), ) .height(70) - .on_press(Message::InstallMods(InstallModsMessage::Click(i))) + .on_press(InstallModsMessage::Click(i).into()) ) .spacing(5) .into() @@ -285,12 +278,12 @@ impl MenuModsDownload { images: &'a ImageState, tick_timer: usize, ) -> Element<'a> { - // Parses the markdown description of the mod. + // Parses the Markdown description of the mod. let markdown_description = if let Some(desc) = &self.description { column!(MarkWidget::new(desc) .on_clicking_link(Message::CoreOpenLink) .on_drawing_image(|img| { images.view(img.url, img.width, img.height, "".into()) }) - .on_updating_state(|n| Message::InstallMods(InstallModsMessage::TickDesc(n))) + .on_updating_state(|n| InstallModsMessage::TickDesc(n).into()) .font(FONT_DEFAULT) .font_mono(FONT_MONO)) } else { @@ -311,8 +304,7 @@ impl MenuModsDownload { widget::scrollable( column!( row!( - back_button() - .on_press(Message::InstallMods(InstallModsMessage::BackToMainScreen)), + back_button().on_press(InstallModsMessage::BackToMainScreen.into()), widget::tooltip( button_with_icon(icons::globe(), "Open Mod Page", 14) .on_press(Message::CoreOpenLink(url.clone())), diff --git a/quantum_launcher/src/menu_renderer/mods/presets.rs b/quantum_launcher/src/menu_renderer/mods/presets.rs index 7f2dc704b..cd5e26f50 100644 --- a/quantum_launcher/src/menu_renderer/mods/presets.rs +++ b/quantum_launcher/src/menu_renderer/mods/presets.rs @@ -35,9 +35,7 @@ impl MenuEditPresets { let p_main = widget::row![ widget::column![ - back_button().on_press(Message::ManageMods( - ManageModsMessage::ScreenOpenWithoutUpdate - )), + back_button().on_press(ManageModsMessage::ScreenOpenWithoutUpdate.into()), widget::text( r"Mod Presets (.qmp files) are a simple way to share @@ -57,9 +55,9 @@ Modrinth/Curseforge modpack" "Include mod settings/configuration (config folder)", self.include_config ) - .on_toggle(|t| Message::EditPresets(EditPresetsMessage::ToggleIncludeConfig(t))), + .on_toggle(|t| EditPresetsMessage::ToggleIncludeConfig(t).into()), button_with_icon(icons::floppydisk(), "Build Preset", 16) - .on_press(Message::EditPresets(EditPresetsMessage::BuildYourOwn)), + .on_press(EditPresetsMessage::BuildYourOwn.into()), ] .padding(10) .spacing(10), @@ -72,7 +70,7 @@ Modrinth/Curseforge modpack" "Select All" } ) - .on_press(Message::EditPresets(EditPresetsMessage::SelectAll))] + .on_press(EditPresetsMessage::SelectAll.into())] .padding({ let p: iced::Padding = 10.into(); p.bottom(0) @@ -108,14 +106,12 @@ Modrinth/Curseforge modpack" widget::checkbox(entry.name(), selected_mods.contains(&entry.clone().into())) .on_toggle(move |t| match entry { ModListEntry::Downloaded { id, config } => { - Message::EditPresets(EditPresetsMessage::ToggleCheckbox( - (config.name.clone(), id.clone()), - t, - )) + EditPresetsMessage::ToggleCheckbox((config.name.clone(), id.clone()), t) + .into() + } + ModListEntry::Local { file_name } => { + EditPresetsMessage::ToggleCheckboxLocal(file_name.clone(), t).into() } - ModListEntry::Local { file_name } => Message::EditPresets( - EditPresetsMessage::ToggleCheckboxLocal(file_name.clone(), t), - ), }) .into() } else { @@ -130,9 +126,7 @@ Modrinth/Curseforge modpack" impl MenuRecommendedMods { pub fn view(&'_ self) -> Element<'_> { - let back_button = back_button().on_press(Message::ManageMods( - ManageModsMessage::ScreenOpenWithoutUpdate, - )); + let back_button = back_button().on_press(ManageModsMessage::ScreenOpenWithoutUpdate.into()); match self { MenuRecommendedMods::Loading { progress, .. } => progress.view().padding(10).into(), @@ -154,15 +148,11 @@ impl MenuRecommendedMods { widget::column!( back_button, button_with_icon(icons::download(), "Download Recommended Mods", 16) - .on_press(Message::RecommendedMods( - crate::state::RecommendedModMessage::Download - )), + .on_press(crate::state::RecommendedModMessage::Download.into()), widget::column(mods.iter().enumerate().map(|(i, (e, n))| { let elem: Element = widget::checkbox(n.name, *e) .on_toggle(move |n| { - Message::RecommendedMods(crate::state::RecommendedModMessage::Toggle( - i, n, - )) + crate::state::RecommendedModMessage::Toggle(i, n).into() }) .into(); widget::column!( diff --git a/quantum_launcher/src/menu_renderer/onboarding/mod.rs b/quantum_launcher/src/menu_renderer/onboarding/mod.rs index 25bd3c2ac..046ffee45 100644 --- a/quantum_launcher/src/menu_renderer/onboarding/mod.rs +++ b/quantum_launcher/src/menu_renderer/onboarding/mod.rs @@ -16,7 +16,7 @@ pub static IMG_LOGO: LazyLock = LazyLock::new(|| { Handle::from_bytes(include_bytes!("../../../../assets/icon/ql_logo.png").as_slice()) }); -pub fn x86_warning() -> iced::widget::Container<'static, Message, LauncherTheme> { +pub fn x86_warning() -> widget::Container<'static, Message, LauncherTheme> { widget::container( widget::column![ widget::text( diff --git a/quantum_launcher/src/menu_renderer/onboarding/welcome.rs b/quantum_launcher/src/menu_renderer/onboarding/welcome.rs index f95fb86b6..c292c51ee 100644 --- a/quantum_launcher/src/menu_renderer/onboarding/welcome.rs +++ b/quantum_launcher/src/menu_renderer/onboarding/welcome.rs @@ -1,4 +1,4 @@ -use iced::widget; +use iced::{widget, Alignment}; use ql_instances::auth::AccountType; use crate::{ @@ -8,7 +8,7 @@ use crate::{ button_with_icon, center_x, get_mode_selector, onboarding::x86_warning, settings::get_theme_selector, Element, DISCORD, }, - state::{AccountMessage, MenuWelcome, Message}, + state::{AccountMessage, MainMenuMessage, MenuWelcome, Message}, }; use super::IMG_LOGO; @@ -36,73 +36,81 @@ impl MenuWelcome { get_mode_selector(config), widget::horizontal_space(), ] + .align_y(Alignment::Center) .spacing(10), widget::row![ widget::horizontal_space(), "Select Color Scheme:", - get_theme_selector().wrap(), + widget::row![get_theme_selector().wrap()].width(250), widget::horizontal_space(), ] .spacing(10), widget::Space::with_height(5), - center_x("Oh, and also..."), - center_x( - button_with_icon(icons::discord(), "Join our Discord", 16) - .on_press(Message::CoreOpenLink(DISCORD.to_owned())) - ), + widget::row![ + widget::horizontal_space(), + "Oh, and also consider", + button_with_icon(icons::discord(), "Join our Discord", 14) + .padding([4, 8]) + .on_press(Message::CoreOpenLink(DISCORD.to_owned())), + widget::horizontal_space(), + ] + .align_y(Alignment::Center) + .spacing(10), widget::Space::with_height(5), center_x(widget::button("Continue").on_press(Message::WelcomeContinueToAuth)), widget::vertical_space(), ] .spacing(10) .into(), - MenuWelcome::P3Auth => widget::column![ - widget::vertical_space(), - center_x( - widget::button("Login to Microsoft").on_press(Message::Account( - AccountMessage::OpenMenu { - is_from_welcome_screen: true, - kind: AccountType::Microsoft - } - )) - ), - center_x(widget::button("Login to ely.by").on_press(Message::Account( - AccountMessage::OpenMenu { - is_from_welcome_screen: true, - kind: AccountType::ElyBy - } - ))), - center_x( - widget::button("Login to littleskin").on_press(Message::Account( + MenuWelcome::P3Auth => { + let next = Message::MScreenOpen { + message: Some("Install Minecraft by clicking \"+ New\"".to_owned()), + clear_selection: true, + is_server: Some(false), + }; + widget::column![ + widget::vertical_space(), + center_x( + widget::button("Login to Microsoft").on_press(Message::Account( + AccountMessage::OpenMenu { + is_from_welcome_screen: true, + kind: AccountType::Microsoft + } + )) + ), + center_x(widget::button("Login to ely.by").on_press(Message::Account( AccountMessage::OpenMenu { is_from_welcome_screen: true, - kind: AccountType::LittleSkin + kind: AccountType::ElyBy } - )) - ), - widget::Space::with_height(7), - center_x(widget::text("OR").size(20)), - widget::Space::with_height(7), - center_x( - widget::text_input("Enter username...", &config.username) - .width(200) - .on_input(Message::LaunchUsernameSet) - ), - center_x( - widget::button(center_x("Continue")) - .width(200) - .on_press_maybe((!config.username.is_empty()).then_some( - Message::MScreenOpen { - message: None, - clear_selection: true, - is_server: Some(false) + ))), + center_x( + widget::button("Login to littleskin").on_press(Message::Account( + AccountMessage::OpenMenu { + is_from_welcome_screen: true, + kind: AccountType::LittleSkin } )) - ), - widget::vertical_space(), - ] - .spacing(5) - .into(), + ), + widget::Space::with_height(7), + center_x(widget::text("OR").size(20)), + widget::Space::with_height(7), + center_x( + widget::text_input("Enter username...", &config.username) + .width(200) + .on_input(|t| MainMenuMessage::UsernameSet(t).into()) + .on_submit_maybe((!config.username.is_empty()).then(|| next.clone())) + ), + center_x( + widget::button(center_x("Continue")) + .width(200) + .on_press_maybe((!config.username.is_empty()).then(|| next.clone())) + ), + widget::vertical_space(), + ] + .spacing(5) + .into() + } } } } diff --git a/quantum_launcher/src/menu_renderer/settings.rs b/quantum_launcher/src/menu_renderer/settings.rs index f374adf99..72e460c5b 100644 --- a/quantum_launcher/src/menu_renderer/settings.rs +++ b/quantum_launcher/src/menu_renderer/settings.rs @@ -27,7 +27,8 @@ pub static IMG_ICED: LazyLock = LazyLock::new(|| { const SETTINGS_SPACING: f32 = 10.0; const SETTING_WIDTH: u16 = 180; -pub const PREFIX_EXPLANATION: &str = "Commands to add before the game launch command\nEg: 'prime-run' to force NVIDIA GPU on Linux with Optimus"; +pub const PREFIX_EXPLANATION: &str = + "Commands to add before the game launch command\nEg: prime-run/gamemoderun/mangohud"; impl MenuLauncherSettings { pub fn view<'a>(&'a self, config: &'a LauncherConfig) -> Element<'a> { @@ -48,7 +49,7 @@ impl MenuLauncherSettings { tab, &self.selected_tab, text, - Message::LauncherSettings(LauncherSettingsMessage::ChangeTab(*tab)), + LauncherSettingsMessage::ChangeTab(*tab).into(), ) }) ) @@ -87,7 +88,7 @@ impl MenuLauncherSettings { )) ]; - let idle_fps = config.ui.unwrap_or_default().get_idle_fps(); + let idle_fps = config.c_idle_fps(); checkered_list::([ widget::column![widget::text("User Interface").size(20)].into(), @@ -97,8 +98,7 @@ impl MenuLauncherSettings { .spacing(5) .align_y(Alignment::Center), widget::Space::with_height(5), - "Theme:", - get_theme_selector().wrap() + widget::row!["Theme:", get_theme_selector().wrap()].spacing(5), ] .spacing(5) .into(), @@ -123,7 +123,7 @@ impl MenuLauncherSettings { widget::column![ // TODO: This requires launcher restart // widget::checkbox("Custom Window Decorations", !config.c_window_decorations()).on_toggle(|n| { - // Message::LauncherSettings(LauncherSettingsMessage::ToggleWindowDecorations(n)) + // LauncherSettingsMessage::ToggleWindowDecorations(n).into() // }), // widget::text("Use custom window borders and close/minimize/maximize buttons").size(12), // widget::Space::with_height(5), @@ -136,10 +136,10 @@ impl MenuLauncherSettings { widget::Space::with_height(5), widget::checkbox("Remember window size", config.window.as_ref().is_none_or(|n| n.save_window_size)) - .on_toggle(|n| Message::LauncherSettings(LauncherSettingsMessage::ToggleWindowSize(n))), + .on_toggle(|n| LauncherSettingsMessage::ToggleWindowSize(n).into()), widget::Space::with_height(5), widget::checkbox("Remember last selected instance", config.persistent.clone().unwrap_or_default().selected_remembered) - .on_toggle(|n| Message::LauncherSettings(LauncherSettingsMessage::ToggleInstanceRemembering(n))), + .on_toggle(|n| LauncherSettingsMessage::ToggleInstanceRemembering(n).into()), ] .spacing(5) .into(), @@ -188,7 +188,8 @@ fn get_ui_opacity(config: &LauncherConfig) -> widget::Column<'static, Message, L pub fn get_theme_selector() -> widget::Row<'static, Message, LauncherTheme> { widget::row(LauncherThemeColor::ALL.iter().map(|color| { - widget::button(widget::text(color.to_string()).size(14)) + widget::button(widget::text(color.to_string()).size(13)) + .padding([2, 4]) .style(|theme: &LauncherTheme, s| { LauncherTheme { color: *color, @@ -230,7 +231,7 @@ impl LauncherSettingsTab { widget::horizontal_rule(1), "Global Java Arguments:", get_args_list(config.extra_java_args.as_deref(), |msg| { - Message::LauncherSettings(LauncherSettingsMessage::GlobalJavaArgs(msg)) + LauncherSettingsMessage::GlobalJavaArgs(msg).into() }), widget::Space::with_height(5), "Global Pre-Launch Prefix:", @@ -240,16 +241,13 @@ impl LauncherSettingsTab { .global_settings .as_ref() .and_then(|n| n.pre_launch_prefix.as_deref()), - |n| Message::LauncherSettings(LauncherSettingsMessage::GlobalPreLaunchPrefix( - n - )), + |n| LauncherSettingsMessage::GlobalPreLaunchPrefix(n).into(), ), args_split_by_space(menu.arg_split_by_space), widget::horizontal_rule(1), widget::row![ - button_with_icon(icons::bin(), "Clear Java installs", 16).on_press( - Message::LauncherSettings(LauncherSettingsMessage::ClearJavaInstalls) - ), + button_with_icon(icons::bin(), "Clear Java installs", 16) + .on_press(LauncherSettingsMessage::ClearJavaInstalls.into()), widget::text( "Might fix some Java problems.\nPerfectly safe, will be redownloaded." ) diff --git a/quantum_launcher/src/menu_renderer/shortcuts.rs b/quantum_launcher/src/menu_renderer/shortcuts.rs index cb58808d8..ec028e5f2 100644 --- a/quantum_launcher/src/menu_renderer/shortcuts.rs +++ b/quantum_launcher/src/menu_renderer/shortcuts.rs @@ -82,11 +82,11 @@ impl MenuShortcut { widget::container( column![ widget::checkbox(ACTION_BUTTON_MENU, self.add_to_menu) - .on_toggle(|t| Message::Shortcut(ShortcutMessage::ToggleAddToMenu(t))) + .on_toggle(|t| ShortcutMessage::ToggleAddToMenu(t).into()) .size(12) .text_size(12), widget::checkbox("Add to Desktop", self.add_to_desktop) - .on_toggle(|t| Message::Shortcut(ShortcutMessage::ToggleAddToDesktop(t))) + .on_toggle(|t| ShortcutMessage::ToggleAddToDesktop(t).into()) .size(12) .text_size(12), widget::Space::with_height(4), @@ -96,7 +96,7 @@ impl MenuShortcut { .on_press_maybe( disabled_tooltip .is_none() - .then_some(Message::Shortcut(ShortcutMessage::SaveMenu)) + .then_some(ShortcutMessage::SaveMenu.into()) ) ), ] @@ -117,7 +117,7 @@ impl MenuShortcut { .on_press_maybe( disabled_tooltip .is_none() - .then_some(Message::Shortcut(ShortcutMessage::SaveCustom)) + .then_some(ShortcutMessage::SaveCustom.into()) ) ), ]) @@ -159,20 +159,20 @@ impl MenuShortcut { "Name:", widget::text_input("(Required)", &self.shortcut.name) .size(14) - .on_input(|n| Message::Shortcut(ShortcutMessage::EditName(n))) + .on_input(|n| ShortcutMessage::EditName(n).into()) )] .push_maybe(SHOW_DESC.then(|| { ifield( "Description:", widget::text_input("Leave blank for none", &self.shortcut.description) .size(14) - .on_input(|n| Message::Shortcut(ShortcutMessage::EditDescription(n))), + .on_input(|n| ShortcutMessage::EditDescription(n).into()), ) })) .push(ifield( "Account:", row![widget::pick_list(accounts, Some(&self.account), |n| { - Message::Shortcut(ShortcutMessage::AccountSelected(n)) + ShortcutMessage::AccountSelected(n).into() }) .text_size(14) .width(Length::Fill)] @@ -181,7 +181,7 @@ impl MenuShortcut { widget::text_input("Enter username...", &self.account_offline) .size(14) .width(Length::Fill) - .on_input(|n| Message::Shortcut(ShortcutMessage::AccountOffline(n))), + .on_input(|n| ShortcutMessage::AccountOffline(n).into()), ), ) .spacing(5), @@ -198,5 +198,5 @@ fn open_folder_button() -> widget::Button<'static, Message, LauncherTheme> { .spacing(10), ) .width(FOLDER_BUTTON_WIDTH) - .on_press(Message::Shortcut(ShortcutMessage::OpenFolder)) + .on_press(ShortcutMessage::OpenFolder.into()) } diff --git a/quantum_launcher/src/menu_renderer/sidebar/drop_recv.rs b/quantum_launcher/src/menu_renderer/sidebar/drop_recv.rs new file mode 100644 index 000000000..a29d133a9 --- /dev/null +++ b/quantum_launcher/src/menu_renderer/sidebar/drop_recv.rs @@ -0,0 +1,94 @@ +use iced::{ + widget::{self, column}, + Length, +}; + +use crate::{ + config::sidebar::{SDragLocation, SDragTo, SidebarNode, SidebarNodeKind, SidebarSelection}, + menu_renderer::sidebar::LEVEL_WIDTH, + state::{MenuLaunch, Message, SidebarMessage}, + stylesheet::{color::Color, styles::LauncherTheme}, +}; + +pub fn drag_drop_receiver( + menu: &MenuLaunch, + selection: &SidebarSelection, + node: &SidebarNode, +) -> Option> { + let (_, dragged_to) = menu.get_modal_drag()?; + + let (is_hovered, offset) = dragged_to.as_ref().map_or((false, SDragTo::Before), |n| { + (n.sel == *selection, n.offset) + }); + + Some( + column![drop_box( + SDragTo::Before, + is_hovered && matches!(offset, SDragTo::Before), + selection + ),] + .push_maybe(bottom_drop_box( + node, + is_hovered && matches!(offset, SDragTo::After | SDragTo::Inside), + selection, + )), + ) +} + +fn bottom_drop_box( + node: &SidebarNode, + show: bool, + selection: &SidebarSelection, +) -> Option> { + if let SidebarNodeKind::Folder(f) = &node.kind { + return f + .children + .is_empty() + .then(|| drop_box(SDragTo::Inside, show, selection)); + } + Some(drop_box(SDragTo::After, show, selection)) +} + +fn drop_box<'a>( + offset: SDragTo, + show: bool, + selection: &SidebarSelection, +) -> widget::MouseArea<'a, Message, LauncherTheme> { + let hover = |entered| { + SidebarMessage::DragHover { + entered, + location: SDragLocation { + offset, + sel: selection.clone(), + }, + } + .into() + }; + + let elem = show.then_some(bar(4)); + widget::mouse_area(match offset { + SDragTo::Before => widget::Column::new().push_maybe(elem).push(empty()), + SDragTo::After => widget::column![empty()].push_maybe(elem), + SDragTo::Inside => widget::column![empty()].push_maybe( + show.then(|| widget::row![widget::Space::new(LEVEL_WIDTH, Length::Fill), bar(12)]), + ), + }) + .on_press( + SidebarMessage::DragDrop(Some(SDragLocation { + offset, + sel: selection.clone(), + })) + .into(), + ) + .on_enter(hover(true)) + .on_exit(hover(false)) +} + +fn empty() -> widget::Space { + widget::Space::new(Length::Fill, Length::Fill) +} + +fn bar(thickness: u16) -> widget::Rule<'static, LauncherTheme> { + widget::horizontal_rule(thickness) + .style(move |t: &LauncherTheme| t.style_rule(Color::SecondLight, thickness)) +} diff --git a/quantum_launcher/src/menu_renderer/sidebar/mod.rs b/quantum_launcher/src/menu_renderer/sidebar/mod.rs new file mode 100644 index 000000000..88d531d31 --- /dev/null +++ b/quantum_launcher/src/menu_renderer/sidebar/mod.rs @@ -0,0 +1,402 @@ +use iced::{ + widget::{self, column, row}, + Alignment, Length, +}; +use ql_core::InstanceSelection; + +use crate::{ + config::sidebar::{FolderId, SidebarFolder, SidebarNode, SidebarNodeKind, SidebarSelection}, + icons, + menu_renderer::{ + ctx_button, ctxbox, offset, sidebar::drop_recv::drag_drop_receiver, underline, + underline_maybe, Element, CTXI_SIZE, FONT_MONO, + }, + state::{ + EditInstanceMessage, LaunchModal, LaunchTab, Launcher, MainMenuMessage, MenuLaunch, + Message, SidebarMessage, State, + }, + stylesheet::{color::Color, styles::LauncherTheme, widgets::StyleButton}, +}; + +mod drop_recv; + +const LEVEL_WIDTH: u16 = 15; + +#[derive(Clone, Copy)] +pub enum NodeMode { + InTree(u16), + Dragged, +} + +impl NodeMode { + pub fn get_space(self) -> widget::Space { + widget::Space::with_width(match self { + NodeMode::InTree(n) => LEVEL_WIDTH * n, + NodeMode::Dragged => 0, + }) + } +} + +impl Launcher { + pub(super) fn get_node_rendered<'a>( + &'a self, + menu: &'a MenuLaunch, + node: &'a SidebarNode, + mode: NodeMode, + ) -> Element<'a> { + // Tbh should be careful about careless heap allocations + let selection = SidebarSelection::from_node(node); + let is_selected = self.node_is_instance_selected(node); + + let show_drag_handle = !matches!( + menu.modal, + Some(LaunchModal::SDragging { .. } | LaunchModal::SRenamingFolder(_, _, _)) + ); + + let button: Element = match &node.kind { + SidebarNodeKind::Instance(_) => { + self.get_node_instance(node, &selection, mode, is_selected) + } + SidebarNodeKind::Folder(f) => self.get_node_folder(node, &selection, mode, f), + }; + + widget::stack!( + self.node_wrap_in_context_menu(selection.clone(), button, node.name.clone()), + indent_guide_lines(mode, is_selected), + ) + .push_maybe( + show_drag_handle + .then(|| widget::row![widget::horizontal_space(), drag_handle(&selection)]), + ) + .into() + } + + fn get_node_folder<'a>( + &'a self, + node: &'a SidebarNode, + selection: &SidebarSelection, + mode: NodeMode, + folder: &'a SidebarFolder, + ) -> Element<'a> { + let State::Launch(menu) = &self.state else { + return widget::Column::new().into(); + }; + let is_drag_happening = matches!(&menu.modal, Some(LaunchModal::SDragging { .. })); + + let drop_receiver = drag_drop_receiver(menu, selection, node); + + let text = if folder.is_expanded { + widget::text(&node.name) + } else { + widget::text!("{}...", node.name) + } + .size(15) + .style(move |t: &LauncherTheme| t.style_text(Color::Mid)); + + let view = widget::stack!(underline( + widget::row![ + widget::Space::with_width(2), + widget::text(if folder.is_expanded { "- " } else { "+ " }) + .font(FONT_MONO) + .size(14) + .style(move |t: &LauncherTheme| t.style_text(Color::Light)), + text, + ] + .width(Length::Fill) + .align_y(Alignment::Center) + .padding([4, 10]), + Color::Dark, + )); + + let space = mode.get_space(); + + match mode { + NodeMode::InTree(nesting) => { + let regular = || { + column![node_button( + row![space, view.push_maybe(drop_receiver)], + is_drag_happening + ) + .on_press(SidebarMessage::ToggleFolderVisibility(folder.id).into())] + }; + + if let Some(LaunchModal::SRenamingFolder(id, name, is_creating)) = &menu.modal { + if folder.id == *id { + column![renaming_folder(*id, name, *is_creating)] + } else { + regular() + } + } else { + regular() + } + .push_maybe(folder.is_expanded.then(|| { + widget::column(folder.children.iter().map(|node| { + self.get_node_rendered(menu, node, NodeMode::InTree(nesting + 1)) + })) + })) + .into() + } + NodeMode::Dragged => drag_tooltip(row![space, view]).into(), + } + } + + fn get_node_instance<'a>( + &'a self, + node: &'a SidebarNode, + selection: &SidebarSelection, + mode: NodeMode, + is_selected: bool, + ) -> Element<'a> { + let State::Launch(menu) = &self.state else { + return widget::Column::new().into(); + }; + let is_drag = matches!(&menu.modal, Some(LaunchModal::SDragging { .. })); + + let text = widget::text(&node.name) + .size(15) + .style(move |t: &LauncherTheme| t.style_text(Color::SecondLight)); + + let view = widget::stack!(underline_maybe( + widget::row![widget::Space::with_width(2), text] + .push_maybe(self.get_running_icon(menu, &node.name)) + .padding([5, 10]) + .width(Length::Fill), + Color::Dark, + !is_selected + )); + match mode { + NodeMode::InTree(_) => node_button( + row![ + mode.get_space(), + view.push_maybe(drag_drop_receiver(menu, selection, node)) + ], + is_drag, + ) + .on_press_maybe((!is_selected).then(|| { + MainMenuMessage::InstanceSelected(InstanceSelection::new( + &node.name, + menu.is_viewing_server, + )) + .into() + })) + .into(), + NodeMode::Dragged => drag_tooltip(row![mode.get_space(), view]).into(), + } + } + + fn node_is_instance_selected(&self, node: &SidebarNode) -> bool { + self.selected_instance + .as_ref() + .is_some_and(|sel| node == sel) + } + + fn node_wrap_in_context_menu<'a>( + &self, + selection: SidebarSelection, + elem: impl Into>, + name: String, + ) -> widget::MouseArea<'a, Message, LauncherTheme> { + widget::mouse_area(elem).on_right_press( + MainMenuMessage::Modal(Some(LaunchModal::SCtxMenu( + Some((selection, name)), + self.window_state.mouse_pos, + ))) + .into(), + ) + } + + pub(super) fn sidebar_drag_tooltip<'a>(&'a self, menu: &'a MenuLaunch) -> Option> { + if let Some(LaunchModal::SDragging { being_dragged, .. }) = &menu.modal { + if let Some(node) = self + .config + .sidebar + .as_ref() + .and_then(|n| n.get_node(being_dragged)) + { + let node = self.get_node_rendered(menu, node, NodeMode::Dragged); + let (x, y) = self.window_state.mouse_pos; + let (winwidth, winheight) = self.window_state.size; + Some(offset( + node, + (x - 200.0).clamp(0.0, winwidth), + (y - 16.0).clamp(0.0, winheight), + )) + } else { + None + } + } else { + None + } + } + + pub(super) fn sidebar_context_menu<'a>(menu: &'a MenuLaunch) -> Option> { + let Some(LaunchModal::SCtxMenu(instance, (x, y))) = &menu.modal else { + return None; + }; + + let instance = instance.as_ref(); + + let new_folder_b = ctx_button(icons::new_s(CTXI_SIZE), "New Folder") + .on_press_with(move || SidebarMessage::NewFolder(instance.map(|n| n.0.clone())).into()); + + let Some((inst, name)) = instance else { + // Right clicked in empty space + return Some(offset(ctxbox(new_folder_b).width(120), *x, *y)); + }; + + Some(offset( + ctxbox( + column![ + new_folder_b, + widget::Space::with_height(5), + widget::horizontal_rule(2), + widget::Space::with_height(5), + ctx_button(icons::file_s(CTXI_SIZE), "Change Icon"), + ctx_button(icons::edit_s(CTXI_SIZE), "Rename").on_press_with( + move || match inst { + SidebarSelection::Instance(name, kind) => { + Message::Multiple(vec![ + MainMenuMessage::InstanceSelected(InstanceSelection::new( + name, + kind.is_server(), + )) + .into(), + MainMenuMessage::ChangeTab(LaunchTab::Edit).into(), + EditInstanceMessage::RenameToggle.into(), + ]) + } + SidebarSelection::Folder(folder_id) => { + MainMenuMessage::Modal(Some(LaunchModal::SRenamingFolder( + *folder_id, + name.clone(), + false, + ))) + .into() + } + } + ), + ] + .push_maybe(if let SidebarSelection::Folder(id) = inst { + Some( + ctx_button(icons::bin_s(CTXI_SIZE), "Delete Folder") + .on_press_with(|| SidebarMessage::DeleteFolder(*id).into()), + ) + } else { + None + }), + ) + .width(150), + *x, + *y, + )) + } +} + +fn renaming_folder( + id: FolderId, + name: &str, + is_creating: bool, +) -> widget::Row<'_, Message, LauncherTheme> { + let text_input = widget::text_input("Enter name...", name) + .id("MenuLaunch:rename_folder") + .on_input(move |s| { + MainMenuMessage::Modal(Some(LaunchModal::SRenamingFolder(id, s, is_creating))).into() + }) + .on_submit(SidebarMessage::FolderRenameConfirm.into()) + .size(13) + .padding([3, 5]); + + let done_button = widget::button(icons::checkmark_s(12)) + .style(|t: &LauncherTheme, s| t.style_button(s, StyleButton::RoundDark)) + .padding([4, 8]) + .on_press(SidebarMessage::FolderRenameConfirm.into()); + + row![text_input, done_button,] + .push_maybe((!is_creating).then(|| { + widget::button(icons::close_s(12)) + .style(|t: &LauncherTheme, s| t.style_button(s, StyleButton::RoundDark)) + .padding([4, 8]) + .on_press(MainMenuMessage::Modal(None).into()) + })) + .align_y(Alignment::Center) + .padding(5) + .spacing(2) +} + +/// The `| | |` lines in indentation. Eg: +/// +/// ```txt +/// SomeFolder/ +/// |- Instance +/// |- Folder/ +/// | |- Instance +/// | |- Instance +/// ``` +fn indent_guide_lines( + nesting: NodeMode, + is_selected: bool, +) -> widget::Row<'static, Message, LauncherTheme> { + match nesting { + NodeMode::InTree(nesting) => widget::row((0..nesting).map(|_| { + row![ + widget::Space::with_width(LEVEL_WIDTH - 2), + widget::vertical_rule(1).style(move |t: &LauncherTheme| t.style_rule( + if is_selected { + Color::Mid + } else { + Color::SecondDark + }, + 1 + )) + ] + .into() + })), + NodeMode::Dragged => widget::Row::new(), + } +} + +fn drag_tooltip<'a>( + node_view: impl Into>, +) -> widget::Container<'a, Message, LauncherTheme> { + widget::container(node_view) + .style(|t: &LauncherTheme| { + t.style_container_bg_semiround([true; 4], Some((Color::ExtraDark, 0.9))) + }) + .width(200) +} + +fn drag_handle(selection: &SidebarSelection) -> widget::MouseArea<'static, Message, LauncherTheme> { + widget::mouse_area( + widget::row![widget::text("=") + .size(20) + .style(|t: &LauncherTheme| t.style_text(Color::ExtraDark))] + .padding([0, 4]) + .align_y(Alignment::Center), + ) + .on_press( + MainMenuMessage::Modal(Some(LaunchModal::SDragging { + being_dragged: selection.clone(), + dragged_to: None, + })) + .into(), + ) +} + +fn node_button<'a>( + inner: impl Into>, + is_drag: bool, +) -> widget::Button<'a, Message, LauncherTheme> { + widget::button(inner) + .style(move |n: &LauncherTheme, status| { + n.style_button( + status, + if is_drag { + StyleButton::FlatExtraDarkDead + } else { + StyleButton::FlatExtraDark + }, + ) + }) + .padding(0) + .width(Length::Fill) +} diff --git a/quantum_launcher/src/message_handler/iced_event.rs b/quantum_launcher/src/message_handler/iced_event.rs index 7a3999660..dbc20ccb6 100644 --- a/quantum_launcher/src/message_handler/iced_event.rs +++ b/quantum_launcher/src/message_handler/iced_event.rs @@ -1,10 +1,11 @@ use crate::message_update::MSG_RESIZE; use crate::state::{ AutoSaveKind, CreateInstanceMessage, LaunchTab, Launcher, LauncherSettingsMessage, - LauncherSettingsTab, MenuCreateInstance, MenuCreateInstanceChoosing, MenuEditMods, - MenuEditPresets, MenuExportInstance, MenuInstallFabric, MenuInstallOptifine, MenuInstallPaper, - MenuLauncherSettings, MenuLauncherUpdate, MenuLoginAlternate, MenuLoginMS, MenuRecommendedMods, - Message, State, + LauncherSettingsTab, MainMenuMessage, ManageModsMessage, MenuCreateInstance, + MenuCreateInstanceChoosing, MenuEditMods, MenuEditPresets, MenuExportInstance, + MenuInstallFabric, MenuInstallOptifine, MenuInstallPaper, MenuLauncherSettings, + MenuLauncherUpdate, MenuLoginAlternate, MenuLoginMS, MenuRecommendedMods, MenuWelcome, Message, + State, }; use iced::{ keyboard::{self, key::Named, Key}, @@ -131,18 +132,14 @@ impl Launcher { // ======== // MANAGE MODS MENU // ======== - ("a", true, _, true, State::EditMods(_)) => { - Message::ManageMods(crate::state::ManageModsMessage::SelectAll) - } + ("a", true, _, true, State::EditMods(_)) => ManageModsMessage::SelectAll.into(), // Ctrl-F search in mods list (with toggling) #[rustfmt::skip] ("f", true, _, _, State::EditMods(MenuEditMods { search: Some(_), .. })) => { - Message::ManageMods(crate::state::ManageModsMessage::SetSearch(None)) + ManageModsMessage::SetSearch(None).into() }, ("f", true, _, _, State::EditMods(_)) => Message::Multiple(vec![ - Message::ManageMods(crate::state::ManageModsMessage::SetSearch(Some( - String::new(), - ))), + ManageModsMessage::SetSearch(Some(String::new())).into(), Message::CoreFocusNext, ]), @@ -156,29 +153,27 @@ impl Launcher { // Misc ("a", true, _, true, State::EditJarMods(_)) => { - Message::ManageJarMods(crate::state::ManageJarModsMessage::SelectAll) + crate::state::ManageJarModsMessage::SelectAll.into() } // ======== // MAIN MENU // ======== - ("n", true, _, _, State::Launch(n)) => { - Message::CreateInstance(CreateInstanceMessage::ScreenOpen { - is_server: n.is_viewing_server, - }) + ("n", true, _, _, State::Launch(n)) => CreateInstanceMessage::ScreenOpen { + is_server: n.is_viewing_server, } + .into(), ("1", ctrl, alt, _, State::Launch(_)) if ctrl | alt => { - Message::MChangeTab(LaunchTab::Buttons) + MainMenuMessage::ChangeTab(LaunchTab::Buttons).into() } ("2", ctrl, alt, _, State::Launch(_)) if ctrl | alt => { - Message::MChangeTab(LaunchTab::Edit) + MainMenuMessage::ChangeTab(LaunchTab::Edit).into() } ("3", ctrl, alt, _, State::Launch(_)) if ctrl | alt => { - Message::MChangeTab(LaunchTab::Log) - } - (",", true, _, _, State::Launch(_)) => { - Message::LauncherSettings(LauncherSettingsMessage::Open) + MainMenuMessage::ChangeTab(LaunchTab::Log).into() } + (",", true, _, _, State::Launch(_)) => LauncherSettingsMessage::Open.into(), + _ => Message::Nothing, }; return Task::done(msg); @@ -219,9 +214,23 @@ impl Launcher { { if let Key::Named(Named::Enter) = key { if modifiers.command() { - return Task::done(Message::CreateInstance(CreateInstanceMessage::Start)); + return Task::done(CreateInstanceMessage::Start.into()); } } + } else if let State::Welcome(menu) = &mut self.state { + if let Key::Named(Named::Enter) = key { + *menu = match menu { + MenuWelcome::P1InitialScreen => MenuWelcome::P2Theme, + MenuWelcome::P2Theme => MenuWelcome::P3Auth, + MenuWelcome::P3Auth => { + return Task::done(Message::MScreenOpen { + message: Some("Install Minecraft by clicking \"+ New\"".to_owned()), + clear_selection: true, + is_server: Some(false), + }); + } + }; + } } self.keys_pressed.insert(key); @@ -416,7 +425,7 @@ impl Launcher { ) } - fn hide_submenu(&mut self) -> bool { + pub fn hide_submenu(&mut self) -> bool { if let State::EditMods(menu) = &mut self.state { if menu.modal.is_some() { menu.modal = None; diff --git a/quantum_launcher/src/message_handler/mod.rs b/quantum_launcher/src/message_handler/mod.rs index fcd79a06f..d69ca85c0 100644 --- a/quantum_launcher/src/message_handler/mod.rs +++ b/quantum_launcher/src/message_handler/mod.rs @@ -252,7 +252,7 @@ impl Launcher { } else { let (a, b) = Task::perform( ql_mod_manager::store::check_for_updates(instance.clone()), - |n| Message::ManageMods(ManageModsMessage::UpdateCheckResult(n.strerr())), + |n| ManageModsMessage::UpdateCheckResult(n.strerr()).into(), ) .abortable(); (a, Some(b.abort_on_drop())) @@ -349,7 +349,7 @@ impl Launcher { let selected_instance = self.selected_instance.clone().unwrap(); Task::perform( ql_mod_manager::store::apply_updates(selected_instance, updates, Some(sender)), - |n| Message::ManageMods(ManageModsMessage::UpdateModsFinished(n.strerr())), + |n| ManageModsMessage::UpdateModsFinished(n.strerr()).into(), ) } else { Task::none() @@ -478,7 +478,7 @@ impl Launcher { vec![path], Some(sender), ), - |n| Message::ManageMods(ManageModsMessage::AddFileDone(n.strerr())), + |n| ManageModsMessage::AddFileDone(n.strerr()).into(), ) } @@ -523,7 +523,7 @@ impl Launcher { instance_name, Some(sender), ), - |n| Message::EditPresets(EditPresetsMessage::LoadComplete(n.strerr())), + |n| EditPresetsMessage::LoadComplete(n.strerr()).into(), ) } Err(err) => { diff --git a/quantum_launcher/src/message_update/accounts.rs b/quantum_launcher/src/message_update/accounts.rs index 175648dba..0d1a64351 100644 --- a/quantum_launcher/src/message_update/accounts.rs +++ b/quantum_launcher/src/message_update/accounts.rs @@ -50,7 +50,7 @@ impl Launcher { self.state = State::ConfirmAction { msg1: format!("log out of your account: {}", self.account_selected), msg2: "You can always log in later".to_owned(), - yes: Message::Account(AccountMessage::LogoutConfirm), + yes: AccountMessage::LogoutConfirm.into(), no: Message::MScreenOpen { message: None, clear_selection: false, @@ -84,12 +84,8 @@ impl Launcher { expires_in, ), |resp| match resp { - Ok(account) => { - Message::Account(AccountMessage::AltLoginResponse(Ok(account))) - } - Err(e) => Message::Account(AccountMessage::LittleSkinDeviceCodeError( - e.to_string(), - )), + Ok(account) => AccountMessage::AltLoginResponse(Ok(account)).into(), + Err(e) => AccountMessage::LittleSkinDeviceCodeError(e.to_string()).into(), }, ); } @@ -149,10 +145,11 @@ impl Launcher { AccountType::Microsoft => { self.state = State::GenericMessage("Loading Login...".to_owned()); return Task::perform(auth::ms::login_1_link(), move |n| { - Message::Account(AccountMessage::Response1 { + AccountMessage::Response1 { r: n.strerr(), is_from_welcome_screen, - }) + } + .into() }); } AccountType::ElyBy | AccountType::LittleSkin => { @@ -213,7 +210,7 @@ impl Launcher { AccountType::ElyBy }, ), - |n| Message::Account(AccountMessage::AltLoginResponse(n.strerr())), + |n| AccountMessage::AltLoginResponse(n.strerr()).into(), ); } } @@ -277,7 +274,7 @@ impl Launcher { account.refresh_token.clone(), Some(sender), ), - |n| Message::Account(AccountMessage::RefreshComplete(n.strerr())), + |n| AccountMessage::RefreshComplete(n.strerr()).into(), ) } AccountType::ElyBy | AccountType::LittleSkin => Task::perform( @@ -286,7 +283,7 @@ impl Launcher { account.refresh_token.clone(), account.account_type, ), - |n| Message::Account(AccountMessage::RefreshComplete(n.strerr())), + |n| AccountMessage::RefreshComplete(n.strerr()).into(), ), } } @@ -305,16 +302,7 @@ impl Launcher { self.accounts_dropdown.insert(0, username.clone()); let config_accounts = self.config.accounts.get_or_insert_with(HashMap::new); - config_accounts.insert( - username.clone(), - ConfigAccount { - uuid: data.uuid.clone(), - skin: None, - account_type: Some(data.account_type.to_string()), - keyring_identifier: Some(data.username.clone()), - username_nice: Some(data.nice_username.clone()), - }, - ); + config_accounts.insert(username.clone(), ConfigAccount::from_account(&data)); self.account_selected.clone_from(&username); self.accounts.insert(username.clone(), data); @@ -326,7 +314,7 @@ impl Launcher { let (sender, receiver) = std::sync::mpsc::channel(); self.state = State::AccountLoginProgress(ProgressBar::with_recv(receiver)); Task::perform(auth::ms::login_3_xbox(token, Some(sender), true), |n| { - Message::Account(AccountMessage::Response3(n.strerr())) + AccountMessage::Response3(n.strerr()).into() }) } @@ -336,7 +324,7 @@ impl Launcher { is_from_welcome_screen: bool, ) -> Task { let (task, handle) = Task::perform(auth::ms::login_2_wait(code.clone()), |n| { - Message::Account(AccountMessage::Response2(n.strerr())) + AccountMessage::Response2(n.strerr()).into() }) .abortable(); diff --git a/quantum_launcher/src/message_update/create_instance.rs b/quantum_launcher/src/message_update/create_instance.rs index e47416adc..3787ac037 100644 --- a/quantum_launcher/src/message_update/create_instance.rs +++ b/quantum_launcher/src/message_update/create_instance.rs @@ -120,9 +120,7 @@ impl Launcher { return Task::perform( ql_packager::import_instance(file.clone(), true, Some(send)), - |n| { - Message::CreateInstance(CreateInstanceMessage::ImportResult(n.strerr())) - }, + |n| CreateInstanceMessage::ImportResult(n.strerr()).into(), ); } } @@ -168,7 +166,7 @@ then go to "Mods->Add File""#, pub fn go_to_create_screen(&mut self, is_server: bool) -> Task { let (task, handle) = Task::perform(ql_instances::list_versions(), |n| { - Message::CreateInstance(CreateInstanceMessage::VersionsLoaded(n.strerr())) + CreateInstanceMessage::VersionsLoaded(n.strerr()).into() }) .abortable(); @@ -250,7 +248,7 @@ then go to "Mods->Add File""#, .strerr() .map(InstanceSelection::Server) }, - |n| Message::CreateInstance(CreateInstanceMessage::End(n)), + |n| CreateInstanceMessage::End(n).into(), ) } else { Task::perform( @@ -260,11 +258,9 @@ then go to "Mods->Add File""#, Some(sender), download_assets, ), - |n| { - Message::CreateInstance(CreateInstanceMessage::End( - n.strerr().map(InstanceSelection::Instance), - )) - }, + |n| CreateInstanceMessage::End( + n.strerr().map(InstanceSelection::Instance), + ).into(), ) }; }); diff --git a/quantum_launcher/src/message_update/edit_instance.rs b/quantum_launcher/src/message_update/edit_instance.rs index 79afb6e88..e54861c82 100644 --- a/quantum_launcher/src/message_update/edit_instance.rs +++ b/quantum_launcher/src/message_update/edit_instance.rs @@ -12,8 +12,8 @@ use crate::{ message_handler::format_memory, state::{ dir_watch, get_entries, CustomJarState, EditInstanceMessage, LaunchTab, Launcher, - MenuCreateInstance, MenuEditInstance, MenuLaunch, Message, ProgressBar, State, - ADD_JAR_NAME, NONE_JAR_NAME, OPEN_FOLDER_JAR_NAME, REMOVE_JAR_NAME, + MainMenuMessage, MenuCreateInstance, MenuEditInstance, MenuLaunch, Message, ProgressBar, + State, ADD_JAR_NAME, NONE_JAR_NAME, OPEN_FOLDER_JAR_NAME, REMOVE_JAR_NAME, }, }; @@ -292,7 +292,7 @@ impl Launcher { if let Err(err) = t { Message::Error(err) } else { - Message::MChangeTab(LaunchTab::Edit) + MainMenuMessage::ChangeTab(LaunchTab::Edit).into() } }, ) @@ -358,10 +358,8 @@ impl Launcher { *menu .config .custom_jar - .get_or_insert_with(CustomJarConfig::default) = CustomJarConfig { - name: file_name.clone(), - autoset_main_class: false, - }; + .get_or_insert_with(CustomJarConfig::default) = + CustomJarConfig::new(file_name.clone()); Task::perform( tokio::fs::copy(path, LAUNCHER_DIR.join("custom_jars").join(file_name)), diff --git a/quantum_launcher/src/message_update/launch.rs b/quantum_launcher/src/message_update/launch.rs new file mode 100644 index 000000000..8e499929d --- /dev/null +++ b/quantum_launcher/src/message_update/launch.rs @@ -0,0 +1,161 @@ +use iced::Task; +use ql_core::InstanceSelection; + +use crate::{ + config::sidebar::SidebarSelection, + message_handler::{SIDEBAR_LIMIT_LEFT, SIDEBAR_LIMIT_RIGHT}, + state::{ + AutoSaveKind, LaunchModal, LaunchTab, Launcher, MainMenuMessage, MenuLaunch, Message, + SidebarMessage, State, + }, +}; + +impl Launcher { + pub fn update_main_menu(&mut self, msg: MainMenuMessage) -> Task { + match msg { + MainMenuMessage::ChangeTab(tab) => { + // UX tweak: dragging instance to tab will open tab for that instance + if let State::Launch(MenuLaunch { modal, .. }) = &mut self.state { + if let Some(LaunchModal::SDragging { + being_dragged: SidebarSelection::Instance(name, kind), + .. + }) = modal + { + if self.selected_instance.is_none() { + self.selected_instance = + Some(InstanceSelection::new(name, kind.is_server())) + } + } + *modal = None; + } + + self.load_edit_instance(Some(tab)); + if let (LaunchTab::Log, Some(instance)) = (tab, self.selected_instance.clone()) { + self.load_logs(instance); + } + } + MainMenuMessage::Modal(modal) => { + if let State::Launch(menu) = &mut self.state { + let t = if let Some(LaunchModal::SRenamingFolder(_, _, _)) = &modal { + iced::widget::text_input::focus("MenuLaunch:rename_folder") + } else { + Task::none() + }; + menu.modal = match (&modal, &menu.modal) { + // Unset if you click on it again + ( + Some(LaunchModal::InstanceOptions), + Some(LaunchModal::InstanceOptions), + ) => None, + _ => modal.clone(), + }; + return t; + } + } + MainMenuMessage::InstanceSelected(inst) => { + self.selected_instance = Some(inst); + return self.on_instance_selected(); + } + MainMenuMessage::UsernameSet(username) => { + self.config.username = username; + self.autosave.remove(&AutoSaveKind::LauncherConfig); + } + } + Task::none() + } + + fn sidebar_update_state(&mut self) { + self.hide_submenu(); + self.config.c_sidebar().fix(); + self.autosave.remove(&AutoSaveKind::LauncherConfig); + } + + pub fn update_sidebar(&mut self, message: SidebarMessage) -> Task { + match message { + SidebarMessage::Resize(ratio) => { + if let State::Launch(menu) = &mut self.state { + // self.autosave.remove(&AutoSaveKind::LauncherConfig); + let window_width = self.window_state.size.0; + let ratio = ratio * window_width; + menu.resize_sidebar( + ratio.clamp(SIDEBAR_LIMIT_LEFT, window_width - SIDEBAR_LIMIT_RIGHT) + / window_width, + ); + } + } + SidebarMessage::Scroll(total) => { + if let State::Launch(MenuLaunch { + sidebar_scrolled: sidebar_height, + .. + }) = &mut self.state + { + *sidebar_height = total; + } + } + SidebarMessage::NewFolder(at_position) => { + let folder_id = self + .config + .c_sidebar() + .new_folder_at(at_position, "New Folder"); + self.sidebar_update_state(); + if let State::Launch(menu) = &mut self.state { + menu.modal = Some(LaunchModal::SRenamingFolder( + folder_id, + "New Folder".to_owned(), + true, + )); + return iced::widget::text_input::focus("MenuLaunch:rename_folder"); + } + } + SidebarMessage::DeleteFolder(folder) => { + self.config.c_sidebar().delete_folder(folder); + self.sidebar_update_state(); + } + SidebarMessage::ToggleFolderVisibility(id) => { + let sidebar = self.config.c_sidebar(); + sidebar.toggle_visibility(id); + self.sidebar_update_state(); + } + SidebarMessage::DragDrop(location) => { + if let State::Launch(MenuLaunch { + modal: Some(LaunchModal::SDragging { being_dragged, .. }), + .. + }) = &mut self.state + { + self.config.c_sidebar().drag_drop(being_dragged, location); + if let Some(sel) = &self.selected_instance { + if being_dragged != sel { + self.selected_instance = None; + } + } + } + self.sidebar_update_state(); + } + SidebarMessage::DragHover { location, entered } => { + if let State::Launch(MenuLaunch { + modal: Some(LaunchModal::SDragging { dragged_to, .. }), + .. + }) = &mut self.state + { + if entered { + *dragged_to = Some(location); + } else if dragged_to.as_ref().is_some_and(|n| *n == location) { + *dragged_to = None; + } + } + } + SidebarMessage::FolderRenameConfirm => { + if let State::Launch(MenuLaunch { + modal: Some(LaunchModal::SRenamingFolder(id, name, _)), + .. + }) = &self.state + { + self.config.c_sidebar().rename_folder(*id, name); + self.hide_submenu(); + } + self.sidebar_update_state(); + } + } + Task::none() + } +} diff --git a/quantum_launcher/src/message_update/manage_mods.rs b/quantum_launcher/src/message_update/manage_mods.rs index 4b17f4ab0..55d6426bb 100644 --- a/quantum_launcher/src/message_update/manage_mods.rs +++ b/quantum_launcher/src/message_update/manage_mods.rs @@ -97,7 +97,7 @@ impl Launcher { .map(delete_file_wrapper) .map(|n| { Task::perform(n, |n| { - Message::ManageMods(ManageModsMessage::LocalDeleteFinished(n)) + ManageModsMessage::LocalDeleteFinished(n).into() }) }), ); @@ -121,7 +121,7 @@ impl Launcher { } } return Task::perform(delete_file_wrapper(mods_dir.join(&name)), |n| { - Message::ManageMods(ManageModsMessage::LocalDeleteFinished(n)) + ManageModsMessage::LocalDeleteFinished(n).into() }); } ManageModsMessage::DeleteFinished(Ok(_)) => { @@ -301,11 +301,11 @@ impl Launcher { let toggle_downloaded = Task::perform( ql_mod_manager::store::toggle_mods(ids_downloaded.clone(), instance_name.clone()), - |n| Message::ManageMods(ManageModsMessage::ToggleFinished(n.strerr())), + |n| ManageModsMessage::ToggleFinished(n.strerr()).into(), ); let toggle_local = Task::perform( ql_mod_manager::store::toggle_mods_local(ids_local, instance_name.clone()), - |n| Message::ManageMods(ManageModsMessage::ToggleFinished(n.strerr())), + |n| ManageModsMessage::ToggleFinished(n.strerr()).into(), ) .chain(MenuEditMods::update_locally_installed_mods( &menu.mods, @@ -345,7 +345,7 @@ impl Launcher { paths.clone(), Some(sender), ), - move |n| Message::ManageMods(ManageModsMessage::AddFileDone(n.strerr())), + move |n| ManageModsMessage::AddFileDone(n.strerr()).into(), ); if delete_file { files_task.chain(Task::perform( @@ -379,7 +379,7 @@ impl Launcher { Task::perform( ql_mod_manager::store::delete_mods(ids, selected_instance), - |n| Message::ManageMods(ManageModsMessage::DeleteFinished(n.strerr())), + |n| ManageModsMessage::DeleteFinished(n.strerr()).into(), ) } diff --git a/quantum_launcher/src/message_update/mod.rs b/quantum_launcher/src/message_update/mod.rs index 573f2f4ea..e3955ec99 100644 --- a/quantum_launcher/src/message_update/mod.rs +++ b/quantum_launcher/src/message_update/mod.rs @@ -13,6 +13,7 @@ use ql_mod_manager::{ mod accounts; mod create_instance; mod edit_instance; +mod launch; mod manage_mods; mod presets; mod recommended; @@ -109,7 +110,7 @@ impl Launcher { ) .await }, - |m| Message::InstallFabric(InstallFabricMessage::End(m.strerr())), + |m| InstallFabricMessage::End(m.strerr()).into(), ); } } @@ -117,7 +118,7 @@ impl Launcher { let instance_name = self.selected_instance.clone().unwrap(); let (task, handle) = Task::perform( loaders::fabric::get_list_of_versions(instance_name, is_quilt), - |m| Message::InstallFabric(InstallFabricMessage::VersionsLoaded(m.strerr())), + |m| InstallFabricMessage::VersionsLoaded(m.strerr()).into(), ) .abortable(); @@ -220,7 +221,7 @@ impl Launcher { let id = ModId::from_pair(&hit.id, backend); return Task::perform(get_description(id), |n| { - Message::InstallMods(InstallModsMessage::LoadData(n.strerr())) + InstallModsMessage::LoadData(n.strerr()).into() }); } } @@ -303,7 +304,7 @@ impl Launcher { .await .map(|not_allowed| (id, not_allowed)) }, - |n| Message::InstallMods(InstallModsMessage::DownloadComplete(n.strerr())), + |n| InstallModsMessage::DownloadComplete(n.strerr()).into(), ); } InstallModsMessage::Uninstall(index) => { @@ -327,7 +328,7 @@ impl Launcher { return Task::perform( ql_mod_manager::store::delete_mods(vec![mod_id], selected_instance), - |n| Message::InstallMods(InstallModsMessage::UninstallComplete(n.strerr())), + |n| InstallModsMessage::UninstallComplete(n.strerr()).into(), ); } InstallModsMessage::UninstallComplete(Ok(ids)) => { @@ -370,8 +371,8 @@ impl Launcher { msg1: format!("install the modpack: {}", hit.title), msg2: "This might take a while, install many files, and use a lot of network..." .to_owned(), - yes: Message::InstallMods(InstallModsMessage::InstallModpack(id)), - no: Message::InstallMods(InstallModsMessage::Open), + yes: InstallModsMessage::InstallModpack(id).into(), + no: InstallModsMessage::Open.into(), }; Task::none() } else { @@ -381,7 +382,7 @@ impl Launcher { .await .map(|not_allowed| (id, not_allowed)) }, - |n| Message::InstallMods(InstallModsMessage::DownloadComplete(n.strerr())), + |n| InstallModsMessage::DownloadComplete(n.strerr()).into(), ) } } @@ -407,7 +408,7 @@ impl Launcher { let url = version.get_url().0; return Task::perform( loaders::optifine::install_b173(selected_instance, url), - |n| Message::InstallOptifine(InstallOptifineMessage::End(n.strerr())), + |n| InstallOptifineMessage::End(n.strerr()).into(), ); } @@ -493,7 +494,7 @@ impl Launcher { Some(j_sender), optifine_unique_version, ), - |n| Message::InstallOptifine(InstallOptifineMessage::End(n.strerr())), + |n| InstallOptifineMessage::End(n.strerr()).into(), ) .chain(Task::perform( async move { @@ -641,10 +642,8 @@ impl Launcher { self.state = State::ConfirmAction { msg1: "delete auto-installed Java files".to_owned(), msg2: "They will get reinstalled automatically as needed".to_owned(), - yes: Message::LauncherSettings(LauncherSettingsMessage::ClearJavaInstallsConfirm), - no: Message::LauncherSettings(LauncherSettingsMessage::ChangeTab( - state::LauncherSettingsTab::Internal, - )), + yes: LauncherSettingsMessage::ClearJavaInstallsConfirm.into(), + no: LauncherSettingsMessage::ChangeTab(state::LauncherSettingsTab::Internal).into(), } } @@ -842,14 +841,14 @@ impl Launcher { menu.is_uploading_mclogs = true; } - let instance = self.instance(); + let instance = self.selected_instance.clone().unwrap(); - if let Some(log) = self.logs.get(instance) { + if let Some(log) = self.logs.get(&instance) { let log_content = log.log.join(""); if !log_content.trim().is_empty() { return Task::perform( - crate::mclog_upload::upload_log(log_content), - |res| Message::GameLog(GameLogMessage::Uploaded(res.strerr())), + crate::mclog_upload::upload_log(log_content, instance), + |res| GameLogMessage::Uploaded(res.strerr()).into(), ); } } diff --git a/quantum_launcher/src/message_update/presets.rs b/quantum_launcher/src/message_update/presets.rs index 6731ac555..ba7795221 100644 --- a/quantum_launcher/src/message_update/presets.rs +++ b/quantum_launcher/src/message_update/presets.rs @@ -62,7 +62,7 @@ impl Launcher { selected_mods, include_config, ), - |n| Message::EditPresets(EditPresetsMessage::BuildYourOwnEnd(n.strerr())), + |n| EditPresetsMessage::BuildYourOwnEnd(n.strerr()).into(), ); }); } diff --git a/quantum_launcher/src/message_update/recommended.rs b/quantum_launcher/src/message_update/recommended.rs index 01179bcfc..72719f256 100644 --- a/quantum_launcher/src/message_update/recommended.rs +++ b/quantum_launcher/src/message_update/recommended.rs @@ -70,9 +70,7 @@ impl Launcher { return Task::perform( ql_mod_manager::store::download_mods_bulk(ids, instance, Some(sender)), - |n| { - Message::RecommendedMods(RecommendedModMessage::DownloadEnd(n.strerr())) - }, + |n| RecommendedModMessage::DownloadEnd(n.strerr()).into(), ); } } @@ -125,7 +123,7 @@ impl Launcher { loader, sender, ), - |n| Message::RecommendedMods(RecommendedModMessage::ModCheckResult(n.strerr())), + |n| RecommendedModMessage::ModCheckResult(n.strerr()).into(), ) } } diff --git a/quantum_launcher/src/message_update/shortcuts.rs b/quantum_launcher/src/message_update/shortcuts.rs index 3fd405a54..4f344611e 100644 --- a/quantum_launcher/src/message_update/shortcuts.rs +++ b/quantum_launcher/src/message_update/shortcuts.rs @@ -61,9 +61,7 @@ impl Launcher { .save_file(), |f| { if let Some(f) = f { - Message::Shortcut(ShortcutMessage::SaveCustomPicked( - f.path().to_owned(), - )) + ShortcutMessage::SaveCustomPicked(f.path().to_owned()).into() } else { Message::Nothing } @@ -74,7 +72,7 @@ impl Launcher { let shortcut = self.shortcut_prepare()?; return Ok(Task::perform( async move { shortcut.generate(&path).await }, - |n| Message::Shortcut(ShortcutMessage::Done(n.strerr())), + |n| ShortcutMessage::Done(n.strerr()).into(), )); } ShortcutMessage::SaveMenu => { @@ -170,7 +168,7 @@ impl Launcher { fn shortcut_menu(shortcut: Shortcut) -> Task { Task::perform( async move { shortcut.generate_to_applications().await }, - |n| Message::Shortcut(ShortcutMessage::Done(n.strerr())), + |n| ShortcutMessage::Done(n.strerr()).into(), ) } @@ -180,6 +178,6 @@ fn shortcut_desktop(shortcut: &Shortcut) -> Result, String> { let s = shortcut.clone(); Ok(Task::perform( async move { s.generate(&desktop).await }, - |n| Message::Shortcut(ShortcutMessage::Done(n.strerr())), + |n| ShortcutMessage::Done(n.strerr()).into(), )) } diff --git a/quantum_launcher/src/mods_store.rs b/quantum_launcher/src/mods_store.rs index df29e2495..6074fc03d 100644 --- a/quantum_launcher/src/mods_store.rs +++ b/quantum_launcher/src/mods_store.rs @@ -61,7 +61,7 @@ impl MenuModsDownload { let backend = self.backend; Task::perform( ql_mod_manager::store::search(query, offset, backend, self.query_type), - |n| Message::InstallMods(InstallModsMessage::SearchResult(n.strerr())), + |n| InstallModsMessage::SearchResult(n.strerr()).into(), ) } } diff --git a/quantum_launcher/src/state/menu.rs b/quantum_launcher/src/state/menu.rs index a199c22c4..fc0be4970 100644 --- a/quantum_launcher/src/state/menu.rs +++ b/quantum_launcher/src/state/menu.rs @@ -4,7 +4,12 @@ use std::{ }; use crate::{ - config::SIDEBAR_WIDTH, message_handler::get_locally_installed_mods, state::NotesMessage, + config::{ + sidebar::{FolderId, SDragLocation, SidebarSelection}, + SIDEBAR_WIDTH, + }, + message_handler::get_locally_installed_mods, + state::NotesMessage, }; use ezshortcut::Shortcut; use frostmark::MarkState; @@ -51,10 +56,17 @@ impl std::fmt::Display for LaunchTab { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone)] pub enum LaunchModal { InstanceOptions, - // More in the future + + // Sidebar + SCtxMenu(Option<(SidebarSelection, String)>, (f32, f32)), + SDragging { + being_dragged: SidebarSelection, + dragged_to: Option, + }, + SRenamingFolder(FolderId, String, bool), } pub enum InstanceNotes { @@ -141,9 +153,20 @@ impl MenuLaunch { pub fn reload_notes(&mut self, instance: InstanceSelection) -> Task { self.notes = None; Task::perform(ql_instances::notes::read(instance), |n| { - Message::Notes(NotesMessage::Loaded(n.strerr())) + NotesMessage::Loaded(n.strerr()).into() }) } + + pub fn get_modal_drag(&self) -> Option<(&SidebarSelection, Option<&SDragLocation>)> { + if let Some(LaunchModal::SDragging { + being_dragged, + dragged_to, + }) = &self.modal + { + return Some((being_dragged, dragged_to.as_ref())); + } + None + } } /// The screen where you can edit an instance/server. @@ -268,7 +291,7 @@ impl MenuEditMods { } Task::perform( get_locally_installed_mods(selected_instance.get_dot_minecraft_path(), blacklist), - |n| Message::ManageMods(ManageModsMessage::LocalIndexLoaded(n)), + |n| ManageModsMessage::LocalIndexLoaded(n).into(), ) } @@ -649,7 +672,7 @@ pub struct MenuShortcut { pub struct MenuLicense { pub selected_tab: LicenseTab, - pub content: iced::widget::text_editor::Content, + pub content: widget::text_editor::Content, } #[derive(Debug, Clone, Copy, PartialEq)] diff --git a/quantum_launcher/src/state/message.rs b/quantum_launcher/src/state/message.rs index 41dbce92b..f14264031 100644 --- a/quantum_launcher/src/state/message.rs +++ b/quantum_launcher/src/state/message.rs @@ -1,6 +1,7 @@ use std::{collections::HashSet, path::PathBuf, process::ExitStatus}; use crate::{ + config::sidebar::{FolderId, SDragLocation, SidebarSelection}, message_handler::ForgeKind, state::{LaunchModal, MenuEditModsModal}, stylesheet::styles::{LauncherThemeColor, LauncherThemeLightness}, @@ -41,7 +42,7 @@ pub enum InstallFabricMessage { pub enum InstallPaperMessage { End(Res), VersionSelected(PaperVersion), - VersionsLoaded(Res>), + VersionsLoaded(Res>), ButtonClicked, ScreenOpen, } @@ -353,6 +354,30 @@ pub enum GameLogMessage { Uploaded(Res), } +#[derive(Debug, Clone)] +pub enum SidebarMessage { + Resize(f32), + Scroll(f32), + FolderRenameConfirm, + + NewFolder(Option), + DeleteFolder(FolderId), + ToggleFolderVisibility(FolderId), + DragDrop(Option), + DragHover { + location: SDragLocation, + entered: bool, + }, +} + +#[derive(Debug, Clone)] +pub enum MainMenuMessage { + ChangeTab(LaunchTab), + Modal(Option), + InstanceSelected(InstanceSelection), + UsernameSet(String), +} + #[derive(Debug, Clone)] pub enum ShortcutMessage { Open, @@ -398,24 +423,18 @@ pub enum Message { EditPresets(EditPresetsMessage), ExportMods(ExportModsMessage), RecommendedMods(RecommendedModMessage), - - LaunchInstanceSelected(InstanceSelection), - LaunchUsernameSet(String), - LaunchStart, - LaunchEnd(Res), - LaunchKill, - LaunchGameExited(Res<(ExitStatus, InstanceSelection, Option)>), + MainMenu(MainMenuMessage), + SidebarMessage(SidebarMessage), MScreenOpen { message: Option, clear_selection: bool, is_server: Option, }, - MChangeTab(LaunchTab), - MModal(Option), - - MSidebarResize(f32), - MSidebarScroll(f32), + LaunchStart, + LaunchEnd(Res), + LaunchKill, + LaunchGameExited(Res<(ExitStatus, InstanceSelection, Option)>), DeleteInstanceMenu, DeleteInstance, @@ -449,6 +468,7 @@ pub enum Message { CoreCleanComplete(Res), CoreFocusNext, CoreTryQuit, + CoreHideModal, CoreImageDownloaded(Res), @@ -469,3 +489,33 @@ pub enum Message { LicenseChangeTab(LicenseTab), LicenseAction(widget::text_editor::Action), } + +macro_rules! from_m { + ($field:ident, $t:ty) => { + impl From<$t> for Message { + fn from(value: $t) -> Self { + Message::$field(value) + } + } + }; +} + +from_m!(MainMenu, MainMenuMessage); +from_m!(SidebarMessage, SidebarMessage); +from_m!(ManageMods, ManageModsMessage); +from_m!(ManageJarMods, ManageJarModsMessage); +from_m!(InstallMods, InstallModsMessage); +from_m!(InstallOptifine, InstallOptifineMessage); +from_m!(InstallFabric, InstallFabricMessage); +from_m!(EditPresets, EditPresetsMessage); +from_m!(ExportMods, ExportModsMessage); +from_m!(RecommendedMods, RecommendedModMessage); + +from_m!(Account, AccountMessage); +from_m!(CreateInstance, CreateInstanceMessage); +from_m!(EditInstance, EditInstanceMessage); +from_m!(LauncherSettings, LauncherSettingsMessage); +from_m!(Notes, NotesMessage); +from_m!(GameLog, GameLogMessage); +from_m!(Window, WindowMessage); +from_m!(Shortcut, ShortcutMessage); diff --git a/quantum_launcher/src/state/mod.rs b/quantum_launcher/src/state/mod.rs index a4e578dd3..591f2a1e8 100644 --- a/quantum_launcher/src/state/mod.rs +++ b/quantum_launcher/src/state/mod.rs @@ -106,7 +106,7 @@ pub struct CustomJarState { impl CustomJarState { pub fn load() -> Task { Task::perform(load_custom_jars(), |n| { - Message::EditInstance(EditInstanceMessage::CustomJarLoaded(n.strerr())) + EditInstanceMessage::CustomJarLoaded(n.strerr()).into() }) } } diff --git a/quantum_launcher/src/stylesheet/styles.rs b/quantum_launcher/src/stylesheet/styles.rs index ce2716a9b..a677fe389 100644 --- a/quantum_launcher/src/stylesheet/styles.rs +++ b/quantum_launcher/src/stylesheet/styles.rs @@ -551,7 +551,9 @@ impl LauncherTheme { | StyleButton::RoundDark | StyleButton::SemiDark(_) | StyleButton::SemiDarkBorder(_) => Color::Dark, - StyleButton::FlatExtraDark | StyleButton::SemiExtraDark(_) => Color::ExtraDark, + StyleButton::FlatExtraDark + | StyleButton::SemiExtraDark(_) + | StyleButton::FlatExtraDarkDead => Color::ExtraDark, }; widget::button::Style { background: Some(self.get_bg(color)), @@ -582,6 +584,7 @@ impl LauncherTheme { | StyleButton::SemiDark(_) | StyleButton::SemiDarkBorder(_) => Color::Mid, StyleButton::FlatExtraDark | StyleButton::SemiExtraDark(_) => Color::Dark, + StyleButton::FlatExtraDarkDead => Color::ExtraDark, }; widget::button::Style { background: Some(self.get_bg(color)), @@ -601,11 +604,15 @@ impl LauncherTheme { }, widget::button::Status::Disabled => { let color = match style { - StyleButton::Flat | StyleButton::Round | StyleButton::RoundDark => Color::Dark, - StyleButton::FlatDark - | StyleButton::SemiDark(_) + StyleButton::Flat + | StyleButton::Round + | StyleButton::RoundDark + | StyleButton::FlatDark => Color::Dark, + StyleButton::SemiDark(_) | StyleButton::SemiDarkBorder(_) - | StyleButton::SemiExtraDark(_) => Color::ExtraDark, + | StyleButton::SemiExtraDark(_) + | StyleButton::FlatExtraDarkDead => Color::ExtraDark, + // Selected button StyleButton::FlatExtraDark => Color::SecondDark, }; widget::button::Style { @@ -613,7 +620,7 @@ impl LauncherTheme { text_color: self.get(Color::ExtraDark), border: if let StyleButton::Round = style { let (palette, color) = self.get_base(Color::SecondDark); - iced::Border { + Border { color: palette.get(color), width: 0.5, radius: BORDER_RADIUS.into(), diff --git a/quantum_launcher/src/stylesheet/widgets.rs b/quantum_launcher/src/stylesheet/widgets.rs index ec1b5b906..9e1a261a5 100644 --- a/quantum_launcher/src/stylesheet/widgets.rs +++ b/quantum_launcher/src/stylesheet/widgets.rs @@ -20,6 +20,7 @@ pub enum StyleButton { Flat, FlatDark, FlatExtraDark, + FlatExtraDarkDead, /// top right, top left, /// bottom right, bottom left SemiDark([bool; 4]), @@ -41,6 +42,7 @@ impl IsFlat for StyleButton { StyleButton::Flat | StyleButton::FlatDark | StyleButton::FlatExtraDark + | StyleButton::FlatExtraDarkDead | StyleButton::SemiDark(_) | StyleButton::SemiDarkBorder(_) | Self::SemiExtraDark(_) => true, @@ -53,7 +55,8 @@ impl IsFlat for StyleButton { | StyleButton::RoundDark | StyleButton::Flat | StyleButton::FlatDark - | StyleButton::FlatExtraDark => [false; 4], + | StyleButton::FlatExtraDark + | StyleButton::FlatExtraDarkDead => [false; 4], StyleButton::SemiDark(n) | StyleButton::SemiDarkBorder(n) | Self::SemiExtraDark(n) => { *n } @@ -281,23 +284,23 @@ impl widget::text_input::Catalog for LauncherTheme { icon: self.get(Color::Light), placeholder: self.get(Color::Mid), value: self.get(Color::White), - selection: self.get(Color::Light), + selection: self.get(Color::Mid), }, widget::text_input::Status::Hovered => widget::text_input::Style { - background: self.get_bg(Color::Dark), + background: self.get_bg(Color::ExtraDark), border: self.get_border(Color::Mid), icon: self.get(Color::Light), placeholder: self.get(Color::Mid), value: self.get(Color::White), - selection: self.get(Color::Light), + selection: self.get(Color::Mid), }, widget::text_input::Status::Focused => widget::text_input::Style { background: self.get_bg(Color::Dark), - border: self.get_border(Color::SecondLight), + border: self.get_border(Color::Mid), icon: self.get(Color::Light), placeholder: self.get(Color::Mid), value: self.get(Color::White), - selection: self.get(Color::Light), + selection: self.get(Color::Mid), }, widget::text_input::Status::Disabled => widget::text_input::Style { background: self.get_bg(Color::ExtraDark), @@ -305,7 +308,7 @@ impl widget::text_input::Catalog for LauncherTheme { icon: self.get(Color::Light), placeholder: self.get(Color::Mid), value: self.get(Color::White), - selection: self.get(Color::Light), + selection: self.get(Color::Mid), }, } } diff --git a/quantum_launcher/src/tick.rs b/quantum_launcher/src/tick.rs index 0bd23f919..82b3d649b 100644 --- a/quantum_launcher/src/tick.rs +++ b/quantum_launcher/src/tick.rs @@ -4,7 +4,6 @@ use std::{ sync::Arc, }; -use chrono::Datelike; use iced::{widget::text_editor, Task}; use ql_core::{ constants::OS_NAME, json::InstanceConfigJson, InstanceSelection, IntoIoError, IntoJsonError, @@ -110,7 +109,7 @@ impl Launcher { let selected_instance = self.selected_instance.clone().unwrap(); return Task::perform( async move { (jarmods.save(&selected_instance).await.strerr(), jarmods) }, - |n| Message::ManageJarMods(ManageJarModsMessage::AutosaveFinished(n)), + |n| ManageJarModsMessage::AutosaveFinished(n).into(), ); } } @@ -188,7 +187,7 @@ impl Launcher { return; }; let cmd = Task::perform(Launcher::save_config(instance, config), |n| { - Message::EditInstance(EditInstanceMessage::ConfigSaved(n.strerr())) + EditInstanceMessage::ConfigSaved(n.strerr()).into() }); commands.push(cmd); } @@ -200,32 +199,24 @@ impl Launcher { log_state: &mut Option, ) { while let Some(message) = process.receiver.as_ref().and_then(|n| n.try_recv().ok()) { - let message = message.to_string().replace('\t', &" ".repeat(8)); + let message = message.to_string(); - let log_start = || { - vec![ - format!( - "{} ({})\n", + logs.entry(instance.clone()) + .or_insert_with(|| { + let log_start = format!( + "[00:00:00] [launcher/INFO] {} (OS: {OS_NAME})\n", if instance.is_server() { "Starting Minecraft server" } else { "Launching Minecraft" }, - Self::get_current_date_formatted() - ), - format!("OS: {OS_NAME}\n"), - ] - }; + ); - logs.entry(instance.clone()) - .or_insert_with(|| { - let mut text = log_start().join("\n"); - text.push('\n'); *log_state = Some(LogState { - content: text_editor::Content::with_text(&text), + content: text_editor::Content::with_text(&log_start), }); InstanceLog { - log: log_start(), + log: vec![log_start], has_crashed: false, command: String::new(), } @@ -237,19 +228,6 @@ impl Launcher { } } - fn get_current_date_formatted() -> String { - // Get the current date and time in UTC - let now = chrono::Local::now(); - - // Extract the day, month, and year - let day = now.day(); - let month = now.format("%B").to_string(); // Full month name (e.g., "September") - let year = now.year(); - - // Return the formatted string - format!("{day} {month} {year}") - } - async fn save_config( instance: InstanceSelection, config: InstanceConfigJson, @@ -272,7 +250,7 @@ impl MenuModsDownload { pub fn tick(selected_instance: InstanceSelection) -> Task { Task::perform( async move { ModIndex::load(&selected_instance).await }, - |n| Message::InstallMods(InstallModsMessage::IndexUpdated(n.strerr())), + |n| InstallModsMessage::IndexUpdated(n.strerr()).into(), ) } } @@ -356,10 +334,11 @@ impl MenuCreateInstance { } } -fn update_log_render_state(log_state: Option<&mut LogState>, message: String) { +fn update_log_render_state(log_state: Option<&mut LogState>, mut message: String) { if let Some(state) = log_state { use iced::widget::text_editor::{Action, Edit, Motion}; // TODO: preserve selection + message = message.replace('\t', " "); let content = &mut state.content; content.perform(Action::Move(Motion::DocumentEnd)); content.perform(Action::Edit(Edit::Paste(Arc::new(message)))); diff --git a/quantum_launcher/src/update.rs b/quantum_launcher/src/update.rs index b591c515c..d7d63b31f 100644 --- a/quantum_launcher/src/update.rs +++ b/quantum_launcher/src/update.rs @@ -7,7 +7,6 @@ use tokio::io::AsyncWriteExt; use owo_colors::OwoColorize; use crate::{ - message_handler::{SIDEBAR_LIMIT_LEFT, SIDEBAR_LIMIT_RIGHT}, state::{ AutoSaveKind, CustomJarState, GameProcess, LaunchTab, Launcher, LauncherSettingsMessage, ManageModsMessage, MenuExportInstance, MenuLaunch, MenuLicense, MenuWelcome, Message, @@ -61,6 +60,8 @@ impl Launcher { self.state = State::Welcome(MenuWelcome::P3Auth); } + Message::MainMenu(msg) => return self.update_main_menu(msg), + Message::SidebarMessage(msg) => return self.update_sidebar(msg), Message::Account(msg) => return self.update_account(msg), Message::ManageMods(msg) => return self.update_manage_mods(msg), Message::ExportMods(msg) => return self.update_export_mods(msg), @@ -74,18 +75,10 @@ impl Launcher { Err(e) => self.set_error(e), }, - Message::LaunchInstanceSelected(inst) => { - self.selected_instance = Some(inst); - return self.on_instance_selected(); - } Message::LauncherSettings(msg) => return self.update_launcher_settings(msg), Message::InstallOptifine(msg) => return self.update_install_optifine(msg), Message::InstallPaper(msg) => return self.update_install_paper(msg), - Message::LaunchUsernameSet(username) => { - self.config.username = username; - self.autosave.remove(&AutoSaveKind::LauncherConfig); - } Message::LaunchStart => return self.launch_start(), Message::LaunchEnd(result) => return self.finish_launching(result), Message::CreateInstance(message) => return self.update_create_instance(message), @@ -157,7 +150,7 @@ impl Launcher { // tasks.push( // iced::window::get_latest() // .and_then(iced::window::get_maximized) - // .map(|m| Message::Window(WindowMessage::IsMaximized(m))), + // .map(|m| WindowMessage::IsMaximized(m).into()), // ); let custom_jars_changed = self @@ -258,19 +251,13 @@ impl Launcher { Message::ShowScreen("Uninstalling...".to_owned()), (*msg).clone(), ]), - no: Message::ManageMods(ManageModsMessage::ScreenOpenWithoutUpdate), + no: ManageModsMessage::ScreenOpenWithoutUpdate.into(), } } Message::ShowScreen(msg) => { self.state = State::GenericMessage(msg); } Message::CoreEvent(event, status) => return self.iced_event(event, status), - Message::MChangeTab(launch_tab_id) => { - self.load_edit_instance(Some(launch_tab_id)); - if let LaunchTab::Log = launch_tab_id { - self.load_logs(self.instance().clone()); - } - } Message::CoreLogToggle => { self.is_log_open = !self.is_log_open; } @@ -283,26 +270,6 @@ impl Launcher { Message::CoreLogScrollAbsolute(lines) => { self.log_scroll = lines; } - Message::MSidebarResize(ratio) => { - if let State::Launch(menu) = &mut self.state { - // self.autosave.remove(&AutoSaveKind::LauncherConfig); - let window_width = self.window_state.size.0; - let ratio = ratio * window_width; - menu.resize_sidebar( - ratio.clamp(SIDEBAR_LIMIT_LEFT, window_width - SIDEBAR_LIMIT_RIGHT) - / window_width, - ); - } - } - Message::MSidebarScroll(total) => { - if let State::Launch(MenuLaunch { - sidebar_scrolled: sidebar_height, - .. - }) = &mut self.state - { - *sidebar_height = total; - } - } Message::ExportInstanceOpen => { self.state = State::ExportInstance(MenuExportInstance { @@ -426,20 +393,17 @@ impl Launcher { Message::CoreFocusNext => { return iced::widget::focus_next(); } - Message::MModal(m) => { - if let State::Launch(menu) = &mut self.state { - menu.modal = match (m, menu.modal) { - // Unset if you click on it again - (Some(m), Some(n)) if m == n => None, - (m, _) => m, - } - } + Message::CoreHideModal => { + self.hide_submenu(); } } Task::none() } fn core_list_loaded(&mut self, list: Vec, is_server: bool) { + self.config.update_sidebar(&list, is_server); + self.autosave.remove(&AutoSaveKind::LauncherConfig); + let persistent = self.config.c_persistent(); if is_server { if let Some(n) = &persistent.selected_server { @@ -470,9 +434,8 @@ impl Launcher { if is_auto_theme && interval { Task::perform(tokio::task::spawn_blocking(dark_light::detect), |n| { - Message::LauncherSettings(LauncherSettingsMessage::LoadedSystemTheme( - n.strerr().and_then(|n| n.strerr()), - )) + LauncherSettingsMessage::LoadedSystemTheme(n.strerr().and_then(|n| n.strerr())) + .into() }) } else { Task::none() diff --git a/quantum_launcher/src/view.rs b/quantum_launcher/src/view.rs index 0b43630c7..1078ee7dd 100644 --- a/quantum_launcher/src/view.rs +++ b/quantum_launcher/src/view.rs @@ -167,7 +167,7 @@ impl Launcher { State::RecommendedMods(menu) => menu.view(), }; - if let State::Launch(_) = &self.state { + widget::mouse_area(if let State::Launch(_) = &self.state { menu } else { let round = !self.config.uses_system_decorations(); @@ -195,7 +195,9 @@ impl Launcher { .height(Length::Fill), ) .into() - } + }) + .on_press(Message::CoreHideModal) + .into() } pub fn view_window_decorations(&self) -> widget::Row<'_, Message, LauncherTheme> { @@ -229,18 +231,15 @@ impl Launcher { ); let wcls_space = widget::mouse_area(widget::column![].height(Length::Fill).width(6.5)) - .on_press(Message::Window(WindowMessage::ClickClose)); - let wcls = win_button( - icons::close_s(ICON_SIZE), - Message::Window(WindowMessage::ClickClose), - ); + .on_press(WindowMessage::ClickClose.into()); + let wcls = win_button(icons::close_s(ICON_SIZE), WindowMessage::ClickClose.into()); let wmax = win_button( icons::maximize_s(ICON_SIZE), - Message::Window(WindowMessage::ClickMaximize), + WindowMessage::ClickMaximize.into(), ); let wmin = win_button( icons::minimize_s(ICON_SIZE), - Message::Window(WindowMessage::ClickMinimize), + WindowMessage::ClickMinimize.into(), ); if right { widget::Row::new() @@ -268,7 +267,7 @@ impl Launcher { ) -> widget::MouseArea<'static, Message, LauncherTheme> { widget::mouse_area(widget::column![].width(w).height(h)) .interaction(i) - .on_press(Message::Window(WindowMessage::Resized(d))) + .on_press(WindowMessage::Resized(d).into()) } widget::stack!(