diff --git a/Cargo.lock b/Cargo.lock index b5c33126..4a987b86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -930,6 +930,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "concat-idents" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f76990911f2267d837d9d0ad060aa63aaad170af40904b29461734c339030d4d" +dependencies = [ + "quote", + "syn 2.0.48", +] + [[package]] name = "concurrent-queue" version = "2.4.0" @@ -1419,11 +1429,22 @@ dependencies = [ "log", "raw-window-handle 0.5.2", "smithay-clipboard", - "web-time", + "web-time 0.2.3", "webbrowser", "winit", ] +[[package]] +name = "egui_animation" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9ccebd83685921eceb3a89df08ce02bbee7c9f7b79472f1a45d12332188a174" +dependencies = [ + "egui", + "hello_egui_utils", + "simple-easing", +] + [[package]] name = "egui_commonmark" version = "0.11.0" @@ -1435,6 +1456,18 @@ dependencies = [ "pulldown-cmark", ] +[[package]] +name = "egui_dnd" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ff7872d7974d88b5aa5c55d76d4e43b1628005974cb45758402224526233b2" +dependencies = [ + "egui", + "egui_animation", + "simple-easing", + "web-time 1.1.0", +] + [[package]] name = "egui_extras" version = "0.25.0" @@ -2143,6 +2176,16 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hello_egui_utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7831675138f982346346b0473a4b4e4fe5794a847ef546b0d794a2e4d8c1a364" +dependencies = [ + "concat-idents", + "egui", +] + [[package]] name = "hermit-abi" version = "0.3.3" @@ -2722,6 +2765,7 @@ dependencies = [ "eframe", "egui", "egui_commonmark", + "egui_dnd", "fs-err", "futures", "hex", @@ -2730,6 +2774,7 @@ dependencies = [ "include_dir", "indexmap 2.2.6", "inventory", + "itertools 0.12.0", "mint_lib", "mockall", "modio", @@ -4252,6 +4297,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simple-easing" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "832ddd7df0d98d6fd93b973c330b7c8e0742d5cb8f1afc7dea89dba4d2531aa1" + [[package]] name = "slab" version = "0.4.9" @@ -5428,6 +5479,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webbrowser" version = "0.8.12" @@ -5798,7 +5859,7 @@ dependencies = [ "wayland-protocols", "wayland-protocols-plasma", "web-sys", - "web-time", + "web-time 0.2.3", "windows-sys 0.48.0", "x11-dl", "x11rb 0.13.0", diff --git a/Cargo.toml b/Cargo.toml index b62b0c2b..84781b0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,6 +94,8 @@ postcard.workspace = true fs-err.workspace = true snafu.workspace = true strum = { version = "0.26", features = ["derive"] } +itertools.workspace = true +egui_dnd = "0.6.0" [target.'cfg(target_env = "msvc")'.dependencies] hook = { path = "hook", artifact = "cdylib", optional = true, target = "x86_64-pc-windows-msvc"} diff --git a/src/gui/message.rs b/src/gui/message.rs index 5c2edc1a..78ab7f74 100644 --- a/src/gui/message.rs +++ b/src/gui/message.rs @@ -154,7 +154,6 @@ impl ResolveMods { } } app.resolve_mod.clear(); - app.sort_mods(); app.state.mod_data.save().unwrap(); app.last_action = Some(LastAction::success( "mods successfully resolved".to_string(), diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 21df8ec8..c58ee319 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -24,6 +24,7 @@ use eframe::{ epaint::{text::LayoutJob, Color32, Stroke}, }; use egui_commonmark::{CommonMarkCache, CommonMarkViewer}; +use itertools::Itertools as _; use mint_lib::error::ResultExt as _; use mint_lib::mod_info::{ModioTags, RequiredStatus}; use mint_lib::update::GitHubRelease; @@ -94,7 +95,7 @@ impl GuiTheme { } } -#[derive(PartialEq, Debug, EnumIter, Clone, serde::Serialize, serde::Deserialize)] +#[derive(PartialEq, Debug, EnumIter, Clone, Copy, serde::Serialize, serde::Deserialize)] pub enum SortBy { Enabled, Name, @@ -253,6 +254,8 @@ impl App { } fn ui_profile(&mut self, ui: &mut Ui, profile: &str) { + let sorting_config = self.get_sorting_config(); + let ModData { profiles, groups, .. } = self.state.mod_data.deref_mut().deref_mut(); @@ -270,7 +273,7 @@ impl App { add_deps: None, }; - let mut ui_profile = |ui: &mut Ui, profile: &mut ModProfile| { + let ui_profile = |ui: &mut Ui, profile: &mut ModProfile| { let enabled_specs = profile .mods .iter() @@ -704,22 +707,62 @@ impl App { } }; - profile - .mods - .iter_mut() - .enumerate() - .for_each(|(index, item)| { - let mut frame = egui::Frame::none(); - if index % 2 == 1 { - frame.fill = ui.visuals().faint_bg_color - } - frame.show(ui, |ui| { - ui.horizontal(|ui| { - ui_item(&mut ctx, ui, item, index); + if let Some(sorting_config) = sorting_config { + let comp = sort_mods(sorting_config); + profile + .mods + .iter_mut() + .map(|m| { + // fetch ModInfo up front because doing it in the comparator is slow + let ModOrGroup::Individual(mc) = m else { + unimplemented!("Item is not Individual \n{:?}", m); + }; + let info = self.state.store.get_mod_info(&mc.spec); + (m, info) + }) + .enumerate() + .sorted_by(|a, b| comp((a.1 .0, a.1 .1.as_ref()), (b.1 .0, b.1 .1.as_ref()))) + .enumerate() + .for_each(|(visual_index, (store_index, item))| { + let mut frame = egui::Frame::none(); + if visual_index % 2 == 1 { + frame.fill = ui.visuals().faint_bg_color + } + frame.show(ui, |ui| { + ui.horizontal(|ui| { + ui_item(&mut ctx, ui, item.0, store_index); + }); }); }); - }); + } else { + let res = egui_dnd::dnd(ui, ui.id()) + .with_mouse_config(egui_dnd::DragDropConfig::mouse()) + .show( + profile.mods.iter_mut().enumerate(), + |ui, (_index, item), handle, state| { + let mut frame = egui::Frame::none(); + if state.dragged { + frame.fill = ui.visuals().extreme_bg_color + } else if state.index % 2 == 1 { + frame.fill = ui.visuals().faint_bg_color + } + frame.show(ui, |ui| { + ui.horizontal(|ui| { + handle.ui(ui, |ui| { + ui.label(" ☰ "); + }); + ui_item(&mut ctx, ui, item, state.index); + }); + }); + }, + ); + + if res.final_update().is_some() { + res.update_vec(&mut profile.mods); + ctx.needs_save = true; + } + } if let Some(remove) = ctx.btn_remove { profile.mods.remove(remove); ctx.needs_save = true; @@ -1521,104 +1564,72 @@ impl App { } } - fn get_sorting_config(&mut self) -> (SortBy, bool) { - let default_config = SortingConfig::default(); - let sorting_config = match self.state.config.sorting_config.as_ref() { - Some(config) => config.clone(), - None => default_config, - }; - (sorting_config.sort_category, sorting_config.is_ascending) + fn get_sorting_config(&self) -> Option { + self.state.config.sorting_config.clone() } - fn update_sorting_config(&mut self, sort_category: SortBy, is_ascending: bool) { - self.state.config.sorting_config = Some(SortingConfig { + fn update_sorting_config(&mut self, sort_category: Option, is_ascending: bool) { + self.state.config.sorting_config = sort_category.map(|sort_category| SortingConfig { sort_category, is_ascending, }); self.state.config.save().unwrap(); } +} - fn sort_mods(&mut self) { - let (sort_category, is_ascending) = self.get_sorting_config(); - - let profile = self.state.mod_data.active_profile.clone(); - let ModData { profiles, .. } = self.state.mod_data.deref_mut().deref_mut(); - - if let Some(active_profile) = profiles.get_mut(&profile) { - active_profile.mods.sort_by(|a, b| { - if matches!(a, ModOrGroup::Group { .. }) || matches!(b, ModOrGroup::Group { .. }) { - unimplemented!("Groups in sorting not implemented"); - } - - let ModOrGroup::Individual(mc_a) = a else { - debug!("Item is not Individual \n{:?}", a); - return Ordering::Equal; - }; - let ModOrGroup::Individual(mc_b) = b else { - debug!("Item is not Individual \n{:?}", b); - return Ordering::Equal; - }; +fn sort_mods( + config: SortingConfig, +) -> impl Fn((&ModOrGroup, Option<&ModInfo>), (&ModOrGroup, Option<&ModInfo>)) -> Ordering { + move |(a, info_a), (b, info_b)| { + if matches!(a, ModOrGroup::Group { .. }) || matches!(b, ModOrGroup::Group { .. }) { + unimplemented!("Groups in sorting not implemented"); + } - let Some(info_a) = self.state.store.get_mod_info(&mc_a.spec) else { - debug!("Failed to get mod info for \n{:?}", mc_a); - return Ordering::Equal; - }; - let Some(info_b) = self.state.store.get_mod_info(&mc_b.spec) else { - debug!("Failed to get mod info for \n{:?}", mc_b); - return Ordering::Equal; - }; + let ModOrGroup::Individual(mc_a) = a else { + debug!("Item is not Individual \n{:?}", a); + return Ordering::Equal; + }; + let ModOrGroup::Individual(mc_b) = b else { + debug!("Item is not Individual \n{:?}", b); + return Ordering::Equal; + }; - let handle_missing_modio_tags = || -> Option { - let mut order = if info_a.modio_tags.is_none() && info_b.modio_tags.is_none() { - Ordering::Equal - } else if info_a.modio_tags.is_none() { - Ordering::Less - } else if info_b.modio_tags.is_none() { - Ordering::Greater - } else { - return None; - }; - if is_ascending { - order = order.reverse(); - } - Some(order) - }; + fn map_cmp(a: &V, b: &V, map: F) -> Ordering + where + M: Ord, + F: Fn(&V) -> M, + { + map(a).cmp(&map(b)) + } - let name_order = info_a.name.to_lowercase().cmp(&info_b.name.to_lowercase()); - let approval_order = handle_missing_modio_tags().unwrap_or_else(|| { - info_a - .modio_tags - .clone() - .unwrap() - .approval_status - .cmp(&info_b.modio_tags.clone().unwrap().approval_status) - }); - let required_order = handle_missing_modio_tags().unwrap_or_else(|| { - info_a - .modio_tags - .clone() - .unwrap() - .required_status - .cmp(&info_b.modio_tags.clone().unwrap().required_status) - }); - let mut order = match sort_category { - SortBy::Enabled => mc_b.enabled.cmp(&mc_a.enabled), - SortBy::Name => name_order, - SortBy::Priority => mc_a.priority.cmp(&mc_b.priority), - SortBy::Provider => info_b.provider.cmp(info_a.provider), - SortBy::RequiredStatus => required_order, - SortBy::ApprovalCategory => approval_order, - }; + let name_order = map_cmp(&(mc_a, info_a), &(mc_b, info_b), |(mc, info)| { + (info.map(|i| i.name.to_lowercase()), &mc.spec.url) + }); + let provider_order = map_cmp(&info_a, &info_b, |info| info.map(|i| i.provider)); + let approval_order = map_cmp(&info_a, &info_b, |info| { + info.and_then(|i| i.modio_tags.as_ref()) + .map(|t| t.approval_status) + }); + let required_order = map_cmp(&info_a, &info_b, |info| { + info.and_then(|i| i.modio_tags.as_ref()) + .map(|t| std::cmp::Reverse(t.required_status)) + }); + let mut order = match config.sort_category { + SortBy::Enabled => mc_b.enabled.cmp(&mc_a.enabled), + SortBy::Name => name_order, + SortBy::Priority => mc_a.priority.cmp(&mc_b.priority), + SortBy::Provider => provider_order, + SortBy::RequiredStatus => required_order, + SortBy::ApprovalCategory => approval_order, + }; - if !is_ascending { - order = order.reverse(); - } - if sort_category != SortBy::Name { - order = order.then(name_order); - } - order - }); + if config.is_ascending { + order = order.reverse(); + } + if config.sort_category != SortBy::Name { + order = order.then(name_order); } + order } } @@ -1945,7 +1956,6 @@ impl eframe::App for App { self.state.mod_data.deref_mut().deref_mut(), Some(buttons), ) { - self.sort_mods(); self.state.mod_data.save().unwrap(); } @@ -1987,28 +1997,30 @@ impl eframe::App for App { ui.horizontal(|ui| { ui.label("Sort by: "); - let (mut sort_category, mut is_ascending) = self.get_sorting_config(); + let (mut sort_category, mut is_ascending) = self + .get_sorting_config() + .map(|c| (Some(c.sort_category), c.is_ascending)) + .unwrap_or_default(); + let mut clicked = ui.radio_value(&mut sort_category, None, "Manual").clicked(); for category in SortBy::iter() { let mut radio_label = category.as_str().to_owned(); - if sort_category == category { - if is_ascending { - radio_label.push_str(" ⏶"); - } else { - radio_label.push_str(" ⏷"); - } + if sort_category == Some(category) { + radio_label.push_str(if is_ascending { " ⏶" } else { " ⏷" }); } - let resp = ui.radio_value(&mut sort_category, category, radio_label); + let resp = ui.radio_value(&mut sort_category, Some(category), radio_label); if resp.clicked() { + clicked = true; if resp.changed() { is_ascending = true; } else { is_ascending = !is_ascending; } - self.update_sorting_config(sort_category.clone(), is_ascending); - self.sort_mods(); }; } + if clicked { + self.update_sorting_config(sort_category, is_ascending); + } ui.add_space(16.); // TODO: actually implement mod groups. diff --git a/src/state/mod.rs b/src/state/mod.rs index c62961ca..e0bef7c9 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -419,7 +419,7 @@ impl Default for Config!["0.0.0"] { .map(DRGInstallation::main_pak), gui_theme: None, disable_fix_exploding_gas: false, - sorting_config: Some(SortingConfig::default()), + sorting_config: None, } } }