From 0e5c54bc1c80cf647f101d4a1bdbe03ce1410eef Mon Sep 17 00:00:00 2001 From: katie Date: Tue, 19 Mar 2024 12:19:40 -0700 Subject: [PATCH] lots of minor things --- Cargo.toml | 4 +- src/assets.rs | 19 ++- src/assets/atlas.hex | Bin 262144 -> 262144 bytes src/assets/build/atlas.png | Bin 23074 -> 24095 bytes src/cli.rs | 228 +++++++++++++++++++---------------- src/element_action.rs | 31 ++--- src/elements/array.rs | 7 +- src/elements/chunk.rs | 14 ++- src/elements/compound.rs | 16 +-- src/elements/list.rs | 7 +- src/main.rs | 75 +++++++++--- src/search_box.rs | 107 ++++++++++++---- src/tab.rs | 34 +++--- src/text.rs | 1 + src/vertex_buffer_builder.rs | 6 +- src/window.rs | 2 +- src/workbench.rs | 98 ++++++++++++--- web/favicon.png | Bin 0 -> 1146 bytes web/index.html | 7 +- 19 files changed, 435 insertions(+), 221 deletions(-) create mode 100644 web/favicon.png diff --git a/Cargo.toml b/Cargo.toml index a954542..d3ff7ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nbtworkbench" -version = "1.2.2" +version = "1.2.3" edition = "2021" description = "A modern NBT Editor written in Rust designed for performance and efficiency." license-file = "LICENSE" @@ -8,7 +8,7 @@ repository = "https://github.com/RealRTTV/nbtworkbench" keywords = ["nbt", "window", "unsafe", "editor", "tree"] categories = ["graphics", "rendering", "text-editors", "parser-implementations"] -[lib] +[target.'cfg(target_arch = "wasm32")'.lib] crate-type = ["cdylib", "rlib"] path = "src/main.rs" diff --git a/src/assets.rs b/src/assets.rs index e709520..598a087 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -99,12 +99,10 @@ pub const OPEN_ARRAY_IN_HEX_UV: Vec2u = Vec2u::new(35, 131); pub const OPEN_IN_TXT: Vec2u = Vec2u::new(51, 131); pub const SORT_COMPOUND_BY_NAME: Vec2u = Vec2u::new(67, 131); pub const SORT_COMPOUND_BY_TYPE: Vec2u = Vec2u::new(83, 131); -pub const SORT_COMPOUND_BY_NOTHING: Vec2u = Vec2u::new(0, 160); +pub const SORT_COMPOUND_BY_NOTHING: Vec2u = Vec2u::new(3, 163); pub const FREEHAND_MODE_UV: Vec2u = Vec2u::new(0, 144); pub const ENABLED_FREEHAND_MODE_UV: Vec2u = Vec2u::new(16, 144); pub const STEAL_ANIMATION_OVERLAY_UV: Vec2u = Vec2u::new(64, 144); -pub const STAMP_BACKDROP_UV: Vec2u = Vec2u::new(16, 160); -pub const MAGNIFYING_GLASS_UV: Vec2u = Vec2u::new(96, 48); pub const BYTE_UV: Vec2u = Vec2u::new(0, 0); pub const SHORT_UV: Vec2u = Vec2u::new(16, 0); @@ -139,6 +137,12 @@ pub const ALERT_UV: Vec2u = Vec2u::new(112, 144); pub const BACKDROP_UV: Vec2u = Vec2u::new(32, 160); pub const ADD_SEARCH_BOOKMARKS: Vec2u = Vec2u::new(48, 160); pub const REMOVE_SEARCH_BOOKMARKS: Vec2u = Vec2u::new(64, 160); +pub const SEARCH_KEYS: Vec2u = Vec2u::new(80, 160); +pub const SEARCH_VALUES: Vec2u = Vec2u::new(80, 176); +pub const SEARCH_KEYS_AND_VALUES: Vec2u = Vec2u::new(80, 192); +pub const STRING_SEARCH_MODE: Vec2u = Vec2u::new(96, 160); +pub const REGEX_SEARCH_MODE: Vec2u = Vec2u::new(96, 176); +pub const SNBT_SEARCH_MODE: Vec2u = Vec2u::new(96, 192); pub const BASE_Z: u8 = 5; pub const JUST_OVERLAPPING_BASE_Z: u8 = BASE_Z + 1; @@ -160,10 +164,8 @@ pub const TOOLTIP_Z: u8 = 250; #[allow(clippy::cast_ptr_alignment)] pub fn icon() -> Vec { - #[cfg(all(debug_assertions, not(target_arch = "wasm32")))] - let start = unsafe { core::arch::x86_64::_rdtsc() }; // error!("Hello, world!"); - let original = match (since_epoch().as_micros() & 7) as u8 { + let original = match (since_epoch().as_millis() & 7) as u8 { // it's a good random only because its used once 0 => OTHERSIDE_MUSIC_DISC_ICON, 1 => PIGSTEP_MUSIC_DISC_ICON, @@ -209,10 +211,5 @@ pub fn icon() -> Vec { } } let mut scaled = ManuallyDrop::new(core::hint::black_box(scaled)); - #[cfg(all(debug_assertions, not(target_arch = "wasm32")))] - crate::log!( - "took {} cycles", - unsafe { core::arch::x86_64::_rdtsc() } - start - ); unsafe { Vec::from_raw_parts(scaled.as_mut_ptr().cast::(), 16384, 16384) } } diff --git a/src/assets/atlas.hex b/src/assets/atlas.hex index 8bccc00f76088bcc5154bfcdfc27f7f758e5dfb7..8dfa91dbaa56c433b72ea928fdcc1bde737e90a6 100644 GIT binary patch delta 12943 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 I|Chr50qWH24*&oF delta 10025 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 diff --git a/src/assets/build/atlas.png b/src/assets/build/atlas.png index 61ff933db83e737368199469d7772bf21c828acf..590a8a533c4c8b5b6bded630f71142c71f43ee48 100644 GIT binary patch literal 24095 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 UD!fJnin;?za%!@r&y7F*4@hT~LI3~& literal 23074 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 Option { - if !str.starts_with("/") { - return None - } - - str = str.split_off(1); - - let mut flags = 0_u8; - while let Some(char) = str.pop() { - match char { - 'i' => flags |= 0b000001, - 'g' => flags |= 0b000010, - 'm' => flags |= 0b000100, - 's' => flags |= 0b001000, - 'u' => flags |= 0b010000, - 'y' => flags |= 0b100000, - '/' => break, - _ => return None - } - } - - RegexBuilder::new(&str) - .case_insensitive(flags & 0b1 > 0) - .multi_line(flags & 0b100 > 0) - .dot_matches_new_line(flags & 0b1000 > 0) - .unicode(flags & 0b10000 > 0) - .swap_greed(flags & 0b10000 > 0) - .build().ok() -} - -fn get_paths(args: &mut Vec) -> Vec { +fn get_paths(mut args: Vec) -> (PathBuf, Vec) { if args.is_empty() { error!("Could not find path argument"); std::process::exit(1); } let path = args.remove(0); match glob(&path) { - Ok(paths) => paths.filter_map(|result| result.ok()).collect::>(), + Ok(paths) => { + let root = if let Some(astrix_index) = path.bytes().position(|x| x == b'*') && let Some(slash_index) = path.bytes().take(astrix_index).rposition(|x| x == b'/' || x== b'\\') { + PathBuf::from(&path[..=slash_index]) + } else if let Some(slash_index) = path.bytes().rposition(|x| x == b'/' || x== b'\\') { + PathBuf::from(&path[..=slash_index]) + } else { + panic!("{path}") + }; + let paths = paths.filter_map(|result| result.ok()).filter_map(|p| p.strip_prefix(&root).ok().map(|x| x.to_path_buf())).collect::>(); + (root, paths) + }, Err(e) => { error!("Glob error: {e}"); std::process::exit(1); @@ -72,30 +52,49 @@ fn get_paths(args: &mut Vec) -> Vec { } } -fn get_predicate(mut args: Vec) -> SearchPredicate { - let snbt = { - if let Some("-s" | "--snbt") = args.get(0).map(String::as_str) { - args.remove(0); - true - } else if let Some("-s" | "--snbt") = args.get(1).map(String::as_str) { - args.remove(1); - true - } else { - false +fn get_predicate(args: &mut Vec) -> SearchPredicate { + let Some(query) = args.pop() else { + error!("Could not find "); + std::process::exit(0) + }; + + let search_flags = match get_argument("--search", args).or_else(|| get_argument("-s", args)).as_deref() { + Some("key") => 0b10_u8, + Some("value") => 0b01_u8, + Some("all") | None => 0b11_u8, + Some(x) => { + error!("Invalid search kind '{x}', valid ones are: `key`, `value`, and `all`."); + std::process::exit(1); } }; - let predicate = args.as_slice().join(" "); - if predicate.is_empty() { - error!("Predicate cannot be empty"); - std::process::exit(1); - } - if snbt && let Some((key, snbt)) = NbtElement::from_str(&predicate, SortAlgorithm::None) { - SearchPredicate::Snbt(key.map(|x| x.into_string()), snbt) - } else if let Some(regex) = create_regex(predicate.clone()) { - SearchPredicate::Regex(regex) - } else { - SearchPredicate::String(predicate) + match get_argument("--mode", args).or_else(|| get_argument("-m", args)).as_deref() { + Some("normal") | None => SearchPredicate { + search_flags, + inner: SearchPredicateInner::String(query), + }, + Some("regex") => if let Some(regex) = create_regex(query) { + SearchPredicate { + search_flags, + inner: SearchPredicateInner::Regex(regex), + } + } else { + error!("Invalid regex, valid regexes look like: `/[0-9]+/g`"); + std::process::exit(1); + }, + Some("snbt") => if let Some((key, snbt)) = NbtElement::from_str(&query, SortAlgorithm::Name) { + SearchPredicate { + search_flags, + inner: SearchPredicateInner::Snbt(key.map(CompactString::into_string), snbt), + } + } else { + error!(r#"Invalid snbt, valid snbt look like: `key:"minecraft:air"` or `{{id:"minecraft:looting",lvl:3s}}` (note that some terminals use "" to contain one parameter and that inner ones will have to be escaped)"#); + std::process::exit(1); + }, + Some(x) => { + error!("Invalid mode '{x}', valid ones are: `normal', `regex`, and `snbt`."); + std::process::exit(1); + } } } @@ -103,12 +102,16 @@ fn file_size(path: impl AsRef) -> Option { File::open(path).ok().and_then(|file| file.metadata().ok()).map(|metadata| metadata.len() as u64) } -fn increment_progress_bar(completed: &AtomicU64, size: u64, total: u64) { +fn increment_progress_bar(completed: &AtomicU64, size: u64, total: u64, action: &str) { let finished = completed.fetch_add(size, Ordering::Relaxed); - print!("\rSearching... ({n} / {total} bytes) ({p:.1}% complete)", n = finished, p = 100.0 * finished as f64 / total as f64); + print!("\r{action}... ({n} / {total} bytes) ({p:.1}% complete)", n = finished, p = 100.0 * finished as f64 / total as f64); let _ = std::io::Write::flush(&mut std::io::stdout()); } +fn get_argument(key: &str, args: &mut Vec) -> Option { + Some(args.remove(args.iter().position(|x| x.strip_prefix(key).is_some_and(|x| x.starts_with("=")))?).split_off(key.len() + 1)) +} + #[inline] #[cfg(not(target_arch = "wasm32"))] pub fn find() -> ! { @@ -116,8 +119,8 @@ pub fn find() -> ! { // one for the exe, one for the `find` args.drain(..2).for_each(|_| ()); - let paths = get_paths(&mut args); - let predicate = get_predicate(args); + let predicate = get_predicate(&mut args); + let (root, paths) = get_paths(args); let completed = AtomicU64::new(0); let total_size = paths.iter().filter_map(file_size).sum::(); @@ -126,7 +129,9 @@ pub fn find() -> ! { let _ = std::io::Write::flush(&mut std::io::stdout()); let results = std::thread::scope(|s| { let mut results = Vec::new(); - for path in paths { + for p in paths { + let mut path = root.clone(); + path.push(p); results.push(s.spawn(|| 'a: { let mut workbench = Workbench::new(&mut WindowProperties::Fake); workbench.tabs.clear(); @@ -135,8 +140,8 @@ pub fn find() -> ! { Ok(bytes) => bytes, Err(e) => { error!("File read error: {e}"); - increment_progress_bar(&completed, file_size(&path).unwrap_or(0), total_size); - break 'a None + increment_progress_bar(&completed, file_size(&path).unwrap_or(0), total_size, "Searching"); + break 'a None; } }; @@ -144,14 +149,14 @@ pub fn find() -> ! { if let Err(e) = workbench.on_open_file(&path, bytes, &mut WindowProperties::Fake) { error!("File parse error: {e}"); - increment_progress_bar(&completed, len, total_size); - break 'a None + increment_progress_bar(&completed, len, total_size, "Searching"); + break 'a None; } let tab = workbench.tabs.remove(0); let bookmarks = SearchBox::search0(&tab.value, &predicate); - std::thread::spawn(move || drop(tab)); - increment_progress_bar(&completed, len, total_size); + std::thread::Builder::new().stack_size(50_331_648 /*48MiB*/).spawn(move || drop(tab)).expect("Failed to spawn thread"); + increment_progress_bar(&completed, len, total_size, "Searching"); if !bookmarks.is_empty() { Some(SearchResult { path, @@ -166,7 +171,7 @@ pub fn find() -> ! { results.into_iter().filter_map(|x| x.join().ok()).filter_map(std::convert::identity).collect::>() }); - log!("\rSearching... ({total_size} / {total_size} bytes) (100.0% complete)"); + log!("\rSearching ({total_size} / {total_size} bytes) (100.0% complete)"); if results.is_empty() { log!("No results found.") @@ -185,42 +190,48 @@ pub fn reformat() -> ! { let mut args = std::env::args().collect::>(); args.drain(..2); - let remap_extension = { - if let Some("--remap-extension" | "-re") = args.get(0).map(String::as_str) { - args.remove(0); - true - } else { - false + let format_arg = get_argument("--format", &mut args).or_else(|| get_argument("-f", &mut args)); + let (extension, format) = match format_arg.as_deref() { + Some(x @ "nbt") => (x, FileFormat::Nbt), + Some(x @ ("dat" | "dat_old" | "gzip")) => (if x == "gzip" { "dat" } else { x }, FileFormat::Gzip), + Some(x @ "zlib") => (x, FileFormat::Zlib), + Some(x @ "snbt") => (x, FileFormat::Snbt), + None => { + error!("`--format` not specified."); + std::process::exit(1); + } + Some(x) => { + error!("Invalid format '{x}'"); + std::process::exit(1); } }; - let paths = get_paths(&mut args); - - let (extension, format) = { - match args.get(0).map(String::as_str) { - Some(x @ "nbt") => (x, FileFormat::Nbt), - Some(x @ ("dat" | "dat_old" | "gzip")) => (if x == "gzip" { "dat" } else { x }, FileFormat::Gzip), - Some(x @ "zlib") => (x, FileFormat::Zlib), - Some(x @ "snbt") => (x, FileFormat::Snbt), - Some(format) => { - error!("Unknown format '{format}'"); - std::process::exit(1); - } - None => { - error!("No format supplied"); - std::process::exit(1); - } - } + let extension = if let Some(extension) = get_argument("--out-ext", &mut args).or_else(|| get_argument("-e", &mut args)) { + extension + } else { + extension.to_owned() + }; + + let out_dir = if let Some(out_dir) = get_argument("--out-dir", &mut args).or_else(|| get_argument("-d", &mut args)) { + Some(PathBuf::from(out_dir)) + } else { + None }; + let (root, paths) = get_paths(args); + let completed = AtomicU64::new(0); let total_size = paths.iter().filter_map(file_size).sum::(); print!("Reformatting... (0 / {total_size} bytes) (0.0% complete)"); let _ = std::io::Write::flush(&mut std::io::stdout()); std::thread::scope(|s| { - for path in paths { + for p in paths { + let mut pa = root.clone(); + pa.push(&p); s.spawn(|| 'a: { + let p = p; + let path = pa; let mut workbench = Workbench::new(&mut WindowProperties::Fake); workbench.tabs.clear(); @@ -228,8 +239,8 @@ pub fn reformat() -> ! { Ok(bytes) => bytes, Err(e) => { error!("File read error: {e}"); - increment_progress_bar(&completed, file_size(&path).unwrap_or(0), total_size); - break 'a + increment_progress_bar(&completed, file_size(&path).unwrap_or(0), total_size, "Reformatting"); + break 'a; } }; @@ -237,34 +248,41 @@ pub fn reformat() -> ! { if let Err(e) = workbench.on_open_file(&path, bytes, &mut WindowProperties::Fake) { error!("File parse error: {e}"); - increment_progress_bar(&completed, len, total_size); - break 'a + increment_progress_bar(&completed, len, total_size, "Reformatting"); + break 'a; } - let mut tab = workbench.tabs.remove(0); + let tab = workbench.tabs.remove(0); if let FileFormat::Nbt | FileFormat::Snbt | FileFormat::Gzip | FileFormat::Zlib = tab.compression {} else { error!("Tab had invalid file format {}", tab.compression.to_string()); } let out = format.encode(&tab.value); - std::thread::spawn(move || drop(tab)); + std::thread::Builder::new().stack_size(50_331_648 /*48MiB*/).spawn(move || drop(tab)).expect("Failed to spawn thread"); - let path = if remap_extension { - path.with_extension(extension) + let name = path.file_stem().expect("File must have stem").to_string_lossy().into_owned() + "." + &extension; + + let mut new_path = if let Some(out_dir) = out_dir.as_deref() { + out_dir.to_path_buf() } else { - path + root.to_path_buf() }; + new_path.push(p); + let new_path = new_path.with_file_name(&name); + if let Err(e) = std::fs::create_dir_all(&new_path) { + error!("File directory creation error: {e}") + } - if let Err(e) = std::fs::write(path, out) { + if let Err(e) = std::fs::write(new_path, out) { error!("File write error: {e}") } - increment_progress_bar(&completed, len, total_size); + increment_progress_bar(&completed, len, total_size, "Reformatting"); }); } }); - log!("\rReformatting... ({total_size} / {total_size} bytes) (100.0% complete)"); + log!("\rReformatting ({total_size} / {total_size} bytes) (100.0% complete)"); std::process::exit(0); } diff --git a/src/element_action.rs b/src/element_action.rs index b171191..6645b84 100644 --- a/src/element_action.rs +++ b/src/element_action.rs @@ -10,7 +10,7 @@ use compact_str::CompactString; use notify::{EventKind, PollWatcher, RecursiveMode, Watcher}; use uuid::Uuid; -use crate::{Bookmark, panic_unchecked, set_clipboard, FileUpdateSubscription}; +use crate::{Bookmark, panic_unchecked, set_clipboard, FileUpdateSubscription, since_epoch}; #[cfg(not(target_arch = "wasm32"))] use crate::{FileUpdateSubscriptionType, assets::{OPEN_ARRAY_IN_HEX_UV, OPEN_IN_TXT}}; use crate::assets::{ACTION_WHEEL_Z, COPY_FORMATTED_UV, COPY_RAW_UV, SORT_COMPOUND_BY_NAME, SORT_COMPOUND_BY_TYPE}; @@ -41,39 +41,39 @@ impl ElementAction { Self::CopyRaw => { builder.draw_texture_z(pos, ACTION_WHEEL_Z, COPY_RAW_UV, (10, 10)); if hovered { - builder.draw_tooltip(&["Copy minified snbt to clipboard"], pos); + builder.draw_tooltip(&["Copy minified snbt to clipboard"], pos, false); } } Self::CopyFormatted => { builder.draw_texture_z(pos, ACTION_WHEEL_Z, COPY_FORMATTED_UV, (10, 10)); if hovered { - builder.draw_tooltip(&["Copy formatted snbt to clipboard"], pos); + builder.draw_tooltip(&["Copy formatted snbt to clipboard"], pos, false); } } #[cfg(not(target_arch = "wasm32"))] Self::OpenArrayInHex => { builder.draw_texture_z(pos, ACTION_WHEEL_Z, OPEN_ARRAY_IN_HEX_UV, (10, 10)); if hovered { - builder.draw_tooltip(&["Open raw contents in hex editor"], pos); + builder.draw_tooltip(&["Open raw contents in hex editor"], pos, false); } } #[cfg(not(target_arch = "wasm32"))] Self::OpenInTxt => { builder.draw_texture_z(pos, ACTION_WHEEL_Z, OPEN_IN_TXT, (10, 10)); if hovered { - builder.draw_tooltip(&["Open formatted snbt in text editor"], pos); + builder.draw_tooltip(&["Open formatted snbt in text editor"], pos, false); } } Self::SortCompoundByName => { builder.draw_texture_z(pos, ACTION_WHEEL_Z, SORT_COMPOUND_BY_NAME, (10, 10)); if hovered { - builder.draw_tooltip(&["Sort compound by name"], pos); + builder.draw_tooltip(&["Sort compound by name"], pos, false); } } Self::SortCompoundByType => { builder.draw_texture_z(pos, ACTION_WHEEL_Z, SORT_COMPOUND_BY_TYPE, (10, 10)); if hovered { - builder.draw_tooltip(&["Sort compound by type"], pos); + builder.draw_tooltip(&["Sort compound by type"], pos, false); } } } @@ -116,12 +116,13 @@ impl ElementAction { #[must_use] #[cfg(not(target_arch = "wasm32"))] fn open_file(str: &str) -> bool { - if cfg!(target_os = "windows") { - Command::new("cmd").args(["/c", "start", str]).status() - } else if cfg!(target_os = "macos") { - Command::new("open").arg(str).status() - } else { - Command::new("xdg-open").arg(str).status() + 'a: { + #[cfg(target_os = "windows")] + break 'a Command::new("cmd").args(["/c", "start", str]).status(); + #[cfg(target_os = "apple")] + break 'a Command::new("open").arg(str).status(); + #[cfg(target_os = "linux")] + break 'a Command::new("xdg-open").arg(str).status(); }.is_ok() } @@ -145,7 +146,7 @@ impl ElementAction { Self::OpenArrayInHex => { use std::io::Write; - let hash = (unsafe { core::arch::x86_64::_rdtsc() as usize }).wrapping_mul(element as *mut NbtElement as usize); + let hash = (since_epoch().as_millis() as usize).wrapping_mul(element as *mut NbtElement as usize); let path = std::env::temp_dir().join(format!( "nbtworkbench-{hash:0width$x}.hex", width = usize::BITS as usize / 8 @@ -221,7 +222,7 @@ impl ElementAction { Self::OpenInTxt => { use std::io::Write; - let hash = (unsafe { core::arch::x86_64::_rdtsc() as usize }).wrapping_mul(element as *mut NbtElement as usize); + let hash = (since_epoch().as_millis() as usize).wrapping_mul(element as *mut NbtElement as usize); let path = std::env::temp_dir().join(format!( "nbtworkbench-{hash:0width$x}.txt", width = usize::BITS as usize / 8 diff --git a/src/elements/array.rs b/src/elements/array.rs index adb9df6..c57edb6 100644 --- a/src/elements/array.rs +++ b/src/elements/array.rs @@ -3,13 +3,18 @@ macro_rules! array { ($element_field:ident, $name:ident, $t:ty, $my_id:literal, $id:literal, $char:literal, $uv:ident, $element_uv:ident) => { #[derive(Default)] #[repr(C)] - #[derive(PartialEq)] pub struct $name { values: Box>, max_depth: u32, open: bool, } + impl PartialEq for $name { + fn eq(&self, other: &Self) -> bool { + self.values == other.values + } + } + impl Clone for $name { #[allow(clippy::cast_ptr_alignment)] #[inline] diff --git a/src/elements/chunk.rs b/src/elements/chunk.rs index afc65f9..3a7c3c0 100644 --- a/src/elements/chunk.rs +++ b/src/elements/chunk.rs @@ -20,7 +20,6 @@ use crate::{DropFn, RenderContext, SortAlgorithm, StrExt}; use crate::color::TextColor; #[repr(C)] -#[derive(PartialEq)] pub struct NbtRegion { pub chunks: Box<(Vec, [NbtElement; 32 * 32])>, height: u32, @@ -29,6 +28,12 @@ pub struct NbtRegion { open: bool, } +impl PartialEq for NbtRegion { + fn eq(&self, other: &Self) -> bool { + self.chunks == other.chunks + } +} + impl Clone for NbtRegion { #[allow(clippy::cast_ptr_alignment)] #[inline] @@ -705,7 +710,6 @@ impl Debug for NbtRegion { #[repr(C)] #[allow(clippy::module_name_repetitions)] -#[derive(PartialEq)] pub struct NbtChunk { inner: Box, last_modified: u32, @@ -715,6 +719,12 @@ pub struct NbtChunk { pub z: u8, } +impl PartialEq for NbtChunk { + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner + } +} + impl Clone for NbtChunk { #[allow(clippy::cast_ptr_alignment)] #[inline] diff --git a/src/elements/compound.rs b/src/elements/compound.rs index e5882d7..2752ab3 100644 --- a/src/elements/compound.rs +++ b/src/elements/compound.rs @@ -22,7 +22,6 @@ use crate::color::TextColor; #[allow(clippy::module_name_repetitions)] #[repr(C)] -#[derive(PartialEq)] pub struct NbtCompound { pub entries: Box, height: u32, @@ -31,6 +30,12 @@ pub struct NbtCompound { open: bool, } +impl PartialEq for NbtCompound { + fn eq(&self, other: &Self) -> bool { + self.entries == other.entries + } +} + impl Clone for NbtCompound { #[allow(clippy::cast_ptr_alignment)] fn clone(&self) -> Self { @@ -694,14 +699,11 @@ pub struct CompoundMap { impl PartialEq for CompoundMap { fn eq(&self, other: &Self) -> bool { - if self.entries.len() != other.entries.len() { return false } + // disabled to make the comparison work like the nbt predicate in mc. + // if self.entries.len() != other.entries.len() { return false } for entry in &self.entries { - if let Some(idx) = other.idx_of(&entry.key) { - if other.get_idx(idx) != Some((&entry.key, &entry.value)) { - return false - } - } else { + if other.idx_of(&entry.key).and_then(|idx| other.get_idx(idx)) != Some((&entry.key, &entry.value)) { return false } } diff --git a/src/elements/list.rs b/src/elements/list.rs index 923aa8e..0b126e2 100644 --- a/src/elements/list.rs +++ b/src/elements/list.rs @@ -16,7 +16,6 @@ use crate::color::TextColor; #[allow(clippy::module_name_repetitions)] #[repr(C)] -#[derive(PartialEq)] pub struct NbtList { pub elements: Box>, height: u32, @@ -26,6 +25,12 @@ pub struct NbtList { open: bool, } +impl PartialEq for NbtList { + fn eq(&self, other: &Self) -> bool { + self.elements == other.elements + } +} + impl Clone for NbtList { #[allow(clippy::cast_ptr_alignment)] #[inline] diff --git a/src/main.rs b/src/main.rs index 5fdd643..a833297 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,9 +31,11 @@ clippy::cast_possible_wrap, clippy::multiple_crate_versions )] +#![feature(adt_const_params)] #![feature(array_chunks)] #![feature(box_patterns)] #![feature(const_black_box)] +#![feature(const_collections_with_hasher)] #![feature(core_intrinsics)] #![feature(iter_array_chunks)] #![feature(iter_next_chunk)] @@ -45,9 +47,6 @@ #![feature(optimize_attribute)] #![feature(stmt_expr_attributes)] #![feature(unchecked_math)] -#![feature(const_collections_with_hasher)] -#![feature(slice_first_last_chunk)] -#![feature(const_maybe_uninit_zeroed)] #![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")] use std::cell::UnsafeCell; @@ -59,6 +58,7 @@ use std::rc::Rc; use std::time::Duration; use compact_str::{CompactString, ToCompactString}; +use regex::{Regex, RegexBuilder}; use static_assertions::const_assert_eq; #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::wasm_bindgen; @@ -67,7 +67,7 @@ use winit::window::Window; use elements::element::NbtElement; use vertex_buffer_builder::VertexBufferBuilder; -use crate::assets::{BASE_TEXT_Z, BASE_Z, BOOKMARK_UV, BOOKMARK_Z, END_LINE_NUMBER_SEPARATOR_UV, HEADER_SIZE, HIDDEN_BOOKMARK_UV, INSERTION_UV, INVALID_STRIPE_UV, LINE_NUMBER_SEPARATOR_UV, LINE_NUMBER_Z, SCROLLBAR_BOOKMARK_Z, SELECTED_TOGGLE_OFF_UV, SELECTED_TOGGLE_ON_UV, SELECTION_UV, SORT_COMPOUND_BY_NAME, SORT_COMPOUND_BY_NOTHING, SORT_COMPOUND_BY_TYPE, STAMP_BACKDROP_UV, TEXT_UNDERLINE_UV, TOGGLE_Z, UNSELECTED_TOGGLE_OFF_UV, UNSELECTED_TOGGLE_ON_UV}; +use crate::assets::{BASE_TEXT_Z, BASE_Z, BOOKMARK_UV, BOOKMARK_Z, END_LINE_NUMBER_SEPARATOR_UV, HEADER_SIZE, HIDDEN_BOOKMARK_UV, HOVERED_WIDGET_UV, INSERTION_UV, INVALID_STRIPE_UV, LINE_NUMBER_SEPARATOR_UV, LINE_NUMBER_Z, SCROLLBAR_BOOKMARK_Z, SELECTED_TOGGLE_OFF_UV, SELECTED_TOGGLE_ON_UV, SORT_COMPOUND_BY_NAME, SORT_COMPOUND_BY_NOTHING, SORT_COMPOUND_BY_TYPE, TEXT_UNDERLINE_UV, TOGGLE_Z, UNSELECTED_TOGGLE_OFF_UV, UNSELECTED_TOGGLE_ON_UV, UNSELECTED_WIDGET_UV}; use crate::color::TextColor; use crate::elements::compound::{CompoundMap}; use crate::elements::element::{NbtByteArray, NbtIntArray, NbtLongArray}; @@ -229,7 +229,6 @@ pub fn wasm_main() { #[cfg(not(target_arch = "wasm32"))] pub fn main() -> ! { - const HELP: &str = "Usage:\n nbtworkbench -? | /? | --help | -h\n nbtworkbench --version | -v\n nbtworkbench find [--snbt | -s] ...\n nbtworkbench reformat [--remap-extension | -re] \n\nOptions:\n --snbt, -s Try to parse query as SNBT\n --remap-extension, -re Remap file extension on reformat"; let first_arg = std::env::args().nth(1); if let Some("find") = first_arg.as_deref() { @@ -240,7 +239,22 @@ pub fn main() -> ! { println!("{}", env!("CARGO_PKG_VERSION")); std::process::exit(0); } else if let Some("-?" | "/?" | "--help" | "-h") = first_arg.as_deref() { - println!("{HELP}"); + println!( + r#"Usage: + nbtworkbench --version|-v + nbtworkbench -?|-h|--help|/? + nbtworkbench find [(--mode|-m)=normal|regex|snbt] [(--search|-s)=key|value|all] + nbtworkbench reformat (--format|-f)= [(--out-dir|-d)=] [(--out-ext|-e)=] + + Options: + --version, -v Displays the version of nbtworkbench you're running. + -?, -h, --help, /? Displays this dialog. + --mode, -m Changes the `find` mode to take the field as either, a containing substring, a regex (match whole), or snbt. [default: normal] + --search, -s Searches for results matching the in either, the key, the value, or both (note that substrings and regex saerch the same pattern in both key and value, while the regex uses it's key field to match equal strings). [default: all] + --format, -f Specifies the format to be reformatted to; either `nbt`, `snbt`, `dat/dat_old/gzip` or `zlib`. + --out-dir, -d Specifies the output directory. [default: ./] + --out-ext, -e Specifies the output file extension (if not specified, it will infer from --format)"# + ); std::process::exit(0); } else { pollster::block_on(window::run()) @@ -314,6 +328,37 @@ pub fn set_clipboard(value: String) -> bool { return web_sys::window().map(|window| window.navigator()).and_then(|navigator| navigator.clipboard()).map(|clipboard| clipboard.write_text(&value)).is_some(); } +#[must_use] +pub fn create_regex(mut str: String) -> Option { + if !str.starts_with("/") { + return None; + } + + str = str.split_off(1); + + let mut flags = 0_u8; + while let Some(char) = str.pop() { + match char { + 'i' => flags |= 0b000001, + 'g' => flags |= 0b000010, + 'm' => flags |= 0b000100, + 's' => flags |= 0b001000, + 'u' => flags |= 0b010000, + 'y' => flags |= 0b100000, + '/' => break, + _ => return None + } + } + + RegexBuilder::new(&str) + .case_insensitive(flags & 0b1 > 0) + .multi_line(flags & 0b100 > 0) + .dot_matches_new_line(flags & 0b1000 > 0) + .unicode(flags & 0b10000 > 0) + .swap_greed(flags & 0b10000 > 0) + .build().ok() +} + #[must_use] pub fn since_epoch() -> Duration { #[cfg(not(target_arch = "wasm32"))] @@ -537,14 +582,16 @@ impl SortAlgorithm { Self::Type => SORT_COMPOUND_BY_TYPE, }; - builder.draw_texture((264, 26), STAMP_BACKDROP_UV, (16, 16)); + let widget_uv = if (264..280).contains(&ctx.mouse_x) && (26..42).contains(&ctx.mouse_y) { + builder.draw_tooltip(&[&format!("Compound Sorting Algorithm ({self})")], (ctx.mouse_x, ctx.mouse_y), false); + HOVERED_WIDGET_UV + } else { + UNSELECTED_WIDGET_UV + }; + builder.draw_texture((264, 26), widget_uv, (16, 16)); builder.draw_texture((267, 29), uv, (10, 10)); - let hovering = (264..280).contains(&ctx.mouse_x) && (26..42).contains(&ctx.mouse_y); - if hovering { - builder.draw_texture((264, 26), SELECTION_UV, (16, 16)); - builder.draw_tooltip(&[&format!("Compound Sorting Algorithm ({self})")], (ctx.mouse_x, ctx.mouse_y)); - } + } pub fn cycle(self) -> Self { @@ -844,7 +891,7 @@ impl RenderContext { errors.push("Error! The current key is a duplicate of another one."); } let color_before = core::mem::replace(&mut builder.color, TextColor::Red.to_raw()); - builder.draw_tooltip(&errors, (self.mouse_x, self.mouse_y)); + builder.draw_tooltip(&errors, (self.mouse_x, self.mouse_y), false); builder.color = color_before; } } @@ -1294,7 +1341,7 @@ impl StrExt for str { if (x as u32) < 56832 { VertexBufferBuilder::CHAR_WIDTH[x as usize] as usize } else { - 0 + VertexBufferBuilder::CHAR_WIDTH[56829] as usize } }) .sum() diff --git a/src/search_box.rs b/src/search_box.rs index 0488e24..0b23d2c 100644 --- a/src/search_box.rs +++ b/src/search_box.rs @@ -1,19 +1,25 @@ use std::ops::{Deref, DerefMut}; use std::time::Duration; +use compact_str::CompactString; use regex::Regex; use winit::event::MouseButton; use winit::keyboard::KeyCode; -use crate::assets::{ADD_SEARCH_BOOKMARKS, BASE_Z, BOOKMARK_UV, DARK_STRIPE_UV, HIDDEN_BOOKMARK_UV, HOVERED_WIDGET_UV, REMOVE_SEARCH_BOOKMARKS, UNSELECTED_WIDGET_UV}; +use crate::assets::{ADD_SEARCH_BOOKMARKS, BASE_Z, BOOKMARK_UV, DARK_STRIPE_UV, HIDDEN_BOOKMARK_UV, HOVERED_WIDGET_UV, REGEX_SEARCH_MODE, REMOVE_SEARCH_BOOKMARKS, SEARCH_KEYS, SEARCH_KEYS_AND_VALUES, SEARCH_VALUES, SNBT_SEARCH_MODE, STRING_SEARCH_MODE, UNSELECTED_WIDGET_UV}; use crate::color::TextColor; -use crate::{Bookmark, combined_two_sorted, flags, since_epoch, StrExt}; +use crate::{Bookmark, combined_two_sorted, create_regex, flags, since_epoch, SortAlgorithm, StrExt}; use crate::elements::element::NbtElement; use crate::text::{Cachelike, SearchBoxKeyResult, Text}; use crate::vertex_buffer_builder::{Vec2u, VertexBufferBuilder}; +pub struct SearchPredicate { + pub search_flags: u8, + pub inner: SearchPredicateInner, +} + #[derive(Debug)] -pub enum SearchPredicate { +pub enum SearchPredicateInner { String(String), Regex(Regex), Snbt(Option, NbtElement), @@ -21,16 +27,21 @@ pub enum SearchPredicate { impl SearchPredicate { fn matches(&self, key: Option<&str>, value: &NbtElement) -> bool { - match self { - Self::String(str) => { + match &self.inner { + SearchPredicateInner::String(str) => { let (value, color) = value.value(); - (color != TextColor::TreeKey && value.contains(str)) || key.is_some_and(|k| k.contains(str)) + ((self.search_flags & 0b01) > 0 && color != TextColor::TreeKey && value.contains(str)) || ((self.search_flags & 0b10) > 0 && key.is_some_and(|k| k.contains(str))) } - Self::Regex(regex) => { + SearchPredicateInner::Regex(regex) => { let (value, color) = value.value(); - color != TextColor::TreeKey && regex.is_match(&value) + ((self.search_flags & 0b01) > 0 && color != TextColor::TreeKey && regex.is_match(&value)) || ((self.search_flags & 0b10) > 0 && key.is_some_and(|k| regex.is_match(k))) } - Self::Snbt(k, element) => k.as_ref().is_some_and(|k| key.is_some_and(|key| key == k)) || value == element + SearchPredicateInner::Snbt(k, element) => { + // cmp order does matter + let a = element == value; + let b = k.as_deref() == key; + ((self.search_flags == 0b11) & a & b) | ((self.search_flags == 0b01) & a) | ((self.search_flags == 0b10) & b) + }, } } } @@ -72,6 +83,8 @@ pub struct SearchBoxAdditional { selected: bool, horizontal_scroll: usize, hits: Option<(usize, Duration)>, + flags: u8, + mode: u8, } pub struct SearchBox(Text); @@ -92,7 +105,7 @@ impl DerefMut for SearchBox { impl SearchBox { pub fn new() -> Self { - Self(Text::new(String::new(), 0, true, SearchBoxAdditional { selected: false, horizontal_scroll: 0, hits: None })) + Self(Text::new(String::new(), 0, true, SearchBoxAdditional { selected: false, horizontal_scroll: 0, hits: None, flags: 0b01, mode: 0 })) } pub const fn uninit() -> Self { @@ -113,7 +126,7 @@ impl SearchBox { (16, 16), ); - let hover = (pos.x..builder.window_width() - 215 - 17).contains(&mouse_x) && (23..45).contains(&mouse_y); + let hover = (pos.x..builder.window_width() - 215 - 17 - 16 - 16).contains(&mouse_x) && (23..45).contains(&mouse_y); builder.horizontal_scroll = self.horizontal_scroll; @@ -131,21 +144,49 @@ impl SearchBox { } if let Some((hits, stat)) = self.hits && (self.is_selected() || hover) { - builder.draw_tooltip(&[&format!("{hits} hits for \"{arg}\" ({ms}ms)", arg = self.value, ms = stat.as_millis())], if !self.is_selected() && hover { mouse } else { (284, 30) }); + builder.draw_tooltip(&[&format!("{hits} hit{s} for \"{arg}\" ({ms}ms)", s = if hits == 1 { "" } else { "s" }, arg = self.value, ms = stat.as_millis())], if !self.is_selected() && hover { mouse } else { (284, 30) }, true); } builder.horizontal_scroll = 0; - let bookmark_uv = if shift { REMOVE_SEARCH_BOOKMARKS } else { ADD_SEARCH_BOOKMARKS }; - let widget_uv = if (builder.window_width() - 215 - 17..builder.window_width() - 215 - 1).contains(&mouse_x) && (26..42).contains(&mouse_y) { - builder.draw_tooltip(&[if shift { "Remove all bookmarks" } else { "Add search bookmarks" }], mouse); - HOVERED_WIDGET_UV - } else { - UNSELECTED_WIDGET_UV - }; + { + let bookmark_uv = if shift { REMOVE_SEARCH_BOOKMARKS } else { ADD_SEARCH_BOOKMARKS }; + let widget_uv = if (builder.window_width() - 215 - 17 - 16 - 16..builder.window_width() - 215 - 1 - 16 - 16).contains(&mouse_x) && (26..42).contains(&mouse_y) { + builder.draw_tooltip(&[if shift { "Remove all bookmarks (Shift + Enter)" } else { "Add search bookmarks (Enter)" }], mouse, false); + HOVERED_WIDGET_UV + } else { + UNSELECTED_WIDGET_UV + }; + + builder.draw_texture_z((builder.window_width() - 215 - 17 - 16 - 16, 26), BASE_Z, widget_uv, (16, 16)); + builder.draw_texture_z((builder.window_width() - 215 - 17 - 16 - 16, 26), BASE_Z, bookmark_uv, (16, 16)); + } + + { + let search_uv = match self.flags { 0b01 => SEARCH_VALUES, 0b10 => SEARCH_KEYS, _ => SEARCH_KEYS_AND_VALUES }; + let widget_uv = if (builder.window_width() - 215 - 17 - 16..builder.window_width() - 215 - 1 - 16).contains(&mouse_x) && (26..42).contains(&mouse_y) { + builder.draw_tooltip(&[match self.flags { 0b01 => "Values only", 0b10 => "Keys only", _ => "Keys + Values" }], mouse, false); + HOVERED_WIDGET_UV + } else { + UNSELECTED_WIDGET_UV + }; - builder.draw_texture_z((builder.window_width() - 215 - 17, 26), BASE_Z, widget_uv, (16, 16)); - builder.draw_texture_z((builder.window_width() - 215 - 17, 26), BASE_Z, bookmark_uv, (16, 16)); + builder.draw_texture_z((builder.window_width() - 215 - 17 - 16, 26), BASE_Z, widget_uv, (16, 16)); + builder.draw_texture_z((builder.window_width() - 215 - 17 - 16, 26), BASE_Z, search_uv, (16, 16)); + } + + { + let mode_uv = match self.mode { 0 => STRING_SEARCH_MODE, 1 => REGEX_SEARCH_MODE, _ => SNBT_SEARCH_MODE }; + let widget_uv = if (builder.window_width() - 215 - 17..builder.window_width() - 215 - 1).contains(&mouse_x) && (26..42).contains(&mouse_y) { + builder.draw_tooltip(&[match self.mode { 0 => "String Mode", 1 => "Regex Mode", _ => "SNBT Mode" }], mouse, false); + HOVERED_WIDGET_UV + } else { + UNSELECTED_WIDGET_UV + }; + + builder.draw_texture_z((builder.window_width() - 215 - 17, 26), BASE_Z, widget_uv, (16, 16)); + builder.draw_texture_z((builder.window_width() - 215 - 17, 26), BASE_Z, mode_uv, (16, 16)); + } } #[inline] @@ -183,7 +224,7 @@ impl SearchBox { } #[inline] - pub fn on_widget(&mut self, shift: bool, bookmarks: &mut Vec, root: &mut NbtElement) { + pub fn on_bookmark_widget(&mut self, shift: bool, bookmarks: &mut Vec, root: &mut NbtElement) { if shift { bookmarks.clear(); } else { @@ -191,13 +232,27 @@ impl SearchBox { } } + #[inline] + pub fn on_search_widget(&mut self, shift: bool) { + self.flags = ((self.flags as i8 - 1).wrapping_add((!shift) as i8 * 2 - 1).rem_euclid(3) + 1) as u8; + } + + #[inline] + pub fn on_mode_widget(&mut self, shift: bool) { + self.mode = (self.mode as i8).wrapping_add((!shift) as i8 * 2 - 1).rem_euclid(3) as u8; + } + #[inline] pub fn search(&mut self, bookmarks: &mut Vec, root: &NbtElement, count_only: bool) { if self.value.is_empty() { return; } - let predicate = SearchPredicate::String(self.value.clone()); + let predicate = match self.mode { + 0 => SearchPredicate { inner: SearchPredicateInner::String(self.value.clone()), search_flags: self.flags }, + 1 => if let Some(regex) = create_regex(self.value.clone()) { SearchPredicate { inner: SearchPredicateInner::Regex(regex), search_flags: self.flags } } else { return }, + _ => if let Some((key, value)) = NbtElement::from_str(&self.value, SortAlgorithm::None) { SearchPredicate { inner: SearchPredicateInner::Snbt(key.map(CompactString::into_string), value), search_flags: self.flags } } else { return }, + }; let start = since_epoch(); let new_bookmarks = Self::search0(root, &predicate); self.hits = Some((new_bookmarks.len(), since_epoch() - start)); @@ -248,7 +303,7 @@ impl SearchBox { pub fn post_input(&mut self, window_dims: (usize, usize)) { let (window_width, _) = window_dims; self.0.post_input(); - let field_width = window_width - 215 - 284 - 17; + let field_width = window_width - 215 - 284 - 17 - 16 - 16; let precursor_width = self.value.split_at(self.cursor).0.width(); // 8px space just to look cleaner let horizontal_scroll = (precursor_width + 8).saturating_sub(field_width); @@ -260,6 +315,10 @@ impl SearchBox { let before = self.value.clone(); let result = 'a: { if let KeyCode::Enter | KeyCode::NumpadEnter = key && flags == flags!(Shift) { + break 'a SearchBoxKeyResult::ClearAllBookmarks + } + + if let KeyCode::Enter | KeyCode::NumpadEnter = key && flags == flags!(Alt) { break 'a SearchBoxKeyResult::FinishCountOnly } diff --git a/src/tab.rs b/src/tab.rs index 9e40a86..9454a88 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -66,7 +66,7 @@ impl Tab { } pub fn save(&mut self, force_dialog: bool) -> Result<()> { - #[cfg(target_os = "windows")] { + #[cfg(any(target_os = "windows", target_os = "apple", target_os = "linux"))] { let path = self.path.as_deref().unwrap_or(self.name.as_ref().as_ref()); if !path.exists() || force_dialog { let mut builder = native_dialog::FileDialog::new(); @@ -209,7 +209,7 @@ impl Tab { let freehand_uv = { let hovering = (248..264).contains(&mouse_x) && (26..42).contains(&mouse_y); if hovering { - builder.draw_tooltip(&["Freehand Mode (Alt + F)"], (mouse_x, mouse_y)); + builder.draw_tooltip(&["Freehand Mode (Ctrl + Shift + F)"], (mouse_x, mouse_y), false); } if self.freehand_mode { @@ -232,24 +232,24 @@ impl Tab { None }; for (idx, (selected, unselected, name)) in [ - (BYTE_UV, BYTE_GRAYSCALE_UV, "Byte"), - (SHORT_UV, SHORT_GRAYSCALE_UV, "Short"), - (INT_UV, INT_GRAYSCALE_UV, "Int"), - (LONG_UV, LONG_GRAYSCALE_UV, "Long"), - (FLOAT_UV, FLOAT_GRAYSCALE_UV, "Float"), - (DOUBLE_UV, DOUBLE_GRAYSCALE_UV, "Double"), - (BYTE_ARRAY_UV, BYTE_ARRAY_GHOST_UV, "Byte Array"), - (INT_ARRAY_UV, INT_ARRAY_GHOST_UV, "Int Array"), - (LONG_ARRAY_UV, LONG_ARRAY_GHOST_UV, "Long Array"), - (STRING_UV, STRING_GHOST_UV, "String"), - (LIST_UV, LIST_GHOST_UV, "List"), - (COMPOUND_UV, COMPOUND_GHOST_UV, "Compound"), + (BYTE_UV, BYTE_GRAYSCALE_UV, "Byte (Alt + 1)"), + (SHORT_UV, SHORT_GRAYSCALE_UV, "Short (Alt + 2)"), + (INT_UV, INT_GRAYSCALE_UV, "Int (Alt + 3)"), + (LONG_UV, LONG_GRAYSCALE_UV, "Long (Alt + 4)"), + (FLOAT_UV, FLOAT_GRAYSCALE_UV, "Float (Alt + 5)"), + (DOUBLE_UV, DOUBLE_GRAYSCALE_UV, "Double (Alt + 6)"), + (BYTE_ARRAY_UV, BYTE_ARRAY_GHOST_UV, "Byte Array (Alt + 7)"), + (INT_ARRAY_UV, INT_ARRAY_GHOST_UV, "Int Array (Alt + 8)"), + (LONG_ARRAY_UV, LONG_ARRAY_GHOST_UV, "Long Array (Alt + 9)"), + (STRING_UV, STRING_GHOST_UV, "String (Alt + 0)"), + (LIST_UV, LIST_GHOST_UV, "List (Alt + -)"), + (COMPOUND_UV, COMPOUND_GHOST_UV, "Compound (Alt + +)"), ] .into_iter() .enumerate() { let uv = if mx == Some(idx * 16) && !skip_tooltips { - builder.draw_tooltip(&[name], (mouse_x, mouse_y)); + builder.draw_tooltip(&[name], (mouse_x, mouse_y), false); selected } else { unselected @@ -260,7 +260,7 @@ impl Tab { { let uv = if mx == Some(192) && self.value.id() == NbtRegion::ID && !skip_tooltips { - builder.draw_tooltip(&["Chunk"], (mouse_x, mouse_y)); + builder.draw_tooltip(&["Chunk"], (mouse_x, mouse_y), false); CHUNK_UV } else { CHUNK_GHOST_UV @@ -270,7 +270,7 @@ impl Tab { { let uv = if mx == Some(208) && !skip_tooltips { - builder.draw_tooltip(&["Clipboard"], (mouse_x, mouse_y)); + builder.draw_tooltip(&["Clipboard"], (mouse_x, mouse_y), false); UNKNOWN_NBT_UV } else { UNKNOWN_NBT_GHOST_UV diff --git a/src/text.rs b/src/text.rs index 0d31213..926b8c9 100644 --- a/src/text.rs +++ b/src/text.rs @@ -35,6 +35,7 @@ pub enum SearchBoxKeyResult { Escape, Finish, FinishCountOnly, + ClearAllBookmarks, } #[repr(u8)] diff --git a/src/vertex_buffer_builder.rs b/src/vertex_buffer_builder.rs index 4e247b6..732bdc7 100644 --- a/src/vertex_buffer_builder.rs +++ b/src/vertex_buffer_builder.rs @@ -123,14 +123,14 @@ impl VertexBufferBuilder { } #[inline] - pub fn draw_tooltip(&mut self, text: &[&str], pos: impl Into<(usize, usize)>) { + pub fn draw_tooltip(&mut self, text: &[&str], pos: impl Into<(usize, usize)>, force_draw_right: bool) { use core::fmt::Write; let (mut x, y) = pos.into(); let y = y + 16; let text_width = text.iter().map(|x| x.width()).max().unwrap_or(0); - if x + text_width + 3 >= self.window_width() { - x = self.window_width().saturating_sub(text_width + 3); + if x >= self.window_width() / 2 && !force_draw_right { + x = x.saturating_sub(text_width + 3); } let old_text_z = core::mem::replace(&mut self.text_z, TOOLTIP_Z); let old_text_coords = core::mem::replace(&mut self.text_coords, (x + 3, y + 3)); diff --git a/src/window.rs b/src/window.rs index 9929a2d..bdc5982 100644 --- a/src/window.rs +++ b/src/window.rs @@ -48,7 +48,7 @@ pub async fn run() -> ! { ) .expect("valid format"), )); - #[cfg(not(target_arch = "wasm32"))] { + #[cfg(target_os = "windows")] { builder = builder .with_drag_and_drop(true) .with_transparent(std::env::args().any(|x| x.eq("--transparent"))); diff --git a/src/workbench.rs b/src/workbench.rs index 3608fa1..3d2bbd8 100644 --- a/src/workbench.rs +++ b/src/workbench.rs @@ -4,7 +4,6 @@ use std::fmt::Write; use std::fs::read; use std::path::{Path, PathBuf}; use std::str::FromStr; -use std::string::String; use std::sync::mpsc::TryRecvError; use std::time::Duration; @@ -287,10 +286,22 @@ impl Workbench { self.search_box.deselect(); } } - if button == MouseButton::Left { - if (self.window_width - 215 - 17..self.window_width - 215 - 1).contains(&self.mouse_x) && (23..45).contains(&self.mouse_y) { + if let MouseButton::Left | MouseButton::Right = button { + let shift = (self.held_keys.contains(&KeyCode::ShiftLeft) || self.held_keys.contains(&KeyCode::ShiftRight)) ^ (button == MouseButton::Right); + + if (self.window_width - 215 - 17 - 16 - 16..self.window_width - 215 - 1 - 16 - 16).contains(&self.mouse_x) && (26..42).contains(&self.mouse_y) { let tab = tab_mut!(self); - self.search_box.on_widget(self.held_keys.contains(&KeyCode::ShiftLeft) || self.held_keys.contains(&KeyCode::ShiftRight), &mut tab.bookmarks, &mut tab.value); + self.search_box.on_bookmark_widget(shift, &mut tab.bookmarks, &mut tab.value); + return true; + } + + if (self.window_width - 215 - 17 - 16..self.window_width - 215 - 1 - 16).contains(&self.mouse_x) && (26..42).contains(&self.mouse_y) { + self.search_box.on_search_widget(shift); + return true; + } + + if (self.window_width - 215 - 17..self.window_width - 215 - 1).contains(&self.mouse_x) && (26..42).contains(&self.mouse_y) { + self.search_box.on_mode_widget(shift); return true; } } @@ -309,14 +320,14 @@ impl Workbench { } match core::mem::replace(&mut self.held_entry, HeldEntry::Empty) { - HeldEntry::Empty => {} + HeldEntry::Empty => {}, HeldEntry::FromAether(x) => { self.drop(x, None, left_margin); - break 'a; + break 'a } HeldEntry::FromKnown(x, indices) => { self.drop(x, Some(indices), left_margin); - break 'a; + break 'a } } @@ -361,7 +372,9 @@ impl Workbench { Some(_) => {}, None => { if button == MouseButton::Right { - self.action_wheel = Some((((x - left_margin) & !15) + left_margin + 6, ((y - HEADER_SIZE) & !15) + HEADER_SIZE + 7)); + let tab = tab_mut!(self); + let depth = Traverse::new(tab.scroll() / 16 + (y - HEADER_SIZE) / 16, &mut tab.value).enumerate().last().0; + self.action_wheel = Some((left_margin + depth * 16 + 16 + 6, ((y - HEADER_SIZE) & !15) + HEADER_SIZE + 7)); break 'a; } } @@ -1240,7 +1253,7 @@ impl Workbench { self.set_tab(self.tab.saturating_sub(1), window_properties); } #[cfg(not(target_arch = "wasm32"))] - std::thread::spawn(move || drop(tab)); + std::thread::Builder::new().stack_size(50_331_648 /*48MiB*/).spawn(move || drop(tab)).expect("Failed to spawn thread"); #[cfg(target_arch = "wasm32")] drop(tab); true @@ -1248,7 +1261,7 @@ impl Workbench { #[inline] fn open_file(&mut self, window_properties: &mut WindowProperties) { - #[cfg(target_os = "windows")] { + #[cfg(any(target_os = "windows", target_os = "apple", target_os = "linux"))] { match native_dialog::FileDialog::new().set_location("~/Downloads").add_filter("NBT File", &["nbt", "snbt", "dat", "dat_old", "dat_mcr", "old"]).add_filter("Region File", &["mca", "mcr"]).show_open_single_file() { Err(e) => self.alert(Alert::new("Error!", TextColor::Red, e.to_string())), Ok(None) => {}, @@ -1266,6 +1279,7 @@ impl Workbench { } #[inline] + #[must_use] fn left_margin(&self) -> usize { tab!(self).left_margin(self.held_entry.element()) } @@ -1352,7 +1366,7 @@ impl Workbench { #[inline] fn try_select_search_box(&mut self, button: MouseButton) -> bool { - if (283..self.window_width - 215 - 17).contains(&self.mouse_x) && (23..45).contains(&self.mouse_y) { + if (283..self.window_width - 215 - 17 - 16 - 16).contains(&self.mouse_x) && (23..45).contains(&self.mouse_y) { self.search_box.select(self.mouse_x - 283, button); true } else { @@ -1968,6 +1982,11 @@ impl Workbench { self.search_box.post_input((self.window_width, self.window_height)); return true; } + SearchBoxKeyResult::ClearAllBookmarks => { + tab.bookmarks.clear(); + self.search_box.post_input((self.window_width, self.window_height)); + return true; + } result @ (SearchBoxKeyResult::Finish | SearchBoxKeyResult::FinishCountOnly) => { self.search_box.search(&mut tab.bookmarks, &mut tab.value, result == SearchBoxKeyResult::FinishCountOnly); self.search_box.post_input((self.window_width, self.window_height)); @@ -2067,6 +2086,10 @@ impl Workbench { // }); // return true; // } + if key == KeyCode::KeyF && flags == flags!(Ctrl) { + self.search_box.select(0, MouseButton::Left); + return true; + } if key == KeyCode::KeyV && flags == flags!(Ctrl) && let Some(element) = get_clipboard().and_then(|x| NbtElement::from_str(&x, self.sort_algorithm)) && (element.1.id() != NbtChunk::ID || tab.value.id() == NbtRegion::ID) { let old_held_entry = core::mem::replace(&mut self.held_entry, HeldEntry::FromAether(element)); let HeldEntry::FromAether(pair) = core::mem::replace(&mut self.held_entry, old_held_entry) else { @@ -2091,7 +2114,7 @@ impl Workbench { self.held_entry = HeldEntry::Empty; return true; } - { + if flags == flags!(Ctrl) { if key == KeyCode::Digit1 { self.set_tab(0, window_properties); return true; @@ -2129,6 +2152,44 @@ impl Workbench { return true; } } + if flags == flags!(Alt) { + let id = if key == KeyCode::Digit1 { + NbtByte::ID + } else if key == KeyCode::Digit2 { + NbtShort::ID + } else if key == KeyCode::Digit3 { + NbtInt::ID + } else if key == KeyCode::Digit4 { + NbtLong::ID + } else if key == KeyCode::Digit5 { + NbtFloat::ID + } else if key == KeyCode::Digit6 { + NbtDouble::ID + } else if key == KeyCode::Digit7 { + NbtByteArray::ID + } else if key == KeyCode::Digit8 { + NbtIntArray::ID + } else if key == KeyCode::Digit9 { + NbtLongArray::ID + } else if key == KeyCode::Digit0 { + NbtString::ID + } else if key == KeyCode::Minus { + NbtList::ID + } else if key == KeyCode::Equal { + NbtCompound::ID + } else { + 0 + }; + if let Some(element) = NbtElement::from_id(id) { + if let HeldEntry::FromKnown(element, indices) = core::mem::replace(&mut self.held_entry, HeldEntry::FromAether((None, element))) { + tab.append_to_history(WorkbenchAction::Remove { + indices, + element, + }); + } + return true; + } + } 'a: { if key == KeyCode::KeyR && flags == flags!(Ctrl) { if tab.history_changed { @@ -2184,7 +2245,7 @@ impl Workbench { tab.horizontal_scroll = tab.horizontal_scroll(self.held_entry.element()); } } - if key == KeyCode::KeyF && flags == flags!(Alt) { + if key == KeyCode::KeyF && flags == flags!(Ctrl + Shift) { tab.freehand_mode = !tab.freehand_mode; return true; } @@ -2429,7 +2490,7 @@ impl Workbench { builder.draw_texture((0, 26), OPEN_FOLDER_UV, (16, 16)); if (0..16).contains(&ctx.mouse_x) && (26..42).contains(&ctx.mouse_y) { builder.draw_texture((0, 26), SELECTION_UV, (16, 16)); - builder.draw_tooltip(&["Open File"], (self.mouse_x, self.mouse_y)); + builder.draw_tooltip(&["Open File (Ctrl + O)"], (self.mouse_x, self.mouse_y), false); } builder.draw_texture_region_z( (17, 22), @@ -2517,10 +2578,10 @@ impl Workbench { }; builder.draw_texture((offset, 3), uv, (3, 16)); if (offset..offset + 16).contains(&self.mouse_x) && (3..19).contains(&self.mouse_y) { - builder.draw_tooltip(&[tab.value.display_name()], (self.mouse_x, self.mouse_y)); + builder.draw_tooltip(&[tab.value.display_name()], (self.mouse_x, self.mouse_y), false); } offset += 2; - tab.draw_icon(builder, (offset, 3), JUST_OVERLAPPING_BASE_TEXT_Z); + tab.draw_icon(builder, (offset, 2), JUST_OVERLAPPING_BASE_TEXT_Z); offset += 1; builder.draw_texture_region_z( (offset, 3), @@ -2543,8 +2604,11 @@ impl Workbench { (16, 16), ); builder.draw_texture((offset - 16, 3), tab.compression.uv(), (16, 16)); + if (offset - 32..offset - 16).contains(&self.mouse_x) && (3..19).contains(&self.mouse_y) { + builder.draw_tooltip(&["Save"], (self.mouse_x, self.mouse_y), false); + } if (offset - 16..offset).contains(&self.mouse_x) && (3..19).contains(&self.mouse_y) { - builder.draw_tooltip(&[tab.compression.into_str()], (self.mouse_x, self.mouse_y)); + builder.draw_tooltip(&[tab.compression.into_str()], (self.mouse_x, self.mouse_y), false); } offset += 6; } diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..74ffd2323582eb627af9388a8667ecab57073d7c GIT binary patch literal 1146 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G!U;i$lZxy-8q?;Kn_c~qpu?a z!^VE@KZ&eBK4*bPWHAE+-(e7DJf6QI1*nGK)5S5QBJS;-ji*CX8IFJa9p=1bPov@# z#Vswqk{xO$R}!{t?aKb|5a3fyKaI2aUT1mw^P4iDouq&bw$+ah%n6@oS6P4O$8)>iAD7*K&?P6qzQCD5 z!G}1M^L6u$XnX(vZ%(Qo>*TzD|4-^1v((q$B`cU43>b+;8;+*WoIdaQ`uWT2i~gUm z@csGo!95G#$X`Fbb^rd$j;@-2@YZ)u(oEdDcK-AD-!{M32A}V}JSBPkervzkpI%qz z_gtRx`}#h)<(l5}|KIv9xTbb~gE<3-FwrRbuU!Ms=(;zLwI7>2JhyS^!Sn6^pR@4Y zSy6Yf@Wb7w`C_*J?w$_6C$n5rJ+H>xuRZJK=lD~#3FQ4b%4eo#6e|d`k`ny2O-!U`%GbP^TuIZiSyYId`)A_jj zop8n5rI&$rOjPFYyF6vFyS&tL&G7a2CDqmDSl8CfS$1dd@8`f+{(CPTsA#eNdZ3;8 z`FA7#$j2O#W@zCg8m+AotNbp1M}5P#?RxX1muvp+x3lqUfAwIy{jrw;duxBbvdBGk z>-o>=pV$}@nTSTS?N0xx-|+P}&$Vq|KYxjS&P#OYvXGMbKx!L&e@ZKKHJMKdkDmeJ Nd%F6$taD0e0sx~Ai~#@u literal 0 HcmV?d00001 diff --git a/web/index.html b/web/index.html index 2059b85..3072be8 100644 --- a/web/index.html +++ b/web/index.html @@ -1,10 +1,15 @@ + NBT Workbench - NBT Workbench + + + + +