Skip to content

Commit 619fcd5

Browse files
committed
Add mod details window with a file tree of the mod's files
1 parent 95ed000 commit 619fcd5

8 files changed

Lines changed: 559 additions & 40 deletions

File tree

Cargo.lock

Lines changed: 13 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ cbor4ii = { version = "1.2", features = ["serde1", "use_std"] }
1313
clap = { version = "4.6", features = ["derive"] }
1414
compact_str = { version = "0.9", features = ["serde"] }
1515
foldhash = "0.2"
16+
nary_tree = { git = "https://github.com/MonterraByte/nary-tree", rev = "0ee83bb2b144f9ef72fa8a35941e611f5c63e3f7" }
1617
ptree = { version = "0.5", default-features = false, features = ["ansi"] }
1718
thiserror = "2"
1819
tracing = "0.1"

core/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ compact_str = { workspace = true }
1010
const_format = { version = "0.2" }
1111
itertools = "0.14"
1212
ptree = { workspace = true }
13-
nary_tree = "0.4"
13+
nary_tree = { workspace = true }
1414
recycle_vec = "1.1"
1515
replace_with = "0.1"
1616
serde = { version = "1", features = ["derive"] }

gui/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ path = "src/main.rs"
1111
[dependencies]
1212
anyhow = { workspace = true }
1313
clap = { workspace = true }
14+
compact_str = { workspace = true }
1415
foldhash = { workspace = true }
1516
mmm-core = { path = "../core" }
1617
mmm-edit = { path = "../edit" }
18+
nary_tree = { workspace = true }
1719
eframe = "0.34"
1820
egui_extras = "0.34"
21+
egui_ltreeview = "0.7"
1922
tracing = { workspace = true }
2023
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
2124

