Skip to main content

tui_lipan/widgets/hex_area/
mod.rs

1//! Hex area widget.
2
3mod layout;
4mod node;
5mod reconcile;
6
7pub use layout::measure_hex_area;
8pub use node::HexAreaNode;
9pub use reconcile::reconcile_hex_area;
10
11use std::sync::Arc;
12
13use crate::callback::{Callback, KeyHandler};
14use crate::core::element::{Element, ElementKind};
15use crate::style::{BorderStyle, LayoutConstraints, Length, Padding, Rect, Style, StyleSlot};
16use crate::widgets::ScrollEvent;
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19pub(crate) enum HexAreaHitPart {
20    HexHigh,
21    HexLow,
22    Ascii,
23}
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
26pub(crate) struct HexAreaPointerHit {
27    pub index: usize,
28    pub part: HexAreaHitPart,
29}
30
31/// Cursor movement event emitted by [`HexArea`].
32#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
33pub struct HexAreaCursorEvent {
34    /// New cursor byte index.
35    pub cursor: usize,
36    /// Optional selection anchor byte index.
37    pub anchor: Option<usize>,
38}
39
40/// Change event emitted by [`HexArea`].
41#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct HexAreaChangeEvent {
43    /// Updated bytes.
44    pub bytes: Arc<[u8]>,
45    /// Updated cursor byte index.
46    pub cursor: usize,
47    /// Updated selection anchor.
48    pub anchor: Option<usize>,
49}
50
51/// Edit kind emitted by [`HexArea`].
52#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
53pub enum HexAreaEditKind {
54    /// Replaced existing byte.
55    Replace,
56    /// Inserted a new byte.
57    Insert,
58    /// Deleted an existing byte.
59    Delete,
60}
61
62/// Edit event emitted by [`HexArea`].
63#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
64pub struct HexAreaEditEvent {
65    /// Byte index affected by this edit.
66    pub index: usize,
67    /// Previous byte value, if any.
68    pub before: Option<u8>,
69    /// New byte value, if any.
70    pub after: Option<u8>,
71    /// Edit kind.
72    pub kind: HexAreaEditKind,
73}
74
75/// Hex/ASCII binary data viewer.
76#[derive(Clone)]
77pub struct HexArea {
78    pub(crate) bytes: Arc<[u8]>,
79    pub(crate) cursor: usize,
80    pub(crate) anchor: Option<usize>,
81    pub(crate) read_only: bool,
82    pub(crate) bytes_per_row: u16,
83    pub(crate) show_ascii: bool,
84    pub(crate) show_offsets: bool,
85    pub(crate) uppercase_hex: bool,
86    pub(crate) scroll_offset: Option<usize>,
87    pub(crate) style: Style,
88    pub(crate) hover_style: StyleSlot,
89    pub(crate) focus_style: StyleSlot,
90    pub(crate) focus_content_style: Style,
91    pub(crate) selection_style: StyleSlot,
92    pub(crate) cursor_style: Style,
93    pub(crate) pending_edit_style: Style,
94    pub(crate) border: bool,
95    pub(crate) border_style: BorderStyle,
96    pub(crate) padding: Padding,
97    pub(crate) width: Length,
98    pub(crate) height: Length,
99    pub(crate) disabled: bool,
100    pub(crate) disabled_style: Style,
101    pub(crate) focusable: bool,
102    pub(crate) tab_stop: bool,
103    pub(crate) on_focus: Option<Callback<()>>,
104    pub(crate) on_blur: Option<Callback<()>>,
105    pub(crate) on_cursor_change: Option<Callback<HexAreaCursorEvent>>,
106    pub(crate) on_change: Option<Callback<HexAreaChangeEvent>>,
107    pub(crate) on_edit: Option<Callback<HexAreaEditEvent>>,
108    pub(crate) on_scroll: Option<Callback<ScrollEvent>>,
109    pub(crate) on_key: Option<KeyHandler>,
110}
111
112impl Default for HexArea {
113    fn default() -> Self {
114        Self {
115            bytes: Arc::from([]),
116            cursor: 0,
117            anchor: None,
118            read_only: true,
119            bytes_per_row: 16,
120            show_ascii: true,
121            show_offsets: true,
122            uppercase_hex: true,
123            scroll_offset: None,
124            style: Style::default(),
125            hover_style: StyleSlot::Inherit,
126            focus_style: StyleSlot::Inherit,
127            focus_content_style: Style::default(),
128            selection_style: StyleSlot::Inherit,
129            cursor_style: Style::default(),
130            pending_edit_style: Style::default(),
131            border: true,
132            border_style: BorderStyle::Plain,
133            padding: Padding::default(),
134            width: Length::Flex(1),
135            height: Length::Flex(1),
136            disabled: false,
137            disabled_style: Style::default(),
138            focusable: true,
139            tab_stop: true,
140            on_focus: None,
141            on_blur: None,
142            on_cursor_change: None,
143            on_change: None,
144            on_edit: None,
145            on_scroll: None,
146            on_key: None,
147        }
148    }
149}
150
151impl HexArea {
152    /// Create a new hex area.
153    pub fn new(bytes: impl Into<Arc<[u8]>>) -> Self {
154        Self {
155            bytes: bytes.into(),
156            ..Self::default()
157        }
158    }
159
160    /// Set bytes to render.
161    pub fn bytes(mut self, bytes: impl Into<Arc<[u8]>>) -> Self {
162        self.bytes = bytes.into();
163        self
164    }
165
166    /// Set cursor byte index.
167    pub fn cursor(mut self, cursor: usize) -> Self {
168        self.cursor = cursor;
169        self
170    }
171
172    /// Set optional selection anchor byte index.
173    pub fn anchor(mut self, anchor: Option<usize>) -> Self {
174        self.anchor = anchor;
175        self
176    }
177
178    /// Set read-only mode.
179    pub fn read_only(mut self, read_only: bool) -> Self {
180        self.read_only = read_only;
181        self
182    }
183
184    /// Set bytes rendered per row.
185    pub fn bytes_per_row(mut self, bytes_per_row: u16) -> Self {
186        self.bytes_per_row = bytes_per_row.max(1);
187        self
188    }
189
190    /// Toggle ASCII preview column.
191    pub fn show_ascii(mut self, show_ascii: bool) -> Self {
192        self.show_ascii = show_ascii;
193        self
194    }
195
196    /// Toggle offsets gutter.
197    pub fn show_offsets(mut self, show_offsets: bool) -> Self {
198        self.show_offsets = show_offsets;
199        self
200    }
201
202    /// Toggle uppercase hex formatting.
203    pub fn uppercase_hex(mut self, uppercase_hex: bool) -> Self {
204        self.uppercase_hex = uppercase_hex;
205        self
206    }
207
208    /// Set controlled row scroll offset.
209    pub fn scroll_offset(mut self, scroll_offset: Option<usize>) -> Self {
210        self.scroll_offset = scroll_offset;
211        self
212    }
213
214    /// Set base style.
215    pub fn style(mut self, style: Style) -> Self {
216        self.style = style;
217        self
218    }
219
220    /// Set hover style.
221    pub fn hover_style(mut self, style: Style) -> Self {
222        self.hover_style = StyleSlot::Replace(style);
223        self
224    }
225
226    /// Extend the active theme's hover style with additional fields.
227    pub fn extend_hover_style(mut self, style: Style) -> Self {
228        self.hover_style = StyleSlot::Extend(style);
229        self
230    }
231
232    /// Inherit hover style from the active theme.
233    pub fn inherit_hover_style(mut self) -> Self {
234        self.hover_style = StyleSlot::Inherit;
235        self
236    }
237
238    /// Set hover style slot directly for composite forwarding.
239    pub fn hover_style_slot(mut self, slot: StyleSlot) -> Self {
240        self.hover_style = slot;
241        self
242    }
243
244    /// Set focus chrome style.
245    pub fn focus_style(mut self, style: Style) -> Self {
246        self.focus_style = StyleSlot::Replace(style);
247        self
248    }
249
250    /// Extend the active theme's focus style with additional fields.
251    pub fn extend_focus_style(mut self, style: Style) -> Self {
252        self.focus_style = StyleSlot::Extend(style);
253        self
254    }
255
256    /// Inherit focus style from the active theme.
257    pub fn inherit_focus_style(mut self) -> Self {
258        self.focus_style = StyleSlot::Inherit;
259        self
260    }
261
262    /// Set focus style slot directly for composite forwarding.
263    pub fn focus_style_slot(mut self, slot: StyleSlot) -> Self {
264        self.focus_style = slot;
265        self
266    }
267
268    /// Set focused content text style.
269    pub fn focus_content_style(mut self, style: Style) -> Self {
270        self.focus_content_style = style;
271        self
272    }
273
274    /// Set selection highlight style.
275    pub fn selection_style(mut self, style: Style) -> Self {
276        self.selection_style = StyleSlot::Replace(style);
277        self
278    }
279
280    /// Extend the active theme's selection style with additional fields.
281    pub fn extend_selection_style(mut self, style: Style) -> Self {
282        self.selection_style = StyleSlot::Extend(style);
283        self
284    }
285
286    /// Inherit selection style from the active theme.
287    pub fn inherit_selection_style(mut self) -> Self {
288        self.selection_style = StyleSlot::Inherit;
289        self
290    }
291
292    /// Set selection style slot directly for composite forwarding.
293    pub fn selection_style_slot(mut self, slot: StyleSlot) -> Self {
294        self.selection_style = slot;
295        self
296    }
297
298    /// Set cursor cell style.
299    pub fn cursor_style(mut self, style: Style) -> Self {
300        self.cursor_style = style;
301        self
302    }
303
304    /// Set style used for a half-entered nibble edit.
305    pub fn pending_edit_style(mut self, style: Style) -> Self {
306        self.pending_edit_style = style;
307        self
308    }
309
310    /// Set border visibility.
311    pub fn border(mut self, border: bool) -> Self {
312        self.border = border;
313        self
314    }
315
316    /// Set border style.
317    pub fn border_style(mut self, border_style: BorderStyle) -> Self {
318        self.border_style = border_style;
319        self
320    }
321
322    /// Set padding.
323    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
324        self.padding = padding.into();
325        self
326    }
327
328    /// Set width.
329    pub fn width(mut self, width: Length) -> Self {
330        self.width = width;
331        self
332    }
333
334    /// Set height.
335    pub fn height(mut self, height: Length) -> Self {
336        self.height = height;
337        self
338    }
339
340    /// Set disabled state.
341    pub fn disabled(mut self, disabled: bool) -> Self {
342        self.disabled = disabled;
343        self
344    }
345
346    /// Set disabled style.
347    pub fn disabled_style(mut self, style: Style) -> Self {
348        self.disabled_style = style;
349        self
350    }
351
352    /// Control whether the node is focusable.
353    pub fn focusable(mut self, focusable: bool) -> Self {
354        self.focusable = focusable;
355        self
356    }
357
358    /// Control whether the hex area participates in Tab / Shift+Tab traversal.
359    pub fn tab_stop(mut self, tab_stop: bool) -> Self {
360        self.tab_stop = tab_stop;
361        self
362    }
363
364    /// Set the callback fired when the hex area gains focus.
365    pub fn on_focus(mut self, cb: Callback<()>) -> Self {
366        self.on_focus = Some(cb);
367        self
368    }
369
370    /// Set the callback fired when the hex area loses focus.
371    pub fn on_blur(mut self, cb: Callback<()>) -> Self {
372        self.on_blur = Some(cb);
373        self
374    }
375
376    /// Set cursor change callback.
377    pub fn on_cursor_change(mut self, cb: Callback<HexAreaCursorEvent>) -> Self {
378        self.on_cursor_change = Some(cb);
379        self
380    }
381
382    /// Set change callback.
383    pub fn on_change(mut self, cb: Callback<HexAreaChangeEvent>) -> Self {
384        self.on_change = Some(cb);
385        self
386    }
387
388    /// Set edit callback.
389    pub fn on_edit(mut self, cb: Callback<HexAreaEditEvent>) -> Self {
390        self.on_edit = Some(cb);
391        self
392    }
393
394    /// Set scroll callback.
395    pub fn on_scroll(mut self, cb: Callback<ScrollEvent>) -> Self {
396        self.on_scroll = Some(cb);
397        self
398    }
399
400    /// Set custom key handler.
401    pub fn on_key(mut self, handler: KeyHandler) -> Self {
402        self.on_key = Some(handler);
403        self
404    }
405}
406
407impl From<HexArea> for Element {
408    fn from(value: HexArea) -> Self {
409        let (min_w, min_h) = measure_hex_area(&value);
410        let mut layout = LayoutConstraints::default();
411        if value.focusable {
412            layout.focus_min_w = min_w;
413            layout.focus_min_h = min_h;
414        }
415        Element::new(ElementKind::HexArea(Box::new(value))).with_layout(layout)
416    }
417}
418
419impl crate::layout::hash::LayoutHash for HexArea {
420    fn layout_hash(
421        &self,
422        hasher: &mut impl std::hash::Hasher,
423        _recurse: &dyn Fn(&Element) -> Option<u64>,
424    ) -> Option<()> {
425        use std::hash::Hash;
426        self.width.hash(hasher);
427        self.height.hash(hasher);
428        self.border.hash(hasher);
429        self.border_style.hash(hasher);
430        self.padding.hash(hasher);
431        self.bytes_per_row.hash(hasher);
432        self.show_ascii.hash(hasher);
433        self.show_offsets.hash(hasher);
434
435        let needs_content =
436            matches!(self.width, Length::Auto) || matches!(self.height, Length::Auto);
437        if needs_content {
438            self.bytes.len().hash(hasher);
439        }
440        Some(())
441    }
442}
443
444pub(crate) struct HexAreaPointerHitArgs {
445    pub bytes_len: usize,
446    pub cursor: usize,
447    pub bytes_per_row: u16,
448    pub show_offsets: bool,
449    pub show_ascii: bool,
450    pub scroll_offset: Option<usize>,
451    pub border: bool,
452    pub padding: Padding,
453}
454
455pub(crate) fn pointer_hit(
456    rect: Rect,
457    args: HexAreaPointerHitArgs,
458    x: u16,
459    y: u16,
460) -> Option<HexAreaPointerHit> {
461    let HexAreaPointerHitArgs {
462        bytes_len,
463        cursor,
464        bytes_per_row,
465        show_offsets,
466        show_ascii,
467        scroll_offset,
468        border,
469        padding,
470    } = args;
471    if bytes_len == 0 {
472        return None;
473    }
474
475    let inner = rect.inner(border, padding);
476    if inner.w == 0 || inner.h == 0 || !inner.contains(x as i16, y as i16) {
477        return None;
478    }
479
480    let bytes_per_row = bytes_per_row.max(1) as usize;
481    let total_rows = bytes_len.div_ceil(bytes_per_row).max(1);
482    let visible_rows = inner.h as usize;
483    let clamped_cursor = cursor.min(bytes_len.saturating_sub(1));
484    let start_row = scroll_offset.map_or_else(
485        || {
486            if visible_rows == 0 {
487                0
488            } else {
489                let cursor_row = clamped_cursor / bytes_per_row;
490                cursor_row.saturating_sub(visible_rows.saturating_sub(1))
491            }
492        },
493        |offset| offset.min(total_rows.saturating_sub(1)),
494    );
495
496    let rel_y = (y as i16).saturating_sub(inner.y) as usize;
497    let row = start_row.saturating_add(rel_y);
498    if row >= total_rows {
499        return None;
500    }
501
502    let row_start = row.saturating_mul(bytes_per_row);
503    let rel_x = (x as i16).saturating_sub(inner.x) as usize;
504
505    let offsets_w: usize = if show_offsets { 10 } else { 0 };
506    let hex_w = bytes_per_row.saturating_mul(3).saturating_sub(1);
507    let ascii_start = offsets_w.saturating_add(hex_w).saturating_add(2);
508
509    if rel_x >= offsets_w {
510        let local = rel_x - offsets_w;
511        if local < hex_w {
512            let col = local / 3;
513            let in_cell = local % 3;
514            if in_cell != 2 {
515                let index = row_start.saturating_add(col);
516                if index < bytes_len {
517                    let part = if in_cell == 0 {
518                        HexAreaHitPart::HexHigh
519                    } else {
520                        HexAreaHitPart::HexLow
521                    };
522                    return Some(HexAreaPointerHit { index, part });
523                }
524            }
525        }
526    }
527
528    if show_ascii && rel_x >= ascii_start {
529        let col = rel_x - ascii_start;
530        if col < bytes_per_row {
531            let index = row_start.saturating_add(col);
532            if index < bytes_len {
533                return Some(HexAreaPointerHit {
534                    index,
535                    part: HexAreaHitPart::Ascii,
536                });
537            }
538        }
539    }
540
541    None
542}
543
544#[cfg(test)]
545mod tests {
546    use super::{HexAreaHitPart, HexAreaPointerHitArgs, pointer_hit};
547    use crate::style::{Padding, Rect};
548
549    #[test]
550    fn pointer_hit_maps_hex_cells() {
551        let hit = pointer_hit(
552            Rect {
553                x: 0,
554                y: 0,
555                w: 80,
556                h: 4,
557            },
558            HexAreaPointerHitArgs {
559                bytes_len: 32,
560                cursor: 0,
561                bytes_per_row: 16,
562                show_offsets: true,
563                show_ascii: true,
564                scroll_offset: Some(0),
565                border: false,
566                padding: Padding::default(),
567            },
568            10,
569            0,
570        )
571        .expect("expected hex hit");
572
573        assert_eq!(hit.index, 0);
574        assert_eq!(hit.part, HexAreaHitPart::HexHigh);
575
576        let hit_low = pointer_hit(
577            Rect {
578                x: 0,
579                y: 0,
580                w: 80,
581                h: 4,
582            },
583            HexAreaPointerHitArgs {
584                bytes_len: 32,
585                cursor: 0,
586                bytes_per_row: 16,
587                show_offsets: true,
588                show_ascii: true,
589                scroll_offset: Some(0),
590                border: false,
591                padding: Padding::default(),
592            },
593            11,
594            0,
595        )
596        .expect("expected low nibble hit");
597
598        assert_eq!(hit_low.index, 0);
599        assert_eq!(hit_low.part, HexAreaHitPart::HexLow);
600    }
601
602    #[test]
603    fn pointer_hit_maps_ascii_cells() {
604        let hit = pointer_hit(
605            Rect {
606                x: 0,
607                y: 0,
608                w: 80,
609                h: 4,
610            },
611            HexAreaPointerHitArgs {
612                bytes_len: 32,
613                cursor: 0,
614                bytes_per_row: 16,
615                show_offsets: true,
616                show_ascii: true,
617                scroll_offset: Some(0),
618                border: false,
619                padding: Padding::default(),
620            },
621            59,
622            0,
623        )
624        .expect("expected ascii hit");
625
626        assert_eq!(hit.index, 0);
627        assert_eq!(hit.part, HexAreaHitPart::Ascii);
628    }
629}