diff --git a/src/assets.rs b/src/assets.rs index ba5c3b2..ddc39e8 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -104,6 +104,7 @@ 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); @@ -135,8 +136,9 @@ pub const INT_ARRAY_GHOST_UV: Vec2u = Vec2u::new(112, 16); pub const LONG_ARRAY_GHOST_UV: Vec2u = Vec2u::new(0, 48); pub const CHUNK_GHOST_UV: Vec2u = Vec2u::new(64, 48); pub const ALERT_UV: Vec2u = Vec2u::new(112, 144); +pub const BACKDROP_UV: Vec2u = Vec2u::new(32, 160); -pub const BASE_Z: u8 = 0; +pub const BASE_Z: u8 = 5; pub const JUST_OVERLAPPING_BASE_Z: u8 = BASE_Z + 1; pub const BASE_TEXT_Z: u8 = 10; pub const JUST_OVERLAPPING_BASE_TEXT_Z: u8 = BASE_TEXT_Z + 1; diff --git a/src/assets/atlas.hex b/src/assets/atlas.hex index 38480bf..a929b59 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 9282928..c133713 100644 Binary files a/src/assets/build/atlas.png and b/src/assets/build/atlas.png differ diff --git a/src/main.rs b/src/main.rs index e3bf454..1a243c6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,9 +32,11 @@ )] #![feature(array_chunks)] #![feature(box_patterns)] +#![feature(const_black_box)] #![feature(core_intrinsics)] #![feature(iter_array_chunks)] #![feature(iter_next_chunk)] +#![feature(lazy_cell)] #![feature(let_chains)] #![feature(maybe_uninit_array_assume_init)] #![feature(maybe_uninit_uninit_array)] @@ -42,7 +44,6 @@ #![feature(optimize_attribute)] #![feature(stmt_expr_attributes)] #![feature(unchecked_math)] -#![feature(lazy_cell)] #![feature(const_collections_with_hasher)] #![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")] @@ -54,7 +55,7 @@ use std::rc::Rc; use std::time::Duration; use compact_str::{CompactString, ToCompactString}; -use static_assertions::{const_assert, const_assert_eq}; +use static_assertions::const_assert_eq; #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::wasm_bindgen; use winit::window::Window; @@ -64,11 +65,8 @@ 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::color::TextColor; -use crate::elements::chunk::{NbtChunk, NbtRegion}; -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; +use crate::elements::compound::{CompoundMap}; +use crate::elements::element::{NbtByteArray, NbtIntArray, NbtLongArray}; use crate::tree_travel::Navigate; use crate::vertex_buffer_builder::Vec2u; use crate::workbench::Workbench; @@ -88,6 +86,8 @@ mod window; pub mod workbench; mod workbench_action; mod element_action; +mod search_box; +mod text; #[macro_export] macro_rules! flags { @@ -198,7 +198,7 @@ extern "C" { fn save(name: &str, bytes: Vec); } -pub static mut WORKBENCH: UnsafeCell = UnsafeCell::new(Workbench::uninit()); +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) })); #[cfg(target_arch = "wasm32")] diff --git a/src/search_box.rs b/src/search_box.rs new file mode 100644 index 0000000..7c594e5 --- /dev/null +++ b/src/search_box.rs @@ -0,0 +1,153 @@ +use std::ops::{Deref, DerefMut}; + +use winit::keyboard::KeyCode; +use crate::assets::DARK_STRIPE_UV; + +use crate::color::TextColor; +use crate::StrExt; +use crate::text::{Cachelike, KeyResult, Text}; +use crate::vertex_buffer_builder::{Vec2u, VertexBufferBuilder}; + +#[derive(Clone, Eq)] +pub struct SearchBoxCache { + value: String, + cursor: usize, + selection: Option, +} + +impl PartialEq for SearchBoxCache { + fn eq(&self, other: &Self) -> bool { + self.value == other.value + } +} + +impl Cachelike for SearchBoxCache { + fn new(text: &Text) -> Self where Self: Sized { + Self { + value: text.value.clone(), + cursor: text.cursor, + selection: text.selection, + } + } + + fn revert(self, text: &mut Text) where Self: Sized { + text.value = self.value; + text.cursor = self.cursor; + text.selection = self.selection; + } +} + +#[derive(Clone)] +pub struct SearchBoxAdditional { + selected: bool, + horizontal_scroll: usize, +} + +pub struct SearchBox(Text); + +impl Deref for SearchBox { + type Target = Text; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for SearchBox { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl SearchBox { + pub fn new() -> Self { + Self(Text::new(String::new(), 0, true, SearchBoxAdditional { selected: false, horizontal_scroll: 0 })) + } + + pub const fn uninit() -> Self { + Self(Text::uninit()) + } + + pub fn render(&self, builder: &mut VertexBufferBuilder) { + use std::fmt::Write; + + let pos = Vec2u::new(284, 23); + + builder.draw_texture_region_z( + pos, + 0, + DARK_STRIPE_UV, + (builder.window_width() - 215 - pos.x, 22), + (16, 16), + ); + + builder.horizontal_scroll = self.horizontal_scroll; + if self.value.is_empty() { + builder.settings(pos + (0, 3), false, 0); + builder.color = TextColor::Gray.to_raw(); + let _ = write!(builder, "Search..."); + } + if self.is_selected() { + self.0.render(builder, TextColor::White, pos + (0, 3), 0); + } else { + builder.settings(pos + (0, 3), false, 0); + builder.color = TextColor::White.to_raw(); + let _ = write!(builder, "{}", self.value); + } + builder.horizontal_scroll = 0; + } + + #[inline] + pub fn deselect(&mut self) { + self.selected = false; + self.cursor = 0; + self.selection = None; + // better ui this way + // self.horizontal_scroll = 0; + } + + #[inline] + pub fn select(&mut self, x: usize) { + let x = x + self.horizontal_scroll; + self.cursor = 'a: { + let mut current_x = 0; + for (idx, char) in self.value.char_indices() { + let width = if (x as u32) < 56832 { VertexBufferBuilder::CHAR_WIDTH[char as usize] as usize } else { 0 }; + if current_x + width / 2 >= x { + break 'a idx; + } + current_x += width; + } + self.value.len() + }; + self.selected = true; + self.interact(); + } + + #[inline] + pub fn search(&mut self) { + todo!() + } + + #[inline] + #[must_use] + pub fn is_selected(&self) -> bool { + self.selected + } + + #[inline] + 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; + 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); + self.horizontal_scroll = horizontal_scroll; + } + + #[must_use] + pub fn on_key_press(&mut self, key: KeyCode, char: Option, flags: u8) -> KeyResult { + self.0.on_key_press(key, char, flags) + } +} diff --git a/src/selected_text.rs b/src/selected_text.rs index 2d67403..b49cd6e 100644 --- a/src/selected_text.rs +++ b/src/selected_text.rs @@ -1,63 +1,78 @@ use std::fmt::Write; -use std::intrinsics::{likely, unlikely}; -use std::time::Duration; +use std::ops::{Deref, DerefMut}; use winit::keyboard::KeyCode; +use crate::{flags, OptionExt, StrExt}; 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}; use crate::color::TextColor; +use crate::selected_text::SelectedTextKeyResult::{Down, ForceClose, ForceOpen, Keyfix, ShiftDown, ShiftUp, Up, Valuefix}; +use crate::text::{Cachelike, SelectedTextKeyResult, Text}; +use crate::vertex_buffer_builder::VertexBufferBuilder; #[derive(Clone, Debug)] #[allow(clippy::module_name_repetitions)] // yeah no it's better like this pub struct SelectedTextCache { - keyfix: Option>, + keyfix: Option<(Box, TextColor)>, value: Box, - valuefix: Option>, + valuefix: Option<(Box, TextColor)>, cursor: usize, selection: Option, } -impl SelectedTextCache { - pub fn eq(&self, text: &SelectedText) -> bool { (self.value.as_ref() == text.value.as_str()) & (self.keyfix.as_ref().map(Box::as_ref) == text.keyfix.as_ref().map(|x| x.0.as_str())) & (self.valuefix.as_ref().map(Box::as_ref) == text.valuefix.as_ref().map(|x| x.0.as_str())) } +impl PartialEq for SelectedTextCache { + fn eq(&self, other: &Self) -> bool { + self.keyfix == other.keyfix && self.value == other.value && self.valuefix == other.valuefix + } +} + +impl Cachelike for SelectedTextCache { + fn new(text: &Text) -> Self where Self: Sized { + Self { + keyfix: text.additional.keyfix.clone().map(|(a, b)| (a.into_boxed_str(), b)), + valuefix: text.additional.valuefix.clone().map(|(a, b)| (a.into_boxed_str(), b)), + value: text.value.clone().into_boxed_str(), + cursor: text.cursor, + selection: text.selection, + } + } + + fn revert(self, text: &mut Text) where Self: Sized { + let Self { keyfix, value, valuefix, cursor, selection } = self; + text.additional.keyfix = keyfix.map(|(a, b)| (a.into_string(), b)); + text.additional.valuefix = valuefix.map(|(a, b)| (a.into_string(), b)); + text.value = value.into_string(); + text.cursor = cursor; + text.selection = selection; + } +} + +#[derive(Clone)] +pub struct SelectedText(pub Text); + +impl Deref for SelectedText { + type Target = Text; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} - pub fn ne(&self, text: &SelectedText) -> bool { (self.value.as_ref() != text.value.as_str()) | (self.keyfix.as_ref().map(Box::as_ref) != text.keyfix.as_ref().map(|x| x.0.as_str())) | (self.valuefix.as_ref().map(Box::as_ref) != text.valuefix.as_ref().map(|x| x.0.as_str())) } +impl DerefMut for SelectedText { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } } #[derive(Clone)] -pub struct SelectedText { +pub struct SelectedTextAdditional { pub y: usize, pub indices: Box<[usize]>, - pub cursor: usize, - pub value: String, pub value_color: TextColor, - pub selection: Option, pub keyfix: Option<(String, TextColor)>, pub prefix: (String, TextColor), pub suffix: (String, TextColor), pub valuefix: Option<(String, TextColor)>, - pub editable: bool, - pub last_interaction: Duration, - pub undos: LinkedQueue, - pub redos: LinkedQueue, -} - -#[repr(u8)] -pub enum KeyResult { - Failed, - NothingSpecial, - Revert, - Finish, - Keyfix, - Valuefix, - Up(bool), - Down(bool), - ForceClose, - ForceOpen, - ShiftUp, - ShiftDown, } impl SelectedText { @@ -84,23 +99,15 @@ impl SelectedText { if mouse_x <= target_x { return Some( - Self { + Self(Text::new(key.into_string(), 0, true, SelectedTextAdditional { y, indices: indices.into_boxed_slice(), - cursor: 0, - value: key.into_string(), value_color: key_color, - selection: None, keyfix: None, prefix: (String::new(), TextColor::White), suffix, valuefix, - editable: true, - last_interaction: since_epoch(), - undos: LinkedQueue::new(), - redos: LinkedQueue::new(), - } - .post_process(), + })), ); } @@ -115,23 +122,15 @@ impl SelectedText { && mouse_x < target_x + key_width + 7 { return Some( - Self { + Self(Text::new(key.clone().into_string(), key.len(), true, SelectedTextAdditional { y, indices: indices.into_boxed_slice(), - cursor: key.len(), - value: key.into_string(), value_color: key_color, - selection: None, keyfix: None, prefix: (String::new(), TextColor::White), suffix, valuefix, - editable: true, - last_interaction: since_epoch(), - undos: LinkedQueue::new(), - redos: LinkedQueue::new(), - } - .post_process(), + })), ); } @@ -146,24 +145,15 @@ impl SelectedText { cursor += char.len_utf8(); x -= width; } else if x < key_width { - return Some( - Self { - y, - indices: indices.into_boxed_slice(), - cursor, - value: key.into_string(), - value_color: key_color, - selection: None, - keyfix: None, - prefix: (String::new(), TextColor::White), - suffix, - valuefix, - editable: true, - last_interaction: since_epoch(), - undos: LinkedQueue::new(), - redos: LinkedQueue::new(), - } - .post_process(), + return Some(Self(Text::new(key.into_string(), cursor, true, SelectedTextAdditional { + y, + indices: indices.into_boxed_slice(), + value_color: key_color, + keyfix: None, + prefix: (String::new(), TextColor::White), + suffix, + valuefix, + })), ); } } @@ -190,24 +180,15 @@ impl SelectedText { }; if mouse_x <= value_x { - return Some( - Self { - y, - indices: indices.into_boxed_slice(), - cursor: 0, - value: value.as_ref().to_owned(), - value_color: *value_color, - selection: None, - keyfix, - prefix, - suffix: (String::new(), TextColor::White), - valuefix: None, - editable: true, - last_interaction: since_epoch(), - undos: LinkedQueue::new(), - redos: LinkedQueue::new(), - } - .post_process(), + return Some(Self(Text::new(value.as_ref().to_owned(), 0, true, SelectedTextAdditional { + y, + indices: indices.into_boxed_slice(), + value_color: *value_color, + keyfix, + prefix, + suffix: (String::new(), TextColor::White), + valuefix: None, + })), ); } @@ -224,24 +205,15 @@ impl SelectedText { > value_x + value_width && mouse_x < value_x + value_width + 5 { - return Some( - Self { - y, - indices: indices.into_boxed_slice(), - cursor: value.len(), - value: value.as_ref().to_owned(), - value_color: *value_color, - selection: None, - keyfix, - prefix, - suffix: (String::new(), TextColor::White), - valuefix: None, - editable: true, - last_interaction: since_epoch(), - undos: LinkedQueue::new(), - redos: LinkedQueue::new(), - } - .post_process(), + return Some(Self(Text::new(value.as_ref().to_owned(), value.len(), true, SelectedTextAdditional { + y, + indices: indices.into_boxed_slice(), + value_color: *value_color, + keyfix, + prefix, + suffix: (String::new(), TextColor::White), + valuefix: None, + })), ); } @@ -256,24 +228,15 @@ impl SelectedText { cursor += char.len_utf8(); x -= width; } else if x < value_width { - return Some( - Self { - y, - indices: indices.into_boxed_slice(), - cursor, - value: value.as_ref().to_owned(), - value_color: *value_color, - selection: None, - keyfix, - prefix, - suffix: (String::new(), TextColor::White), - valuefix: None, - editable: true, - last_interaction: since_epoch(), - undos: LinkedQueue::new(), - redos: LinkedQueue::new(), - } - .post_process(), + return Some(Self(Text::new(value.as_ref().to_owned(), cursor, true, SelectedTextAdditional { + y, + indices: indices.into_boxed_slice(), + value_color: *value_color, + keyfix, + prefix, + suffix: (String::new(), TextColor::White), + valuefix: None, + })), ); } } @@ -288,528 +251,70 @@ impl SelectedText { 0 }; if key.as_ref().is_none_or(|(_, _, display)| !*display) && value.as_ref().is_none_or(|(_, _, display)| !*display) && mouse_x <= target_x + full_width && mouse_x + 16 >= target_x { - Some( - Self { - y, - indices: indices.into_boxed_slice(), - cursor: 0, - value: if key.is_some() { - if chunk { ", " } else { ": " }.to_owned() - } else { - String::new() - }, - value_color: TextColor::TreeKey, - selection: None, - keyfix: key.map(|(x, color, _)| (x.into_string(), color)), - prefix: (String::new(), TextColor::White), - suffix: (String::new(), TextColor::White), - valuefix: value.map(|(x, color, _)| (x.into_string(), color)), - editable: false, - last_interaction: since_epoch(), - undos: LinkedQueue::new(), - redos: LinkedQueue::new(), - } - .post_process(), + Some(Self(Text::new(if key.is_some() { if chunk { ", " } else { ": " }.to_owned() } else { String::new() }, 0, false, SelectedTextAdditional { + y, + indices: indices.into_boxed_slice(), + value_color: TextColor::TreeKey, + keyfix: key.map(|(x, color, _)| (x.into_string(), color)), + prefix: (String::new(), TextColor::White), + suffix: (String::new(), TextColor::White), + valuefix: value.map(|(x, color, _)| (x.into_string(), color)), + })), ) } else { None } } - #[must_use] - pub fn post_process(mut self) -> Self { - self.save_state_in_history(); - self - } - - pub fn save_state_in_history(&mut self) { - self.undos.push(SelectedTextCache { - keyfix: self.keyfix.clone().map(|(x, _)| x.into_boxed_str()), - value: self.value.clone().into_boxed_str(), - valuefix: self.valuefix.clone().map(|(x, _)| x.into_boxed_str()), - cursor: self.cursor, - selection: self.selection, - }); - } - - pub fn handle_history(&mut self) { - let should_cache = core::mem::replace(&mut self.last_interaction, since_epoch()).as_millis() >= 1_500; - if should_cache && self.editable && self.undos.get().is_none_or(|x| x.ne(self)) { - if self.redos.pop().is_none_or(|x| x.ne(self)) { - self.redos = LinkedQueue::new(); - } - - self.save_state_in_history(); - } - - if self.undos.get().is_some_and(|x| x.eq(self)) - && let Some(undo) = self.undos.get_mut() - { - undo.cursor = self.cursor; - undo.selection = self.selection; - } - } - pub fn width(&self) -> usize { self.prefix.0.width() + self.keyfix.as_ref().map(|x| x.0.width()).unwrap_or(0) + self.value.width() + self.valuefix.as_ref().map(|x| x.0.width()).unwrap_or(0) + self.suffix.0.width() } #[cfg_attr(not(debug_assertions), inline)] #[allow(clippy::cognitive_complexity, clippy::too_many_lines)] // i handled this fn well #[must_use] - pub fn on_key_press(&mut self, key: KeyCode, mut char: Option, flags: u8) -> KeyResult { - if key == KeyCode::Escape && flags == flags!() { return Revert } - - if let KeyCode::Enter | KeyCode::NumpadEnter = key - && flags == flags!() - { - return Finish; - } - - if key == KeyCode::KeyZ && flags == flags!(Ctrl) && self.editable { - let SelectedTextCache { - keyfix, - value, - valuefix, - cursor, - selection, - } = 'a: { - while let Some(cache) = self.undos.pop() { - if unlikely(cache.ne(self)) { - self.undos.push(cache.clone()); - break 'a cache; - } - } - return Failed; - }; - - let new_keyfix = keyfix.map(|keyfix| (keyfix.into_string(), self.keyfix.as_ref().map_or(TextColor::White, |(_, x)| *x))); - let new_valuefix = valuefix.map(|valuefix| (valuefix.into_string(), self.valuefix.as_ref().map_or(TextColor::White, |(_, x)| *x))); - - self.redos.push(SelectedTextCache { - keyfix: core::mem::replace(&mut self.keyfix, new_keyfix).map(|x| x.0.into_boxed_str()), - value: core::mem::replace(&mut self.value, value.into_string()).into_boxed_str(), - valuefix: core::mem::replace(&mut self.valuefix, new_valuefix).map(|x| x.0.into_boxed_str()), - cursor: core::mem::replace(&mut self.cursor, cursor), - selection: core::mem::replace(&mut self.selection, selection), - }); - self.last_interaction = Duration::ZERO; - return NothingSpecial; - } - - if (key == KeyCode::KeyY && flags == flags!(Ctrl) || key == KeyCode::KeyZ && flags == flags!(Ctrl + Shift)) && self.editable { - let SelectedTextCache { - keyfix, - value, - valuefix, - cursor, - selection, - } = 'a: { - while let Some(cache) = self.redos.pop() { - if likely(cache.ne(self)) { - self.redos.push(cache.clone()); - break 'a cache; - } - } - return Failed; - }; - - let new_keyfix = keyfix.map(|keyfix| (keyfix.into_string(), self.keyfix.as_ref().map_or(TextColor::White, |(_, x)| *x))); - let new_valuefix = valuefix.map(|valuefix| (valuefix.into_string(), self.valuefix.as_ref().map_or(TextColor::White, |(_, x)| *x))); - - self.undos.push(SelectedTextCache { - keyfix: core::mem::replace(&mut self.keyfix, new_keyfix).map(|x| x.0.into_boxed_str()), - value: core::mem::replace(&mut self.value, value.into_string()).into_boxed_str(), - valuefix: core::mem::replace(&mut self.valuefix, new_valuefix).map(|x| x.0.into_boxed_str()), - cursor: core::mem::replace(&mut self.cursor, cursor), - selection: core::mem::replace(&mut self.selection, selection), - }); - self.last_interaction = Duration::ZERO; - return NothingSpecial; - } - - if key == KeyCode::KeyA && flags == flags!(Ctrl) && self.editable { - self.cursor = 0; - self.selection = Some(self.value.len()); - return NothingSpecial; - } - - if let Some(selection) = self.selection - && flags == flags!() - { - if let KeyCode::Backspace | KeyCode::Delete = key - && self.editable - { - let (low_selection, high_selection) = if self.cursor < selection { - (self.cursor, selection) - } else { - (selection, self.cursor) - }; - let (left, right) = self.value.split_at(low_selection); - let (_, right) = right.split_at(high_selection - low_selection); - self.value = format!("{left}{right}"); - self.selection = None; - self.cursor = low_selection; - return NothingSpecial; - } - } - - if key == KeyCode::KeyX && flags == flags!(Ctrl) && self.editable { - if let Some(selection) = self.selection { - let (start, end) = if self.cursor < selection { - (self.cursor, selection) - } else { - (selection, self.cursor) - }; - let (low, right) = self.value.split_at(start); - let (cut, high) = right.split_at(end - start); - if set_clipboard(cut.to_owned()) { - self.value = format!("{low}{high}"); - self.selection = None; - } - return NothingSpecial; - } - } - - if key == KeyCode::KeyC && flags == flags!(Ctrl) && self.editable { - if let Some(selection) = self.selection { - let (start, end) = if self.cursor < selection { - (self.cursor, selection) - } else { - (selection, self.cursor) - }; - let (_, right) = self.value.split_at(start); - let (cut, _) = right.split_at(end - start); - set_clipboard(cut.to_owned()); - return NothingSpecial; - } - } - - if key == KeyCode::KeyV && flags == flags!(Ctrl) && self.editable { - if let Some(clipboard) = get_clipboard() { - if let Some(selection) = self.selection.take() { - let (start, end) = if self.cursor < selection { - (self.cursor, selection) - } else { - (selection, self.cursor) - }; - let (left, right) = self.value.split_at(start); - self.cursor = left.len() + clipboard.len(); - let (_, right) = right.split_at(end - start); - self.value = format!("{left}{clipboard}{right}"); - } else { - let (left, right) = self.value.split_at(self.cursor); - self.value = format!("{left}{clipboard}{right}"); - self.cursor += clipboard.len(); - } - return NothingSpecial; - } - } - - if key == KeyCode::Home && flags & !flags!(Shift) == 0 && self.editable { - if flags == flags!(Shift) { - let new = self.selection.map_or(self.cursor, |x| x.min(self.cursor)); - self.selection = if new == 0 { None } else { Some(new) }; - } else { - self.selection = None; - } - self.cursor = 0; - return NothingSpecial; - } - - if key == KeyCode::End && flags & !flags!(Shift) == 0 && self.editable { - if flags == flags!(Shift) { - let new = self.selection.map_or(self.cursor, |x| x.max(self.cursor)); - self.selection = if new == self.value.len() { - None - } else { - Some(new) - }; - } else { - self.selection = None; - } - self.cursor = self.value.len(); - return NothingSpecial; - } - - if key == KeyCode::Backspace && flags < 2 && self.editable { - let (left, right) = self.value.split_at(self.cursor); - if flags & flags!(Ctrl) > 0 { - if !left.is_empty() { - let mut end = left.len() - 1; - while end > 0 { - if left.as_bytes()[end].is_ascii_whitespace() { - end -= 1; - } else { - break; - } - } - let last_byte = left.as_bytes()[end]; - let last_jump_char_boundary = is_jump_char_boundary(last_byte); - while end > 0 { - let byte = left.as_bytes()[end]; - if is_utf8_char_boundary(byte) && (!last_jump_char_boundary && is_jump_char_boundary(byte) || last_jump_char_boundary && byte != last_byte) { - // this is to fix "string |" [CTRL + BACKSPACE] => "strin" instead of "string" - end += 1; - break; - } - end -= 1; - } - let (left, _) = left.split_at(end); - self.value = format!("{left}{right}"); - self.cursor = end; - } - } else { - if !left.is_empty() { - let mut end = left.len() - 1; - while end > 0 { - if is_utf8_char_boundary(left.as_bytes()[end]) { - break; - } - end -= 1; - } - let (left, _) = left.split_at(end); - self.cursor = left.len(); - self.value = format!("{left}{right}"); - } - } - - return NothingSpecial; - } - - if key == KeyCode::Delete && self.editable { - let (left, right) = self.value.split_at(self.cursor); - if flags & flags!(Ctrl) > 0 { - if !right.is_empty() { - let mut start = 1; - while start < right.len() { - if right.as_bytes()[start].is_ascii_whitespace() { - start += 1; - } else { - break; - } - } - let first_byte = right.as_bytes()[start]; - let first_jump_char_boundary = is_jump_char_boundary(first_byte); - while start < right.len() { - let byte = right.as_bytes()[start]; - if is_utf8_char_boundary(byte) && (!first_jump_char_boundary && is_jump_char_boundary(byte) || first_jump_char_boundary && byte != first_byte) { - start -= 1; - break; - } - start += 1; - } - let (_, right) = right.split_at(start); - self.cursor = left.len(); - self.value = format!("{left}{right}"); - } - } else { - if !right.is_empty() { - let mut start = 1; - while start < right.len() { - if is_utf8_char_boundary(right.as_bytes()[start]) { - break; - } - start += 1; - } - let (_, right) = right.split_at(start); - self.cursor = left.len(); - self.value = format!("{left}{right}"); - } - } - return NothingSpecial; - } - + pub fn on_key_press(&mut self, key: KeyCode, char: Option, flags: u8) -> SelectedTextKeyResult { if key == KeyCode::ArrowUp { - if flags & !flags!(Ctrl) == 0 { - return Up(flags == flags!(Ctrl)); - } else if flags == flags!(Ctrl + Shift) { - return ShiftUp; - } + if flags & !flags!(Ctrl) == 0 { + return Up(flags == flags!(Ctrl)); + } else if flags == flags!(Ctrl + Shift) { + return ShiftUp; + } } if key == KeyCode::ArrowDown { - if flags & !flags!(Ctrl) == 0 { - return Down(flags == flags!(Ctrl)); - } else if flags == flags!(Ctrl + Shift) { - return ShiftDown; - } + if flags & !flags!(Ctrl) == 0 { + return Down(flags == flags!(Ctrl)); + } else if flags == flags!(Ctrl + Shift) { + return ShiftDown; + } } if key == KeyCode::ArrowLeft { + if flags & flags!(Shift) == 0 && self.selection.is_none() && self.cursor == 0 && self.keyfix.is_some() { return Keyfix } if flags == flags!(Alt) || flags == flags!(Shift + Alt) { return ForceClose } - - if self.editable { - if flags & flags!(Shift) == 0 && self.selection.is_none() && self.cursor == 0 && self.keyfix.is_some() { return Keyfix } - - if flags & flags!(Shift) == 0 - && let Some(selection) = self.selection.take() - { - self.cursor = selection.min(self.cursor); - return NothingSpecial; - } - - let mut new = self.cursor; - if flags & flags!(Ctrl) > 0 { - if new > 0 { - new -= 1; - while new > 0 { - if self.value.as_bytes()[new].is_ascii_whitespace() { - new -= 1; - } else { - break; - } - } - let last_byte = self.value.as_bytes()[new]; - let last_jump_char_boundary = is_jump_char_boundary(last_byte); - while new > 0 { - let byte = self.value.as_bytes()[new]; - if is_utf8_char_boundary(byte) && (!last_jump_char_boundary && is_jump_char_boundary(byte) || last_jump_char_boundary && byte != last_byte) { - // this is to fix "string |" [CTRL + BACKSPACE] => "strin" instead of "string" - new += 1; - break; - } - new -= 1; - } - } - } else { - if new > 0 { - new -= 1; - while new > 0 { - if is_utf8_char_boundary(self.value.as_bytes()[new]) { - break; - } - - new -= 1; - } - } - } - - if flags & flags!(Shift) > 0 { - if self.selection.is_none() { - self.selection = Some(self.cursor); - } - } else { - self.selection = None; - } - - self.cursor = new; - - if self.selection.is_some_and(|x| x == self.cursor) { - self.selection = None; - } - } - return NothingSpecial; } if key == KeyCode::ArrowRight { + if flags & flags!(Shift) == 0 && self.selection.is_none() && self.cursor == self.value.len() && self.valuefix.is_some() { return Valuefix } if flags == flags!(Alt) || flags == flags!(Shift + Alt) { return ForceOpen } - - if self.editable { - if flags & flags!(Shift) == 0 && self.selection.is_none() && self.cursor == self.value.len() && self.valuefix.is_some() { return Valuefix } - - if flags & flags!(Shift) == 0 - && let Some(selection) = self.selection.take() - { - self.cursor = selection.max(self.cursor); - return NothingSpecial; - } - - let mut new = self.cursor; - if flags & flags!(Ctrl) > 0 { - if new < self.value.len() { - new += 1; - if new < self.value.len() { - while new < self.value.len() { - if self.value.as_bytes()[new].is_ascii_whitespace() { - new += 1; - } else { - break; - } - } - let first_byte = self.value.as_bytes()[new]; - let first_jump_char_boundary = is_jump_char_boundary(first_byte); - while new < self.value.len() { - let byte = self.value.as_bytes()[new]; - if is_utf8_char_boundary(byte) && (!first_jump_char_boundary && is_jump_char_boundary(byte) || first_jump_char_boundary && byte != first_byte) { - break; - } - new += 1; - } - } - } - } else { - if new < self.value.len() { - new += 1; - while new < self.value.len() { - if is_utf8_char_boundary(self.value.as_bytes()[new]) { - break; - } - - new += 1; - } - } - } - - if flags & flags!(Shift) > 0 { - if self.selection.is_none() { - self.selection = Some(self.cursor); - } - } else { - self.selection = None; - } - self.cursor = new; - - if self.selection.is_some_and(|x| x == self.cursor) { - self.selection = None; - } - } - return NothingSpecial; } - if let KeyCode::Enter | KeyCode::NumpadEnter = key - && flags == flags!(Shift) - && self.editable - { - char = Some('\n'); - } - - if let Some(char) = char - && self.editable - { - if let Some(selection) = self.selection { - let (low_selection, high_selection) = if self.cursor < selection { - (self.cursor, selection) - } else { - (selection, self.cursor) - }; - let (left, right) = self.value.split_at(low_selection); - let (_, right) = right.split_at(high_selection - low_selection); - self.value = format!("{left}{char}{right}"); - self.selection = None; - self.cursor = low_selection + char.len_utf8(); - } else { - let (left, right) = self.value.split_at(self.cursor); - self.value = format!("{left}{char}{right}"); - self.cursor += char.len_utf8(); - } - - return NothingSpecial; - } + self.0.on_key_press(key, char, flags).into() + } - Failed + #[inline] + pub fn post_input(&mut self) { + self.0.post_input() } #[inline] pub fn render(&self, builder: &mut VertexBufferBuilder, left_margin: usize) { let x = self.indices.len() * 16 + 32 + 4 + left_margin; - let y = if builder.scroll() > self.y { - return; - } else { - self.y - builder.scroll() - }; - + let y = if builder.scroll() > self.y { return; } else { self.y - builder.scroll() }; if y < HEADER_SIZE { return } let prefix_width = self.prefix.0.as_str().width() + self.keyfix.as_ref().map_or(0, |x| x.0.width()); + self.0.render(builder, self.value_color, (x + prefix_width, y).into(), SELECTED_TEXT_Z); + builder.draw_texture_z((x - 4 - 16, y), ELEMENT_HIGHLIGHT_Z, SELECTION_UV, (16, 16)); builder.settings((x, y), false, BASE_TEXT_Z); if let Some((keyfix, keyfix_color)) = self.keyfix.as_ref() { @@ -820,8 +325,7 @@ impl SelectedText { builder.color = self.prefix.1.to_raw(); let _ = write!(builder, "{}", self.prefix.0); - builder.color = self.value_color.to_raw(); - let _ = write!(builder, "{}", self.value); + builder.settings((x + prefix_width + self.value.width(), y), false, BASE_TEXT_Z); builder.color = self.suffix.1.to_raw(); let _ = write!(builder, "{}", self.suffix.0); @@ -830,47 +334,5 @@ impl SelectedText { builder.color = valuefix_color.to_raw(); let _ = write!(builder, "{valuefix}"); } - - if self.editable { - let cursor_prefixing = self.value.split_at(self.cursor).0; - let duration_from_last_interaction = since_epoch() - self.last_interaction; - if let Some(selection) = self.selection - && self.editable - { - let (start, end) = if self.cursor > selection { - (selection, self.cursor) - } else { - (self.cursor, selection) - }; - let start = self.value.split_at(start).0.width(); - let end = self.value.split_at(end).0.width(); - builder.draw_texture_region_z( - (prefix_width + start + x, y), - SELECTED_TEXT_Z, - SELECTION_UV + (1, 1), - (end - start - 1, 16), - (14, 14), - ); - if duration_from_last_interaction < Duration::from_millis(500) || duration_from_last_interaction.subsec_millis() < 500 { - builder.draw_texture_region_z( - (x + cursor_prefixing.width() + prefix_width - 1, y), - SELECTED_TEXT_Z, - SELECTION_UV, - (2, 16), - (1, 16), - ); - } - } else { - if duration_from_last_interaction < Duration::from_millis(500) || duration_from_last_interaction.subsec_millis() < 500 { - builder.draw_texture_region_z( - (x + cursor_prefixing.width() + prefix_width, y), - SELECTED_TEXT_Z, - SELECTION_UV, - (2, 16), - (1, 16), - ); - } - } - } } } diff --git a/src/tab.rs b/src/tab.rs index d793ea5..ed5fdfb 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -15,7 +15,8 @@ use crate::color::TextColor; use crate::elements::chunk::NbtRegion; use crate::elements::compound::NbtCompound; use crate::elements::element::NbtElement; -use crate::selected_text::SelectedText; +use crate::selected_text::{SelectedText, SelectedTextAdditional}; +use crate::text::Text; use crate::tree_travel::Navigate; use crate::vertex_buffer_builder::{Vec2u, VertexBufferBuilder}; use crate::workbench_action::WorkbenchAction; @@ -361,17 +362,7 @@ impl Tab { #[allow(clippy::too_many_lines)] pub fn close_selected_text(&mut self, ignore_invalid_format: bool, window_properties: &mut WindowProperties) -> bool { unsafe { - if let Some(SelectedText { - indices, - value, - prefix, - suffix, - keyfix, - valuefix, - editable: true, - .. - }) = self.selected_text.clone() - { + if let Some(SelectedText(Text { value, editable: true, additional: SelectedTextAdditional { indices, keyfix, prefix, suffix, valuefix, .. }, .. })) = self.selected_text.clone() { if let Some((&last, rem)) = indices.split_last() { let value = CompactString::from(value); let key = prefix.0.is_empty() && !suffix.0.is_empty(); diff --git a/src/text.rs b/src/text.rs new file mode 100644 index 0000000..6ce28a9 --- /dev/null +++ b/src/text.rs @@ -0,0 +1,589 @@ +use std::intrinsics::{likely, unlikely}; +use std::ops::{Deref, DerefMut}; +use std::time::Duration; + +use winit::keyboard::KeyCode; + +use crate::{flags, get_clipboard, is_jump_char_boundary, is_utf8_char_boundary, LinkedQueue, OptionExt, set_clipboard, since_epoch, StrExt}; +use crate::assets::{BASE_TEXT_Z, SELECTED_TEXT_Z, SELECTION_UV}; +use crate::color::TextColor; +use crate::text::KeyResult::{Failed, Finish, NothingSpecial, Revert}; +use crate::vertex_buffer_builder::{Vec2u, VertexBufferBuilder}; + +#[repr(u8)] +pub enum SelectedTextKeyResult { + Failed, + NothingSpecial, + Revert, + Finish, + Keyfix, + Valuefix, + Up(bool), + Down(bool), + ForceClose, + ForceOpen, + ShiftUp, + ShiftDown, +} + +#[repr(u8)] +pub enum KeyResult { + Failed, + NothingSpecial, + Revert, + Finish, +} + +impl From for SelectedTextKeyResult { + fn from(value: KeyResult) -> Self { + match value { + Failed => Self::Failed, + NothingSpecial => Self::NothingSpecial, + Revert => Self::Revert, + Finish => Self::Finish, + } + } +} + +pub trait Cachelike: PartialEq + Clone { + fn new(text: &Text) -> Self where Self: Sized; + + fn revert(self, text: &mut Text) where Self: Sized; +} + +#[derive(Clone)] +pub struct Text> { + pub value: String, + pub cursor: usize, + pub selection: Option, + pub editable: bool, + pub additional: Additional, + last_interaction: Duration, + undos: LinkedQueue, + redos: LinkedQueue, +} + +impl> Deref for Text { + type Target = Additional; + + fn deref(&self) -> &Self::Target { + &self.additional + } +} + +impl> DerefMut for Text { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.additional + } +} + +impl> Text { + pub fn new(value: String, cursor: usize, editable: bool, additional: Additional) -> Self { + let mut this = Self { + value, + cursor, + selection: None, + last_interaction: since_epoch(), + editable, + undos: LinkedQueue::new(), + redos: LinkedQueue::new(), + additional, + }; + this.save_state_in_history(); + this + } + + pub const fn uninit() -> Self { + Self { + value: String::new(), + cursor: 0, + selection: None, + last_interaction: Duration::ZERO, + editable: true, + undos: LinkedQueue::new(), + redos: LinkedQueue::new(), + additional: unsafe { core::mem::zeroed() }, + } + } + + #[inline] + pub fn interact(&mut self) { + self.last_interaction = since_epoch(); + } + + #[must_use] + pub fn on_key_press(&mut self, key: KeyCode, mut char: Option, flags: u8) -> KeyResult { + if key == KeyCode::Escape && flags == flags!() { return Revert } + + if let KeyCode::Enter | KeyCode::NumpadEnter = key && flags == flags!() { + return Finish; + } + + if key == KeyCode::KeyZ && flags == flags!(Ctrl) && self.editable { + let current = Cache::new(&self); + + let cache = 'a: { + while let Some(cache) = self.undos.pop() { + if unlikely(cache.ne(¤t)) { + self.undos.push(cache.clone()); + break 'a cache; + } + } + return Failed; + }; + + self.redos.push(current); + self.last_interaction = Duration::ZERO; + cache.revert(self); + return NothingSpecial; + } + + if (key == KeyCode::KeyY && flags == flags!(Ctrl) || key == KeyCode::KeyZ && flags == flags!(Ctrl + Shift)) && self.editable { + let current = Cache::new(&self); + + let cache = 'a: { + while let Some(cache) = self.redos.pop() { + if likely(cache.ne(¤t)) { + self.redos.push(cache.clone()); + break 'a cache; + } + } + return Failed; + }; + + self.undos.push(current); + self.last_interaction = Duration::ZERO; + cache.revert(self); + return NothingSpecial; + } + + if key == KeyCode::KeyA && flags == flags!(Ctrl) && self.editable { + self.cursor = 0; + self.selection = Some(self.value.len()); + return NothingSpecial; + } + + if let Some(selection) = self.selection + && flags == flags!() + { + if let KeyCode::Backspace | KeyCode::Delete = key + && self.editable + { + let (low_selection, high_selection) = if self.cursor < selection { + (self.cursor, selection) + } else { + (selection, self.cursor) + }; + let (left, right) = self.value.split_at(low_selection); + let (_, right) = right.split_at(high_selection - low_selection); + self.value = format!("{left}{right}"); + self.selection = None; + self.cursor = low_selection; + return NothingSpecial; + } + } + + if key == KeyCode::KeyX && flags == flags!(Ctrl) && self.editable { + if let Some(selection) = self.selection { + let (start, end) = if self.cursor < selection { + (self.cursor, selection) + } else { + (selection, self.cursor) + }; + let (low, right) = self.value.split_at(start); + let (cut, high) = right.split_at(end - start); + if set_clipboard(cut.to_owned()) { + self.value = format!("{low}{high}"); + self.selection = None; + } + return NothingSpecial; + } + } + + if key == KeyCode::KeyC && flags == flags!(Ctrl) && self.editable { + if let Some(selection) = self.selection { + let (start, end) = if self.cursor < selection { + (self.cursor, selection) + } else { + (selection, self.cursor) + }; + let (_, right) = self.value.split_at(start); + let (cut, _) = right.split_at(end - start); + set_clipboard(cut.to_owned()); + return NothingSpecial; + } + } + + if key == KeyCode::KeyV && flags == flags!(Ctrl) && self.editable { + if let Some(clipboard) = get_clipboard() { + if let Some(selection) = self.selection.take() { + let (start, end) = if self.cursor < selection { + (self.cursor, selection) + } else { + (selection, self.cursor) + }; + let (left, right) = self.value.split_at(start); + self.cursor = left.len() + clipboard.len(); + let (_, right) = right.split_at(end - start); + self.value = format!("{left}{clipboard}{right}"); + } else { + let (left, right) = self.value.split_at(self.cursor); + self.value = format!("{left}{clipboard}{right}"); + self.cursor += clipboard.len(); + } + return NothingSpecial; + } + } + + if key == KeyCode::Home && flags & !flags!(Shift) == 0 && self.editable { + if flags == flags!(Shift) { + let new = self.selection.map_or(self.cursor, |x| x.min(self.cursor)); + self.selection = if new == 0 { None } else { Some(new) }; + } else { + self.selection = None; + } + self.cursor = 0; + return NothingSpecial; + } + + if key == KeyCode::End && flags & !flags!(Shift) == 0 && self.editable { + if flags == flags!(Shift) { + let new = self.selection.map_or(self.cursor, |x| x.max(self.cursor)); + self.selection = if new == self.value.len() { + None + } else { + Some(new) + }; + } else { + self.selection = None; + } + self.cursor = self.value.len(); + return NothingSpecial; + } + + if key == KeyCode::Backspace && flags < 2 && self.editable { + let (left, right) = self.value.split_at(self.cursor); + if flags & flags!(Ctrl) > 0 { + if !left.is_empty() { + let mut end = left.len() - 1; + while end > 0 { + if left.as_bytes()[end].is_ascii_whitespace() { + end -= 1; + } else { + break; + } + } + let last_byte = left.as_bytes()[end]; + let last_jump_char_boundary = is_jump_char_boundary(last_byte); + while end > 0 { + let byte = left.as_bytes()[end]; + if is_utf8_char_boundary(byte) && (!last_jump_char_boundary && is_jump_char_boundary(byte) || last_jump_char_boundary && byte != last_byte) { + // this is to fix "string |" [CTRL + BACKSPACE] => "strin" instead of "string" + end += 1; + break; + } + end -= 1; + } + let (left, _) = left.split_at(end); + self.value = format!("{left}{right}"); + self.cursor = end; + } + } else { + if !left.is_empty() { + let mut end = left.len() - 1; + while end > 0 { + if is_utf8_char_boundary(left.as_bytes()[end]) { + break; + } + end -= 1; + } + let (left, _) = left.split_at(end); + self.cursor = left.len(); + self.value = format!("{left}{right}"); + } + } + + return NothingSpecial; + } + + if key == KeyCode::Delete && self.editable { + let (left, right) = self.value.split_at(self.cursor); + if flags & flags!(Ctrl) > 0 { + if !right.is_empty() { + let mut start = 1; + while start < right.len() { + if right.as_bytes()[start].is_ascii_whitespace() { + start += 1; + } else { + break; + } + } + let first_byte = right.as_bytes()[start]; + let first_jump_char_boundary = is_jump_char_boundary(first_byte); + while start < right.len() { + let byte = right.as_bytes()[start]; + if is_utf8_char_boundary(byte) && (!first_jump_char_boundary && is_jump_char_boundary(byte) || first_jump_char_boundary && byte != first_byte) { + start -= 1; + break; + } + start += 1; + } + let (_, right) = right.split_at(start); + self.cursor = left.len(); + self.value = format!("{left}{right}"); + } + } else { + if !right.is_empty() { + let mut start = 1; + while start < right.len() { + if is_utf8_char_boundary(right.as_bytes()[start]) { + break; + } + start += 1; + } + let (_, right) = right.split_at(start); + self.cursor = left.len(); + self.value = format!("{left}{right}"); + } + } + return NothingSpecial; + } + + if key == KeyCode::ArrowLeft { + if self.editable { + if flags & flags!(Shift) == 0 + && let Some(selection) = self.selection.take() + { + self.cursor = selection.min(self.cursor); + return NothingSpecial; + } + + let mut new = self.cursor; + if flags & flags!(Ctrl) > 0 { + if new > 0 { + new -= 1; + while new > 0 { + if self.value.as_bytes()[new].is_ascii_whitespace() { + new -= 1; + } else { + break; + } + } + let last_byte = self.value.as_bytes()[new]; + let last_jump_char_boundary = is_jump_char_boundary(last_byte); + while new > 0 { + let byte = self.value.as_bytes()[new]; + if is_utf8_char_boundary(byte) && (!last_jump_char_boundary && is_jump_char_boundary(byte) || last_jump_char_boundary && byte != last_byte) { + // this is to fix "string |" [CTRL + BACKSPACE] => "strin" instead of "string" + new += 1; + break; + } + new -= 1; + } + } + } else { + if new > 0 { + new -= 1; + while new > 0 { + if is_utf8_char_boundary(self.value.as_bytes()[new]) { + break; + } + + new -= 1; + } + } + } + + if flags & flags!(Shift) > 0 { + if self.selection.is_none() { + self.selection = Some(self.cursor); + } + } else { + self.selection = None; + } + + self.cursor = new; + + if self.selection.is_some_and(|x| x == self.cursor) { + self.selection = None; + } + } + return NothingSpecial; + } + + if key == KeyCode::ArrowRight { + if self.editable { + if flags & flags!(Shift) == 0 + && let Some(selection) = self.selection.take() + { + self.cursor = selection.max(self.cursor); + return NothingSpecial; + } + + let mut new = self.cursor; + if flags & flags!(Ctrl) > 0 { + if new < self.value.len() { + new += 1; + if new < self.value.len() { + while new < self.value.len() { + if self.value.as_bytes()[new].is_ascii_whitespace() { + new += 1; + } else { + break; + } + } + let first_byte = self.value.as_bytes()[new]; + let first_jump_char_boundary = is_jump_char_boundary(first_byte); + while new < self.value.len() { + let byte = self.value.as_bytes()[new]; + if is_utf8_char_boundary(byte) && (!first_jump_char_boundary && is_jump_char_boundary(byte) || first_jump_char_boundary && byte != first_byte) { + break; + } + new += 1; + } + } + } + } else { + if new < self.value.len() { + new += 1; + while new < self.value.len() { + if is_utf8_char_boundary(self.value.as_bytes()[new]) { + break; + } + + new += 1; + } + } + } + + if flags & flags!(Shift) > 0 { + if self.selection.is_none() { + self.selection = Some(self.cursor); + } + } else { + self.selection = None; + } + self.cursor = new; + + if self.selection.is_some_and(|x| x == self.cursor) { + self.selection = None; + } + } + return NothingSpecial; + } + + if let KeyCode::Enter | KeyCode::NumpadEnter = key + && flags == flags!(Shift) + && self.editable + { + char = Some('\n'); + } + + if let Some(char) = char && self.editable { + if let Some(selection) = self.selection { + let (low_selection, high_selection) = if self.cursor < selection { + (self.cursor, selection) + } else { + (selection, self.cursor) + }; + let (left, right) = self.value.split_at(low_selection); + let (_, right) = right.split_at(high_selection - low_selection); + self.value = format!("{left}{char}{right}"); + self.selection = None; + self.cursor = low_selection + char.len_utf8(); + } else { + let (left, right) = self.value.split_at(self.cursor); + self.value = format!("{left}{char}{right}"); + self.cursor += char.len_utf8(); + } + + return NothingSpecial; + } + + Failed + } + + pub fn render(&self, builder: &mut VertexBufferBuilder, color: TextColor, pos: Vec2u, z: u8) { + use std::fmt::Write; + + let (x, y) = pos.into(); + + builder.settings((x, y), false, z); + + builder.color = color.to_raw(); + let _ = write!(builder, "{}", self.value); + + if self.editable { + let cursor_prefixing = self.value.split_at(self.cursor).0; + let duration_from_last_interaction = since_epoch() - self.last_interaction; + if let Some(selection) = self.selection && self.editable { + let (start, end) = if self.cursor > selection { + (selection, self.cursor) + } else { + (self.cursor, selection) + }; + let start = self.value.split_at(start).0.width(); + let end = self.value.split_at(end).0.width(); + builder.draw_texture_region_z( + (start + x, y), + z + 1, + SELECTION_UV + (1, 1), + (end - start - 1, 16), + (14, 14), + ); + if duration_from_last_interaction < Duration::from_millis(500) || duration_from_last_interaction.subsec_millis() < 500 { + builder.draw_texture_region_z( + (x + cursor_prefixing.width() - 1, y), + z + 1, + SELECTION_UV, + (2, 16), + (1, 16), + ); + } + } else { + if duration_from_last_interaction < Duration::from_millis(500) || duration_from_last_interaction.subsec_millis() < 500 { + builder.draw_texture_region_z( + (x + cursor_prefixing.width(), y), + z + 1, + SELECTION_UV, + (2, 16), + (1, 16), + ); + } + } + } + } + + #[inline] + pub fn save_state_in_history(&mut self) { + self.undos.push(Cache::new(&self)); + } + + #[inline] + pub fn post_input(&mut self) { + let current = Cache::new(self); + + let should_cache = core::mem::replace(&mut self.last_interaction, since_epoch()).as_millis() >= 1_500; + if should_cache && self.editable && self.undos.get().is_none_or(|x| x.ne(¤t)) { + if self.redos.pop().is_none_or(|x| x.ne(¤t)) { + self.redos = LinkedQueue::new(); + } + + self.save_state_in_history(); + } + + if self.undos.get().is_some_and(|x| x.eq(¤t)) && let Some(undo) = self.undos.get_mut() { + *undo = current; + } + } + + #[inline] + pub fn clear(&mut self) { + self.value.clear(); + self.cursor = 0; + self.redos.clear(); + self.undos.clear(); + self.selection = None; + self.last_interaction = since_epoch(); + } +} diff --git a/src/window.rs b/src/window.rs index 47087c2..bd8c74d 100644 --- a/src/window.rs +++ b/src/window.rs @@ -27,9 +27,9 @@ use crate::workbench::Workbench; use crate::{assets, WORKBENCH, WINDOW_PROPERTIES, error, OptionExt, since_epoch, WindowProperties}; pub const WINDOW_HEIGHT: usize = 420; -pub const WINDOW_WIDTH: usize = 620; +pub const WINDOW_WIDTH: usize = 720; pub const MIN_WINDOW_HEIGHT: usize = HEADER_SIZE + 16; -pub const MIN_WINDOW_WIDTH: usize = 520; +pub const MIN_WINDOW_WIDTH: usize = 620; pub async fn run() -> ! { let event_loop = EventLoop::new().expect("Event loop was unconstructable"); @@ -74,7 +74,7 @@ pub async fn run() -> ! { let mut state = State::new(&window, window_size).await; unsafe { std::ptr::write(std::ptr::addr_of_mut!(WINDOW_PROPERTIES), UnsafeCell::new(WindowProperties::new(Rc::clone(&window)))); } let window_properties = unsafe { WINDOW_PROPERTIES.get_mut() }; - unsafe { *WORKBENCH.get_mut() = Workbench::new(window_properties); } + unsafe { std::ptr::write(std::ptr::addr_of_mut!(WORKBENCH), UnsafeCell::new(Workbench::new(window_properties))); } let workbench = unsafe { WORKBENCH.get_mut() }; event_loop.run(|event, _| match event { Event::WindowEvent { event, window_id } if window_id == window.id() => { diff --git a/src/workbench.rs b/src/workbench.rs index d5fb02d..4d6b076 100644 --- a/src/workbench.rs +++ b/src/workbench.rs @@ -19,7 +19,7 @@ use winit::keyboard::{KeyCode, PhysicalKey}; use zune_inflate::DeflateDecoder; use crate::alert::Alert; -use crate::assets::{ACTION_WHEEL_Z, 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::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; @@ -27,7 +27,8 @@ use crate::elements::element::NbtElement; use crate::elements::element::{NbtByte, NbtByteArray, NbtDouble, NbtFloat, NbtInt, NbtIntArray, NbtLong, NbtLongArray, NbtShort}; use crate::elements::list::{NbtList, ValueIterator}; use crate::elements::string::NbtString; -use crate::selected_text::{KeyResult, SelectedText}; +use crate::selected_text::{SelectedText, SelectedTextAdditional}; +use crate::text::{KeyResult, SelectedTextKeyResult, Text}; use crate::tab::{FileFormat, Tab}; use crate::tree_travel::{Navigate, Traverse, TraverseParents}; use crate::vertex_buffer_builder::Vec2u; @@ -35,6 +36,7 @@ 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, @@ -60,10 +62,13 @@ pub struct Workbench { pub scale: usize, steal_animation_data: Option<(Duration, Vec2u)>, sort_algorithm: SortAlgorithm, + search_box: SearchBox, } impl Workbench { - pub const fn uninit() -> Self { + #[inline] + #[must_use] + pub const unsafe fn uninit() -> Self { Self { tabs: vec![], tab: 0, @@ -87,7 +92,8 @@ impl Workbench { alerts: vec![], scale: 0, steal_animation_data: None, - sort_algorithm: SortAlgorithm::Type, + sort_algorithm: SortAlgorithm::None, + search_box: SearchBox::uninit(), } } @@ -118,6 +124,7 @@ impl Workbench { scale: 1, steal_animation_data: None, sort_algorithm: SortAlgorithm::Type, + search_box: SearchBox::new(), }; 'create_tab: { if let Some(path) = &std::env::args() @@ -274,6 +281,13 @@ impl Workbench { if button == MouseButton::Left { self.steal_animation_data = None; } + if button == MouseButton::Left { + if self.try_select_search_box() { + return true; + } else { + self.search_box.deselect(); + } + } self.held_mouse_keys.remove(&button); if y < 19 && x > 2 && y > 3 { self.click_tab(button, window_properties); @@ -311,11 +325,13 @@ impl Workbench { break 'a; } } + if button == MouseButton::Left { if self.try_select_text() { break 'a; } } + if button == MouseButton::Left { if self.bookmark_line() { break 'a; @@ -1328,6 +1344,16 @@ impl Workbench { true } + #[inline] + fn try_select_search_box(&mut self) -> bool { + if (283..self.window_width - 215).contains(&self.mouse_x) && (23..45).contains(&self.mouse_y) { + self.search_box.select(self.mouse_x - 283); + true + } else { + false + } + } + #[inline] fn try_select_text(&mut self) -> bool { let left_margin = self.left_margin(); @@ -1387,69 +1413,29 @@ impl Workbench { #[inline] pub fn keyfix(&mut self, window_properties: &mut WindowProperties) { let tab = tab_mut!(self); - if let Some(SelectedText { - y, - indices, - cursor, - value, - selection, - keyfix, - prefix, - suffix, - valuefix, - editable: true, - last_interaction: _, - undos: _, - redos: _, - value_color, - }) = tab.selected_text.clone() + if let Some(SelectedText(Text { value, cursor, editable: true, additional: SelectedTextAdditional { y, indices, value_color, keyfix, prefix, suffix, valuefix }, .. })) = tab.selected_text.clone() && let Some((keyfix, keyfix_color)) = keyfix && valuefix.is_none() && suffix.0.is_empty() && cursor == 0 { if !tab.close_selected_text(false, window_properties) { return } - tab.selected_text = Some( - SelectedText { - y, - indices, - cursor: keyfix.len(), - selection, - keyfix: None, - prefix: (String::new(), TextColor::White), - suffix: prefix, - valuefix: Some((value, value_color)), - value: keyfix, - editable: true, - last_interaction: since_epoch(), - undos: LinkedQueue::new(), - redos: LinkedQueue::new(), - value_color: keyfix_color, - } - .post_process(), - ); + tab.selected_text = Some(SelectedText(Text::new(keyfix.clone(), keyfix.len(), true, SelectedTextAdditional { + y, + indices, + value_color: keyfix_color, + keyfix: None, + prefix: (String::new(), TextColor::White), + suffix: prefix, + valuefix: Some((value, value_color)), + }))); } } #[inline] pub fn valuefix(&mut self, window_properties: &mut WindowProperties) { let tab = tab_mut!(self); - if let Some(SelectedText { - y, - indices, - cursor, - value, - selection, - keyfix, - prefix, - suffix, - valuefix, - editable: true, - last_interaction: _, - undos: _, - redos: _, - value_color, - }) = tab.selected_text.clone() + if let Some(SelectedText(Text { value, cursor, editable: true, additional: SelectedTextAdditional { y, indices, value_color, keyfix, prefix, suffix, valuefix }, .. })) = tab.selected_text.clone() && let Some((valuefix, valuefix_color)) = valuefix && keyfix.is_none() && prefix.0.is_empty() @@ -1457,32 +1443,22 @@ impl Workbench { { // normally won't occur, but im future proofing if !tab.close_selected_text(false, window_properties) { return } - tab.selected_text = Some( - SelectedText { - y, - indices, - cursor: 0, - selection, - keyfix: Some((value, value_color)), - prefix: suffix, - suffix: (String::new(), TextColor::White), - valuefix: None, - value: valuefix, - value_color: valuefix_color, - editable: true, - last_interaction: since_epoch(), - undos: LinkedQueue::new(), - redos: LinkedQueue::new(), - } - .post_process(), - ); + tab.selected_text = Some(SelectedText(Text::new(valuefix, 0, true, SelectedTextAdditional { + y, + indices, + value_color: valuefix_color, + keyfix: Some((value, value_color)), + prefix: suffix, + suffix: (String::new(), TextColor::White), + valuefix: None, + }))); } } #[inline] pub fn shift_selected_text_up(&mut self) { let tab = tab_mut!(self); - if let Some(SelectedText { y, indices, .. }) = &mut tab.selected_text { + if let Some(SelectedText(Text { additional: SelectedTextAdditional { y, indices, .. }, .. })) = &mut tab.selected_text { if indices.is_empty() { return } // well it could be empty let child_idx = unsafe { indices @@ -1539,7 +1515,7 @@ impl Workbench { #[inline] pub fn shift_selected_text_down(&mut self) { let tab = tab_mut!(self); - if let Some(SelectedText { y, indices, .. }) = &mut tab.selected_text { + if let Some(SelectedText(Text { additional: SelectedTextAdditional { y, indices, .. }, .. })) = &mut tab.selected_text { // well it could be empty if indices.is_empty() { return } let child_idx = unsafe { @@ -1600,16 +1576,7 @@ impl Workbench { pub unsafe fn selected_text_up(&mut self, ctrl: bool, window_properties: &mut WindowProperties) { let left_margin = self.left_margin(); let tab = tab_mut!(self); - if let Some(SelectedText { - y, - indices, - cursor, - keyfix, - prefix, - value: str_value, - .. - }) = tab.selected_text.clone() - { + if let Some(SelectedText(Text { value: str_value, cursor, additional: SelectedTextAdditional { y, indices, keyfix, prefix, .. }, .. })) = tab.selected_text.clone() { let Some(&last_index) = indices.last() else { return; }; @@ -1727,7 +1694,7 @@ impl Workbench { let left_margin = self.left_margin(); let tab = tab_mut!(self); - let total = if let Some(SelectedText { indices, .. }) = tab.selected_text.as_ref() { + let total = if let Some(SelectedText(Text { additional: SelectedTextAdditional { indices, .. }, .. })) = tab.selected_text.as_ref() { let mut total = sum_indices(indices.iter().copied(), &tab.value); total += 1; // move down // down needs a check that it doesn't surpass the end @@ -1737,16 +1704,7 @@ impl Workbench { return; }; - if let Some(SelectedText { - y, - indices, - cursor, - keyfix, - prefix, - value: str_value, - .. - }) = tab.selected_text.clone() - { + if let Some(SelectedText(Text { value: str_value, cursor, additional: SelectedTextAdditional { y, indices, keyfix, prefix, .. }, .. })) = tab.selected_text.clone() { if !tab.close_selected_text(false, window_properties) { return } let cache_cursor_x = self.cache_cursor_x; let original_indices_len = indices.len(); @@ -1860,7 +1818,7 @@ impl Workbench { #[inline] pub fn force_close(&mut self) { let tab = tab_mut!(self); - if let Some(SelectedText { indices, y, .. }) = tab.selected_text.as_ref() { + if let Some(SelectedText(Text { additional: SelectedTextAdditional { y, indices, .. }, .. })) = tab.selected_text.as_ref() { let indices = indices.clone(); let (_, _, element, line_number) = Navigate::new(indices.iter().copied(), &mut tab.value).last(); let decrement = element.height() - 1; @@ -1887,7 +1845,7 @@ impl Workbench { #[inline] pub fn force_open(&mut self) { let tab = tab_mut!(self); - if let Some(SelectedText { indices, y, .. }) = tab.selected_text.as_ref() { + if let Some(SelectedText(Text { additional: SelectedTextAdditional { y, indices, .. }, .. })) = tab.selected_text.as_ref() { let indices = indices.clone(); let shift = self.held_keys.contains(&KeyCode::ShiftLeft) | self.held_keys.contains(&KeyCode::ShiftRight); let (_, _, element, line_number) = Navigate::new(indices.iter().copied(), &mut tab.value).last(); @@ -1993,72 +1951,90 @@ impl Workbench { 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 left_margin = self.left_margin(); let tab = tab_mut!(self); + if self.search_box.is_selected() { + match self.search_box.on_key_press(key, char, flags) { + KeyResult::Failed => {} // next thing please + KeyResult::NothingSpecial => { + self.search_box.post_input((self.window_width, self.window_height)); + return true; + } + KeyResult::Revert => { + self.search_box.post_input((self.window_width, self.window_height)); + return true; + } + KeyResult::Finish => { + self.search_box.post_input((self.window_width, self.window_height)); + self.search_box.search(); + return true; + } + } + } if let Some(selected_text) = &mut tab.selected_text { match selected_text.on_key_press(key, char, flags) { - KeyResult::NothingSpecial => { - selected_text.handle_history(); + SelectedTextKeyResult::NothingSpecial => { + selected_text.post_input(); self.cache_cursor_x = None; self.refresh_selected_text_horizontal_scroll(); return true; } - KeyResult::Revert => { + SelectedTextKeyResult::Revert => { tab.selected_text = None; self.cache_cursor_x = None; return true; } - KeyResult::Finish => { + SelectedTextKeyResult::Finish => { // we just won't let you leave if you didn't fix it ;) let _ = tab.close_selected_text(true, window_properties); self.cache_cursor_x = None; return true; } - KeyResult::Keyfix => { + SelectedTextKeyResult::Keyfix => { self.keyfix(window_properties); self.refresh_selected_text_horizontal_scroll(); self.cache_cursor_x = None; return true; } - KeyResult::Valuefix => { + SelectedTextKeyResult::Valuefix => { self.valuefix(window_properties); self.refresh_selected_text_horizontal_scroll(); self.cache_cursor_x = None; return true; } - KeyResult::Up(ctrl) => { + SelectedTextKeyResult::Up(ctrl) => { unsafe { self.selected_text_up(ctrl, window_properties); } self.refresh_selected_text_horizontal_scroll(); return true; } - KeyResult::Down(ctrl) => { + SelectedTextKeyResult::Down(ctrl) => { unsafe { self.selected_text_down(ctrl, window_properties); } self.refresh_selected_text_horizontal_scroll(); return true; } - KeyResult::ForceClose => { - selected_text.handle_history(); + SelectedTextKeyResult::ForceClose => { + selected_text.post_input(); self.force_close(); return true; } - KeyResult::ForceOpen => { - selected_text.handle_history(); + SelectedTextKeyResult::ForceOpen => { + selected_text.post_input(); self.force_open(); return true; } - KeyResult::ShiftUp => { - selected_text.handle_history(); + SelectedTextKeyResult::ShiftUp => { + selected_text.post_input(); self.shift_selected_text_up(); return true; } - KeyResult::ShiftDown => { - selected_text.handle_history(); + SelectedTextKeyResult::ShiftDown => { + selected_text.post_input(); self.shift_selected_text_down(); return true; } - KeyResult::Failed => {} // next thing pls + SelectedTextKeyResult::Failed => {} // next thing pls } } // todo, custom help file @@ -2359,6 +2335,33 @@ impl Workbench { pub fn render(&mut self, builder: &mut VertexBufferBuilder) { if self.raw_window_width < MIN_WINDOW_WIDTH || self.raw_window_height < MIN_WINDOW_HEIGHT { return; } + { + builder.draw_texture_region_z( + (281, 22), + BASE_Z, + LINE_NUMBER_SEPARATOR_UV, + (2, 23), + (2, 16), + ); + self.search_box.render(builder); + } + + builder.draw_texture_region_z( + (0, 23), + BASE_Z - 1, + BACKDROP_UV, + (283, 22), + (16, 16), + ); + + builder.draw_texture_region_z( + (self.window_width - 215, 23), + BASE_Z - 1, + BACKDROP_UV, + (215, 22), + (16, 16), + ); + for n in 0..(builder.window_height() - HEADER_SIZE + 15) / 16 { let uv = if n % 2 == 0 { DARK_STRIPE_UV + (1, 1) @@ -2393,7 +2396,7 @@ impl Workbench { .map(|x| x.y) .and_then(|x| x.checked_sub(builder.scroll())) .unwrap_or(0); - let (selected_key, selected_value, selecting_key) = if let Some(selected) = tab.selected_text.as_ref() && selected.editable { + let (selected_key, selected_value, selecting_key) = if let Some(SelectedText(selected)) = tab.selected_text.as_ref() && selected.editable { if selected.keyfix.is_some() { // Health: __20.0__ (selected.keyfix.clone().map(|x| x.0.clone().into_boxed_str()), Some(selected.value.clone().into_boxed_str()), false) } else if selected.valuefix.is_some() { // __Health__: 20.0 @@ -2430,22 +2433,6 @@ impl Workbench { (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, &mut ctx,