From 86e13d5b01ee3eaec9d9df1473ccdf0514605682 Mon Sep 17 00:00:00 2001 From: alice pellerin Date: Sun, 22 Mar 2026 03:55:42 -0500 Subject: [PATCH] improve popup rendering --- Cargo.lock | 4 +-- src/action.rs | 73 ++++++++++++++++++++++++++------------------ src/buffer/mod.rs | 36 +++++++++------------- src/buffer/widget.rs | 10 +++--- src/main.rs | 10 ++---- 5 files changed, 69 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1184b15..15d22a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1370,9 +1370,9 @@ dependencies = [ [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" diff --git a/src/action.rs b/src/action.rs index 9f00f94..68bd08a 100644 --- a/src/action.rs +++ b/src/action.rs @@ -1,5 +1,5 @@ use std::{cmp::min, collections::hash_set::Entry, convert::identity, fs::File, io::Write, iter, mem::{replace, swap}}; -use ratatui::{style::Stylize, text::Span}; +use ratatui::{style::{Color, Stylize}, text::Span}; use crate::{BYTES_OF_PADDING, BYTES_PER_LINE, LINES_OF_PADDING, app::WindowSize, buffer::{Buffer, Mode, PartialAction, Popup}, cursor::Cursor, edit_action::EditAction}; #[derive(Clone, Copy)] @@ -680,7 +680,7 @@ impl Buffer { let max_contents_index = self.max_contents_index(); let mark_after_primary = mark_after( - self.primary_cursor.head + 1, + self.primary_cursor.head, &sorted_marks, max_contents_index ); @@ -690,7 +690,7 @@ impl Buffer { for cursor in &mut self.cursors { let mark_after_cursor = mark_after( - cursor.head + 1, + cursor.head, &sorted_marks, max_contents_index ); @@ -705,21 +705,21 @@ impl Buffer { fn extend_to_null(&mut self, window_size: WindowSize) { if let Some(null_offset_after_primary) = self.contents[self.primary_cursor.head..] .iter() - .skip(2) + .skip(1) .position(|&byte| byte == 0) { self.primary_cursor.tail = self.primary_cursor.head; - self.primary_cursor.head += null_offset_after_primary + 1; + self.primary_cursor.head += null_offset_after_primary; } for cursor in &mut self.cursors { if let Some(null_offset_after_primary) = self.contents[cursor.head..] .iter() - .skip(2) + .skip(1) .position(|&byte| byte == 0) { cursor.tail = cursor.head; - cursor.head += null_offset_after_primary + 1; + cursor.head += null_offset_after_primary; } } @@ -730,21 +730,21 @@ impl Buffer { fn extend_to_FF(&mut self, window_size: WindowSize) { if let Some(null_offset_after_primary) = self.contents[self.primary_cursor.head..] .iter() - .skip(2) + .skip(1) .position(|&byte| byte == 0xFF) { self.primary_cursor.tail = self.primary_cursor.head; - self.primary_cursor.head += null_offset_after_primary + 1; + self.primary_cursor.head += null_offset_after_primary; } for cursor in &mut self.cursors { if let Some(null_offset_after_primary) = self.contents[cursor.head..] .iter() - .skip(2) + .skip(1) .position(|&byte| byte == 0xFF) { cursor.tail = cursor.head; - cursor.head += null_offset_after_primary + 1; + cursor.head += null_offset_after_primary; } } @@ -764,17 +764,18 @@ impl Buffer { let nat = bytes_to_nat(selection); - let int = nat.and_then(|nat| nat_to_int_if_different(nat, selection.len())); + let int = nat + .and_then(|nat| nat_to_int_if_different(nat, selection.len())) + .map(|int| Span::from(format!("{int}")).white()); let utf8 = str::from_utf8(selection).ok() .and_then(|utf8| { - if utf8.contains(|char: char| char.is_ascii() && !char.is_ascii_graphic()) { - None - } else { - Some(utf8) - } + utf8 + .contains(character_is_allowed_in_strings) + .then_some(utf8) }) - .map(|utf8| utf8.replace('\0', "\\0")); + .map(|utf8| utf8.replace('\0', "\\0")) + .map(|utf8| Span::from(format!("\"{utf8}\"")).red()); #[allow(clippy::cast_precision_loss)] let fixedpoint2012 = nat @@ -783,7 +784,7 @@ impl Buffer { let two_decimals_is_enough = (fixedpoint2012 * 100.0).fract() == 0.0; let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; - format!("20.12: {approximate_symbol}{fixedpoint2012:.2}") + format!("20.12: {approximate_symbol}{fixedpoint2012:.2}").into() }); #[allow(clippy::cast_precision_loss)] @@ -793,7 +794,7 @@ impl Buffer { let two_decimals_is_enough = (fixedpoint1616 * 100.0).fract() == 0.0; let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; - format!("16.16: {approximate_symbol}{fixedpoint1616:.2}") + format!("16.16: {approximate_symbol}{fixedpoint1616:.2}").into() }); #[allow(clippy::cast_precision_loss)] @@ -803,7 +804,7 @@ impl Buffer { let two_decimals_is_enough = (fixedpoint124 * 100.0).fract() == 0.0; let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; - format!("12.4: {approximate_symbol}{fixedpoint124:.2}") + format!("12.4: {approximate_symbol}{fixedpoint124:.2}").into() }); #[allow(clippy::cast_precision_loss)] @@ -813,7 +814,7 @@ impl Buffer { let two_decimals_is_enough = (fixedpoint88 * 100.0).fract() == 0.0; let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; - format!("8.8: {approximate_symbol}{fixedpoint88:.2}") + format!("8.8: {approximate_symbol}{fixedpoint88:.2}").into() }); #[allow(clippy::cast_precision_loss)] @@ -823,24 +824,30 @@ impl Buffer { let two_decimals_is_enough = (fixedpoint412 * 100.0).fract() == 0.0; let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; - format!("4.12: {approximate_symbol}{fixedpoint412:.2}") + format!("4.12: {approximate_symbol}{fixedpoint412:.2}").into() }); - let color = (selection.len() == 3).then(|| [selection[0], selection[1], selection[2]]); + let color = (selection.len() == 3) + .then(|| [selection[0], selection[1], selection[2]]) + .map(|[red, green, blue]| { + Span::from(format!("#{red:02X}{green:02X}{blue:02X}")) + .fg(Color::Rgb(red, green, blue)) + + }); Popup::new( cursor.lower_bound(), - int.map(|int| format!("{int}")) + int .into_iter() - .chain(nat.map(|nat| format!("{nat}"))) - .chain(utf8.map(|utf8| format!("\"{utf8}\""))) + .chain(nat.map(|nat| Span::from(format!("{nat}")).white())) + .chain(utf8) .chain(fixedpoint2012) .chain(fixedpoint1616) .chain(fixedpoint124) .chain(fixedpoint88) .chain(fixedpoint412) - .collect(), - color + .chain(color) + .collect() ) }) ); @@ -919,3 +926,11 @@ fn mark_after(offset: usize, sorted_marks: &[usize], max: usize) -> usize { }, } } + +const fn character_is_allowed_in_strings(character: char) -> bool { + match character { + '\0' | '\t' | '\n' | '\r' => true, + _ if character.is_ascii_control() => false, + _ => true, + } +} diff --git a/src/buffer/mod.rs b/src/buffer/mod.rs index b36423d..d364cda 100644 --- a/src/buffer/mod.rs +++ b/src/buffer/mod.rs @@ -1,7 +1,7 @@ use core::slice::GetDisjointMutIndex; use std::{collections::HashSet, fs::File, io::Read, path::PathBuf}; use crossterm::event::KeyEvent; -use ratatui::{layout::Rect, style::{Color, Stylize}, text::Span, widgets::Widget}; +use ratatui::{layout::{Constraint, Rect}, style::{Color, Stylize}, text::Span, widgets::{Block, Clear, Widget}}; use crate::{BYTES_PER_LINE, action::{Action, AppAction, bytes_to_nat}, app::WindowSize, config::Config, cursor::Cursor, edit_action::EditAction}; mod widget; @@ -50,8 +50,7 @@ pub enum PartialAction { pub struct Popup { at: usize, width: u16, - lines: Vec, - color: Option<[u8; 3]> + lines: Vec> } impl Mode { @@ -86,48 +85,41 @@ impl PartialAction { } impl Popup { - pub fn new(at: usize, lines: Vec, color: Option<[u8; 3]>) -> Self { + pub fn new(at: usize, lines: Vec>) -> Self { Self { at, width: lines .iter() - .map(|line| line.len() as u16) - .chain(color.map(|_| 7)) + .map(|line| line.width() as u16) .max() .unwrap_or(0), - lines, - color + lines } } - fn area_at(&self, x: u16, y: u16) -> Rect { + const fn area_at(&self, x: u16, y: u16) -> Rect { Rect { x, y, width: self.width + 2, - height: self.lines.len() as u16 + u16::from(self.color.is_some()) + height: self.lines.len() as u16 } } } impl Widget for Popup { fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer) { - let width = self.width as usize; - for (line, area) in self.lines.iter().zip(area.rows()) { - Span::from(format!(" {line:^width$} ")) - .white() + Clear.render(area, buf); + + Block::new() .on_dark_gray() .render(area, buf); - } - - if let Some([red, green, blue]) = self.color { - let color_code = format!("#{red:02X}{green:02X}{blue:02X}"); - Span::from(format!(" {color_code:^width$} ")) - .fg(Color::Rgb(red, green, blue)) - .on_dark_gray() - .render(area.rows().next_back().unwrap(), buf); + line.render( + area.centered_horizontally(Constraint::Length(line.width() as u16)), + buf + ); } } } diff --git a/src/buffer/widget.rs b/src/buffer/widget.rs index 75d26d1..d74e718 100644 --- a/src/buffer/widget.rs +++ b/src/buffer/widget.rs @@ -50,10 +50,12 @@ impl Widget for &Buffer { let position_on_screen = popup.at - self.scroll_position; let hex_column = position_on_screen % BYTES_PER_LINE; - let popup_area = popup.area_at( - area.x + byte_column_to_screen_column(hex_column) as u16, - area.y + (position_on_screen / BYTES_PER_LINE) as u16 + 1 - ); + let popup_area = popup + .area_at( + area.x + byte_column_to_screen_column(hex_column) as u16, + area.y + (position_on_screen / BYTES_PER_LINE) as u16 + 1 + ) + .clamp(hex_area); popup.clone().render(popup_area, buf); } diff --git a/src/main.rs b/src/main.rs index 79927ed..6494c55 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,10 +27,11 @@ const BYTES_OF_PADDING: usize = LINES_OF_PADDING * BYTES_PER_LINE; // TODO: // - 4 with large selection crashes -// - also (/) +// - also ( and ) // - also C-F in excavate_defs -// - diffing // - search +// - ascii and bytes (`/` and `A-/`?) +// - diffing // - s/A-k/A-K // - C-a/C-x // - modifications @@ -52,11 +53,6 @@ const BYTES_OF_PADDING: usize = LINES_OF_PADDING * BYTES_PER_LINE; // future directions // - 'views' for bytes (i8/16/etc u8/16/etc 20.12/8.4/etc) // - how to fit??! `-128` longer than `80` -// - popup for different readings for the selected bytes -// - utf8? - -// when AsciiChar is stabilized, use it instead of char -// - actually since im using nightly already, do this fn main() { let mut app = App::new();