From c271ff85ac18709ee1b3884132f2c16eb5d3c060 Mon Sep 17 00:00:00 2001 From: RTTV Date: Sun, 17 Mar 2024 03:38:29 -0400 Subject: [PATCH] - added cmd for keybinds on mac - added touch interaction for web - added snbt as file type - added find command to cli - added help gui to cli - added reformat command to cli --- Cargo.toml | 4 +- src/cli.rs | 270 ++++++++++++++++++++++++++++++++++++++ src/elements/array.rs | 1 + src/elements/chunk.rs | 2 + src/elements/compound.rs | 19 +++ src/elements/element.rs | 28 +++- src/elements/list.rs | 1 + src/elements/primitive.rs | 2 +- src/elements/string.rs | 6 + src/main.rs | 52 +++++--- src/search_box.rs | 57 +++++--- src/tab.rs | 4 +- src/window.rs | 13 +- src/workbench.rs | 49 ++++--- web/index.html | 16 +-- 15 files changed, 449 insertions(+), 75 deletions(-) create mode 100644 src/cli.rs diff --git a/Cargo.toml b/Cargo.toml index de909b2..a954542 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nbtworkbench" -version = "1.2.1" +version = "1.2.2" edition = "2021" description = "A modern NBT Editor written in Rust designed for performance and efficiency." license-file = "LICENSE" @@ -62,6 +62,8 @@ wgsl-inline = { version = "0.2.0", features = ["minify"] } static_assertions = "1.1.0" anyhow = "1.0.79" lz4_flex = { version = "0.11.2", default-features = false, features = ["std", "nightly"] } +regex = "1.10.3" +glob = "0.3.1" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] cli-clipboard = "0.4.0" diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..98c3f80 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,270 @@ +use std::fmt::Formatter; +use std::fs::{File, OpenOptions, read}; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; + +use glob::glob; +use regex::{Regex, RegexBuilder}; + +use crate::{error, log, SortAlgorithm, WindowProperties}; +use crate::elements::element::NbtElement; +use crate::search_box::{SearchBox, SearchPredicate}; +use crate::tab::FileFormat; +use crate::workbench::Workbench; + +struct SearchResult { + path: PathBuf, + lines: Vec, +} + +impl std::fmt::Display for SearchResult { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Found {n} matches in file {path:?} at line numbers:", n = self.lines.len(), path = self.path)?; + for &line in &self.lines { + writeln!(f, "{line}")?; + } + Ok(()) + } +} + +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 { + 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::>(), + Err(e) => { + error!("Glob error: {e}"); + std::process::exit(1); + } + } +} + +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 + } + }; + + 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) + } +} + +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) { + 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); + let _ = std::io::Write::flush(&mut std::io::stdout()); +} + +#[inline] +#[cfg(not(target_arch = "wasm32"))] +pub fn find() -> ! { + let mut args = std::env::args().collect::>(); + // 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 completed = AtomicU64::new(0); + let total_size = paths.iter().filter_map(file_size).sum::(); + + print!("Searching... (0 / {total_size} bytes) (0.0% complete)"); + let _ = std::io::Write::flush(&mut std::io::stdout()); + let results = std::thread::scope(|s| { + let mut results = Vec::new(); + for path in paths { + results.push(s.spawn(|| 'a: { + let mut workbench = Workbench::new(&mut WindowProperties::Fake); + workbench.tabs.clear(); + + let bytes = match read(&path) { + 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 + } + }; + + let len = bytes.len() as u64; + + 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 + } + + 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); + if !bookmarks.is_empty() { + Some(SearchResult { + path, + lines: bookmarks.into_iter().map(|x| x.true_line_number).collect(), + }) + } else { + None + } + })); + } + + 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)"); + + if results.is_empty() { + log!("No results found.") + } + + for result in results { + log!("{result}") + } + + std::process::exit(0); +} + +#[inline] +#[cfg(not(target_arch = "wasm32"))] +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 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 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 { + s.spawn(|| 'a: { + let mut workbench = Workbench::new(&mut WindowProperties::Fake); + workbench.tabs.clear(); + + let bytes = match read(&path) { + Ok(bytes) => bytes, + Err(e) => { + error!("File read error: {e}"); + increment_progress_bar(&completed, file_size(&path).unwrap_or(0), total_size); + break 'a + } + }; + + let len = bytes.len() as u64; + + 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 + } + + let mut 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)); + + let path = if remap_extension { + path.with_extension(extension) + } else { + path + }; + + if let Err(e) = std::fs::write(path, out) { + error!("File write error: {e}") + } + + increment_progress_bar(&completed, len, total_size); + }); + } + }); + + log!("\rReformatting... ({total_size} / {total_size} bytes) (100.0% complete)"); + + std::process::exit(0); +} diff --git a/src/elements/array.rs b/src/elements/array.rs index eb809a3..adb9df6 100644 --- a/src/elements/array.rs +++ b/src/elements/array.rs @@ -3,6 +3,7 @@ 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, diff --git a/src/elements/chunk.rs b/src/elements/chunk.rs index f3ef117..afc65f9 100644 --- a/src/elements/chunk.rs +++ b/src/elements/chunk.rs @@ -20,6 +20,7 @@ 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, @@ -704,6 +705,7 @@ impl Debug for NbtRegion { #[repr(C)] #[allow(clippy::module_name_repetitions)] +#[derive(PartialEq)] pub struct NbtChunk { inner: Box, last_modified: u32, diff --git a/src/elements/compound.rs b/src/elements/compound.rs index 541c46f..e5882d7 100644 --- a/src/elements/compound.rs +++ b/src/elements/compound.rs @@ -22,6 +22,7 @@ use crate::color::TextColor; #[allow(clippy::module_name_repetitions)] #[repr(C)] +#[derive(PartialEq)] pub struct NbtCompound { pub entries: Box, height: u32, @@ -691,6 +692,24 @@ pub struct CompoundMap { pub entries: Vec, } +impl PartialEq for CompoundMap { + fn eq(&self, other: &Self) -> bool { + 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 { + return false + } + } + + true + } +} + impl Clone for CompoundMap { #[allow(clippy::cast_ptr_alignment)] fn clone(&self) -> Self { diff --git a/src/elements/element.rs b/src/elements/element.rs index 8bc44c6..cc70a99 100644 --- a/src/elements/element.rs +++ b/src/elements/element.rs @@ -68,6 +68,32 @@ pub union NbtElement { id: NbtElementDiscriminant, } +impl PartialEq for NbtElement { + fn eq(&self, other: &Self) -> bool { + if self.id() != other.id() { return false } + + unsafe { + match self.id() { + NbtChunk::ID => self.chunk == other.chunk, + NbtRegion::ID => self.region == other.region, + NbtByte::ID => self.byte == other.byte, + NbtShort::ID => self.short == other.short, + NbtInt::ID => self.int == other.int, + NbtLong::ID => self.long == other.long, + NbtFloat::ID => self.float == other.float, + NbtDouble::ID => self.double == other.double, + NbtByteArray::ID => self.byte_array == other.byte_array, + NbtString::ID => self.string == other.string, + NbtList::ID => self.list == other.list, + NbtCompound::ID => self.compound == other.compound, + NbtIntArray::ID => self.int_array == other.int_array, + NbtLongArray::ID => self.long_array == other.long_array, + _ => core::hint::unreachable_unchecked(), + } + } + } +} + impl Clone for NbtElement { #[inline(never)] fn clone(&self) -> Self { @@ -264,7 +290,7 @@ impl NbtElement { if s.is_empty() { return None } let prefix = s.snbt_string_read().and_then(|(prefix, s2)| { - s2.trim_start().strip_prefix(':').map(|s2| { + s2.trim_start().strip_prefix(':').filter(|s| !s.is_empty()).map(|s2| { s = s2.trim_start(); prefix }) diff --git a/src/elements/list.rs b/src/elements/list.rs index b50a353..923aa8e 100644 --- a/src/elements/list.rs +++ b/src/elements/list.rs @@ -16,6 +16,7 @@ use crate::color::TextColor; #[allow(clippy::module_name_repetitions)] #[repr(C)] +#[derive(PartialEq)] pub struct NbtList { pub elements: Box>, height: u32, diff --git a/src/elements/primitive.rs b/src/elements/primitive.rs index 4aca79e..62361d6 100644 --- a/src/elements/primitive.rs +++ b/src/elements/primitive.rs @@ -4,7 +4,7 @@ macro_rules! primitive { primitive!($uv, $s, $name, $t, $id, |x: $t| x.to_compact_string()); }; ($uv:ident, $s:expr, $name:ident, $t:ty, $id:literal, $compact_format:expr) => { - #[derive(Copy, Clone, Default)] + #[derive(Copy, Clone, Default, PartialEq)] #[repr(transparent)] pub struct $name { pub value: $t, diff --git a/src/elements/string.rs b/src/elements/string.rs index 0953b19..1f8a91d 100644 --- a/src/elements/string.rs +++ b/src/elements/string.rs @@ -19,6 +19,12 @@ pub struct NbtString { pub str: TwentyThree, } +impl PartialEq for NbtString { + fn eq(&self, other: &Self) -> bool { + self.str.as_str() == other.str.as_str() + } +} + impl NbtString { pub const ID: u8 = 8; pub(in crate::elements) fn from_str0(s: &str) -> Option<(&str, Self)> { diff --git a/src/main.rs b/src/main.rs index 71aee90..5fdd643 100644 --- a/src/main.rs +++ b/src/main.rs @@ -92,6 +92,7 @@ mod workbench_action; mod element_action; mod search_box; mod text; +mod cli; #[macro_export] macro_rules! flags { @@ -203,11 +204,11 @@ extern "C" { } pub static mut WORKBENCH: UnsafeCell = UnsafeCell::new(unsafe { Workbench::uninit() }); -pub static mut WINDOW_PROPERTIES: UnsafeCell = UnsafeCell::new(WindowProperties::new(unsafe { core::mem::transmute::<_, Rc>(1_usize) })); +pub static mut WINDOW_PROPERTIES: UnsafeCell = UnsafeCell::new(WindowProperties::Fake); #[cfg(target_arch = "wasm32")] #[wasm_bindgen] -pub fn handle_file(name: String, bytes: Vec) { +pub fn open_file(name: String, bytes: Vec) { use crate::alert::Alert; let workbench = unsafe { WORKBENCH.get_mut() }; @@ -227,7 +228,24 @@ pub fn wasm_main() { } #[cfg(not(target_arch = "wasm32"))] -pub fn main() -> ! { pollster::block_on(window::run()) } +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() { + cli::find() + } else if let Some("reformat") = first_arg.as_deref() { + cli::reformat() + } else if let Some("--version" | "-v") = first_arg.as_deref() { + println!("{}", env!("CARGO_PKG_VERSION")); + std::process::exit(0); + } else if let Some("-?" | "/?" | "--help" | "-h") = first_arg.as_deref() { + println!("{HELP}"); + std::process::exit(0); + } else { + pollster::block_on(window::run()) + } +} /// # Refactor /// * render trees using `RenderLine` struct/enum @@ -239,7 +257,6 @@ pub fn main() -> ! { pollster::block_on(window::run()) } /// # Minor Features /// * open icon for exe ver /// * gear icon to swap toolbar with settings panel -/// * sort entries on file read config /// * make floats either exact or "exact enough" /// * __ctrl + h__, open a playground `nbt` file to help with user interaction (bonus points if I have some way to tell if you haven't used this editor before) /// * [`last_modified`](NbtChunk) field actually gets some impl @@ -456,22 +473,23 @@ pub const fn is_utf8_char_boundary(x: u8) -> bool { (x as i8) >= -0x40 } #[must_use] pub fn is_jump_char_boundary(x: u8) -> bool { b" \t\r\n/\\()\"'-.,:;<>~!@#$%^&*|+=[]{}~?|".contains(&x) } -pub struct WindowProperties { - window: Rc, +pub enum WindowProperties { + Real(Rc), + Fake, } impl WindowProperties { pub const fn new(window: Rc) -> Self { - Self { - window, - } + Self::Real(window) } pub fn window_title(&mut self, title: &str) -> &mut Self { - self.window.set_title(title); - #[cfg(target_arch = "wasm32")] - if let Some(document) = web_sys::window().and_then(|window| window.document()) { - let _ = document.set_title(title); + if let WindowProperties::Real(window) = self { + window.set_title(title); + #[cfg(target_arch = "wasm32")] + if let Some(document) = web_sys::window().and_then(|window| window.document()) { + let _ = document.set_title(title); + } } self } @@ -480,8 +498,10 @@ impl WindowProperties { pub fn focus(&mut self) -> &mut Self { use winit::platform::web::WindowExtWebSys; - if let Some(canvas) = self.window.canvas() { - let _ = canvas.focus(); + if let WindowProperties::Real(window) = self { + if let Some(canvas) = window.canvas() { + let _ = canvas.focus(); + } } self } @@ -1115,7 +1135,7 @@ impl StrExt for str { let end_idx = self .char_indices() .find(|(_, c)| !valid_unescaped_char(*c as u8)) - .map(|(idx, _)| idx)?; + .map_or(self.len(), |(idx, _)| idx); let (s, s2) = unsafe { ( self.get_unchecked(..end_idx), diff --git a/src/search_box.rs b/src/search_box.rs index de454d3..0488e24 100644 --- a/src/search_box.rs +++ b/src/search_box.rs @@ -1,5 +1,6 @@ use std::ops::{Deref, DerefMut}; use std::time::Duration; +use regex::Regex; use winit::event::MouseButton; use winit::keyboard::KeyCode; @@ -11,6 +12,29 @@ use crate::elements::element::NbtElement; use crate::text::{Cachelike, SearchBoxKeyResult, Text}; use crate::vertex_buffer_builder::{Vec2u, VertexBufferBuilder}; +#[derive(Debug)] +pub enum SearchPredicate { + String(String), + Regex(Regex), + Snbt(Option, NbtElement), +} + +impl SearchPredicate { + fn matches(&self, key: Option<&str>, value: &NbtElement) -> bool { + match self { + Self::String(str) => { + let (value, color) = value.value(); + (color != TextColor::TreeKey && value.contains(str)) || key.is_some_and(|k| k.contains(str)) + } + Self::Regex(regex) => { + let (value, color) = value.value(); + color != TextColor::TreeKey && regex.is_match(&value) + } + Self::Snbt(k, element) => k.as_ref().is_some_and(|k| key.is_some_and(|key| key == k)) || value == element + } + } +} + #[derive(Clone, Eq)] pub struct SearchBoxCache { value: String, @@ -168,26 +192,29 @@ impl SearchBox { } #[inline] - pub fn search(&mut self, bookmarks: &mut Vec, root: &mut NbtElement, count_only: bool) { + 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 start = since_epoch(); + let new_bookmarks = Self::search0(root, &predicate); + self.hits = Some((new_bookmarks.len(), since_epoch() - start)); + if !count_only { + let old_bookmarks = core::mem::replace(bookmarks, vec![]); + *bookmarks = combined_two_sorted(new_bookmarks.into_boxed_slice(), old_bookmarks.into_boxed_slice()); + } + } + + pub fn search0(root: &NbtElement, predicate: &SearchPredicate) -> Vec { let mut new_bookmarks = Vec::new(); let mut queue = Vec::new(); queue.push((None, &*root, true)); let mut true_line_number = 1; let mut line_number = 0; while let Some((key, element, parent_open)) = queue.pop() { - let (value, color) = element.value(); - let value = if color == TextColor::TreeKey { - None - } else { - Some(value) - }; - - if self.matches(key, value.as_deref()) { + if predicate.matches(key, element) { let mut bookmark = Bookmark::new(true_line_number, line_number); bookmark.uv = if parent_open { BOOKMARK_UV } else { HIDDEN_BOOKMARK_UV }; new_bookmarks.push(bookmark); @@ -208,17 +235,7 @@ impl SearchBox { line_number += 1; } } - - self.hits = Some((new_bookmarks.len(), since_epoch() - start)); - if !count_only { - let old_bookmarks = core::mem::replace(bookmarks, vec![]); - *bookmarks = combined_two_sorted(new_bookmarks.into_boxed_slice(), old_bookmarks.into_boxed_slice()); - } - } - - #[inline] - pub fn matches(&self, key: Option<&str>, value: Option<&str>) -> bool { - key.is_some_and(|key| key.contains(&self.value)) || value.is_some_and(|value| value.contains(&self.value)) + new_bookmarks } #[inline] diff --git a/src/tab.rs b/src/tab.rs index ed5fdfb..9e40a86 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -73,7 +73,7 @@ impl Tab { if self.value.id() == NbtRegion::ID { builder = builder.add_filter("Region File", &["mca", "mcr"]); } else { - builder = builder.add_filter("NBT File", &["nbt", "dat", "dat_old", "dat_mcr", "old"]); + builder = builder.add_filter("NBT File", &["nbt", "snbt", "dat", "dat_old", "dat_mcr", "old"]); } let path = builder.show_save_single_file()?.ok_or_else(|| anyhow!("Save cancelled"))?; self.name = path.file_name().and_then(|x| x.to_str()).expect("Path has a filename").to_string().into_boxed_str(); @@ -541,7 +541,7 @@ impl Tab { } } -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[repr(u8)] pub enum FileFormat { Nbt, diff --git a/src/window.rs b/src/window.rs index 46ec66b..9929a2d 100644 --- a/src/window.rs +++ b/src/window.rs @@ -63,6 +63,7 @@ pub async fn run() -> ! { Some((document, PhysicalSize::new(width as u32, height as u32))) }).and_then(|(document, size)| { let canvas = web_sys::HtmlElement::from(window.canvas()?); + canvas.set_id("canvas"); document.body()?.append_child(&canvas).ok()?; let _ = window.request_inner_size(size); let _ = canvas.focus(); @@ -489,7 +490,17 @@ impl<'window> State<'window> { WindowEvent::MouseInput { state, button, .. } => workbench.on_mouse_input(*state, *button, window_properties), WindowEvent::TouchpadPressure { .. } => false, WindowEvent::AxisMotion { .. } => false, - WindowEvent::Touch(_) => false, + WindowEvent::Touch(touch) => match touch.phase { + TouchPhase::Started => { + workbench.on_mouse_move(touch.location); + workbench.on_mouse_input(ElementState::Pressed, MouseButton::Left, window_properties) + } + TouchPhase::Moved => workbench.on_mouse_move(touch.location), + TouchPhase::Ended | TouchPhase::Cancelled => { + workbench.on_mouse_move(touch.location); + workbench.on_mouse_input(ElementState::Released, MouseButton::Left, window_properties) + } + }, WindowEvent::ScaleFactorChanged { .. } => false, WindowEvent::ThemeChanged(_) => false, WindowEvent::Ime(_) => false, diff --git a/src/workbench.rs b/src/workbench.rs index 209c3b2..3608fa1 100644 --- a/src/workbench.rs +++ b/src/workbench.rs @@ -1,5 +1,3 @@ -use anyhow::{anyhow, Context, Result}; - use std::convert::identity; use std::ffi::OsStr; use std::fmt::Write; @@ -10,7 +8,8 @@ use std::string::String; use std::sync::mpsc::TryRecvError; use std::time::Duration; -use compact_str::{format_compact, CompactString, ToCompactString}; +use anyhow::{anyhow, Context, Result}; +use compact_str::{CompactString, format_compact, ToCompactString}; use fxhash::{FxBuildHasher, FxHashSet}; use uuid::Uuid; use winit::dpi::PhysicalPosition; @@ -18,32 +17,32 @@ use winit::event::{ElementState, KeyEvent, MouseButton, MouseScrollDelta}; use winit::keyboard::{KeyCode, PhysicalKey}; use zune_inflate::DeflateDecoder; +use crate::{Bookmark, DropFn, encompasses, encompasses_or_equal, FileUpdateSubscription, FileUpdateSubscriptionType, flags, get_clipboard, HeldEntry, LinkedQueue, OptionExt, panic_unchecked, Position, recache_along_indices, RenderContext, set_clipboard, since_epoch, SortAlgorithm, StrExt, sum_indices, tab, tab_mut, WindowProperties}; use crate::alert::Alert; use crate::assets::{ACTION_WHEEL_Z, BACKDROP_UV, BASE_TEXT_Z, BASE_Z, BOOKMARK_UV, CLOSED_WIDGET_UV, DARK_STRIPE_UV, EDITED_UV, HEADER_SIZE, HELD_ENTRY_Z, HIDDEN_BOOKMARK_UV, HORIZONTAL_SEPARATOR_UV, HOVERED_STRIPE_UV, HOVERED_WIDGET_UV, JUST_OVERLAPPING_BASE_TEXT_Z, LIGHT_STRIPE_UV, LINE_NUMBER_SEPARATOR_UV, OPEN_FOLDER_UV, SELECTED_ACTION_WHEEL, SELECTED_WIDGET_UV, SELECTION_UV, TRAY_UV, UNEDITED_UV, UNSELECTED_ACTION_WHEEL, UNSELECTED_WIDGET_UV}; use crate::color::TextColor; use crate::elements::chunk::{NbtChunk, NbtRegion}; use crate::elements::compound::NbtCompound; -use crate::elements::element::NbtElement; use crate::elements::element::{NbtByte, NbtByteArray, NbtDouble, NbtFloat, NbtInt, NbtIntArray, NbtLong, NbtLongArray, NbtShort}; +use crate::elements::element::NbtElement; use crate::elements::list::{NbtList, ValueIterator}; use crate::elements::string::NbtString; +use crate::search_box::SearchBox; use crate::selected_text::{SelectedText, SelectedTextAdditional}; -use crate::text::{SearchBoxKeyResult, SelectedTextKeyResult, Text}; use crate::tab::{FileFormat, Tab}; +use crate::text::{SearchBoxKeyResult, SelectedTextKeyResult, Text}; use crate::tree_travel::{Navigate, Traverse, TraverseParents}; use crate::vertex_buffer_builder::Vec2u; use crate::vertex_buffer_builder::VertexBufferBuilder; use crate::window::{MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_WIDTH}; use crate::workbench_action::WorkbenchAction; -use crate::{encompasses, encompasses_or_equal, flags, panic_unchecked, recache_along_indices, sum_indices, Bookmark, DropFn, FileUpdateSubscription, FileUpdateSubscriptionType, HeldEntry, LinkedQueue, OptionExt, Position, RenderContext, StrExt, WindowProperties, tab, tab_mut, get_clipboard, set_clipboard, since_epoch, SortAlgorithm}; -use crate::search_box::SearchBox; pub struct Workbench { pub tabs: Vec, pub tab: usize, - raw_mouse_x: usize, + raw_mouse_x: f64, mouse_x: usize, - raw_mouse_y: usize, + raw_mouse_y: f64, mouse_y: usize, pub window_height: usize, raw_window_height: usize, @@ -72,9 +71,9 @@ impl Workbench { Self { tabs: vec![], tab: 0, - raw_mouse_x: 0, + raw_mouse_x: 0.0, mouse_x: 0, - raw_mouse_y: 0, + raw_mouse_y: 0.0, mouse_y: 0, window_height: 0, raw_window_height: 0, @@ -103,9 +102,9 @@ impl Workbench { let mut workbench = Self { tabs: vec![], tab: 0, - raw_mouse_x: 0, + raw_mouse_x: 0.0, mouse_x: 0, - raw_mouse_y: 0, + raw_mouse_y: 0.0, mouse_y: 0, window_height: WINDOW_HEIGHT, raw_window_height: WINDOW_HEIGHT, @@ -235,7 +234,7 @@ impl Workbench { (pos.x as f32, pos.y as f32) } }; - let ctrl = self.held_keys.contains(&KeyCode::ControlLeft) | self.held_keys.contains(&KeyCode::ControlRight); + let ctrl = self.held_keys.contains(&KeyCode::ControlLeft) | self.held_keys.contains(&KeyCode::ControlRight) | self.held_keys.contains(&KeyCode::SuperLeft) | self.held_keys.contains(&KeyCode::SuperRight); if ctrl { self.set_scale(self.scale.wrapping_add(v.signum() as isize as usize)); return true; @@ -1250,7 +1249,7 @@ impl Workbench { #[inline] fn open_file(&mut self, window_properties: &mut WindowProperties) { #[cfg(target_os = "windows")] { - match native_dialog::FileDialog::new().set_location("~/Downloads").add_filter("NBT File", &["nbt", "dat", "dat_old", "dat_mcr", "old"]).add_filter("Region File", &["mca", "mcr"]).show_open_single_file() { + 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) => {}, Ok(Some(path)) => match std::fs::read(&path) { @@ -1955,7 +1954,7 @@ impl Workbench { if let PhysicalKey::Code(key) = key.physical_key { self.held_keys.insert(key); let char = self.char_from_key(key); - let flags = (self.held_keys.contains(&KeyCode::ControlLeft) as u8 | self.held_keys.contains(&KeyCode::ControlRight) as u8) | ((self.held_keys.contains(&KeyCode::ShiftLeft) as u8 | self.held_keys.contains(&KeyCode::ShiftRight) as u8) << 1) | ((self.held_keys.contains(&KeyCode::AltLeft) as u8 | self.held_keys.contains(&KeyCode::AltRight) as u8) << 2); + let flags = (self.held_keys.contains(&KeyCode::ControlLeft) as u8 | self.held_keys.contains(&KeyCode::ControlRight) as u8 | self.held_keys.contains(&KeyCode::SuperLeft) as u8 | self.held_keys.contains(&KeyCode::SuperRight) as u8) | ((self.held_keys.contains(&KeyCode::ShiftLeft) as u8 | self.held_keys.contains(&KeyCode::ShiftRight) as u8) << 1) | ((self.held_keys.contains(&KeyCode::AltLeft) as u8 | self.held_keys.contains(&KeyCode::AltRight) as u8) << 2); let left_margin = self.left_margin(); let tab = tab_mut!(self); if self.search_box.is_selected() { @@ -2274,10 +2273,10 @@ impl Workbench { #[inline] pub fn on_mouse_move(&mut self, pos: PhysicalPosition) -> bool { - self.raw_mouse_x = pos.x as usize; - self.raw_mouse_y = pos.y as usize; - self.mouse_x = self.raw_mouse_x / self.scale; - self.mouse_y = self.raw_mouse_y / self.scale; + self.raw_mouse_x = pos.x; + self.raw_mouse_y = pos.y; + self.mouse_x = (self.raw_mouse_x / self.scale as f64) as usize; + self.mouse_y = (self.raw_mouse_y / self.scale as f64) as usize; let mouse_y = self.mouse_y; let tab = tab_mut!(self); if let Some(scrollbar_offset) = self.scrollbar_offset && mouse_y >= HEADER_SIZE { @@ -2300,8 +2299,8 @@ impl Workbench { let height_scaling = window_height as f64 / self.raw_window_height as f64; self.raw_window_width = window_width; self.raw_window_height = window_height; - self.raw_mouse_x = (self.raw_mouse_x as f64 * width_scaling).floor() as usize; - self.raw_mouse_y = (self.raw_mouse_y as f64 * height_scaling).floor() as usize; + self.raw_mouse_x = self.raw_mouse_x * width_scaling; + self.raw_mouse_y = self.raw_mouse_y * height_scaling; self.set_scale(self.scale); } @@ -2310,8 +2309,8 @@ impl Workbench { let scale = scale.min(usize::min(self.raw_window_width / MIN_WINDOW_WIDTH, self.raw_window_height / MIN_WINDOW_HEIGHT)).max(1); self.scale = scale; - self.mouse_x = self.raw_mouse_x / self.scale; - self.mouse_y = self.raw_mouse_y / self.scale; + self.mouse_x = (self.raw_mouse_x / self.scale as f64) as usize; + self.mouse_y = (self.raw_mouse_y / self.scale as f64) as usize; self.window_width = self.raw_window_width / self.scale; self.window_height = self.raw_window_height / self.scale; for tab in &mut self.tabs { @@ -2671,7 +2670,7 @@ impl Workbench { clippy::too_many_lines )] fn char_from_key(&self, key: KeyCode) -> Option { - if self.held_keys.contains(&KeyCode::ControlLeft) || self.held_keys.contains(&KeyCode::ControlRight) { return None } + if self.held_keys.contains(&KeyCode::ControlLeft) | self.held_keys.contains(&KeyCode::ControlRight) | self.held_keys.contains(&KeyCode::SuperLeft) | self.held_keys.contains(&KeyCode::SuperRight) { return None } let shift = self.held_keys.contains(&KeyCode::ShiftLeft) || self.held_keys.contains(&KeyCode::ShiftRight); Some(match key { KeyCode::Digit1 => if shift { '!' } else { '1' }, diff --git a/web/index.html b/web/index.html index e92adbf..2059b85 100644 --- a/web/index.html +++ b/web/index.html @@ -3,11 +3,11 @@ - + NBT Workbench - + -