diff --git a/Cargo.toml b/Cargo.toml index 0ca3c9b..11d93d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nbtworkbench" -version = "1.1.0" +version = "1.2.0" edition = "2021" description = "A modern NBT Editor written in Rust designed for performance and efficiency." license-file = "LICENSE" diff --git a/build.rs b/build.rs index 41f6f8c..64a71a1 100644 --- a/build.rs +++ b/build.rs @@ -6,8 +6,8 @@ use std::fs::write; use std::io::Read; use std::mem::MaybeUninit; -const UNICODE: &[u8] = include_bytes!("src/assets/unicode.hex"); -const ATLAS: &[u8] = include_bytes!(r"src/assets/atlas.png"); +const UNICODE: &[u8] = include_bytes!("src/assets/build/unicode.hex"); +const ATLAS: &[u8] = include_bytes!(r"src/assets/build/atlas.png"); fn main() { { write(r"src\assets\atlas.hex", zune_png::PngDecoder::new(ATLAS).decode_raw().unwrap()).unwrap(); } @@ -37,14 +37,14 @@ fn main() { write(r"src/assets/unicode.hex.zib", &buf).unwrap(); } - if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") { + if std::env::var("CARGO_CFG_TARGET_OS").unwrap() == "windows" { if let Err(e) = winres::WindowsResource::new() - .set_icon_with_id("src/assets/icon_16.ico", "16") - .set_icon_with_id("src/assets/icon_32.ico", "32") - .set_icon_with_id("src/assets/icon_48.ico", "48") - .set_icon_with_id("src/assets/icon_64.ico", "64") - .set_icon_with_id("src/assets/icon_128.ico", "128") - .set_icon_with_id("src/assets/icon_256.ico", "!") + .set_icon_with_id(r"src/assets/build/icon_256.ico", "1") + .set_icon_with_id(r"src/assets/build/icon_128.ico", "2") + .set_icon_with_id(r"src/assets/build/icon_64.ico", "3") + .set_icon_with_id(r"src/assets/build/icon_48.ico", "4") + .set_icon_with_id(r"src/assets/build/icon_32.ico", "5") + .set_icon_with_id(r"src/assets/build/icon_16.ico", "6") .compile() { eprintln!("Error! {e}"); diff --git a/clipboard.js b/clipboard.js index 1bdcacd..bcfd33c 100644 --- a/clipboard.js +++ b/clipboard.js @@ -29,9 +29,8 @@ export function getClipboard() { } export function onInput() { - // is not firefox - if (typeof InstallTrigger === 'undefined') { - window.navigator.clipboard.readText().then((str) => clipboard = str) + if (!navigator.userAgent.toLowerCase().includes("firefox")) { + window.navigator.clipboard.readText().then((str) => clipboard = str).catch(x => x) } } diff --git a/src/alert.rs b/src/alert.rs index 8c6ef26..02b63d0 100644 --- a/src/alert.rs +++ b/src/alert.rs @@ -2,7 +2,7 @@ use crate::assets::{ALERT_TEXT_Z, ALERT_UV, ALERT_Z, HEADER_SIZE}; use crate::color::TextColor; use crate::vertex_buffer_builder::{Vec2u, VertexBufferBuilder}; use crate::{since_epoch, smoothstep64, StrExt}; -use std::time::{Duration, Instant}; +use std::time::Duration; pub struct Alert { timestamp: Option, @@ -71,15 +71,15 @@ impl Alert { } pub fn is_invisible(&mut self) -> bool { - let ms = (since_epoch() - *self.timestamp.get_or_insert(since_epoch())).as_millis() as usize; - let display_time = (self.message.len() + self.title.len()) * 200 / 3 + 5000; + let ms = since_epoch().saturating_sub(*self.timestamp.get_or_insert(since_epoch())).as_millis() as usize; + let display_time = (self.message.len() + self.title.len()) * 60 + 3000; ms > 500 + display_time } fn get_inset(&mut self) -> usize { - let mut ms = (since_epoch() - *self.timestamp.get_or_insert(since_epoch())).as_millis() as usize; + let mut ms = since_epoch().saturating_sub(*self.timestamp.get_or_insert(since_epoch())).as_millis() as usize; let width = self.width + 24; - let display_time = (self.message.len() + self.title.len()) * 200 / 3 + 5000; + let display_time = (self.message.len() + self.title.len()) * 60 + 3000; if ms < 250 { return (smoothstep64((250 - ms) as f64 / 250.0) * width as f64) as usize } ms -= 250; if ms < display_time { diff --git a/src/assets.rs b/src/assets.rs index a363935..ba5c3b2 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -1,6 +1,5 @@ use std::mem::ManuallyDrop; -use crate::{since_epoch, log}; -use crate::color::TextColor; +use crate::since_epoch; use crate::vertex_buffer_builder::Vec2u; @@ -14,14 +13,14 @@ pub const UNICODE_LEN: usize = 1_818_624; pub const ICON_WIDTH: usize = 64; pub const ICON_HEIGHT: usize = 64; -const OTHERSIDE_MUSIC_DISC_ICON: &[u8] = include_bytes!("assets/otherside.hex"); -const PIGSTEP_MUSIC_DISC_ICON: &[u8] = include_bytes!("assets/pigstep.hex"); -const MELLOHI_MUSIC_DISC_ICON: &[u8] = include_bytes!("assets/mellohi.hex"); -const FIVE_MUSIC_DISC_ICON: &[u8] = include_bytes!("assets/5.hex"); -const WARD_MUSIC_DISC_ICON: &[u8] = include_bytes!("assets/ward.hex"); -const ELEVEN_MUSIC_DISC_ICON: &[u8] = include_bytes!("assets/11.hex"); -const RELIC_MUSIC_DISC_ICON: &[u8] = include_bytes!("assets/relic.hex"); -const STAL_MUSIC_DISC_ICON: &[u8] = include_bytes!("assets/stal.hex"); +const OTHERSIDE_MUSIC_DISC_ICON: &[u8] = include_bytes!("assets/discs/otherside.hex"); +const PIGSTEP_MUSIC_DISC_ICON: &[u8] = include_bytes!("assets/discs/pigstep.hex"); +const MELLOHI_MUSIC_DISC_ICON: &[u8] = include_bytes!("assets/discs/mellohi.hex"); +const FIVE_MUSIC_DISC_ICON: &[u8] = include_bytes!("assets/discs/5.hex"); +const WARD_MUSIC_DISC_ICON: &[u8] = include_bytes!("assets/discs/ward.hex"); +const ELEVEN_MUSIC_DISC_ICON: &[u8] = include_bytes!("assets/discs/11.hex"); +const RELIC_MUSIC_DISC_ICON: &[u8] = include_bytes!("assets/discs/relic.hex"); +const STAL_MUSIC_DISC_ICON: &[u8] = include_bytes!("assets/discs/stal.hex"); pub const CONNECTION_UV: Vec2u = Vec2u::new(64, 64); pub const UNKNOWN_NBT_UV: Vec2u = Vec2u::new(112, 32); @@ -60,10 +59,10 @@ pub const ZLIB_FILE_TYPE_UV: Vec2u = Vec2u::new(64, 80); pub const SNBT_FILE_TYPE_UV: Vec2u = Vec2u::new(80, 80); pub const MCA_FILE_TYPE_UV: Vec2u = Vec2u::new(96, 80); pub const OPEN_FOLDER_UV: Vec2u = Vec2u::new(112, 80); -pub const UNSELECTED_TOGGLE_OFF_UV: Vec2u = Vec2u::new(0, 64); -pub const UNSELECTED_TOGGLE_ON_UV: Vec2u = Vec2u::new(8, 64); -pub const SELECTED_TOGGLE_OFF_UV: Vec2u = Vec2u::new(0, 72); -pub const SELECTED_TOGGLE_ON_UV: Vec2u = Vec2u::new(8, 72); +pub const UNSELECTED_TOGGLE_ON_UV: Vec2u = Vec2u::new(0, 64); +pub const UNSELECTED_TOGGLE_OFF_UV: Vec2u = Vec2u::new(8, 64); +pub const SELECTED_TOGGLE_ON_UV: Vec2u = Vec2u::new(0, 72); +pub const SELECTED_TOGGLE_OFF_UV: Vec2u = Vec2u::new(8, 72); pub const UNHELD_SCROLLBAR_UV: Vec2u = Vec2u::new(48, 64); pub const HELD_SCROLLBAR_UV: Vec2u = Vec2u::new(54, 64); pub const REMOVE_UV: Vec2u = Vec2u::new(0, 96); @@ -94,13 +93,17 @@ pub const HOVERED_STRIPE_UV: Vec2u = Vec2u::new(112, 128); pub const INVALID_STRIPE_UV: Vec2u = Vec2u::new(112, 112); pub const COPY_RAW_UV: Vec2u = Vec2u::new(3, 131); pub const COPY_FORMATTED_UV: Vec2u = Vec2u::new(19, 131); +#[cfg(not(target_arch = "wasm32"))] pub const OPEN_ARRAY_IN_HEX_UV: Vec2u = Vec2u::new(35, 131); +#[cfg(not(target_arch = "wasm32"))] 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 FREEHAND_MODE_UV: Vec2u = Vec2u::new(0, 144); pub const ENABLED_FREEHAND_MODE_UV: Vec2u = Vec2u::new(16, 144); -pub const STEAL_ANIMATION_OVERLAY: Vec2u = Vec2u::new(64, 144); +pub const STEAL_ANIMATION_OVERLAY_UV: Vec2u = Vec2u::new(64, 144); +pub const STAMP_BACKDROP_UV: Vec2u = Vec2u::new(16, 160); pub const BYTE_UV: Vec2u = Vec2u::new(0, 0); pub const SHORT_UV: Vec2u = Vec2u::new(16, 0); @@ -130,7 +133,6 @@ pub const LIST_GHOST_UV: Vec2u = Vec2u::new(32, 48); pub const COMPOUND_GHOST_UV: Vec2u = Vec2u::new(48, 48); pub const INT_ARRAY_GHOST_UV: Vec2u = Vec2u::new(112, 16); pub const LONG_ARRAY_GHOST_UV: Vec2u = Vec2u::new(0, 48); -pub const REGION_GHOST_UV: Vec2u = Vec2u::new(96, 48); pub const CHUNK_GHOST_UV: Vec2u = Vec2u::new(64, 48); pub const ALERT_UV: Vec2u = Vec2u::new(112, 144); @@ -204,7 +206,7 @@ pub fn icon() -> Vec { } let mut scaled = ManuallyDrop::new(core::hint::black_box(scaled)); #[cfg(all(debug_assertions, not(target_arch = "wasm32")))] - log!( + crate::log!( "took {} cycles", unsafe { core::arch::x86_64::_rdtsc() } - start ); diff --git a/src/assets/atlas.hex b/src/assets/atlas.hex index c118463..38480bf 100644 Binary files a/src/assets/atlas.hex and b/src/assets/atlas.hex differ diff --git a/src/assets/atlas.png b/src/assets/atlas.png deleted file mode 100644 index 846eb74..0000000 Binary files a/src/assets/atlas.png and /dev/null differ diff --git a/src/assets/build/atlas.png b/src/assets/build/atlas.png new file mode 100644 index 0000000..9282928 Binary files /dev/null and b/src/assets/build/atlas.png differ diff --git a/src/assets/icon_128.ico b/src/assets/build/icon_128.ico similarity index 100% rename from src/assets/icon_128.ico rename to src/assets/build/icon_128.ico diff --git a/src/assets/icon_16.ico b/src/assets/build/icon_16.ico similarity index 100% rename from src/assets/icon_16.ico rename to src/assets/build/icon_16.ico diff --git a/src/assets/icon_256.ico b/src/assets/build/icon_256.ico similarity index 100% rename from src/assets/icon_256.ico rename to src/assets/build/icon_256.ico diff --git a/src/assets/icon_32.ico b/src/assets/build/icon_32.ico similarity index 100% rename from src/assets/icon_32.ico rename to src/assets/build/icon_32.ico diff --git a/src/assets/icon_48.ico b/src/assets/build/icon_48.ico similarity index 100% rename from src/assets/icon_48.ico rename to src/assets/build/icon_48.ico diff --git a/src/assets/icon_64.ico b/src/assets/build/icon_64.ico similarity index 100% rename from src/assets/icon_64.ico rename to src/assets/build/icon_64.ico diff --git a/src/assets/unicode.hex b/src/assets/build/unicode.hex similarity index 100% rename from src/assets/unicode.hex rename to src/assets/build/unicode.hex diff --git a/src/assets/11.hex b/src/assets/discs/11.hex similarity index 100% rename from src/assets/11.hex rename to src/assets/discs/11.hex diff --git a/src/assets/5.hex b/src/assets/discs/5.hex similarity index 100% rename from src/assets/5.hex rename to src/assets/discs/5.hex diff --git a/src/assets/mellohi.hex b/src/assets/discs/mellohi.hex similarity index 100% rename from src/assets/mellohi.hex rename to src/assets/discs/mellohi.hex diff --git a/src/assets/otherside.hex b/src/assets/discs/otherside.hex similarity index 100% rename from src/assets/otherside.hex rename to src/assets/discs/otherside.hex diff --git a/src/assets/pigstep.hex b/src/assets/discs/pigstep.hex similarity index 100% rename from src/assets/pigstep.hex rename to src/assets/discs/pigstep.hex diff --git a/src/assets/relic.hex b/src/assets/discs/relic.hex similarity index 100% rename from src/assets/relic.hex rename to src/assets/discs/relic.hex diff --git a/src/assets/stal.hex b/src/assets/discs/stal.hex similarity index 100% rename from src/assets/stal.hex rename to src/assets/discs/stal.hex diff --git a/src/assets/ward.hex b/src/assets/discs/ward.hex similarity index 100% rename from src/assets/ward.hex rename to src/assets/discs/ward.hex diff --git a/src/decoder.rs b/src/decoder.rs index b7f8654..bf3db3c 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -1,23 +1,32 @@ use std::intrinsics::likely; use compact_str::CompactString; +use crate::elements::compound::CompoundMap; +use crate::SortAlgorithm; pub struct Decoder { pub data: *const u8, end: *const u8, + sort: SortAlgorithm, } #[allow(improper_ctypes_definitions)] impl Decoder { #[inline] #[optimize(speed)] - pub const fn new(data: &[u8]) -> Self { + pub const fn new(data: &[u8], sort: SortAlgorithm) -> Self { Self { end: unsafe { data.as_ptr().add(data.len()) }, data: data.as_ptr(), + sort, } } + #[inline] + pub fn sort(&self, map: &mut CompoundMap) { + self.sort.sort(map) + } + #[inline] #[optimize(speed)] #[must_use] diff --git a/src/element_action.rs b/src/element_action.rs index 2fa9e97..b171191 100644 --- a/src/element_action.rs +++ b/src/element_action.rs @@ -2,14 +2,17 @@ use std::cmp::Ordering; use std::convert::identity; #[cfg(not(target_arch = "wasm32"))] use std::fs::OpenOptions; +#[cfg(not(target_arch = "wasm32"))] use std::process::Command; use compact_str::CompactString; +#[cfg(not(target_arch = "wasm32"))] use notify::{EventKind, PollWatcher, RecursiveMode, Watcher}; use uuid::Uuid; -use crate::{Bookmark, panic_unchecked, set_clipboard}; -use crate::{FileUpdateSubscription, FileUpdateSubscriptionType, assets::{OPEN_ARRAY_IN_HEX_UV, OPEN_IN_TXT}}; +use crate::{Bookmark, panic_unchecked, set_clipboard, FileUpdateSubscription}; +#[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}; use crate::elements::chunk::NbtChunk; use crate::elements::compound::NbtCompound; @@ -76,9 +79,42 @@ impl ElementAction { } } + #[must_use] + pub fn by_name(a: (&str, &NbtElement), b: (&str, &NbtElement)) -> Ordering { + let (a_str, _) = a; + let (b_str, _) = b; + a_str.cmp(b_str) + } + + #[must_use] + pub fn by_type(a: (&str, &NbtElement), b: (&str, &NbtElement)) -> Ordering { + const ORDERING: [usize; 256] = { + let mut array = [usize::MAX; 256]; + array[NbtChunk::ID as usize] = 0; + array[NbtCompound::ID as usize] = 1; + array[NbtList::ID as usize] = 2; + array[NbtLongArray::ID as usize] = 3; + array[NbtIntArray::ID as usize] = 4; + array[NbtByteArray::ID as usize] = 5; + array[NbtString::ID as usize] = 6; + array[NbtDouble::ID as usize] = 7; + array[NbtFloat::ID as usize] = 8; + array[NbtLong::ID as usize] = 9; + array[NbtInt::ID as usize] = 10; + array[NbtShort::ID as usize] = 11; + array[NbtByte::ID as usize] = 12; + array + }; + + let (a_str, a_nbt) = a; + let (b_str, b_nbt) = b; + ORDERING[a_nbt.id() as usize].cmp(&ORDERING[b_nbt.id() as usize]).then_with(|| a_str.cmp(b_str)) + } + #[allow(clippy::too_many_lines)] pub fn apply(self, key: Option, indices: Box<[usize]>, tab_uuid: Uuid, true_line_number: usize, line_number: usize, element: &mut NbtElement, bookmarks: &mut Vec, subscription: &mut Option) -> Option { #[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() @@ -89,38 +125,6 @@ impl ElementAction { }.is_ok() } - #[must_use] - fn by_name(a: (&str, &NbtElement), b: (&str, &NbtElement)) -> Ordering { - let (a_str, _) = a; - let (b_str, _) = b; - a_str.cmp(b_str) - } - - #[must_use] - fn by_type(a: (&str, &NbtElement), b: (&str, &NbtElement)) -> Ordering { - const ORDERING: [usize; 256] = { - let mut array = [usize::MAX; 256]; - array[NbtChunk::ID as usize] = 0; - array[NbtCompound::ID as usize] = 1; - array[NbtList::ID as usize] = 2; - array[NbtLongArray::ID as usize] = 3; - array[NbtIntArray::ID as usize] = 4; - array[NbtByteArray::ID as usize] = 5; - array[NbtString::ID as usize] = 6; - array[NbtDouble::ID as usize] = 7; - array[NbtFloat::ID as usize] = 8; - array[NbtLong::ID as usize] = 9; - array[NbtInt::ID as usize] = 10; - array[NbtShort::ID as usize] = 11; - array[NbtByte::ID as usize] = 12; - array - }; - - let (a_str, a_nbt) = a; - let (b_str, b_nbt) = b; - ORDERING[a_nbt.id() as usize].cmp(&ORDERING[b_nbt.id() as usize]).then_with(|| a_str.cmp(b_str)) - } - 'm: { match self { Self::CopyRaw => { @@ -270,9 +274,9 @@ impl ElementAction { let bookmark_end = bookmarks.binary_search(&Bookmark::new(true_line_number + element.true_height() - 1, 0)).map_or_else(identity, |x| x + 1); let bookmark_slice = if bookmark_end > bookmark_start || bookmark_end > bookmarks.len() { &mut [] } else { &mut bookmarks[bookmark_start..bookmark_end] }; let reordering_indices = if let Some(compound) = element.as_compound_mut() { - compound.entries.sort_by(by_name, line_number, true_line_number, true_height, open, bookmark_slice) + compound.entries.sort_by(Self::by_name, line_number, true_line_number, true_height, open, bookmark_slice) } else if let Some(chunk) = element.as_chunk_mut() { - chunk.entries.sort_by(by_name, line_number, true_line_number, true_height, open, bookmark_slice) + chunk.entries.sort_by(Self::by_name, line_number, true_line_number, true_height, open, bookmark_slice) } else { unsafe { panic_unchecked("Unknown element kind for compound sorting") } }; @@ -286,9 +290,9 @@ impl ElementAction { let bookmark_end = bookmarks.binary_search(&Bookmark::new(true_line_number + element.true_height() - 1, 0)).map_or_else(identity, |x| x + 1); let bookmark_slice = if bookmark_end < bookmark_start || bookmark_end > bookmarks.len() { &mut [] } else { &mut bookmarks[bookmark_start..bookmark_end] }; let reordering_indices = if let Some(compound) = element.as_compound_mut() { - compound.entries.sort_by(by_type, line_number, true_line_number, true_height, open, bookmark_slice) + compound.entries.sort_by(Self::by_type, line_number, true_line_number, true_height, open, bookmark_slice) } else if let Some(chunk) = element.as_chunk_mut() { - chunk.entries.sort_by(by_type, line_number, true_line_number, true_height, open, bookmark_slice) + chunk.entries.sort_by(Self::by_type, line_number, true_line_number, true_height, open, bookmark_slice) } else { unsafe { panic_unchecked("Unknown element kind for compound sorting") } }; diff --git a/src/elements/array.rs b/src/elements/array.rs index 05d48f4..eb809a3 100644 --- a/src/elements/array.rs +++ b/src/elements/array.rs @@ -42,7 +42,7 @@ macro_rules! array { pub const ID: u8 = $my_id; #[inline] - pub(in $crate::elements) fn from_str0(mut s: &str) -> Option<(&str, Self)> { + pub(in $crate::elements) fn from_str0(mut s: &str, sort: SortAlgorithm) -> Option<(&str, Self)> { s = s .strip_prefix('[')? .trim_start() @@ -50,7 +50,7 @@ macro_rules! array { .trim_start(); let mut array = Self::new(); while !s.starts_with(']') { - let (s2, element) = NbtElement::from_str0(s)?; + let (s2, element) = NbtElement::from_str0(s, sort)?; array.insert(array.len(), element).ok()?; s = s2.trim_start(); if let Some(s2) = s.strip_prefix(',') { diff --git a/src/elements/chunk.rs b/src/elements/chunk.rs index d2ff3cc..f3ef117 100644 --- a/src/elements/chunk.rs +++ b/src/elements/chunk.rs @@ -3,6 +3,7 @@ use std::fmt::{Debug, Display, Formatter}; use std::intrinsics::likely; use std::mem::{ManuallyDrop, MaybeUninit}; use std::ops::{Deref, DerefMut}; +#[cfg(not(target_arch = "wasm32"))] use std::thread::Scope; use compact_str::{format_compact, CompactString, ToCompactString}; @@ -15,7 +16,7 @@ use crate::elements::list::{ValueIterator, ValueMutIterator}; use crate::encoder::UncheckedBufWriter; use crate::tab::FileFormat; use crate::vertex_buffer_builder::VertexBufferBuilder; -use crate::{DropFn, RenderContext, StrExt}; +use crate::{DropFn, RenderContext, SortAlgorithm, StrExt}; use crate::color::TextColor; #[repr(C)] @@ -76,8 +77,8 @@ impl NbtRegion { pub fn new() -> Self { Self::default() } #[must_use] - pub fn from_bytes(bytes: &[u8]) -> Option { - fn parse(raw: u32, bytes: &[u8]) -> Option<(FileFormat, NbtElement)> { + pub fn from_bytes(bytes: &[u8], sort: SortAlgorithm) -> Option { + fn parse(raw: u32, bytes: &[u8], sort: SortAlgorithm) -> Option<(FileFormat, NbtElement)> { if raw < 512 { return Some((FileFormat::Zlib, unsafe { core::mem::zeroed() })) } let len = (raw as usize & 0xFF) * 4096; @@ -96,6 +97,7 @@ impl NbtRegion { &DeflateDecoder::new_with_options(data, DeflateOptions::default().set_confirm_checksum(false)) .decode_gzip() .ok()?, + sort, )?, ), 2 => ( @@ -104,10 +106,11 @@ impl NbtRegion { &DeflateDecoder::new_with_options(data, DeflateOptions::default().set_confirm_checksum(false)) .decode_zlib() .ok()?, + sort, )?, ), - 3 => (FileFormat::Nbt, NbtElement::from_file(data)?), - 4 => (FileFormat::ChunkLz4, NbtElement::from_file(&lz4_flex::decompress(data, data.len()).ok()?)?), + 3 => (FileFormat::Nbt, NbtElement::from_file(data, sort)?), + 4 => (FileFormat::ChunkLz4, NbtElement::from_file(&lz4_flex::decompress(data, data.len()).ok()?, sort)?), _ => return None, }; if element.id() != NbtCompound::ID { return None } @@ -134,7 +137,7 @@ impl NbtRegion { { let timestamp = u32::from_be_bytes(timestamp); let offset = u32::from_be_bytes(offset); - threads.push((timestamp, s.spawn(move || parse(offset, bytes)))); + threads.push((timestamp, s.spawn(move || parse(offset, bytes, sort)))); } @@ -174,7 +177,7 @@ impl NbtRegion { { let timestamp = u32::from_be_bytes(timestamp); let offset = u32::from_be_bytes(offset); - threads.push((timestamp, parse(offset, bytes))); + threads.push((timestamp, parse(offset, bytes, sort))); } diff --git a/src/elements/compound.rs b/src/elements/compound.rs index 2c10511..cec4804 100644 --- a/src/elements/compound.rs +++ b/src/elements/compound.rs @@ -5,6 +5,7 @@ use std::fmt::{Debug, Display, Formatter, Write}; use std::hash::Hasher; use std::intrinsics::likely; use std::ops::Deref; +#[cfg(not(target_arch = "wasm32"))] use std::thread::Scope; use compact_str::{format_compact, CompactString, ToCompactString}; @@ -16,7 +17,7 @@ use crate::decoder::Decoder; use crate::elements::chunk::NbtChunk; use crate::elements::element::NbtElement; use crate::encoder::UncheckedBufWriter; -use crate::{Bookmark, DropFn, OptionExt, RenderContext, StrExt, VertexBufferBuilder}; +use crate::{Bookmark, DropFn, OptionExt, RenderContext, SortAlgorithm, StrExt, VertexBufferBuilder}; use crate::color::TextColor; #[allow(clippy::module_name_repetitions)] @@ -49,13 +50,13 @@ impl Clone for NbtCompound { impl NbtCompound { pub const ID: u8 = 10; #[optimize(speed)] - pub(in crate::elements) fn from_str0(mut s: &str) -> Option<(&str, Self)> { + pub(in crate::elements) fn from_str0(mut s: &str, sort: SortAlgorithm) -> Option<(&str, Self)> { s = s.strip_prefix('{')?.trim_start(); let mut compound = Self::new(); while !s.starts_with('}') { let (key, s2) = s.snbt_string_read()?; s = s2.trim_start().strip_prefix(':')?.trim_start(); - let (s2, value) = NbtElement::from_str0(s)?; + let (s2, value) = NbtElement::from_str0(s, sort)?; compound.insert_replacing(key, value); s = s2.trim_start(); if let Some(s2) = s.strip_prefix(',') { @@ -65,6 +66,7 @@ impl NbtCompound { } } let s = s.strip_prefix('}')?; + sort.sort(&mut compound.entries); Some((s, compound)) } @@ -85,6 +87,7 @@ impl NbtCompound { }; current_element = decoder.u8(); } + decoder.sort(&mut compound.entries); Some(compound) } } diff --git a/src/elements/element.rs b/src/elements/element.rs index 768d2b1..8bc44c6 100644 --- a/src/elements/element.rs +++ b/src/elements/element.rs @@ -4,6 +4,7 @@ use std::fmt::{Debug, Display, Error, Formatter}; use std::intrinsics::likely; use std::mem::{ManuallyDrop, MaybeUninit}; use std::ops::Deref; +#[cfg(not(target_arch = "wasm32"))] use std::thread::Scope; use std::{fmt, fmt::Write}; @@ -18,10 +19,8 @@ use crate::element_action::ElementAction; use crate::elements::list::{NbtList, ValueIterator, ValueMutIterator}; use crate::elements::string::NbtString; use crate::encoder::UncheckedBufWriter; -use crate::{panic_unchecked, since_epoch}; +use crate::{panic_unchecked, since_epoch, SortAlgorithm, array, primitive, DropFn, RenderContext, StrExt, VertexBufferBuilder, TextColor, assets::JUST_OVERLAPPING_BASE_TEXT_Z}; use crate::tab::FileFormat; -use crate::{array, primitive, DropFn, RenderContext, StrExt, VertexBufferBuilder}; -use crate::{TextColor, assets::JUST_OVERLAPPING_BASE_TEXT_Z}; primitive!(BYTE_UV, { Some('b') }, NbtByte, i8, 1); primitive!(SHORT_UV, { Some('s') }, NbtShort, i16, 2); @@ -259,7 +258,7 @@ impl NbtElement { impl NbtElement { #[must_use] #[allow(clippy::should_implement_trait)] // i can't, sorry :( - pub fn from_str(mut s: &str) -> Option<(Option, Self)> { + pub fn from_str(mut s: &str, sort: SortAlgorithm) -> Option<(Option, Self)> { s = s.trim_start(); if s.is_empty() { return None } @@ -270,20 +269,20 @@ impl NbtElement { prefix }) }); - let (s, element) = Self::from_str0(s).map(|(s, x)| (s.trim_start(), x))?; + let (s, element) = Self::from_str0(s, sort).map(|(s, x)| (s.trim_start(), x))?; if !s.is_empty() { return None } Some((prefix, element)) } #[allow(clippy::too_many_lines)] - pub(in crate::elements) fn from_str0(mut s: &str) -> Option<(&str, Self)> { + pub(in crate::elements) fn from_str0(mut s: &str, sort: SortAlgorithm) -> Option<(&str, Self)> { if let Some(s2) = s.strip_prefix("false") { return Some((s2, Self::Byte(NbtByte { value: 0 }))) } if let Some(s2) = s.strip_prefix("true") { return Some((s2, Self::Byte(NbtByte { value: 1 }))) } - if s.starts_with("[B;") { return NbtByteArray::from_str0(s).map(|(s, x)| (s, Self::ByteArray(x))) } - if s.starts_with("[I;") { return NbtIntArray::from_str0(s).map(|(s, x)| (s, Self::IntArray(x))) } - if s.starts_with("[L;") { return NbtLongArray::from_str0(s).map(|(s, x)| (s, Self::LongArray(x))) } - if s.starts_with('[') { return NbtList::from_str0(s).map(|(s, x)| (s, Self::List(x))) } - if s.starts_with('{') { return NbtCompound::from_str0(s).map(|(s, x)| (s, Self::Compound(x))) } + if s.starts_with("[B;") { return NbtByteArray::from_str0(s, sort).map(|(s, x)| (s, Self::ByteArray(x))) } + if s.starts_with("[I;") { return NbtIntArray::from_str0(s, sort).map(|(s, x)| (s, Self::IntArray(x))) } + if s.starts_with("[L;") { return NbtLongArray::from_str0(s, sort).map(|(s, x)| (s, Self::LongArray(x))) } + if s.starts_with('[') { return NbtList::from_str0(s, sort).map(|(s, x)| (s, Self::List(x))) } + if s.starts_with('{') { return NbtCompound::from_str0(s, sort).map(|(s, x)| (s, Self::Compound(x))) } if s.starts_with('"') { return NbtString::from_str0(s).map(|(s, x)| (s, Self::String(x))) } if let Some(s2) = s.strip_prefix("NaN") { @@ -424,7 +423,7 @@ impl NbtElement { return None; }; s = s[digit_end_idx..].trim_start(); - let (s, inner) = NbtCompound::from_str0(s)?; + let (s, inner) = NbtCompound::from_str0(s, sort)?; ( s, Self::Chunk(NbtChunk::from_compound( @@ -524,8 +523,8 @@ impl NbtElement { #[inline] #[must_use] - pub fn from_file(bytes: &[u8]) -> Option { - let mut decoder = Decoder::new(bytes); + pub fn from_file(bytes: &[u8], sort: SortAlgorithm) -> Option { + let mut decoder = Decoder::new(bytes, sort); decoder.assert_len(3)?; unsafe { if decoder.u8() != 0x0A { return None } @@ -549,8 +548,8 @@ impl NbtElement { #[inline] #[must_use] - pub fn from_mca(bytes: &[u8]) -> Option { - NbtRegion::from_bytes(bytes).map(Self::Region) + pub fn from_mca(bytes: &[u8], sort: SortAlgorithm) -> Option { + NbtRegion::from_bytes(bytes, sort).map(Self::Region) } #[inline] diff --git a/src/elements/list.rs b/src/elements/list.rs index c04d4bc..b50a353 100644 --- a/src/elements/list.rs +++ b/src/elements/list.rs @@ -3,6 +3,7 @@ use std::alloc::{alloc, Layout}; use std::fmt::{Debug, Display, Formatter, Write}; use std::intrinsics::likely; use std::slice::{Iter, IterMut}; +#[cfg(not(target_arch = "wasm32"))] use std::thread::Scope; use crate::assets::{JUST_OVERLAPPING_BASE_TEXT_Z, BASE_Z, CONNECTION_UV, LIST_UV}; @@ -10,7 +11,7 @@ use crate::decoder::Decoder; use crate::elements::chunk::NbtChunk; use crate::elements::element::{id_to_string_name, NbtElement}; use crate::encoder::UncheckedBufWriter; -use crate::{DropFn, OptionExt, RenderContext, StrExt, VertexBufferBuilder}; +use crate::{DropFn, OptionExt, RenderContext, SortAlgorithm, StrExt, VertexBufferBuilder}; use crate::color::TextColor; #[allow(clippy::module_name_repetitions)] @@ -50,11 +51,11 @@ impl Clone for NbtList { impl NbtList { pub const ID: u8 = 9; - pub(in crate::elements) fn from_str0(mut s: &str) -> Option<(&str, Self)> { + pub(in crate::elements) fn from_str0(mut s: &str, sort: SortAlgorithm) -> Option<(&str, Self)> { s = s.strip_prefix('[')?.trim_start(); let mut list = Self::new(vec![], 0); while !s.starts_with(']') { - let (s2, element) = NbtElement::from_str0(s)?; + let (s2, element) = NbtElement::from_str0(s, sort)?; list.insert(list.len(), element).ok()?; s = s2.trim_start(); if let Some(s2) = s.strip_prefix(',') { diff --git a/src/main.rs b/src/main.rs index 665b171..e3bf454 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,25 +49,23 @@ use std::cell::UnsafeCell; use std::cmp::Ordering; use std::convert::identity; -use std::fmt::Write; +use std::fmt::{Display, Formatter, Write}; use std::rc::Rc; -use std::sync::Weak; use std::time::Duration; use compact_str::{CompactString, ToCompactString}; use static_assertions::{const_assert, const_assert_eq}; #[cfg(target_arch = "wasm32")] -use wasm_bindgen::{JsCast, prelude::wasm_bindgen}; +use wasm_bindgen::prelude::wasm_bindgen; use winit::window::Window; use elements::element::NbtElement; use vertex_buffer_builder::VertexBufferBuilder; -use crate::alert::Alert; -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, 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, 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::color::TextColor; use crate::elements::chunk::{NbtChunk, NbtRegion}; -use crate::elements::compound::NbtCompound; +use crate::elements::compound::{CompoundMap, NbtCompound}; use crate::elements::element::{NbtByte, NbtByteArray, NbtDouble, NbtFloat, NbtInt, NbtIntArray, NbtLong, NbtLongArray, NbtShort}; use crate::elements::list::NbtList; use crate::elements::string::NbtString; @@ -179,6 +177,7 @@ macro_rules! log { #[macro_export] macro_rules! debg { () => { + #[cfg(debug_assertions)] $crate::log!("[{}:{}:{}]", file!(), line!(), column!()) }; } @@ -205,6 +204,8 @@ pub static mut WINDOW_PROPERTIES: UnsafeCell = UnsafeCell::new #[cfg(target_arch = "wasm32")] #[wasm_bindgen] pub fn handle_dialog(name: String, bytes: Vec) { + use crate::alert::Alert; + let workbench = unsafe { WORKBENCH.get_mut() }; if let Err(e) = workbench.on_open_file(name.as_str().as_ref(), bytes, unsafe { WINDOW_PROPERTIES.get_mut() }) { @@ -232,7 +233,7 @@ pub fn main() -> ! { pollster::block_on(window::run()) } /// * wiki page for docs on minecraft's format of stuff /// * [chunk](NbtChunk) section rendering /// # Minor Features -/// * save & load for web assembly (and open icon for exe ver) +/// * 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" @@ -292,43 +293,6 @@ 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 encode(bytes: &[u8]) -> Vec { - let len = bytes.len(); - let mut encoded = Vec::with_capacity(((len + 2) / 3) * 4); - let mut data = 0_u32; - let mut bits = 0_u32; - let mut iter = bytes.iter(); - loop { - while bits >= 6 { - encoded.push(match (data >> 26_u32) as u8 & 63 { - x @ 0..=25 => x + b'A', - x @ 26..=51 => x + (b'a' - 26), - x @ 52..=61 => x.wrapping_add(b'0'.wrapping_sub(52)), - 62 => b'+', - 63 => b'/', - // SAFETY: do not change the & 63 without consulting the unsafe assurance - _ => unsafe { std::hint::unreachable_unchecked() }, - }); - bits -= 6; - data <<= 6_u32; - } - if let Some(&byte) = iter.next() { - data |= u32::from(byte) << (24 - bits); - bits += 8; - } else if bits > 0 { - bits = 6; - } else { - break; - } - } - while encoded.len() % 3 != 0 { - encoded.push(b'='); - } - encoded -} - - #[must_use] pub fn since_epoch() -> Duration { #[cfg(not(target_arch = "wasm32"))] @@ -534,6 +498,81 @@ pub enum FileUpdateSubscriptionType { LongArray, } +#[derive(Copy, Clone)] +pub enum SortAlgorithm { + None, + Name, + Type, +} + +impl SortAlgorithm { + pub fn render(self, builder: &mut VertexBufferBuilder, ctx: &mut RenderContext) { + let uv = match self { + Self::None => SORT_COMPOUND_BY_NOTHING, + Self::Name => SORT_COMPOUND_BY_NAME, + Self::Type => SORT_COMPOUND_BY_TYPE, + }; + + builder.draw_texture((264, 26), STAMP_BACKDROP_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 { + match self { + Self::None => Self::Name, + Self::Name => Self::Type, + Self::Type => Self::None, + } + } + + pub fn rev_cycle(self) -> Self { + match self { + Self::None => Self::Type, + Self::Name => Self::None, + Self::Type => Self::Name, + } + } + + pub fn sort(self, map: &mut CompoundMap) { + if let Self::None = self { return; } + let hashes = map.entries.iter().map(|entry| entry.hash).collect::>(); + // yeah, it's hacky but there's not much else I *can* do. plus: it works extremely well. + for (idx, entry) in map.entries.iter_mut().enumerate() { + entry.hash = idx as u64; + } + match self { + Self::Name => map.entries.sort_by(|a, b| element_action::ElementAction::by_name((&a.key, &a.value), (&b.key, &b.value))), + _ => map.entries.sort_by(|a, b| element_action::ElementAction::by_type((&a.key, &a.value), (&b.key, &b.value))), + } + let indices = map.entries.iter().map(|entry| entry.hash as usize).collect::>(); + for (new_idx, &idx) in indices.iter().enumerate() { + // SAFETY: these indices are valid since the length did not change and since the values written were indexes + unsafe { + let hash = *hashes.get_unchecked(idx); + let entry = map.entries.get_unchecked_mut(new_idx); + entry.hash = hash; + *map.indices.find(hash, |&x| x == idx).panic_unchecked("index obviously exists").as_mut() = new_idx; + } + } + } +} + +impl Display for SortAlgorithm { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", match self { + Self::None => "None", + Self::Name => "Name-Based", + Self::Type => "Type-Based", + }) + } +} + pub struct RenderContext { selecting_key: bool, selected_y: usize, @@ -552,12 +591,13 @@ pub struct RenderContext { pub y_offset: usize, // must be sorted least to greatest line_numbers: Vec, + freehand: bool } impl RenderContext { #[must_use] #[allow(clippy::type_complexity)] // forbidden is fine to be like that, c'mon - pub fn new(selected_y: usize, selected_key: Option>, selected_value: Option>, selecting_key: bool, ghost: Option<(u8, usize, usize)>, left_margin: usize, mouse: (usize, usize)) -> Self { + pub fn new(selected_y: usize, selected_key: Option>, selected_value: Option>, selecting_key: bool, ghost: Option<(u8, usize, usize)>, left_margin: usize, mouse: (usize, usize), freehand: bool) -> Self { Self { selecting_key, selected_y, @@ -575,6 +615,7 @@ impl RenderContext { x_offset: 16 + left_margin, y_offset: HEADER_SIZE, line_numbers: vec![], + freehand, } } @@ -608,7 +649,7 @@ impl RenderContext { let x = (pos.0 - self.left_margin) / 16; let y = (pos.1 - HEADER_SIZE) / 16; let hovered = if (self.mouse_x >= self.left_margin) & (self.mouse_y >= HEADER_SIZE) { - (x >= (self.mouse_x - self.left_margin) / 16) & (y == (self.mouse_y - HEADER_SIZE) / 16) + ((x >= (self.mouse_x - self.left_margin) / 16) || self.freehand) & (y == (self.mouse_y - HEADER_SIZE) / 16) } else { false }; @@ -869,18 +910,18 @@ impl LinkedQueue { } #[must_use] - pub fn iter(&self) -> LinkedQueueIterator<'_, T> { - LinkedQueueIterator { + pub fn iter(&self) -> LinkedQueueIter<'_, T> { + LinkedQueueIter { tail: &self.tail, } } } -pub struct LinkedQueueIterator<'a, T> { +pub struct LinkedQueueIter<'a, T> { tail: &'a Option>>, } -impl<'a, T> Iterator for LinkedQueueIterator<'a, T> { +impl<'a, T> Iterator for LinkedQueueIter<'a, T> { type Item = &'a T; fn next(&mut self) -> Option { @@ -1154,7 +1195,7 @@ impl StrExt for str { pub trait OptionExt { /// # Safety /// - /// * This code better be unreachable otherwise it's UB without `debug_assertions`, just a panic with them however. + /// * This code better be unreachable otherwise it's UB without `debug_assertions`, just a panic with them, however. unsafe fn panic_unchecked(self, msg: &str) -> T; #[allow(clippy::wrong_self_convention)] // then why is is_some_and like that, huh? @@ -1169,7 +1210,7 @@ impl OptionExt for Option { /// # Safety /// -/// * This code better be unreachable otherwise it's UB without `debug_assertions`, just a panic with them however. +/// * This code better be unreachable otherwise it's UB without `debug_assertions`, just a panic with them, however. /// /// # Panics /// @@ -1197,18 +1238,3 @@ const_assert_eq!( VertexBufferBuilder::CHAR_WIDTH[b':' as usize], VertexBufferBuilder::CHAR_WIDTH[b',' as usize] ); - -const_assert!(core::mem::size_of::() <= core::mem::size_of::()); -const_assert!(core::mem::size_of::() <= core::mem::size_of::()); -const_assert!(core::mem::size_of::() <= core::mem::size_of::()); -const_assert!(core::mem::size_of::() <= core::mem::size_of::()); -const_assert!(core::mem::size_of::() <= core::mem::size_of::()); -const_assert!(core::mem::size_of::() <= core::mem::size_of::()); -const_assert!(core::mem::size_of::() <= core::mem::size_of::()); -const_assert!(core::mem::size_of::() <= core::mem::size_of::()); -const_assert!(core::mem::size_of::() <= core::mem::size_of::()); -const_assert!(core::mem::size_of::() <= core::mem::size_of::()); -const_assert!(core::mem::size_of::() <= core::mem::size_of::()); -const_assert!(core::mem::size_of::() <= core::mem::size_of::()); -const_assert!(core::mem::size_of::() <= core::mem::size_of::()); -const_assert!(core::mem::size_of::() <= core::mem::size_of::()); diff --git a/src/selected_text.rs b/src/selected_text.rs index 303143d..2d67403 100644 --- a/src/selected_text.rs +++ b/src/selected_text.rs @@ -7,7 +7,7 @@ use winit::keyboard::KeyCode; use crate::assets::{BASE_TEXT_Z, ELEMENT_HIGHLIGHT_Z, HEADER_SIZE, SELECTED_TEXT_Z, SELECTION_UV}; use crate::selected_text::KeyResult::{Down, Failed, Finish, ForceClose, ForceOpen, Keyfix, NothingSpecial, Revert, ShiftDown, ShiftUp, Up, Valuefix}; use crate::vertex_buffer_builder::VertexBufferBuilder; -use crate::{flags, get_clipboard, is_jump_char_boundary, is_utf8_char_boundary, LinkedQueue, OptionExt, set_clipboard, since_epoch, StrExt, WindowProperties}; +use crate::{flags, get_clipboard, is_jump_char_boundary, is_utf8_char_boundary, LinkedQueue, OptionExt, set_clipboard, since_epoch, StrExt}; use crate::color::TextColor; #[derive(Clone, Debug)] diff --git a/src/tab.rs b/src/tab.rs index 07ca661..d793ea5 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -1,5 +1,4 @@ use std::ffi::OsStr; -use std::fs::write; use std::io::Read; use std::ops::Deref; use std::path::{Path, PathBuf}; @@ -11,7 +10,7 @@ use flate2::Compression; use uuid::Uuid; use crate::{Bookmark, LinkedQueue, OptionExt, panic_unchecked, RenderContext, StrExt, WindowProperties}; -use crate::assets::{BYTE_ARRAY_GHOST_UV, BYTE_ARRAY_UV, BYTE_GRAYSCALE_UV, BYTE_UV, CHUNK_GHOST_UV, CHUNK_UV, COMPOUND_GHOST_UV, COMPOUND_ROOT_UV, COMPOUND_UV, DOUBLE_GRAYSCALE_UV, DOUBLE_UV, ENABLED_FREEHAND_MODE_UV, FLOAT_GRAYSCALE_UV, FLOAT_UV, FREEHAND_MODE_UV, GZIP_FILE_TYPE_UV, HEADER_SIZE, HELD_SCROLLBAR_UV, INT_ARRAY_GHOST_UV, INT_ARRAY_UV, INT_GRAYSCALE_UV, INT_UV, JUST_OVERLAPPING_BASE_Z, LINE_NUMBER_SEPARATOR_UV, LIST_GHOST_UV, LIST_UV, LONG_ARRAY_GHOST_UV, LONG_ARRAY_UV, LONG_GRAYSCALE_UV, LONG_UV, MCA_FILE_TYPE_UV, NBT_FILE_TYPE_UV, REDO_UV, REGION_UV, SCROLLBAR_Z, SHORT_GRAYSCALE_UV, SHORT_UV, SNBT_FILE_TYPE_UV, STEAL_ANIMATION_OVERLAY, STRING_GHOST_UV, STRING_UV, UNDO_UV, UNHELD_SCROLLBAR_UV, UNKNOWN_NBT_GHOST_UV, UNKNOWN_NBT_UV, ZLIB_FILE_TYPE_UV}; +use crate::assets::{BASE_Z, BYTE_ARRAY_GHOST_UV, BYTE_ARRAY_UV, BYTE_GRAYSCALE_UV, BYTE_UV, CHUNK_GHOST_UV, CHUNK_UV, COMPOUND_GHOST_UV, COMPOUND_ROOT_UV, COMPOUND_UV, DOUBLE_GRAYSCALE_UV, DOUBLE_UV, ENABLED_FREEHAND_MODE_UV, FLOAT_GRAYSCALE_UV, FLOAT_UV, FREEHAND_MODE_UV, GZIP_FILE_TYPE_UV, HEADER_SIZE, HELD_SCROLLBAR_UV, INT_ARRAY_GHOST_UV, INT_ARRAY_UV, INT_GRAYSCALE_UV, INT_UV, JUST_OVERLAPPING_BASE_Z, LINE_NUMBER_SEPARATOR_UV, LIST_GHOST_UV, LIST_UV, LONG_ARRAY_GHOST_UV, LONG_ARRAY_UV, LONG_GRAYSCALE_UV, LONG_UV, MCA_FILE_TYPE_UV, NBT_FILE_TYPE_UV, REDO_UV, REGION_UV, SCROLLBAR_Z, SHORT_GRAYSCALE_UV, SHORT_UV, SNBT_FILE_TYPE_UV, STEAL_ANIMATION_OVERLAY_UV, STRING_GHOST_UV, STRING_UV, UNDO_UV, UNHELD_SCROLLBAR_UV, UNKNOWN_NBT_GHOST_UV, UNKNOWN_NBT_UV, ZLIB_FILE_TYPE_UV}; use crate::color::TextColor; use crate::elements::chunk::NbtRegion; use crate::elements::compound::NbtCompound; @@ -66,8 +65,8 @@ impl Tab { } pub fn save(&mut self, force_dialog: bool) -> Result<()> { - let path = self.path.as_deref().unwrap_or(self.name.as_ref().as_ref()); #[cfg(target_os = "windows")] { + 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(); if self.value.id() == NbtRegion::ID { @@ -77,12 +76,12 @@ impl Tab { } 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(); - write(&path, self.compression.encode(&self.value))?; + std::fs::write(&path, self.compression.encode(&self.value))?; self.path = Some(path); self.history_changed = false; Ok(()) } else { - write(path, self.compression.encode(&self.value))?; + std::fs::write(path, self.compression.encode(&self.value))?; self.history_changed = false; Ok(()) } @@ -155,18 +154,15 @@ impl Tab { { let mut tail = self.undos.tail.as_deref(); - builder.draw_texture( - (builder.window_width() - 107, 26), + builder.draw_texture_region_z( + (builder.window_width() - 109, 22), + BASE_Z, LINE_NUMBER_SEPARATOR_UV, + (2, 23), (2, 16), ); - builder.draw_texture( - (builder.window_width() - 129, 26), - LINE_NUMBER_SEPARATOR_UV, - (2, 16), - ); - builder.draw_texture((builder.window_width() - 125, 26), UNDO_UV, (16, 16)); - let mut x = builder.window_width() - 104; + builder.draw_texture((builder.window_width() - 105, 26), UNDO_UV, (16, 16)); + let mut x = builder.window_width() - 84; for _ in 0..5_usize { if let Some(t) = tail { t.value.render((x, 26), builder, t.prev.is_none()); @@ -180,18 +176,15 @@ impl Tab { { let mut tail = self.redos.tail.as_deref(); - builder.draw_texture( - (builder.window_width() - 213, 26), - LINE_NUMBER_SEPARATOR_UV, - (2, 16), - ); - builder.draw_texture( - (builder.window_width() - 235, 26), + builder.draw_texture_region_z( + (builder.window_width() - 215, 22), + BASE_Z, LINE_NUMBER_SEPARATOR_UV, + (2, 23), (2, 16), ); - builder.draw_texture((builder.window_width() - 231, 26), REDO_UV, (16, 16)); - let mut x = builder.window_width() - 210; + builder.draw_texture((builder.window_width() - 211, 26), REDO_UV, (16, 16)); + let mut x = builder.window_width() - 190; for _ in 0..5_usize { if let Some(t) = tail { t.value.render((x, 26), builder, t.prev.is_none()); @@ -204,13 +197,16 @@ impl Tab { } { - builder.draw_texture( - (builder.window_width() - 22, 26), + // shifted one left to center between clipboard and freehand + builder.draw_texture_region_z( + (244, 22), + BASE_Z, LINE_NUMBER_SEPARATOR_UV, + (2, 23), (2, 16), ); let freehand_uv = { - let hovering = (builder.window_width() - 16..builder.window_width()).contains(&mouse_x) && (26..42).contains(&mouse_y); + 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)); } @@ -225,7 +221,7 @@ impl Tab { } } }; - builder.draw_texture((builder.window_width() - 18, 26), freehand_uv, (16, 16)); + builder.draw_texture((248, 26), freehand_uv, (16, 16)); } { @@ -285,7 +281,7 @@ impl Tab { if steal_delta > 0.0 { let y = ((mouse_y - HEADER_SIZE) & !15) + HEADER_SIZE; let height = (16.0 * steal_delta).round() as usize; - builder.draw_texture_region_z((ctx.left_margin - 2, y + (16 - height)), JUST_OVERLAPPING_BASE_Z, STEAL_ANIMATION_OVERLAY, (builder.window_width() + 2 - ctx.left_margin, height), (16, 16)); + builder.draw_texture_region_z((ctx.left_margin - 2, y + (16 - height)), JUST_OVERLAPPING_BASE_Z, STEAL_ANIMATION_OVERLAY_UV, (builder.window_width() + 2 - ctx.left_margin, height), (16, 16)); } } diff --git a/src/window.rs b/src/window.rs index 1a9c3b6..47087c2 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,6 +1,5 @@ use std::borrow::Cow; use std::cell::UnsafeCell; -use std::num::NonZeroU64; use std::rc::Rc; use std::time::Duration; #[cfg(target_arch = "wasm32")] @@ -25,7 +24,7 @@ use crate::assets::HEADER_SIZE; use crate::color::TextColor; use crate::vertex_buffer_builder::VertexBufferBuilder; use crate::workbench::Workbench; -use crate::{assets, WORKBENCH, WINDOW_PROPERTIES, debg, error, OptionExt, since_epoch, WindowProperties}; +use crate::{assets, WORKBENCH, WINDOW_PROPERTIES, error, OptionExt, since_epoch, WindowProperties}; pub const WINDOW_HEIGHT: usize = 420; pub const WINDOW_WIDTH: usize = 620; @@ -63,9 +62,10 @@ pub async fn run() -> ! { let height = window.inner_height().ok()?.as_f64()?; Some((document, PhysicalSize::new(width as u32, height as u32))) }).and_then(|(document, size)| { - let canvas = web_sys::Element::from(window.canvas()?); + let canvas = web_sys::HtmlElement::from(window.canvas()?); document.body()?.append_child(&canvas).ok()?; let _ = window.request_inner_size(size); + let _ = canvas.focus(); Some(size) }).expect("Couldn't append canvas to document body") }; @@ -99,7 +99,8 @@ pub async fn run() -> ! { Event::AboutToWait => { #[cfg(target_arch = "wasm32")] { let old_size = window.inner_size(); - let new_size: PhysicalSize = web_sys::window().map(|window| PhysicalSize::new(window.inner_width().ok().as_ref().and_then(JsValue::as_f64).expect("Width must exist") as u32, window.inner_height().ok().as_ref().and_then(JsValue::as_f64).expect("Height must exist") as u32)).expect("Window has dimension properties"); + let scaling_factor = web_sys::window().map_or(1.0, |window| window.device_pixel_ratio()); + let new_size: PhysicalSize = web_sys::window().map(|window| PhysicalSize::new((window.inner_width().ok().as_ref().and_then(JsValue::as_f64).expect("Width must exist") * scaling_factor).ceil() as u32, (window.inner_height().ok().as_ref().and_then(JsValue::as_f64).expect("Height must exist") * scaling_factor).ceil() as u32)).expect("Window has dimension properties"); if new_size != old_size { let _ = window.request_inner_size(new_size); state.resize(workbench, new_size); diff --git a/src/workbench.rs b/src/workbench.rs index c86cbfd..d5fb02d 100644 --- a/src/workbench.rs +++ b/src/workbench.rs @@ -34,7 +34,7 @@ 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}; +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}; pub struct Workbench { pub tabs: Vec, @@ -59,6 +59,7 @@ pub struct Workbench { alerts: Vec, pub scale: usize, steal_animation_data: Option<(Duration, Vec2u)>, + sort_algorithm: SortAlgorithm, } impl Workbench { @@ -86,6 +87,7 @@ impl Workbench { alerts: vec![], scale: 0, steal_animation_data: None, + sort_algorithm: SortAlgorithm::Type, } } @@ -115,6 +117,7 @@ impl Workbench { alerts: vec![], scale: 1, steal_animation_data: None, + sort_algorithm: SortAlgorithm::Type, }; 'create_tab: { if let Some(path) = &std::env::args() @@ -129,7 +132,7 @@ impl Workbench { } workbench.new_custom_tab(window_properties, Tab { #[cfg(debug_assertions)] - value: Box::new(NbtElement::from_file(include_bytes!("assets/test.nbt")).expect("Included debug nbt contains valid data")), + value: Box::new(NbtElement::from_file(include_bytes!("assets/test.nbt"), SortAlgorithm::None).expect("Included debug nbt contains valid data")), #[cfg(debug_assertions)] name: "test.nbt".into(), #[cfg(not(debug_assertions))] @@ -164,7 +167,7 @@ impl Workbench { let (nbt, compressed) = { if path.extension().and_then(OsStr::to_str) == Some("mca") { ( - NbtElement::from_mca(buf.as_slice()).context("Failed to parse MCA file")?, + NbtElement::from_mca(buf.as_slice(), self.sort_algorithm).context("Failed to parse MCA file")?, FileFormat::Mca, ) } else if let Some(0x1F8B) = buf.first_chunk::<2>().copied().map(u16::from_be_bytes) { @@ -173,6 +176,7 @@ impl Workbench { &DeflateDecoder::new(buf.as_slice()) .decode_gzip() .context("Failed to decode gzip compressed NBT")?, + self.sort_algorithm, ) .context("Failed to parse NBT")?, FileFormat::Gzip, @@ -183,17 +187,18 @@ impl Workbench { &DeflateDecoder::new(buf.as_slice()) .decode_zlib() .context("Failed to decode zlib compressed NBT")?, + self.sort_algorithm, ) .context("Failed to parse NBT")?, FileFormat::Zlib, ) - } else if let Some(nbt) = NbtElement::from_file(buf.as_slice()) { + } else if let Some(nbt) = NbtElement::from_file(buf.as_slice(), self.sort_algorithm) { (nbt, FileFormat::Nbt) } else { ( core::str::from_utf8(&buf) .ok() - .and_then(NbtElement::from_str) + .and_then(|s| NbtElement::from_str(s, self.sort_algorithm)) .context(anyhow!( "Failed to find file type for file {}", path.file_name() @@ -329,12 +334,12 @@ impl Workbench { 'a: { let freehand_mode = tab!(self).freehand_mode; - if self.mouse_x >= left_margin && self.mouse_y >= HEADER_SIZE { + if x >= left_margin && y >= HEADER_SIZE { match self.action_wheel.take() { Some(_) => {}, None => { if button == MouseButton::Right { - self.action_wheel = Some((((self.mouse_x - left_margin) & !15) + left_margin + 6, ((self.mouse_y - HEADER_SIZE) & !15) + HEADER_SIZE + 7)); + self.action_wheel = Some((((x - left_margin) & !15) + left_margin + 6, ((y - HEADER_SIZE) & !15) + HEADER_SIZE + 7)); break 'a; } } @@ -357,16 +362,20 @@ impl Workbench { self.steal_animation_data = None; } } - if (self.window_width - 16..self.window_width).contains(&x) && (26..42).contains(&y) { + if let MouseButton::Left | MouseButton::Right = button && (248..264).contains(&x) && (26..42).contains(&y) { let tab = tab_mut!(self); tab.freehand_mode = !tab.freehand_mode; break 'a; } + if let MouseButton::Left | MouseButton::Right = button && (264..280).contains(&x) && (26..42).contains(&y) { + self.sort_algorithm = if button == MouseButton::Left { self.sort_algorithm.cycle() } else { self.sort_algorithm.rev_cycle() }; + break 'a; + } if ((self.window_width - 7)..self.window_width).contains(&x) { let tab = tab_mut!(self); let height = tab.value.height() * 16 + 48; let total = self.window_height - HEADER_SIZE; - if height > total { + if height - 48 > total { let start = total * self.scroll() / height + HEADER_SIZE; let end = start + total * total / height; if (start..=end).contains(&y) { @@ -442,10 +451,10 @@ impl Workbench { #[inline] #[allow(clippy::too_many_lines)] pub fn try_subscription(&mut self) -> Result<()> { - fn write_snbt(subscription: &FileUpdateSubscription, data: &[u8], tab: &mut Tab) -> Result<()> { + fn write_snbt(subscription: &FileUpdateSubscription, data: &[u8], tab: &mut Tab, sort: SortAlgorithm) -> Result<()> { let Some((key, value)) = core::str::from_utf8(data) .ok() - .and_then(NbtElement::from_str) + .and_then(|s| NbtElement::from_str(s, sort)) else { return Err(anyhow!("SNBT failed to parse.")); }; @@ -630,7 +639,7 @@ impl Workbench { }; match subscription.rx.try_recv() { Ok(data) => match subscription.subscription_type { - FileUpdateSubscriptionType::Snbt => write_snbt(subscription, &data, tab)?, + FileUpdateSubscriptionType::Snbt => write_snbt(subscription, &data, tab, self.sort_algorithm)?, FileUpdateSubscriptionType::ByteArray => write_array(subscription, tab, { let mut array = NbtByteArray::new(); for (idx, byte) in data.into_iter().enumerate() { @@ -1079,7 +1088,7 @@ impl Workbench { let tab = tab!(self); let x = self.mouse_x - (16 + 4); if x / 16 == 13 { - match NbtElement::from_str(&get_clipboard().ok_or_else(|| anyhow!("Failed to get clipboard"))?) { + match NbtElement::from_str(&get_clipboard().ok_or_else(|| anyhow!("Failed to get clipboard"))?, self.sort_algorithm) { Some((key, element)) => { if element.id() == NbtChunk::ID && tab.value.id() != NbtRegion::ID { return Err(anyhow!("Chunks are not supported for non-region tabs")); @@ -2076,7 +2085,7 @@ impl Workbench { // }); // return true; // } - if key == KeyCode::KeyV && flags == flags!(Ctrl) && let Some(element) = get_clipboard().and_then(|x| NbtElement::from_str(&x)) && (element.1.id() != NbtChunk::ID || tab.value.id() == NbtRegion::ID) { + 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 { unsafe { panic_unchecked("we just set it you, bozo") } @@ -2147,13 +2156,13 @@ impl Workbench { let Ok(buf) = read(path) else { break 'a }; let (nbt, compression) = { if path.extension().and_then(OsStr::to_str) == Some("mca") { - (NbtElement::from_mca(buf.as_slice()), FileFormat::Mca) + (NbtElement::from_mca(buf.as_slice(), self.sort_algorithm), FileFormat::Mca) } else if buf.first_chunk::<2>().copied().map(u16::from_be_bytes) == Some(0x1F8B) { ( DeflateDecoder::new(buf.as_slice()) .decode_gzip() .ok() - .and_then(|x| NbtElement::from_file(&x)), + .and_then(|x| NbtElement::from_file(&x, self.sort_algorithm)), FileFormat::Zlib, ) } else if let Some(0x7801 | 0x789C | 0x78DA) = buf.first_chunk::<2>().copied().map(u16::from_be_bytes) { @@ -2161,15 +2170,15 @@ impl Workbench { DeflateDecoder::new(buf.as_slice()) .decode_zlib() .ok() - .and_then(|x| NbtElement::from_file(&x)), + .and_then(|x| NbtElement::from_file(&x, self.sort_algorithm)), FileFormat::Zlib, ) - } else if let Some(nbt) = NbtElement::from_file(&buf) { + } else if let Some(nbt) = NbtElement::from_file(&buf, self.sort_algorithm) { (Some(nbt), FileFormat::Nbt) } else { let nbt = core::str::from_utf8(&buf) .ok() - .and_then(NbtElement::from_str) + .and_then(|s| NbtElement::from_str(s, self.sort_algorithm)) .map(|x| x.1); if let Some(NbtCompound::ID | NbtRegion::ID) = nbt.as_ref().map(NbtElement::id) { (nbt, FileFormat::Snbt) @@ -2397,7 +2406,7 @@ impl Workbench { } else { (None, None, false) }; - let mut ctx = RenderContext::new(selected_y, selected_key, selected_value, selecting_key, ghost, left_margin, (self.mouse_x, self.mouse_y)); + let mut ctx = RenderContext::new(selected_y, selected_key, selected_value, selecting_key, ghost, left_margin, (self.mouse_x, self.mouse_y), tab.freehand_mode); if self.mouse_y >= HEADER_SIZE && self.action_wheel.is_none() { builder.draw_texture_region_z( (0, (self.mouse_y & !15) + 1), @@ -2413,7 +2422,29 @@ impl Workbench { builder.draw_texture((0, 26), SELECTION_UV, (16, 16)); builder.draw_tooltip(&["Open File"], (self.mouse_x, self.mouse_y)); } - builder.draw_texture((17, 26), LINE_NUMBER_SEPARATOR_UV, (2, 16)); + builder.draw_texture_region_z( + (17, 22), + BASE_Z, + LINE_NUMBER_SEPARATOR_UV, + (2, 23), + (2, 16), + ); + } + { + builder.draw_texture_region_z( + (281, 22), + BASE_Z, + LINE_NUMBER_SEPARATOR_UV, + (2, 23), + (2, 16), + ); + builder.draw_texture_region_z( + (283, 23), + BASE_Z, + DARK_STRIPE_UV, + (self.window_width - 215 - 283, 22), + (16, 16), + ); } tab.render( builder, @@ -2423,6 +2454,7 @@ impl Workbench { self.action_wheel.is_some(), self.steal_animation_data.as_ref().map(|x| (since_epoch() - x.0).min(Duration::from_millis(500)).as_millis() as f32 / 500.0).unwrap_or(0.0) ); + self.sort_algorithm.render(builder, &mut ctx); if let Some(selected_text) = &tab.selected_text { builder.horizontal_scroll = horizontal_scroll; selected_text.render(builder, left_margin);