gui/src/details.rs

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
// Copyright © 2026 Joaquim Monteiro
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// This program is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU General Public License
14+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
15+
16+
use std::fmt::Write;
17+
use std::fs;
18+
use std::io;
19+
use std::path::PathBuf;
20+
use std::sync::Arc;
21+
use std::thread::{self, JoinHandle};
22+
use std::time::Instant;
23+
24+
use compact_str::CompactString;
25+
use eframe::egui;
26+
use egui::{CentralPanel, CornerRadius, Frame, Ui, ViewportCommand, ViewportId};
27+
use nary_tree::NodeId;
28+
use tracing::error;
29+
30+
use mmm_core::file_tree::util::NodePathBuilder;
31+
use mmm_core::file_tree::{Counters, FileTree, FileTreeBuilder, IterDirError, TreeNodeKind, new_tree};
32+
use mmm_core::instance::{Instance, ModEntryKind, ModIndex};
33+
use mmm_edit::EditableInstance;
34+
use mmm_edit::util::node_ord;
35+
36+
use crate::tree::{TreeDisplay, dnd_handle_actions_fn};
37+
use crate::utils::{Viewport, ViewportResult, show_immediate};
38+
39+
enum Tree {
40+
Some(FileTree),
41+
Pending {
42+
handle: Option<ThreadHandle>,
43+
counter: Arc<Counters>,
44+
previous_count: usize,
45+
message: CompactString,
46+
},
47+
Error(Box<str>),
48+
}
49+
50+
type ThreadHandle = JoinHandle<Result<FileTree, IterDirError>>;
51+
const _: () = assert!(size_of::<ThreadHandle>() == size_of::<Option<ThreadHandle>>());
52+
53+
impl Tree {
54+
fn from_dir(dir: PathBuf) -> Result<Self, io::Error> {
55+
let counter = Counters::new();
56+
let tree_builder = FileTreeBuilder::new().with_counter(Arc::clone(&counter));
57+
58+
let handle = thread::Builder::new().spawn(move || {
59+
let mut tree = new_tree();
60+
tree_builder.iter_dir(&mut tree, dir)?;
61+
tree.root_mut().expect("has root node").sort_recursive_by(node_ord);
62+
Ok(tree)
63+
})?;
64+
65+
Ok(Self::Pending {
66+
handle: Some(handle),
67+
counter,
68+
previous_count: 0,
69+
message: CompactString::const_new("0 files counted"),
70+
})
71+
}
72+
73+
fn update(&mut self) {
74+
if let Tree::Pending { handle, .. } = self
75+
&& handle.as_ref().expect("not joined yet").is_finished()
76+
{
77+
let handle = handle.take().expect("not joined yet");
78+
match handle.join() {
79+
Ok(Ok(tree)) => *self = Tree::Some(tree),
80+
Ok(Err(err)) => {
81+
error!(?err, "failed to build file tree");
82+
*self = Tree::Error(format!("Failed to build file tree:\n{}", err).into_boxed_str());
83+
}
84+
Err(_) => {
85+
error!("file tree thread panicked");
86+
*self = Tree::Error(Box::from("Failed to build file tree:\nThread panicked."));
87+
}
88+
}
89+
}
90+
}
91+
}
92+
93+
pub struct ModDetailsWindow {
94+
viewport: Box<Viewport>,
95+
tree: Tree,
96+
tree_display: TreeDisplay,
97+
raise: bool,
98+
}
99+
100+
impl ModDetailsWindow {
101+
pub fn new(instance: &EditableInstance, idx: ModIndex) -> Result<Self, io::Error> {
102+
let dir = instance.mod_dir(&instance.mods()[idx]).expect("mod is not a separator");
103+
let tree = Tree::from_dir(dir)?;
104+
105+
let mod_decl = &instance.mods()[idx];
106+
assert_eq!(mod_decl.kind(), ModEntryKind::Mod);
107+
108+
let id = ViewportId::from_hash_of(("details", idx, Instant::now()));
109+
let viewport = Viewport::new(id, format!("mmm — Details of {}", mod_decl.name()), None);
110+
let tree_display = TreeDisplay::new();
111+
112+
Ok(Self { viewport, tree, tree_display, raise: false })
113+
}
114+
115+
pub fn raise(&mut self) {
116+
self.raise = true;
117+
}
118+
119+
pub fn update(&mut self, ui: &mut Ui, instance: &EditableInstance, mod_index: ModIndex) -> ViewportResult {
120+
self.tree.update();
121+
122+
show_immediate!(self.viewport, ui, |ui: &mut Ui, _viewport| {
123+
if self.raise {
124+
self.raise = false;
125+
ui.send_viewport_cmd(ViewportCommand::Focus);
126+
}
127+
CentralPanel::default().show_inside(ui, |ui| self.files(ui, instance, mod_index));
128+
})
129+
}
130+
131+
fn files(&mut self, ui: &mut Ui, instance: &EditableInstance, mod_index: ModIndex) {
132+
match &mut self.tree {
133+
Tree::Some(tree) => {
134+
let dnd = dnd_handle_actions_fn(|tree, dnd| {
135+
let target_node = tree.get(dnd.target).expect("node exists");
136+
assert!(matches!(target_node.data().kind, TreeNodeKind::Dir));
137+
138+
let mod_dir = instance.mod_dir(&instance.mods()[mod_index]).expect("not a separator");
139+
140+
let mut target = NodePathBuilder::new(mod_dir.clone());
141+
target.reset_and_push(&target_node);
142+
let mut target = target.into_inner();
143+
target.set_base_to_current();
144+
145+
let mut source = NodePathBuilder::new(mod_dir);
146+
147+
for node in dnd.source {
148+
let mut source_node = tree.get_mut(node).expect("node exists");
149+
let from = source.reset_and_push(&source_node.as_ref());
150+
151+
target.reset_to_base();
152+
let to = target.push(&source_node.data().name);
153+
154+
if let Err(err) = fs::rename(from, to) {
155+
error!(?err, "failed to move '{}' to '{}'", from.display(), to.display());
156+
// TODO: consider refreshing the tree on "not found" errors
157+
continue;
158+
}
159+
160+
source_node.append_to(dnd.target).unwrap();
161+
}
162+
163+
tree.get_mut(dnd.target).unwrap().sort_children_by(node_ord);
164+
});
165+
166+
let tree_height = ui.available_height() - ui.style().spacing.interact_size.y;
167+
Frame::new()
168+
.stroke(ui.style().visuals.window_stroke)
169+
.corner_radius(CornerRadius::same(4))
170+
.show(ui, |ui| {
171+
self.tree_display.display(ui, tree, label_fn, dnd, tree_height);
172+
});
173+
}
174+
Tree::Pending { counter, previous_count, message, .. } => {
175+
let count = counter.unique_files();
176+
if count != *previous_count {
177+
*previous_count = count;
178+
179+
message.clear();
180+
let _ = write!(message, "{} files counted", count);
181+
182+
ui.request_repaint();
183+
}
184+
185+
ui.centered_and_justified(|ui| {
186+
ui.label(message.as_str());
187+
});
188+
}
189+
Tree::Error(err) => {
190+
ui.centered_and_justified(|ui| {
191+
ui.label(err.as_ref());
192+
});
193+
}
194+
}
195+
}
196+
}
197+
198+
fn label_fn(ui: &mut Ui, tree: &mut FileTree, id: &NodeId) {
199+
let node = tree.get(*id).expect("node exists");
200+
ui.label(node.data().name.as_str());
201+
}

0 commit comments

Comments
 (0)