mod layout;
mod node;
mod reconcile;
pub use layout::measure_hex_area;
pub use node::HexAreaNode;
pub use reconcile::reconcile_hex_area;
use std::sync::Arc;
use crate::callback::{Callback, KeyHandler};
use crate::core::element::{Element, ElementKind};
use crate::style::{BorderStyle, LayoutConstraints, Length, Padding, Rect, Style, StyleSlot};
use crate::widgets::ScrollEvent;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) enum HexAreaHitPart {
HexHigh,
HexLow,
Ascii,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) struct HexAreaPointerHit {
pub index: usize,
pub part: HexAreaHitPart,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct HexAreaCursorEvent {
pub cursor: usize,
pub anchor: Option<usize>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HexAreaChangeEvent {
pub bytes: Arc<[u8]>,
pub cursor: usize,
pub anchor: Option<usize>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum HexAreaEditKind {
Replace,
Insert,
Delete,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct HexAreaEditEvent {
pub index: usize,
pub before: Option<u8>,
pub after: Option<u8>,
pub kind: HexAreaEditKind,
}
#[derive(Clone)]
pub struct HexArea {
pub(crate) bytes: Arc<[u8]>,
pub(crate) cursor: usize,
pub(crate) anchor: Option<usize>,
pub(crate) read_only: bool,
pub(crate) bytes_per_row: u16,
pub(crate) show_ascii: bool,
pub(crate) show_offsets: bool,
pub(crate) uppercase_hex: bool,
pub(crate) scroll_offset: Option<usize>,
pub(crate) style: Style,
pub(crate) hover_style: StyleSlot,
pub(crate) focus_style: StyleSlot,
pub(crate) focus_content_style: Style,
pub(crate) selection_style: StyleSlot,
pub(crate) cursor_style: Style,
pub(crate) pending_edit_style: Style,
pub(crate) border: bool,
pub(crate) border_style: BorderStyle,
pub(crate) padding: Padding,
pub(crate) width: Length,
pub(crate) height: Length,
pub(crate) disabled: bool,
pub(crate) disabled_style: Style,
pub(crate) focusable: bool,
pub(crate) on_cursor_change: Option<Callback<HexAreaCursorEvent>>,
pub(crate) on_change: Option<Callback<HexAreaChangeEvent>>,
pub(crate) on_edit: Option<Callback<HexAreaEditEvent>>,
pub(crate) on_scroll: Option<Callback<ScrollEvent>>,
pub(crate) on_key: Option<KeyHandler>,
}
impl Default for HexArea {
fn default() -> Self {
Self {
bytes: Arc::from([]),
cursor: 0,
anchor: None,
read_only: true,
bytes_per_row: 16,
show_ascii: true,
show_offsets: true,
uppercase_hex: true,
scroll_offset: None,
style: Style::default(),
hover_style: StyleSlot::Inherit,
focus_style: StyleSlot::Inherit,
focus_content_style: Style::default(),
selection_style: StyleSlot::Inherit,
cursor_style: Style::default(),
pending_edit_style: Style::default(),
border: true,
border_style: BorderStyle::Plain,
padding: Padding::default(),
width: Length::Flex(1),
height: Length::Flex(1),
disabled: false,
disabled_style: Style::default(),
focusable: true,
on_cursor_change: None,
on_change: None,
on_edit: None,
on_scroll: None,
on_key: None,
}
}
}
impl HexArea {
pub fn new(bytes: impl Into<Arc<[u8]>>) -> Self {
Self {
bytes: bytes.into(),
..Self::default()
}
}
pub fn bytes(mut self, bytes: impl Into<Arc<[u8]>>) -> Self {
self.bytes = bytes.into();
self
}
pub fn cursor(mut self, cursor: usize) -> Self {
self.cursor = cursor;
self
}
pub fn anchor(mut self, anchor: Option<usize>) -> Self {
self.anchor = anchor;
self
}
pub fn read_only(mut self, read_only: bool) -> Self {
self.read_only = read_only;
self
}
pub fn bytes_per_row(mut self, bytes_per_row: u16) -> Self {
self.bytes_per_row = bytes_per_row.max(1);
self
}
pub fn show_ascii(mut self, show_ascii: bool) -> Self {
self.show_ascii = show_ascii;
self
}
pub fn show_offsets(mut self, show_offsets: bool) -> Self {
self.show_offsets = show_offsets;
self
}
pub fn uppercase_hex(mut self, uppercase_hex: bool) -> Self {
self.uppercase_hex = uppercase_hex;
self
}
pub fn scroll_offset(mut self, scroll_offset: Option<usize>) -> Self {
self.scroll_offset = scroll_offset;
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn hover_style(mut self, style: Style) -> Self {
self.hover_style = StyleSlot::Replace(style);
self
}
pub fn extend_hover_style(mut self, style: Style) -> Self {
self.hover_style = StyleSlot::Extend(style);
self
}
pub fn inherit_hover_style(mut self) -> Self {
self.hover_style = StyleSlot::Inherit;
self
}
pub fn hover_style_slot(mut self, slot: StyleSlot) -> Self {
self.hover_style = slot;
self
}
pub fn focus_style(mut self, style: Style) -> Self {
self.focus_style = StyleSlot::Replace(style);
self
}
pub fn extend_focus_style(mut self, style: Style) -> Self {
self.focus_style = StyleSlot::Extend(style);
self
}
pub fn inherit_focus_style(mut self) -> Self {
self.focus_style = StyleSlot::Inherit;
self
}
pub fn focus_style_slot(mut self, slot: StyleSlot) -> Self {
self.focus_style = slot;
self
}
pub fn focus_content_style(mut self, style: Style) -> Self {
self.focus_content_style = style;
self
}
pub fn selection_style(mut self, style: Style) -> Self {
self.selection_style = StyleSlot::Replace(style);
self
}
pub fn extend_selection_style(mut self, style: Style) -> Self {
self.selection_style = StyleSlot::Extend(style);
self
}
pub fn inherit_selection_style(mut self) -> Self {
self.selection_style = StyleSlot::Inherit;
self
}
pub fn selection_style_slot(mut self, slot: StyleSlot) -> Self {
self.selection_style = slot;
self
}
pub fn cursor_style(mut self, style: Style) -> Self {
self.cursor_style = style;
self
}
pub fn pending_edit_style(mut self, style: Style) -> Self {
self.pending_edit_style = style;
self
}
pub fn border(mut self, border: bool) -> Self {
self.border = border;
self
}
pub fn border_style(mut self, border_style: BorderStyle) -> Self {
self.border_style = border_style;
self
}
pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
self.padding = padding.into();
self
}
pub fn width(mut self, width: Length) -> Self {
self.width = width;
self
}
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn disabled_style(mut self, style: Style) -> Self {
self.disabled_style = style;
self
}
pub fn focusable(mut self, focusable: bool) -> Self {
self.focusable = focusable;
self
}
pub fn on_cursor_change(mut self, cb: Callback<HexAreaCursorEvent>) -> Self {
self.on_cursor_change = Some(cb);
self
}
pub fn on_change(mut self, cb: Callback<HexAreaChangeEvent>) -> Self {
self.on_change = Some(cb);
self
}
pub fn on_edit(mut self, cb: Callback<HexAreaEditEvent>) -> Self {
self.on_edit = Some(cb);
self
}
pub fn on_scroll(mut self, cb: Callback<ScrollEvent>) -> Self {
self.on_scroll = Some(cb);
self
}
pub fn on_key(mut self, handler: KeyHandler) -> Self {
self.on_key = Some(handler);
self
}
}
impl From<HexArea> for Element {
fn from(value: HexArea) -> Self {
let (min_w, min_h) = measure_hex_area(&value);
let mut layout = LayoutConstraints::default();
if value.focusable {
layout.focus_min_w = min_w;
layout.focus_min_h = min_h;
}
Element::new(ElementKind::HexArea(Box::new(value))).with_layout(layout)
}
}
impl crate::layout::hash::LayoutHash for HexArea {
fn layout_hash(
&self,
hasher: &mut impl std::hash::Hasher,
_recurse: &dyn Fn(&Element) -> Option<u64>,
) -> Option<()> {
use std::hash::Hash;
self.width.hash(hasher);
self.height.hash(hasher);
self.border.hash(hasher);
self.border_style.hash(hasher);
self.padding.hash(hasher);
self.bytes_per_row.hash(hasher);
self.show_ascii.hash(hasher);
self.show_offsets.hash(hasher);
let needs_content =
matches!(self.width, Length::Auto) || matches!(self.height, Length::Auto);
if needs_content {
self.bytes.len().hash(hasher);
}
Some(())
}
}
pub(crate) struct HexAreaPointerHitArgs {
pub bytes_len: usize,
pub cursor: usize,
pub bytes_per_row: u16,
pub show_offsets: bool,
pub show_ascii: bool,
pub scroll_offset: Option<usize>,
pub border: bool,
pub padding: Padding,
}
pub(crate) fn pointer_hit(
rect: Rect,
args: HexAreaPointerHitArgs,
x: u16,
y: u16,
) -> Option<HexAreaPointerHit> {
let HexAreaPointerHitArgs {
bytes_len,
cursor,
bytes_per_row,
show_offsets,
show_ascii,
scroll_offset,
border,
padding,
} = args;
if bytes_len == 0 {
return None;
}
let inner = rect.inner(border, padding);
if inner.w == 0 || inner.h == 0 || !inner.contains(x as i16, y as i16) {
return None;
}
let bytes_per_row = bytes_per_row.max(1) as usize;
let total_rows = bytes_len.div_ceil(bytes_per_row).max(1);
let visible_rows = inner.h as usize;
let clamped_cursor = cursor.min(bytes_len.saturating_sub(1));
let start_row = scroll_offset.map_or_else(
|| {
if visible_rows == 0 {
0
} else {
let cursor_row = clamped_cursor / bytes_per_row;
cursor_row.saturating_sub(visible_rows.saturating_sub(1))
}
},
|offset| offset.min(total_rows.saturating_sub(1)),
);
let rel_y = (y as i16).saturating_sub(inner.y) as usize;
let row = start_row.saturating_add(rel_y);
if row >= total_rows {
return None;
}
let row_start = row.saturating_mul(bytes_per_row);
let rel_x = (x as i16).saturating_sub(inner.x) as usize;
let offsets_w: usize = if show_offsets { 10 } else { 0 };
let hex_w = bytes_per_row.saturating_mul(3).saturating_sub(1);
let ascii_start = offsets_w.saturating_add(hex_w).saturating_add(2);
if rel_x >= offsets_w {
let local = rel_x - offsets_w;
if local < hex_w {
let col = local / 3;
let in_cell = local % 3;
if in_cell != 2 {
let index = row_start.saturating_add(col);
if index < bytes_len {
let part = if in_cell == 0 {
HexAreaHitPart::HexHigh
} else {
HexAreaHitPart::HexLow
};
return Some(HexAreaPointerHit { index, part });
}
}
}
}
if show_ascii && rel_x >= ascii_start {
let col = rel_x - ascii_start;
if col < bytes_per_row {
let index = row_start.saturating_add(col);
if index < bytes_len {
return Some(HexAreaPointerHit {
index,
part: HexAreaHitPart::Ascii,
});
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::{HexAreaHitPart, HexAreaPointerHitArgs, pointer_hit};
use crate::style::{Padding, Rect};
#[test]
fn pointer_hit_maps_hex_cells() {
let hit = pointer_hit(
Rect {
x: 0,
y: 0,
w: 80,
h: 4,
},
HexAreaPointerHitArgs {
bytes_len: 32,
cursor: 0,
bytes_per_row: 16,
show_offsets: true,
show_ascii: true,
scroll_offset: Some(0),
border: false,
padding: Padding::default(),
},
10,
0,
)
.expect("expected hex hit");
assert_eq!(hit.index, 0);
assert_eq!(hit.part, HexAreaHitPart::HexHigh);
let hit_low = pointer_hit(
Rect {
x: 0,
y: 0,
w: 80,
h: 4,
},
HexAreaPointerHitArgs {
bytes_len: 32,
cursor: 0,
bytes_per_row: 16,
show_offsets: true,
show_ascii: true,
scroll_offset: Some(0),
border: false,
padding: Padding::default(),
},
11,
0,
)
.expect("expected low nibble hit");
assert_eq!(hit_low.index, 0);
assert_eq!(hit_low.part, HexAreaHitPart::HexLow);
}
#[test]
fn pointer_hit_maps_ascii_cells() {
let hit = pointer_hit(
Rect {
x: 0,
y: 0,
w: 80,
h: 4,
},
HexAreaPointerHitArgs {
bytes_len: 32,
cursor: 0,
bytes_per_row: 16,
show_offsets: true,
show_ascii: true,
scroll_offset: Some(0),
border: false,
padding: Padding::default(),
},
59,
0,
)
.expect("expected ascii hit");
assert_eq!(hit.index, 0);
assert_eq!(hit.part, HexAreaHitPart::Ascii);
}
}