improve popup rendering

This commit is contained in:
alice pellerin
2026-03-22 03:55:42 -05:00
parent ab5b7d4720
commit 86e13d5b01
5 changed files with 69 additions and 64 deletions
Generated
+2 -2
View File
@@ -1370,9 +1370,9 @@ dependencies = [
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.2.0" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
+44 -29
View File
@@ -1,5 +1,5 @@
use std::{cmp::min, collections::hash_set::Entry, convert::identity, fs::File, io::Write, iter, mem::{replace, swap}}; 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}; 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)] #[derive(Clone, Copy)]
@@ -680,7 +680,7 @@ impl Buffer {
let max_contents_index = self.max_contents_index(); let max_contents_index = self.max_contents_index();
let mark_after_primary = mark_after( let mark_after_primary = mark_after(
self.primary_cursor.head + 1, self.primary_cursor.head,
&sorted_marks, &sorted_marks,
max_contents_index max_contents_index
); );
@@ -690,7 +690,7 @@ impl Buffer {
for cursor in &mut self.cursors { for cursor in &mut self.cursors {
let mark_after_cursor = mark_after( let mark_after_cursor = mark_after(
cursor.head + 1, cursor.head,
&sorted_marks, &sorted_marks,
max_contents_index max_contents_index
); );
@@ -705,21 +705,21 @@ impl Buffer {
fn extend_to_null(&mut self, window_size: WindowSize) { fn extend_to_null(&mut self, window_size: WindowSize) {
if let Some(null_offset_after_primary) = self.contents[self.primary_cursor.head..] if let Some(null_offset_after_primary) = self.contents[self.primary_cursor.head..]
.iter() .iter()
.skip(2) .skip(1)
.position(|&byte| byte == 0) .position(|&byte| byte == 0)
{ {
self.primary_cursor.tail = self.primary_cursor.head; 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 { for cursor in &mut self.cursors {
if let Some(null_offset_after_primary) = self.contents[cursor.head..] if let Some(null_offset_after_primary) = self.contents[cursor.head..]
.iter() .iter()
.skip(2) .skip(1)
.position(|&byte| byte == 0) .position(|&byte| byte == 0)
{ {
cursor.tail = cursor.head; 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) { fn extend_to_FF(&mut self, window_size: WindowSize) {
if let Some(null_offset_after_primary) = self.contents[self.primary_cursor.head..] if let Some(null_offset_after_primary) = self.contents[self.primary_cursor.head..]
.iter() .iter()
.skip(2) .skip(1)
.position(|&byte| byte == 0xFF) .position(|&byte| byte == 0xFF)
{ {
self.primary_cursor.tail = self.primary_cursor.head; 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 { for cursor in &mut self.cursors {
if let Some(null_offset_after_primary) = self.contents[cursor.head..] if let Some(null_offset_after_primary) = self.contents[cursor.head..]
.iter() .iter()
.skip(2) .skip(1)
.position(|&byte| byte == 0xFF) .position(|&byte| byte == 0xFF)
{ {
cursor.tail = cursor.head; 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 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() let utf8 = str::from_utf8(selection).ok()
.and_then(|utf8| { .and_then(|utf8| {
if utf8.contains(|char: char| char.is_ascii() && !char.is_ascii_graphic()) { utf8
None .contains(character_is_allowed_in_strings)
} else { .then_some(utf8)
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)] #[allow(clippy::cast_precision_loss)]
let fixedpoint2012 = nat let fixedpoint2012 = nat
@@ -783,7 +784,7 @@ impl Buffer {
let two_decimals_is_enough = (fixedpoint2012 * 100.0).fract() == 0.0; let two_decimals_is_enough = (fixedpoint2012 * 100.0).fract() == 0.0;
let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; 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)] #[allow(clippy::cast_precision_loss)]
@@ -793,7 +794,7 @@ impl Buffer {
let two_decimals_is_enough = (fixedpoint1616 * 100.0).fract() == 0.0; let two_decimals_is_enough = (fixedpoint1616 * 100.0).fract() == 0.0;
let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; 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)] #[allow(clippy::cast_precision_loss)]
@@ -803,7 +804,7 @@ impl Buffer {
let two_decimals_is_enough = (fixedpoint124 * 100.0).fract() == 0.0; let two_decimals_is_enough = (fixedpoint124 * 100.0).fract() == 0.0;
let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; 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)] #[allow(clippy::cast_precision_loss)]
@@ -813,7 +814,7 @@ impl Buffer {
let two_decimals_is_enough = (fixedpoint88 * 100.0).fract() == 0.0; let two_decimals_is_enough = (fixedpoint88 * 100.0).fract() == 0.0;
let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; 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)] #[allow(clippy::cast_precision_loss)]
@@ -823,24 +824,30 @@ impl Buffer {
let two_decimals_is_enough = (fixedpoint412 * 100.0).fract() == 0.0; let two_decimals_is_enough = (fixedpoint412 * 100.0).fract() == 0.0;
let approximate_symbol = if two_decimals_is_enough { "" } else { "~" }; 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( Popup::new(
cursor.lower_bound(), cursor.lower_bound(),
int.map(|int| format!("{int}")) int
.into_iter() .into_iter()
.chain(nat.map(|nat| format!("{nat}"))) .chain(nat.map(|nat| Span::from(format!("{nat}")).white()))
.chain(utf8.map(|utf8| format!("\"{utf8}\""))) .chain(utf8)
.chain(fixedpoint2012) .chain(fixedpoint2012)
.chain(fixedpoint1616) .chain(fixedpoint1616)
.chain(fixedpoint124) .chain(fixedpoint124)
.chain(fixedpoint88) .chain(fixedpoint88)
.chain(fixedpoint412) .chain(fixedpoint412)
.collect(), .chain(color)
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,
}
}
+14 -22
View File
@@ -1,7 +1,7 @@
use core::slice::GetDisjointMutIndex; use core::slice::GetDisjointMutIndex;
use std::{collections::HashSet, fs::File, io::Read, path::PathBuf}; use std::{collections::HashSet, fs::File, io::Read, path::PathBuf};
use crossterm::event::KeyEvent; 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}; use crate::{BYTES_PER_LINE, action::{Action, AppAction, bytes_to_nat}, app::WindowSize, config::Config, cursor::Cursor, edit_action::EditAction};
mod widget; mod widget;
@@ -50,8 +50,7 @@ pub enum PartialAction {
pub struct Popup { pub struct Popup {
at: usize, at: usize,
width: u16, width: u16,
lines: Vec<String>, lines: Vec<Span<'static>>
color: Option<[u8; 3]>
} }
impl Mode { impl Mode {
@@ -86,48 +85,41 @@ impl PartialAction {
} }
impl Popup { impl Popup {
pub fn new(at: usize, lines: Vec<String>, color: Option<[u8; 3]>) -> Self { pub fn new(at: usize, lines: Vec<Span<'static>>) -> Self {
Self { Self {
at, at,
width: lines width: lines
.iter() .iter()
.map(|line| line.len() as u16) .map(|line| line.width() as u16)
.chain(color.map(|_| 7))
.max() .max()
.unwrap_or(0), .unwrap_or(0),
lines, lines
color
} }
} }
fn area_at(&self, x: u16, y: u16) -> Rect { const fn area_at(&self, x: u16, y: u16) -> Rect {
Rect { Rect {
x, x,
y, y,
width: self.width + 2, 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 { impl Widget for Popup {
fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer) { 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()) { for (line, area) in self.lines.iter().zip(area.rows()) {
Span::from(format!(" {line:^width$} ")) Clear.render(area, buf);
.white()
Block::new()
.on_dark_gray() .on_dark_gray()
.render(area, buf); .render(area, buf);
}
if let Some([red, green, blue]) = self.color { line.render(
let color_code = format!("#{red:02X}{green:02X}{blue:02X}"); area.centered_horizontally(Constraint::Length(line.width() as u16)),
buf
Span::from(format!(" {color_code:^width$} ")) );
.fg(Color::Rgb(red, green, blue))
.on_dark_gray()
.render(area.rows().next_back().unwrap(), buf);
} }
} }
} }
+4 -2
View File
@@ -50,10 +50,12 @@ impl Widget for &Buffer {
let position_on_screen = popup.at - self.scroll_position; let position_on_screen = popup.at - self.scroll_position;
let hex_column = position_on_screen % BYTES_PER_LINE; let hex_column = position_on_screen % BYTES_PER_LINE;
let popup_area = popup.area_at( let popup_area = popup
.area_at(
area.x + byte_column_to_screen_column(hex_column) as u16, area.x + byte_column_to_screen_column(hex_column) as u16,
area.y + (position_on_screen / BYTES_PER_LINE) as u16 + 1 area.y + (position_on_screen / BYTES_PER_LINE) as u16 + 1
); )
.clamp(hex_area);
popup.clone().render(popup_area, buf); popup.clone().render(popup_area, buf);
} }
+3 -7
View File
@@ -27,10 +27,11 @@ const BYTES_OF_PADDING: usize = LINES_OF_PADDING * BYTES_PER_LINE;
// TODO: // TODO:
// - 4 with large selection crashes // - 4 with large selection crashes
// - also (/) // - also ( and )
// - also C-F in excavate_defs // - also C-F in excavate_defs
// - diffing
// - search // - search
// - ascii and bytes (`/` and `A-/`?)
// - diffing
// - s/A-k/A-K // - s/A-k/A-K
// - C-a/C-x // - C-a/C-x
// - modifications // - modifications
@@ -52,11 +53,6 @@ const BYTES_OF_PADDING: usize = LINES_OF_PADDING * BYTES_PER_LINE;
// future directions // future directions
// - 'views' for bytes (i8/16/etc u8/16/etc 20.12/8.4/etc) // - 'views' for bytes (i8/16/etc u8/16/etc 20.12/8.4/etc)
// - how to fit??! `-128` longer than `80` // - 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() { fn main() {
let mut app = App::new(); let mut app = App::new();