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 8bccc00..8dfa91d 100644 Binary files a/src/assets/atlas.hex and b/src/assets/atlas.hex differ diff --git a/src/assets/build/atlas.png b/src/assets/build/atlas.png index 61ff933..590a8a5 100644 Binary files a/src/assets/build/atlas.png and b/src/assets/build/atlas.png differ diff --git a/src/cli.rs b/src/cli.rs index 98c3f80..b45b35c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,14 +1,14 @@ use std::fmt::Formatter; -use std::fs::{File, OpenOptions, read}; +use std::fs::{File, read}; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; +use compact_str::CompactString; use glob::glob; -use regex::{Regex, RegexBuilder}; -use crate::{error, log, SortAlgorithm, WindowProperties}; +use crate::{create_regex, error, log, SortAlgorithm, WindowProperties}; use crate::elements::element::NbtElement; -use crate::search_box::{SearchBox, SearchPredicate}; +use crate::search_box::{SearchBox, SearchPredicate, SearchPredicateInner}; use crate::tab::FileFormat; use crate::workbench::Workbench; @@ -27,44 +27,24 @@ impl std::fmt::Display for SearchResult { } } -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() -} - -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 0000000..74ffd23 Binary files /dev/null and b/web/favicon.png differ 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 + + + + +