From 1e4948f61e13be1a8dbe8eef4eb3a24d76b9383f Mon Sep 17 00:00:00 2001 From: alice pellerin Date: Tue, 17 Mar 2026 16:04:55 -0500 Subject: [PATCH] partial line rendering --- src/app/widget.rs | 92 ++++++++++++++++++++++++++++++++++++++++++----- src/main.rs | 26 ++++++++++++-- 2 files changed, 108 insertions(+), 10 deletions(-) diff --git a/src/app/widget.rs b/src/app/widget.rs index 8ba053a..c7c663a 100644 --- a/src/app/widget.rs +++ b/src/app/widget.rs @@ -13,16 +13,24 @@ impl Widget for &App { let (chunks, remainder) = bytes_to_render .as_chunks::(); - assert!(remainder.is_empty()); - let hex_lines = chunks .iter() .zip((self.scroll_position..).step_by(BYTES_PER_LINE)) .map(|(bytes, address)| self.render_line(address, bytes)); - let hex_area_text: Text = hex_lines.collect(); + let remainder_address = bytes_end - remainder.len(); + let remainder_line = if !remainder.is_empty() { + Some(self.render_partial_line(remainder_address, remainder)) + } else { + None + }; + + let hex_text: Text = hex_lines + .chain(remainder_line) + .collect(); + let hex_area = Rect::new(area.x, area.y, area.width, area.height - 1); - hex_area_text.render(hex_area, buf); + hex_text.render(hex_area, buf); let status_line_area = Rect::new(area.x, area.bottom() - 1, area.width, 1); self.render_status_line().render(status_line_area, buf); @@ -42,6 +50,15 @@ impl App { .chain(character_panel::render_character_panel(bytes)) .collect() } + + #[allow(mismatched_lifetime_syntaxes)] + fn render_partial_line(&self, address: usize, bytes: &[u8]) -> Line { + iter::once(address::render_address(address)) + .chain(self.render_partial_chunks(address, bytes)) + .chain(iter::once(" ".into())) + .chain(character_panel::render_character_panel(bytes)) + .collect() + } } mod address { @@ -57,7 +74,7 @@ mod address { mod hex { use std::{borrow::Cow, mem}; - use itertools::Itertools; + use itertools::{Itertools, repeat_n}; use ratatui::{style::{Color, Style, Stylize}, text::Span}; use crate::{BYTES_PER_CHUNK, BYTES_PER_LINE, CHUNKS_PER_LINE, app::App, cardinality::HasCardinality, cursor::InCursor, empty_span::empty_span, custom_greys::CustomGreys}; @@ -70,7 +87,7 @@ mod hex { ) -> impl Iterator> { let (chunks, remainder) = bytes.as_chunks::(); - assert!(remainder.is_empty()); + assert!(remainder.is_empty(), "BYTES_PER_LINE should be a multiple of BYTES_PER_CHUNK"); #[allow(unstable_name_collisions)] chunks @@ -88,6 +105,46 @@ mod hex { .flatten() } + pub fn render_partial_chunks( + &self, + address: usize, + bytes: &[u8] + ) -> impl Iterator> { + let (chunks, remainder) = bytes.as_chunks::(); + + let remainder_address = address + chunks.len() * BYTES_PER_CHUNK; + let remainder_chunks: Option> = if !remainder.is_empty() { + Some(self.render_partial_chunk(remainder_address, remainder).collect()) + } else { + None + }; + + let chunks_rendered = chunks.len() + remainder_chunks.iter().len(); + let chunks_not_rendered = CHUNKS_PER_LINE - chunks_rendered; + let spaces_per_chunk = BYTES_PER_CHUNK - 1; + let bytes_not_rendered = BYTES_PER_LINE - bytes.len(); + + let padding_width = 2 * bytes_not_rendered + + spaces_per_chunk * chunks_not_rendered; + + #[allow(unstable_name_collisions)] + chunks + .iter() + .copied() + .zip((address..).step_by(BYTES_PER_CHUNK)) + .map(|(chunk, address)| self.render_chunk(address, &chunk).collect()) + .chain(remainder_chunks) + .interleave( + (address..) + .step_by(BYTES_PER_CHUNK) + .take(CHUNKS_PER_LINE) + .skip(1) + .map(|address| vec![self.render_large_space_before(address)]) + ) + .flatten() + .chain(repeat_n(" ".into(), padding_width)) + } + fn render_chunk( &self, address: usize, @@ -107,6 +164,25 @@ mod hex { ) } + fn render_partial_chunk( + &self, + address: usize, + bytes: &[u8] + ) -> impl Iterator> { + #[allow(unstable_name_collisions)] + bytes + .iter() + .copied() + .zip(address..) + .map(|(byte, address)| self.render_byte_at(address, byte)) + .interleave( + (address..) + .take(BYTES_PER_CHUNK) + .skip(1) + .map(|address| self.render_space_before(address)) + ) + } + fn render_byte_at( &self, address: usize, @@ -201,10 +277,10 @@ mod character_panel { use std::{borrow::Cow, mem}; use ratatui::{style::{Color, Style}, text::Span}; - use crate::{BYTES_PER_LINE, cardinality::HasCardinality, empty_span::empty_span}; + use crate::{cardinality::HasCardinality, empty_span::empty_span}; pub fn render_character_panel( - bytes: &[u8; BYTES_PER_LINE] + bytes: &[u8] ) -> impl Iterator> { bytes .iter() diff --git a/src/main.rs b/src/main.rs index 35eb359..9145a28 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,12 +14,13 @@ const BYTES_PER_CHUNK: usize = 4; const CHUNKS_PER_LINE: usize = BYTES_PER_LINE / BYTES_PER_CHUNK; // TODO: +// - refactor input system +// - undo/redo +// - x/X // - modes // - select // - insert // - zz/zt/zb -// - search -// - jumplist // - modifications // - insert/append // - mode @@ -29,6 +30,27 @@ const CHUNKS_PER_LINE: usize = BYTES_PER_LINE / BYTES_PER_CHUNK; // - mode // - delete // - change +// - highlight cursor in character panel too (but lighter?) +// - edit too +// - modifier on existing keys like teehee? or jump to panel? +// - search +// - jumplist +// - f/t +// - ascii? +// - [/] to cycle view offset? +// - J jump to offset + +// future directions +// - switch between cursor size u8s/u16s/u32s/u64s? +// - +/- +// - multi-cursor +// - s/C +// - split selection by u8/16/32/etc +// - 'views' for bytes (i8/16/etc u8/16/etc 20.12/8.4/etc) +// - how to fit??! `-128` longer than `80` +// - mark offsets? +// - utf8? +// - diffing fn main() { let mut app = App::init();