1#![forbid(unsafe_code)]
10
11use egui::Align2;
12use egui::Color32;
13use egui::FontId;
14use egui::Pos2;
15use egui::Rect;
16use egui::Sense;
17use egui::Stroke;
18use egui::StrokeKind;
19use egui::TextStyle;
20use egui::Ui;
21use egui::Vec2;
22use hxy_core::ByteLen;
23use hxy_core::ByteOffset;
24use hxy_core::ByteRange;
25use hxy_core::ColumnCount;
26use hxy_core::Error as CoreError;
27use hxy_core::HexSource;
28use hxy_core::Selection;
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum ValueHighlight {
33 Background,
36 Text,
38}
39
40pub type ContextMenuFn<'s> = Box<dyn FnOnce(&mut egui::Ui) + 's>;
43
44#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
49pub struct ByteStyle {
50 pub bg: Option<Color32>,
53 pub fg: Option<Color32>,
56}
57
58pub type ByteStylerFn<'s> = Box<dyn Fn(u8, ByteOffset) -> ByteStyle + 's>;
63
64pub type AddressFormatterFn<'s> = Box<dyn Fn(ByteOffset, usize) -> String + 's>;
69
70pub type ColumnHeaderFormatterFn<'s> = Box<dyn Fn(usize) -> String + 's>;
74
75pub struct HexView<'s, S: HexSource + ?Sized> {
76 source: &'s S,
77 columns: ColumnCount,
78 selection: &'s mut Option<Selection>,
79 value_highlight: Option<ValueHighlight>,
80 palette_override: Option<HighlightPalette>,
81 byte_styler: Option<ByteStylerFn<'s>>,
82 address_formatter: Option<AddressFormatterFn<'s>>,
83 column_header_formatter: Option<ColumnHeaderFormatterFn<'s>>,
84 context_menu: Option<ContextMenuFn<'s>>,
85 minimap: bool,
86 minimap_colored: bool,
87 initial_scroll: Option<f32>,
88 scroll_to_byte: Option<ByteOffset>,
91 hover_span: Option<ByteRange>,
96 field_boundaries: &'s [(ByteOffset, ByteLen)],
101 field_colors: &'s [Color32],
107 id_salt: Option<egui::Id>,
109}
110
111impl<'s, S: HexSource + ?Sized> HexView<'s, S> {
112 pub fn new(source: &'s S, selection: &'s mut Option<Selection>) -> Self {
113 Self {
114 source,
115 columns: ColumnCount::DEFAULT,
116 selection,
117 value_highlight: None,
118 palette_override: None,
119 byte_styler: None,
120 address_formatter: None,
121 column_header_formatter: None,
122 context_menu: None,
123 minimap: false,
124 minimap_colored: true,
125 initial_scroll: None,
126 scroll_to_byte: None,
127 hover_span: None,
128 field_boundaries: &[],
129 field_colors: &[],
130 id_salt: None,
131 }
132 }
133
134 pub fn field_boundaries(mut self, boundaries: &'s [(ByteOffset, ByteLen)]) -> Self {
139 self.field_boundaries = boundaries;
140 self
141 }
142
143 pub fn field_colors(mut self, colors: &'s [Color32]) -> Self {
148 self.field_colors = colors;
149 self
150 }
151
152 pub fn id_salt(mut self, salt: impl std::hash::Hash) -> Self {
160 self.id_salt = Some(egui::Id::new(salt));
161 self
162 }
163
164 pub fn hover_span(mut self, span: Option<ByteRange>) -> Self {
167 self.hover_span = span;
168 self
169 }
170
171 pub fn byte_styler(mut self, f: impl Fn(u8, ByteOffset) -> ByteStyle + 's) -> Self {
176 self.byte_styler = Some(Box::new(f));
177 self
178 }
179
180 pub fn address_formatter(mut self, f: impl Fn(ByteOffset, usize) -> String + 's) -> Self {
183 self.address_formatter = Some(Box::new(f));
184 self
185 }
186
187 pub fn column_header_formatter(mut self, f: impl Fn(usize) -> String + 's) -> Self {
190 self.column_header_formatter = Some(Box::new(f));
191 self
192 }
193
194 pub fn scroll_to(mut self, offset: f32) -> Self {
198 self.initial_scroll = Some(offset);
199 self
200 }
201
202 pub fn scroll_to_byte(mut self, byte: ByteOffset) -> Self {
206 self.scroll_to_byte = Some(byte);
207 self
208 }
209
210 pub fn minimap(mut self, enabled: bool) -> Self {
214 self.minimap = enabled;
215 self
216 }
217
218 pub fn minimap_colored(mut self, colored: bool) -> Self {
222 self.minimap_colored = colored;
223 self
224 }
225
226 pub fn columns(mut self, cols: ColumnCount) -> Self {
227 self.columns = cols;
228 self
229 }
230
231 pub fn value_highlight(mut self, mode: Option<ValueHighlight>) -> Self {
234 self.value_highlight = mode;
235 self
236 }
237
238 pub fn palette(mut self, palette: HighlightPalette) -> Self {
242 self.palette_override = Some(palette);
243 self
244 }
245
246 pub fn context_menu(mut self, add_contents: impl FnOnce(&mut egui::Ui) + 's) -> Self {
250 self.context_menu = Some(Box::new(add_contents));
251 self
252 }
253
254 pub fn show(self, ui: &mut Ui) -> HexViewResponse {
255 let Self {
256 source,
257 columns,
258 selection,
259 value_highlight,
260 palette_override,
261 byte_styler,
262 address_formatter,
263 column_header_formatter,
264 context_menu,
265 minimap,
266 minimap_colored,
267 initial_scroll,
268 scroll_to_byte,
269 hover_span,
270 field_boundaries,
271 field_colors,
272 id_salt,
273 } = self;
274 let salt = id_salt.unwrap_or_else(|| ui.id().with("hxy_hex_view"));
275 ui.push_id(salt, |ui| {
276 let palette = value_highlight.map(|mode| {
277 let palette = palette_override
278 .unwrap_or_else(|| HighlightPalette::for_theme_and_mode(ui.visuals().dark_mode, mode));
279 (mode, palette)
280 });
281 let total_rows = row_count(source.len(), columns);
282 let address_chars = address_hex_width(source.len());
283 let font_id = TextStyle::Monospace.resolve(ui.style());
284 let row_height = ui.text_style_height(&TextStyle::Monospace);
285 let char_w = measure_char_width(ui, &font_id);
286 let layout = RowLayout::compute(char_w, address_chars, columns);
287 let source_len = source.len();
288
289 let mut response = HexViewResponse::default();
290
291 paint_column_header(ui, &layout, &font_id, row_height, column_header_formatter.as_deref());
292
293 let scroll_id = ui.id().with("hxy_scroll");
294 let pending_offset = scroll_to_byte
299 .map(|b| {
300 let row = b.get() / u64::from(columns.get());
301 (row as f32) * row_height
302 })
303 .or_else(|| ui.ctx().data_mut(|d| d.remove_temp::<f32>(scroll_id)))
304 .or(initial_scroll);
305
306 let minimap_width = if minimap { (char_w * 8.0).max(48.0) } else { 0.0 };
307 let scrollbar_width = ui.style().spacing.scroll.bar_width.max(10.0);
308 let avail = ui.available_rect_before_wrap();
309 let hex_rect = Rect::from_min_size(
310 avail.min,
311 Vec2::new(avail.width() - minimap_width - scrollbar_width, avail.height()),
312 );
313 let minimap_rect =
314 Rect::from_min_size(Pos2::new(hex_rect.right(), avail.top()), Vec2::new(minimap_width, avail.height()));
315 let scrollbar_rect = Rect::from_min_size(
316 Pos2::new(minimap_rect.right(), avail.top()),
317 Vec2::new(scrollbar_width, avail.height()),
318 );
319
320 let hex_out = ui
321 .scope_builder(egui::UiBuilder::new().max_rect(hex_rect), |ui| {
322 let mut area = egui::ScrollArea::vertical()
326 .auto_shrink([false, false])
327 .id_salt(scroll_id)
328 .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden);
329 if let Some(target) = pending_offset {
330 area = area.vertical_scroll_offset(target);
331 }
332 area.show(ui, |ui| {
333 paint_and_interact(
334 ui,
335 &layout,
336 &font_id,
337 row_height,
338 total_rows,
339 source_len,
340 columns,
341 source,
342 selection,
343 palette.clone(),
344 byte_styler.as_deref(),
345 address_formatter.as_deref(),
346 context_menu,
347 hover_span,
348 field_boundaries,
349 &mut response,
350 );
351 })
352 })
353 .inner;
354
355 response.scroll_offset = hex_out.state.offset.y;
356 response.viewport_height = hex_out.inner_rect.height();
357
358 if minimap {
359 draw_minimap(
360 ui,
361 scroll_id,
362 minimap_rect,
363 source,
364 source_len,
365 palette,
366 minimap_colored,
367 row_height,
368 hex_out.state.offset.y,
369 hex_out.inner_rect.height(),
370 total_rows,
371 hover_span,
372 field_boundaries,
373 field_colors,
374 );
375 }
376
377 draw_scrollbar(
378 ui,
379 scroll_id,
380 scrollbar_rect,
381 hex_out.state.offset.y,
382 hex_out.inner_rect.height(),
383 total_rows as f32 * row_height,
384 );
385
386 response
387 })
388 .inner
389 }
390}
391
392#[derive(Default)]
393pub struct HexViewResponse {
394 pub hovered_offset: Option<ByteOffset>,
395 pub error: Option<CoreError>,
396 pub scroll_offset: f32,
398 pub viewport_height: f32,
400 pub visible_range: Option<ByteRange>,
403 pub cursor_offset: Option<ByteOffset>,
406 pub layout: Option<HexViewLayout>,
410}
411
412#[derive(Clone, Copy, Debug)]
416pub struct HexViewLayout {
417 block_rect: Rect,
418 row_height: f32,
419 columns: ColumnCount,
420 source_len: ByteLen,
421 inner: RowLayout,
422}
423
424impl HexViewLayout {
425 pub fn columns(&self) -> ColumnCount {
427 self.columns
428 }
429
430 pub fn hex_cell_rect(&self, offset: ByteOffset) -> Option<Rect> {
435 let (row_origin, col) = self.row_origin_and_col(offset)?;
436 Some(self.inner.hex_cell_rect(row_origin, col, self.row_height))
437 }
438
439 pub fn ascii_cell_rect(&self, offset: ByteOffset) -> Option<Rect> {
441 let (row_origin, col) = self.row_origin_and_col(offset)?;
442 Some(self.inner.ascii_cell_rect(row_origin, col, self.row_height))
443 }
444
445 pub fn hex_span_rect(&self, row: hxy_core::RowIndex, from: usize, to: usize) -> Option<Rect> {
451 let cols = usize::from(self.columns.get());
452 if from >= cols || to >= cols || from > to {
453 return None;
454 }
455 let row_origin = self.row_origin_for_row(row)?;
456 Some(self.inner.hex_span_rect(row_origin, from, to, self.row_height))
457 }
458
459 fn row_origin_and_col(&self, offset: ByteOffset) -> Option<(Pos2, usize)> {
460 if offset.get() >= self.source_len.get() {
461 return None;
462 }
463 let cols = u64::from(self.columns.get());
464 let row = offset.get() / cols;
465 let col = (offset.get() % cols) as usize;
466 Some((self.row_origin_for_row(hxy_core::RowIndex::new(row))?, col))
467 }
468
469 fn row_origin_for_row(&self, row: hxy_core::RowIndex) -> Option<Pos2> {
470 let y = self.block_rect.top() + row.get() as f32 * self.row_height;
471 Some(Pos2::new(self.block_rect.left(), y))
472 }
473}
474
475fn row_count(len: ByteLen, columns: ColumnCount) -> usize {
476 let len = len.get();
477 if len == 0 {
478 return 0;
479 }
480 let rows = len.div_ceil(columns.as_u64());
481 usize::try_from(rows).unwrap_or(usize::MAX)
482}
483
484fn address_hex_width(len: ByteLen) -> usize {
485 let bits_needed = 64 - len.get().saturating_sub(1).leading_zeros() as usize;
486 bits_needed.div_ceil(4).max(8)
487}
488
489fn measure_char_width(ui: &Ui, font_id: &FontId) -> f32 {
490 let painter = ui.painter();
491 let galley = painter.layout_no_wrap("0".to_string(), font_id.clone(), Color32::WHITE);
492 galley.size().x
493}
494
495#[derive(Clone, Copy, Debug)]
497struct RowLayout {
498 address_w: f32,
499 hex_start_x: f32,
500 hex_cell_w: f32,
501 hex_gap: f32,
502 ascii_start_x: f32,
503 ascii_cell_w: f32,
504 total_width: f32,
505 columns: ColumnCount,
506 address_chars: usize,
507}
508
509impl RowLayout {
510 fn compute(char_w: f32, address_chars: usize, columns: ColumnCount) -> Self {
511 let address_w = char_w * address_chars as f32;
512 let hex_gap = char_w * 0.5;
513 let hex_cell_w = char_w * 2.0;
514 let cols_f = f32::from(columns.get());
515 let hex_total = cols_f * hex_cell_w + (cols_f - 1.0) * hex_gap;
516 let section_gap = char_w * 2.0;
517 let hex_start_x = address_w + section_gap;
518 let ascii_cell_w = char_w;
519 let ascii_start_x = hex_start_x + hex_total + section_gap;
520 let ascii_total = cols_f * ascii_cell_w;
521 Self {
522 address_w,
523 hex_start_x,
524 hex_cell_w,
525 hex_gap,
526 ascii_start_x,
527 ascii_cell_w,
528 total_width: ascii_start_x + ascii_total,
529 columns,
530 address_chars,
531 }
532 }
533
534 fn hex_tint_rect(&self, row_origin: Pos2, col: usize, total_cols: usize, row_height: f32) -> Rect {
538 let cell = self.hex_cell_rect(row_origin, col, row_height);
539 let left = if col == 0 { cell.left() } else { cell.left() - self.hex_gap / 2.0 };
540 let right = if col + 1 >= total_cols { cell.right() } else { cell.right() + self.hex_gap / 2.0 };
541 Rect::from_min_max(Pos2::new(left, cell.top()), Pos2::new(right, cell.bottom()))
542 }
543
544 fn ascii_tint_rect(&self, row_origin: Pos2, col: usize, _total_cols: usize, row_height: f32) -> Rect {
547 self.ascii_cell_rect(row_origin, col, row_height)
548 }
549
550 fn hex_cell_rect(&self, row_origin: Pos2, col: usize, row_height: f32) -> Rect {
551 let left = row_origin.x + self.hex_start_x + (col as f32) * (self.hex_cell_w + self.hex_gap);
552 Rect::from_min_size(Pos2::new(left, row_origin.y), Vec2::new(self.hex_cell_w, row_height))
553 }
554
555 fn hex_span_rect(&self, row_origin: Pos2, from: usize, to: usize, row_height: f32) -> Rect {
558 let start = self.hex_cell_rect(row_origin, from, row_height).left();
559 let end = self.hex_cell_rect(row_origin, to, row_height).right();
560 Rect::from_min_max(Pos2::new(start, row_origin.y), Pos2::new(end, row_origin.y + row_height))
561 }
562
563 fn ascii_cell_rect(&self, row_origin: Pos2, col: usize, row_height: f32) -> Rect {
564 let left = row_origin.x + self.ascii_start_x + (col as f32) * self.ascii_cell_w;
565 Rect::from_min_size(Pos2::new(left, row_origin.y), Vec2::new(self.ascii_cell_w, row_height))
566 }
567
568 fn ascii_span_rect(&self, row_origin: Pos2, from: usize, to: usize, row_height: f32) -> Rect {
569 let start = self.ascii_cell_rect(row_origin, from, row_height).left();
570 let end = self.ascii_cell_rect(row_origin, to, row_height).right();
571 Rect::from_min_max(Pos2::new(start, row_origin.y), Pos2::new(end, row_origin.y + row_height))
572 }
573
574 fn address_rect(&self, row_origin: Pos2, row_height: f32) -> Rect {
575 Rect::from_min_size(row_origin, Vec2::new(self.address_w, row_height))
576 }
577
578 fn hit_test(&self, block_rect: Rect, pos: Pos2, row_height: f32, num_rows: usize) -> Option<HitRowCol> {
583 let x = (pos.x - block_rect.left()).max(0.0);
584 let y = (pos.y - block_rect.top()).clamp(0.0, num_rows.saturating_sub(1) as f32 * row_height);
585 let row = ((y / row_height) as usize).min(num_rows.saturating_sub(1));
586
587 let cols = usize::from(self.columns.get());
588 if x >= self.hex_start_x && x < self.ascii_start_x {
589 let local = x - self.hex_start_x;
590 let stride = self.hex_cell_w + self.hex_gap;
591 let col = ((local / stride) as usize).min(cols - 1);
592 return Some(HitRowCol { row, col });
593 }
594 let ascii_end = self.ascii_start_x + (cols as f32) * self.ascii_cell_w;
595 if x >= self.ascii_start_x && x < ascii_end {
596 let local = x - self.ascii_start_x;
597 let col = ((local / self.ascii_cell_w) as usize).min(cols - 1);
598 return Some(HitRowCol { row, col });
599 }
600 None
601 }
602}
603
604#[derive(Clone, Copy)]
605struct HitRowCol {
606 row: usize,
607 col: usize,
608}
609
610struct PaintCtx<'a> {
612 layout: &'a RowLayout,
613 font_id: &'a FontId,
614 row_height: f32,
615 columns: ColumnCount,
616 palette: Option<(ValueHighlight, HighlightPalette)>,
617 byte_styler: Option<&'a dyn Fn(u8, ByteOffset) -> ByteStyle>,
618 address_formatter: Option<&'a dyn Fn(ByteOffset, usize) -> String>,
619 colors: RowColors,
620 selected_range: Option<ByteRange>,
621 cursor_offset: Option<ByteOffset>,
622 hover_offset: Option<ByteOffset>,
623 hover_span: Option<ByteRange>,
624 field_boundaries: &'a [(ByteOffset, ByteLen)],
625}
626
627struct HitCtx<'a> {
630 layout: &'a RowLayout,
631 block_rect: Rect,
632 row_height: f32,
633 columns: ColumnCount,
634 total_rows: usize,
635 source_len: ByteLen,
636}
637
638#[derive(Clone, Copy)]
639struct RowColors {
640 text: Color32,
641 weak: Color32,
642 selection_bg: Color32,
643 selection_fg: Color32,
644 cursor_stroke: Stroke,
645 hover_stroke: Stroke,
646}
647
648#[allow(clippy::too_many_arguments)]
649fn paint_and_interact<S: HexSource + ?Sized>(
650 ui: &mut Ui,
651 layout: &RowLayout,
652 font_id: &FontId,
653 row_height: f32,
654 total_rows: usize,
655 source_len: ByteLen,
656 columns: ColumnCount,
657 source: &S,
658 selection: &mut Option<Selection>,
659 palette: Option<(ValueHighlight, HighlightPalette)>,
660 byte_styler: Option<&dyn Fn(u8, ByteOffset) -> ByteStyle>,
661 address_formatter: Option<&dyn Fn(ByteOffset, usize) -> String>,
662 context_menu: Option<ContextMenuFn<'_>>,
663 hover_span: Option<ByteRange>,
664 field_boundaries: &[(ByteOffset, ByteLen)],
665 response_out: &mut HexViewResponse,
666) {
667 let cols = usize::from(columns.get());
668 let total_height = total_rows as f32 * row_height;
669 let block_size = Vec2::new(layout.total_width, total_height);
670 let (block_rect, response) = ui.allocate_exact_size(block_size, Sense::click_and_drag());
671
672 let hit = HitCtx { layout, block_rect, row_height, columns, total_rows, source_len };
673
674 let Some((first_visible, last_visible_exclusive)) =
678 visible_rows(&block_rect, row_height, total_rows, ui.clip_rect())
679 else {
680 response_out.hovered_offset = None;
681 if let Some(add) = context_menu {
682 response.context_menu(add);
683 }
684 return;
685 };
686
687 let range_start = (first_visible as u64).saturating_mul(cols as u64).min(source_len.get());
688 let range_end = (last_visible_exclusive as u64).saturating_mul(cols as u64).min(source_len.get());
689 let Ok(read_range) = ByteRange::new(ByteOffset::new(range_start), ByteOffset::new(range_end)) else {
690 return;
691 };
692 let bytes = match source.read(read_range) {
693 Ok(b) => b,
694 Err(e) => {
695 response_out.error = Some(e);
696 return;
697 }
698 };
699
700 let weak = ui.visuals().weak_text_color();
701 let colors = RowColors {
702 text: ui.visuals().text_color(),
703 weak,
704 selection_bg: ui.visuals().selection.bg_fill,
705 selection_fg: ui.visuals().selection.stroke.color,
706 cursor_stroke: Stroke::new(1.5, ui.visuals().strong_text_color()),
707 hover_stroke: Stroke::new(1.0, weak.gamma_multiply(0.9)),
708 };
709
710 let selected_range = selection.and_then(|s| {
711 let r = s.range();
712 if r.is_empty() { None } else { Some(r) }
713 });
714 let cursor_offset = selection.map(|s| s.cursor);
715 let hover_offset = hovered_byte(ui, &response, &hit);
716
717 let ctx = PaintCtx {
718 layout,
719 font_id,
720 row_height,
721 columns,
722 palette,
723 byte_styler,
724 address_formatter,
725 colors,
726 selected_range,
727 cursor_offset,
728 hover_offset,
729 hover_span,
730 field_boundaries,
731 };
732 let painter = ui.painter_at(block_rect);
733 paint_rows(&painter, &ctx, block_rect, first_visible, &bytes);
734
735 apply_interaction(ui, &response, &hit, selection);
736
737 response_out.hovered_offset = hover_offset;
738 response_out.cursor_offset = cursor_offset;
739 response_out.visible_range = Some(read_range);
740 response_out.layout = Some(HexViewLayout { block_rect, row_height, columns, source_len, inner: *layout });
741
742 if let Some(add) = context_menu {
743 response.context_menu(add);
744 }
745}
746
747fn visible_rows(block_rect: &Rect, row_height: f32, total_rows: usize, clip: Rect) -> Option<(usize, usize)> {
748 if total_rows == 0 {
749 return None;
750 }
751 let total_height = total_rows as f32 * row_height;
752 let visible_top = (clip.top() - block_rect.top()).max(0.0);
753 let visible_bottom = (clip.bottom() - block_rect.top()).clamp(0.0, total_height);
754 if visible_bottom <= visible_top {
755 return None;
756 }
757 let first = (visible_top / row_height).floor() as usize;
758 let last = ((visible_bottom / row_height).ceil() as usize).min(total_rows);
759 if first >= last { None } else { Some((first, last)) }
760}
761
762fn paint_rows(painter: &egui::Painter, ctx: &PaintCtx<'_>, block_rect: Rect, first_visible: usize, bytes: &[u8]) {
766 let cols = usize::from(ctx.columns.get());
767
768 for (chunk_idx, chunk) in bytes.chunks(cols).enumerate() {
769 let row_idx = first_visible + chunk_idx;
770 let row_origin = row_origin_for(block_rect, row_idx, ctx.row_height);
771 let row_first_offset = ByteOffset::new((row_idx as u64) * (cols as u64));
772 paint_row_backs_and_glyphs(painter, ctx, row_origin, row_first_offset, chunk);
773 }
774 for (chunk_idx, chunk) in bytes.chunks(cols).enumerate() {
775 let row_idx = first_visible + chunk_idx;
776 let row_origin = row_origin_for(block_rect, row_idx, ctx.row_height);
777 let row_first_offset = ByteOffset::new((row_idx as u64) * (cols as u64));
778 paint_row_marks(painter, ctx, row_origin, row_first_offset, chunk.len());
779 }
780}
781
782fn row_origin_for(block_rect: Rect, row_idx: usize, row_height: f32) -> Pos2 {
783 Pos2::new(block_rect.left(), block_rect.top() + (row_idx as f32) * row_height)
784}
785
786fn paint_row_backs_and_glyphs(
787 painter: &egui::Painter,
788 ctx: &PaintCtx<'_>,
789 row_origin: Pos2,
790 row_first_offset: ByteOffset,
791 chunk: &[u8],
792) {
793 let cols = usize::from(ctx.columns.get());
794
795 painter.text(
796 ctx.layout.address_rect(row_origin, ctx.row_height).left_center(),
797 Align2::LEFT_CENTER,
798 match ctx.address_formatter {
799 Some(f) => f(row_first_offset, ctx.layout.address_chars),
800 None => format_address(row_first_offset, ctx.layout.address_chars),
801 },
802 ctx.font_id.clone(),
803 ctx.colors.weak,
804 );
805
806 if let Some(range) = ctx.selected_range {
807 paint_row_selection(
808 painter,
809 ctx.layout,
810 row_origin,
811 ctx.row_height,
812 row_first_offset,
813 chunk.len(),
814 range,
815 ctx.colors.selection_bg,
816 );
817 }
818 if let Some(range) = ctx.hover_span {
819 let tint = ctx.colors.selection_bg.gamma_multiply(0.45);
824 paint_row_selection(
825 painter,
826 ctx.layout,
827 row_origin,
828 ctx.row_height,
829 row_first_offset,
830 chunk.len(),
831 range,
832 tint,
833 );
834 }
835
836 for (i, byte) in chunk.iter().enumerate() {
837 let byte_offset = ByteOffset::new(row_first_offset.get() + i as u64);
838 let hex_rect = ctx.layout.hex_cell_rect(row_origin, i, ctx.row_height);
839 let ascii_rect = ctx.layout.ascii_cell_rect(row_origin, i, ctx.row_height);
840 let is_sel = ctx.selected_range.is_some_and(|r| r.contains(byte_offset));
841
842 let class_color = ctx.palette.as_ref().map(|(_, p)| p.color_for(*byte));
844 let (palette_bg, palette_fg) = match ctx.palette.as_ref().map(|(m, _)| *m) {
845 Some(ValueHighlight::Background) => {
846 let bg = class_color;
847 let fg = contrast_text_color(class_color.unwrap_or(ctx.colors.text), ctx.colors.text);
848 (bg, fg)
849 }
850 Some(ValueHighlight::Text) => (None, class_color.unwrap_or(ctx.colors.text)),
851 None => (None, ctx.colors.text),
852 };
853
854 let user_style = ctx.byte_styler.map(|f| f(*byte, byte_offset));
856 let bg = user_style.and_then(|s| s.bg).or(palette_bg);
857 let fg_override = user_style.and_then(|s| s.fg);
858
859 if let Some(color) = bg.filter(|_| !is_sel) {
860 let hex_tint = ctx.layout.hex_tint_rect(row_origin, i, cols, ctx.row_height);
861 let ascii_tint = ctx.layout.ascii_tint_rect(row_origin, i, cols, ctx.row_height);
862 painter.rect_filled(hex_tint, 0.0, color);
863 painter.rect_filled(ascii_tint, 0.0, color);
864 }
865
866 let fg = if is_sel {
867 ctx.colors.selection_fg
868 } else if let Some(f) = fg_override {
869 f
870 } else if let Some(color) = bg {
871 contrast_text_color(color, ctx.colors.text)
873 } else {
874 palette_fg
875 };
876
877 painter.text(hex_rect.center(), Align2::CENTER_CENTER, format!("{byte:02X}"), ctx.font_id.clone(), fg);
878 let ch = if (0x20..0x7f).contains(byte) { *byte as char } else { '.' };
879 painter.text(ascii_rect.center(), Align2::CENTER_CENTER, ch.to_string(), ctx.font_id.clone(), fg);
880 }
881}
882
883fn paint_row_marks(
884 painter: &egui::Painter,
885 ctx: &PaintCtx<'_>,
886 row_origin: Pos2,
887 row_first_offset: ByteOffset,
888 chunk_len: usize,
889) {
890 let cols = usize::from(ctx.columns.get());
891 paint_row_field_outlines(painter, ctx, row_origin, row_first_offset, chunk_len, cols);
892 for i in 0..chunk_len.min(cols) {
893 let byte_offset = ByteOffset::new(row_first_offset.get() + i as u64);
894 let hex_rect = ctx.layout.hex_cell_rect(row_origin, i, ctx.row_height);
895 let ascii_rect = ctx.layout.ascii_cell_rect(row_origin, i, ctx.row_height);
896 let hex_mark = hex_rect.expand2(Vec2::new(ctx.layout.hex_gap * 0.35, 2.0));
897 let ascii_mark = ascii_rect.expand2(Vec2::new(0.5, 2.0));
898 if ctx.cursor_offset == Some(byte_offset) {
899 painter.rect_stroke(hex_mark, 2.0, ctx.colors.cursor_stroke, StrokeKind::Middle);
900 painter.rect_stroke(ascii_mark, 2.0, ctx.colors.cursor_stroke, StrokeKind::Middle);
901 } else if ctx.hover_offset == Some(byte_offset) {
902 painter.rect_stroke(hex_mark, 2.0, ctx.colors.hover_stroke, StrokeKind::Middle);
903 painter.rect_stroke(ascii_mark, 2.0, ctx.colors.hover_stroke, StrokeKind::Middle);
904 }
905 }
906}
907
908fn paint_row_field_outlines(
914 painter: &egui::Painter,
915 ctx: &PaintCtx<'_>,
916 row_origin: Pos2,
917 row_first_offset: ByteOffset,
918 chunk_len: usize,
919 cols: usize,
920) {
921 if ctx.field_boundaries.is_empty() || chunk_len == 0 {
922 return;
923 }
924 let stroke = Stroke::new(1.0, ctx.colors.weak.gamma_multiply(0.7));
925 let row_first = row_first_offset.get();
926 let row_last_exclusive = row_first + chunk_len.min(cols) as u64;
927 let row_visible_cols = chunk_len.min(cols);
928
929 let first_idx =
930 ctx.field_boundaries.partition_point(|(start, len)| start.get().saturating_add(len.get()) <= row_first);
931
932 for (start, len) in &ctx.field_boundaries[first_idx..] {
933 let field_start = start.get();
934 let field_end = field_start.saturating_add(len.get());
935 if field_start >= row_last_exclusive {
936 break;
937 }
938 let seg_start = field_start.max(row_first);
939 let seg_end = field_end.min(row_last_exclusive);
940 if seg_start >= seg_end {
941 continue;
942 }
943 let first_col = (seg_start - row_first) as usize;
944 let last_col = (seg_end - row_first - 1) as usize;
945
946 let top_edge = field_start == seg_start;
947 let bottom_edge = field_end == seg_end;
948 let left_edge = field_start == seg_start;
949 let right_edge = field_end == seg_end;
950
951 let hex_rect = hex_outline_rect(ctx.layout, row_origin, ctx.row_height, first_col, last_col, row_visible_cols);
952 let ascii_rect =
953 ascii_outline_rect(ctx.layout, row_origin, ctx.row_height, first_col, last_col, row_visible_cols);
954
955 for rect in [hex_rect, ascii_rect] {
956 paint_rect_edges(painter, rect, stroke, top_edge, bottom_edge, left_edge, right_edge);
957 }
958 }
959}
960
961fn hex_outline_rect(
967 layout: &RowLayout,
968 row_origin: Pos2,
969 row_height: f32,
970 first_col: usize,
971 last_col: usize,
972 total_cols: usize,
973) -> Rect {
974 let left_cell = layout.hex_cell_rect(row_origin, first_col, row_height);
975 let right_cell = layout.hex_cell_rect(row_origin, last_col, row_height);
976 let half_gap = layout.hex_gap * 0.5;
977 let left = if first_col == 0 { left_cell.left() } else { left_cell.left() - half_gap };
978 let right = if last_col + 1 >= total_cols { right_cell.right() } else { right_cell.right() + half_gap };
979 Rect::from_min_max(Pos2::new(left, row_origin.y), Pos2::new(right, row_origin.y + row_height))
980}
981
982fn ascii_outline_rect(
985 layout: &RowLayout,
986 row_origin: Pos2,
987 row_height: f32,
988 first_col: usize,
989 last_col: usize,
990 _total_cols: usize,
991) -> Rect {
992 let left = layout.ascii_cell_rect(row_origin, first_col, row_height).left();
993 let right = layout.ascii_cell_rect(row_origin, last_col, row_height).right();
994 Rect::from_min_max(Pos2::new(left, row_origin.y), Pos2::new(right, row_origin.y + row_height))
995}
996
997fn paint_rect_edges(
998 painter: &egui::Painter,
999 rect: Rect,
1000 stroke: Stroke,
1001 top: bool,
1002 bottom: bool,
1003 left: bool,
1004 right: bool,
1005) {
1006 let ppp = painter.pixels_per_point();
1007 let snap_x = |x: f32| (x * ppp).round() / ppp + 0.5 / ppp;
1008 let snap_y = |y: f32| (y * ppp).round() / ppp + 0.5 / ppp;
1009 let l = snap_x(rect.left());
1010 let r = snap_x(rect.right());
1011 let t = snap_y(rect.top());
1012 let b = snap_y(rect.bottom());
1013 if top {
1014 painter.line_segment([Pos2::new(l, t), Pos2::new(r, t)], stroke);
1015 }
1016 if bottom {
1017 painter.line_segment([Pos2::new(l, b), Pos2::new(r, b)], stroke);
1018 }
1019 if left {
1020 painter.line_segment([Pos2::new(l, t), Pos2::new(l, b)], stroke);
1021 }
1022 if right {
1023 painter.line_segment([Pos2::new(r, t), Pos2::new(r, b)], stroke);
1024 }
1025}
1026
1027#[allow(clippy::too_many_arguments)]
1028fn paint_row_selection(
1029 painter: &egui::Painter,
1030 layout: &RowLayout,
1031 row_origin: Pos2,
1032 row_height: f32,
1033 row_first_offset: ByteOffset,
1034 chunk_len: usize,
1035 selection: ByteRange,
1036 bg: Color32,
1037) {
1038 let cols = usize::from(layout.columns.get());
1039 let row_start = row_first_offset.get();
1040 let row_end = row_start + chunk_len as u64;
1041
1042 let sel_start = selection.start().get();
1043 let sel_end = selection.end().get();
1044 if sel_end <= row_start || sel_start >= row_end {
1045 return;
1046 }
1047
1048 let local_from = (sel_start.saturating_sub(row_start)) as usize;
1049 let local_to_exclusive = (sel_end.min(row_end).saturating_sub(row_start)) as usize;
1050 if local_to_exclusive == 0 || local_from >= cols {
1051 return;
1052 }
1053 let local_to = local_to_exclusive.saturating_sub(1);
1054
1055 let hex_bar = layout.hex_span_rect(row_origin, local_from, local_to, row_height);
1056 let ascii_bar = layout.ascii_span_rect(row_origin, local_from, local_to, row_height);
1057 painter.rect_filled(hex_bar, 2.0, bg);
1058 painter.rect_filled(ascii_bar, 2.0, bg);
1059}
1060
1061fn apply_interaction(ui: &Ui, response: &egui::Response, hit: &HitCtx<'_>, selection: &mut Option<Selection>) {
1062 let cols = usize::from(hit.columns.get());
1063 let shift = ui.input(|i| i.modifiers.shift);
1064 let active = response.dragged() || response.drag_started() || response.clicked();
1065 if !active {
1066 return;
1067 }
1068
1069 let pos = response.interact_pointer_pos().or_else(|| ui.ctx().input(|i| i.pointer.interact_pos()));
1070 let Some(pos) = pos else { return };
1071 let Some(rc) = hit.layout.hit_test(hit.block_rect, pos, hit.row_height, hit.total_rows) else {
1072 return;
1073 };
1074 let Some(hit_offset) = hit_to_offset(rc, cols, hit.source_len) else { return };
1075
1076 if response.drag_started() {
1077 *selection = Some(match (shift, *selection) {
1078 (true, Some(existing)) => Selection { anchor: existing.anchor, cursor: hit_offset },
1079 _ => Selection::caret(hit_offset),
1080 });
1081 } else if response.dragged() {
1082 match selection.as_mut() {
1083 Some(s) => s.cursor = hit_offset,
1084 None => *selection = Some(Selection::caret(hit_offset)),
1085 }
1086 } else if response.clicked() {
1087 *selection = Some(match (shift, *selection) {
1088 (true, Some(existing)) => Selection { anchor: existing.anchor, cursor: hit_offset },
1089 _ => Selection::caret(hit_offset),
1090 });
1091 }
1092}
1093
1094fn hit_to_offset(hit: HitRowCol, cols: usize, source_len: ByteLen) -> Option<ByteOffset> {
1095 let offset = (hit.row as u64).checked_mul(cols as u64)?.checked_add(hit.col as u64)?;
1096 if offset >= source_len.get() {
1097 if source_len.get() == 0 {
1098 return None;
1099 }
1100 return Some(ByteOffset::new(source_len.get() - 1));
1101 }
1102 Some(ByteOffset::new(offset))
1103}
1104
1105#[allow(clippy::too_many_arguments)]
1106fn hovered_byte(ui: &Ui, response: &egui::Response, hit: &HitCtx<'_>) -> Option<ByteOffset> {
1107 let pos = response.hover_pos().or_else(|| {
1108 response.is_pointer_button_down_on().then(|| ui.ctx().input(|i| i.pointer.latest_pos())).flatten()
1109 })?;
1110 if !hit.block_rect.contains(pos) {
1111 return None;
1112 }
1113 let rc = hit.layout.hit_test(hit.block_rect, pos, hit.row_height, hit.total_rows)?;
1114 hit_to_offset(rc, usize::from(hit.columns.get()), hit.source_len)
1115}
1116
1117#[derive(Clone, Debug)]
1121pub enum HighlightPalette {
1122 Class(BytePalette),
1123 Value(ValueGradient),
1124 Custom(std::sync::Arc<[Color32; 256]>),
1127}
1128
1129impl HighlightPalette {
1130 pub fn for_theme_and_mode(dark: bool, mode: ValueHighlight) -> Self {
1131 Self::Class(BytePalette::for_theme_and_mode(dark, mode))
1132 }
1133
1134 pub fn color_for(&self, byte: u8) -> Color32 {
1135 match self {
1136 Self::Class(p) => p.color_for(byte),
1137 Self::Value(g) => g.color_for(byte),
1138 Self::Custom(table) => table[byte as usize],
1139 }
1140 }
1141}
1142
1143#[derive(Clone, Copy, Debug)]
1147pub struct ValueGradient {
1148 pub saturation: f32,
1149 pub lightness: f32,
1150}
1151
1152impl ValueGradient {
1153 pub const BG_DARK: Self = Self { saturation: 0.55, lightness: 0.32 };
1154 pub const BG_LIGHT: Self = Self { saturation: 0.5, lightness: 0.78 };
1155 pub const TEXT_DARK: Self = Self { saturation: 0.75, lightness: 0.68 };
1156 pub const TEXT_LIGHT: Self = Self { saturation: 0.7, lightness: 0.4 };
1157
1158 pub fn for_theme_and_mode(dark: bool, mode: ValueHighlight) -> Self {
1159 match (dark, mode) {
1160 (true, ValueHighlight::Background) => Self::BG_DARK,
1161 (false, ValueHighlight::Background) => Self::BG_LIGHT,
1162 (true, ValueHighlight::Text) => Self::TEXT_DARK,
1163 (false, ValueHighlight::Text) => Self::TEXT_LIGHT,
1164 }
1165 }
1166
1167 pub fn color_for(&self, byte: u8) -> Color32 {
1168 let hue = (f32::from(byte) / 256.0) * 360.0;
1169 hsl_to_rgb(hue, self.saturation, self.lightness)
1170 }
1171}
1172
1173fn hsl_to_rgb(h: f32, s: f32, l: f32) -> Color32 {
1174 let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
1175 let h_norm = h / 60.0;
1176 let x = c * (1.0 - (h_norm.rem_euclid(2.0) - 1.0).abs());
1177 let (r1, g1, b1) = match h_norm as u32 {
1178 0 => (c, x, 0.0),
1179 1 => (x, c, 0.0),
1180 2 => (0.0, c, x),
1181 3 => (0.0, x, c),
1182 4 => (x, 0.0, c),
1183 _ => (c, 0.0, x),
1184 };
1185 let m = l - c / 2.0;
1186 let cv = |v: f32| ((v + m).clamp(0.0, 1.0) * 255.0).round() as u8;
1187 Color32::from_rgb(cv(r1), cv(g1), cv(b1))
1188}
1189
1190#[derive(Clone, Copy, Debug)]
1194pub struct BytePalette {
1195 pub null: Color32,
1196 pub all_bits: Color32,
1197 pub whitespace: Color32,
1198 pub printable: Color32,
1199 pub control: Color32,
1200 pub extended: Color32,
1201}
1202
1203impl BytePalette {
1204 pub fn for_theme_and_mode(dark: bool, mode: ValueHighlight) -> Self {
1209 match (dark, mode) {
1210 (true, ValueHighlight::Background) => Self::BG_DARK,
1211 (false, ValueHighlight::Background) => Self::BG_LIGHT,
1212 (true, ValueHighlight::Text) => Self::TEXT_DARK,
1213 (false, ValueHighlight::Text) => Self::TEXT_LIGHT,
1214 }
1215 }
1216
1217 pub const BG_DARK: Self = Self {
1218 null: Color32::from_rgb(60, 60, 64),
1219 all_bits: Color32::from_rgb(200, 150, 40),
1220 whitespace: Color32::from_rgb(50, 90, 140),
1221 printable: Color32::from_rgb(40, 120, 60),
1222 control: Color32::from_rgb(150, 60, 60),
1223 extended: Color32::from_rgb(120, 60, 140),
1224 };
1225
1226 pub const BG_LIGHT: Self = Self {
1227 null: Color32::from_rgb(220, 220, 220),
1228 all_bits: Color32::from_rgb(245, 215, 110),
1229 whitespace: Color32::from_rgb(180, 210, 240),
1230 printable: Color32::from_rgb(190, 235, 200),
1231 control: Color32::from_rgb(240, 190, 190),
1232 extended: Color32::from_rgb(225, 195, 240),
1233 };
1234
1235 pub const TEXT_DARK: Self = Self {
1236 null: Color32::from_rgb(140, 140, 140),
1237 all_bits: Color32::from_rgb(255, 200, 80),
1238 whitespace: Color32::from_rgb(120, 180, 240),
1239 printable: Color32::from_rgb(120, 220, 140),
1240 control: Color32::from_rgb(240, 130, 130),
1241 extended: Color32::from_rgb(210, 140, 230),
1242 };
1243
1244 pub const TEXT_LIGHT: Self = Self {
1245 null: Color32::from_rgb(120, 120, 120),
1246 all_bits: Color32::from_rgb(180, 120, 20),
1247 whitespace: Color32::from_rgb(30, 90, 180),
1248 printable: Color32::from_rgb(30, 130, 60),
1249 control: Color32::from_rgb(180, 50, 50),
1250 extended: Color32::from_rgb(130, 40, 170),
1251 };
1252
1253 pub fn color_for(&self, byte: u8) -> Color32 {
1254 match ByteClass::of(byte) {
1255 ByteClass::Null => self.null,
1256 ByteClass::AllBits => self.all_bits,
1257 ByteClass::Whitespace => self.whitespace,
1258 ByteClass::Printable => self.printable,
1259 ByteClass::Control => self.control,
1260 ByteClass::Extended => self.extended,
1261 }
1262 }
1263}
1264
1265fn contrast_text_color(bg: Color32, default_fg: Color32) -> Color32 {
1269 if bg.a() == 0 {
1270 return default_fg;
1271 }
1272 let luminance = 0.299 * f32::from(bg.r()) + 0.587 * f32::from(bg.g()) + 0.114 * f32::from(bg.b());
1273 let t = (luminance / 255.0).clamp(0.0, 1.0);
1274 let white = 240.0_f32;
1275 let gray = 30.0_f32;
1276 let v = (white * (1.0 - t) + gray * t).round() as u8;
1277 Color32::from_rgb(v, v, v)
1278}
1279
1280#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1282pub(crate) enum ByteClass {
1283 Null,
1284 AllBits,
1285 Whitespace,
1286 Printable,
1287 Control,
1288 Extended,
1289}
1290
1291impl ByteClass {
1292 pub(crate) fn of(byte: u8) -> Self {
1293 match byte {
1294 0x00 => Self::Null,
1295 0xFF => Self::AllBits,
1296 b'\t' | b'\n' | b'\r' => Self::Whitespace,
1297 0x01..=0x1F | 0x7F => Self::Control,
1298 0x20..=0x7E => Self::Printable,
1299 0x80..=0xFE => Self::Extended,
1300 }
1301 }
1302}
1303
1304#[allow(clippy::too_many_arguments)]
1305fn draw_minimap<S: HexSource + ?Sized>(
1306 ui: &mut Ui,
1307 scroll_id: egui::Id,
1308 minimap_rect: Rect,
1309 source: &S,
1310 source_len: ByteLen,
1311 palette: Option<(ValueHighlight, HighlightPalette)>,
1312 colored: bool,
1313 row_height: f32,
1314 current_offset: f32,
1315 viewport_height: f32,
1316 total_rows: usize,
1317 hover_span: Option<ByteRange>,
1318 field_boundaries: &[(ByteOffset, ByteLen)],
1319 field_colors: &[Color32],
1320) {
1321 if minimap_rect.width() < 1.0 || minimap_rect.height() < 1.0 || source_len.get() == 0 {
1322 return;
1323 }
1324 let cols = 16usize;
1325 let response = ui.allocate_rect(minimap_rect, Sense::click_and_drag());
1326 let painter = ui.painter_at(minimap_rect);
1327 painter.rect_filled(minimap_rect, 0.0, ui.visuals().extreme_bg_color);
1328
1329 let cell_w = (minimap_rect.width() / cols as f32).max(1.0);
1330 let cell_h = 2.0_f32;
1334
1335 let minimap_capacity_rows = (minimap_rect.height() / cell_h).floor() as usize;
1336 if minimap_capacity_rows == 0 {
1337 return;
1338 }
1339 let fallback = ui.visuals().text_color();
1340 let len = source_len.get();
1341 let dark = ui.visuals().dark_mode;
1342
1343 let viewport_top_row_f = (current_offset / row_height).max(0.0);
1348 let viewport_rows_f = (viewport_height / row_height).max(1.0);
1349 let capacity_f = minimap_capacity_rows as f32;
1350 let content_height = total_rows as f32 * row_height;
1351 let max_scroll = (content_height - viewport_height).max(0.0);
1352 let scroll_frac = if max_scroll > 0.0 { (current_offset / max_scroll).clamp(0.0, 1.0) } else { 0.0 };
1353 let max_top = (total_rows as f32 - capacity_f).max(0.0);
1354 let window_top_f = scroll_frac * max_top;
1355 let window_top_row = window_top_f.floor() as u64;
1356 let shown_rows = minimap_capacity_rows.min(total_rows.saturating_sub(window_top_row as usize));
1357
1358 let read_start = window_top_row.saturating_mul(cols as u64).min(len);
1360 let read_end = read_start.saturating_add(shown_rows as u64 * cols as u64).min(len);
1361 let bytes = ByteRange::new(ByteOffset::new(read_start), ByteOffset::new(read_end))
1362 .ok()
1363 .and_then(|r| source.read(r).ok())
1364 .unwrap_or_default();
1365
1366 let field_override = !field_boundaries.is_empty() && !field_colors.is_empty();
1367 for i in 0..shown_rows {
1368 let chunk_start = i * cols;
1369 if chunk_start >= bytes.len() {
1370 break;
1371 }
1372 let chunk_end = (chunk_start + cols).min(bytes.len());
1373 let chunk = &bytes[chunk_start..chunk_end];
1374 let y = minimap_rect.top() + i as f32 * cell_h;
1375 let row_base_offset = read_start + (i as u64) * cols as u64;
1376 for (c, byte) in chunk.iter().enumerate() {
1377 let x = minimap_rect.left() + c as f32 * cell_w;
1378 let offset = row_base_offset + c as u64;
1379 let field_color =
1380 if field_override { field_color_for(field_boundaries, field_colors, offset) } else { None };
1381 let color = field_color.unwrap_or_else(|| {
1382 if colored {
1383 palette.as_ref().map(|(_, p)| p.color_for(*byte)).unwrap_or(fallback)
1384 } else {
1385 grayscale_for_byte(*byte, dark)
1386 }
1387 });
1388 painter.rect_filled(Rect::from_min_size(Pos2::new(x, y), Vec2::new(cell_w, cell_h)), 0.0, color);
1389 }
1390 }
1391
1392 let indicator_top_y = minimap_rect.top() + (viewport_top_row_f - window_top_f) * cell_h;
1396 let indicator_height = viewport_rows_f * cell_h;
1397 let indicator = Rect::from_min_max(
1398 Pos2::new(minimap_rect.left(), indicator_top_y.max(minimap_rect.top())),
1399 Pos2::new(minimap_rect.right(), (indicator_top_y + indicator_height).min(minimap_rect.bottom())),
1400 );
1401 let (fill, outline) = if dark {
1402 (Color32::from_rgba_unmultiplied(255, 255, 255, 70), Color32::WHITE)
1403 } else {
1404 (Color32::from_rgba_unmultiplied(0, 0, 0, 70), Color32::from_rgb(20, 20, 20))
1405 };
1406 painter.rect_filled(indicator, 0.0, fill);
1407 painter.rect_stroke(indicator, 0.0, Stroke::new(2.0, outline), StrokeKind::Inside);
1408 let accent = ui.visuals().selection.bg_fill;
1409 let bracket = Rect::from_min_max(indicator.left_top(), Pos2::new(indicator.left() + 4.0, indicator.bottom()));
1410 painter.rect_filled(bracket, 0.0, accent);
1411
1412 if let Some(span) = hover_span {
1418 paint_hover_span_on_minimap(
1419 &painter,
1420 minimap_rect,
1421 span,
1422 cols as u64,
1423 cell_h,
1424 window_top_row,
1425 shown_rows as u64,
1426 accent,
1427 );
1428 }
1429
1430 let pointer = response
1435 .interact_pointer_pos()
1436 .or_else(|| response.hover_pos().filter(|_| response.is_pointer_button_down_on()));
1437 if let Some(pos) = pointer.filter(|_| response.dragged() || response.clicked() || response.drag_started()) {
1438 let y = (pos.y - minimap_rect.top()).clamp(0.0, minimap_rect.height());
1439 let frac = y / minimap_rect.height();
1440 let target_scroll = (frac * max_scroll).clamp(0.0, max_scroll);
1441 ui.ctx().data_mut(|d| d.insert_temp(scroll_id, target_scroll));
1442 ui.ctx().request_repaint();
1443 }
1444}
1445
1446#[allow(clippy::too_many_arguments)]
1451fn paint_hover_span_on_minimap(
1452 painter: &egui::Painter,
1453 minimap_rect: Rect,
1454 span: ByteRange,
1455 cols: u64,
1456 cell_h: f32,
1457 window_top_row: u64,
1458 shown_rows: u64,
1459 accent: Color32,
1460) {
1461 let start = span.start().get();
1462 let end_exclusive = span.end().get();
1463 if end_exclusive <= start || cols == 0 {
1464 return;
1465 }
1466 let span_first_row = start / cols;
1467 let span_last_row_inclusive = (end_exclusive - 1) / cols;
1468 let window_end_row = window_top_row.saturating_add(shown_rows);
1469
1470 if span_last_row_inclusive < window_top_row {
1473 let top = minimap_rect.top();
1474 let caret = Rect::from_min_size(Pos2::new(minimap_rect.right() - 6.0, top), Vec2::new(6.0, 4.0));
1475 painter.rect_filled(caret, 0.0, accent);
1476 return;
1477 }
1478 if span_first_row >= window_end_row {
1479 let bottom = minimap_rect.bottom();
1480 let caret = Rect::from_min_size(Pos2::new(minimap_rect.right() - 6.0, bottom - 4.0), Vec2::new(6.0, 4.0));
1481 painter.rect_filled(caret, 0.0, accent);
1482 return;
1483 }
1484
1485 let shaded_top_row = span_first_row.max(window_top_row);
1487 let shaded_bot_row_inclusive = span_last_row_inclusive.min(window_end_row.saturating_sub(1));
1488 let rel_top = (shaded_top_row - window_top_row) as f32 * cell_h;
1489 let rel_bot = ((shaded_bot_row_inclusive + 1) - window_top_row) as f32 * cell_h;
1490 let rect = Rect::from_min_max(
1491 Pos2::new(minimap_rect.left(), minimap_rect.top() + rel_top),
1492 Pos2::new(minimap_rect.right(), minimap_rect.top() + rel_bot),
1493 );
1494 painter.rect_filled(rect, 0.0, accent.gamma_multiply(0.35));
1497 let edge = Rect::from_min_size(rect.left_top(), Vec2::new(2.0, rect.height()));
1498 painter.rect_filled(edge, 0.0, accent);
1499}
1500
1501fn draw_scrollbar(
1506 ui: &mut Ui,
1507 scroll_id: egui::Id,
1508 rect: Rect,
1509 current_offset: f32,
1510 viewport_height: f32,
1511 content_height: f32,
1512) {
1513 if rect.width() < 1.0 || rect.height() < 1.0 {
1514 return;
1515 }
1516 let response = ui.allocate_rect(rect, Sense::click_and_drag());
1517 let painter = ui.painter_at(rect);
1518
1519 let track_color = ui.visuals().extreme_bg_color;
1520 painter.rect_filled(rect, 3.0, track_color);
1521
1522 if content_height <= viewport_height {
1523 return;
1524 }
1525
1526 let viewport_frac = (viewport_height / content_height).clamp(0.05, 1.0);
1527 let max_scroll = (content_height - viewport_height).max(1.0);
1528 let scroll_frac = (current_offset / max_scroll).clamp(0.0, 1.0);
1529
1530 let thumb_h = (viewport_frac * rect.height()).max(18.0);
1531 let thumb_top = rect.top() + scroll_frac * (rect.height() - thumb_h);
1532 let thumb_rect =
1533 Rect::from_min_size(Pos2::new(rect.left() + 2.0, thumb_top), Vec2::new(rect.width() - 4.0, thumb_h));
1534
1535 let widget_visuals = if response.is_pointer_button_down_on() {
1536 ui.visuals().widgets.active
1537 } else if response.hovered() {
1538 ui.visuals().widgets.hovered
1539 } else {
1540 ui.visuals().widgets.inactive
1541 };
1542 painter.rect_filled(thumb_rect, 3.0, widget_visuals.bg_fill);
1543
1544 let pointer = response
1545 .interact_pointer_pos()
1546 .or_else(|| response.hover_pos().filter(|_| response.is_pointer_button_down_on()));
1547 if let Some(pos) = pointer.filter(|_| response.dragged() || response.clicked() || response.drag_started()) {
1548 let y = (pos.y - rect.top() - thumb_h * 0.5).clamp(0.0, rect.height() - thumb_h);
1549 let frac = if rect.height() > thumb_h { y / (rect.height() - thumb_h) } else { 0.0 };
1550 let target = (frac * max_scroll).clamp(0.0, max_scroll);
1551 ui.ctx().data_mut(|d| d.insert_temp(scroll_id, target));
1552 ui.ctx().request_repaint();
1553 }
1554}
1555
1556fn field_color_for(boundaries: &[(ByteOffset, ByteLen)], colors: &[Color32], byte_offset: u64) -> Option<Color32> {
1564 let idx = boundaries.partition_point(|(start, _)| start.get() <= byte_offset);
1565 if idx == 0 {
1566 return None;
1567 }
1568 let (start, len) = boundaries[idx - 1];
1569 let end = start.get().saturating_add(len.get());
1570 if byte_offset < end { colors.get(idx - 1).copied() } else { None }
1571}
1572
1573fn grayscale_for_byte(byte: u8, dark: bool) -> Color32 {
1574 let t = f32::from(byte) / 255.0;
1575 let (lo, hi) = if dark { (40.0, 230.0) } else { (40.0, 220.0) };
1576 let v = (lo * (1.0 - t) + hi * t).round() as u8;
1577 Color32::from_rgb(v, v, v)
1578}
1579
1580fn paint_column_header(
1584 ui: &mut Ui,
1585 layout: &RowLayout,
1586 font_id: &FontId,
1587 row_height: f32,
1588 formatter: Option<&dyn Fn(usize) -> String>,
1589) {
1590 let cols = usize::from(layout.columns.get());
1591 let header_height = row_height * 0.75;
1592 let (header_rect, _) = ui.allocate_exact_size(Vec2::new(layout.total_width, header_height), Sense::empty());
1593 let painter = ui.painter_at(header_rect);
1594 let color = ui.visuals().weak_text_color();
1595 let origin = header_rect.min;
1596 for col in 0..cols {
1597 let label = match formatter {
1598 Some(f) => f(col),
1599 None => format!("{col:X}"),
1600 };
1601 let cell = layout.hex_cell_rect(origin, col, header_height);
1602 painter.text(cell.center(), Align2::CENTER_CENTER, &label, font_id.clone(), color);
1603 let ascii_cell = layout.ascii_cell_rect(origin, col, header_height);
1604 painter.text(ascii_cell.center(), Align2::CENTER_CENTER, &label, font_id.clone(), color);
1605 }
1606 let divider_y = header_rect.bottom();
1607 painter.line_segment(
1608 [Pos2::new(header_rect.left(), divider_y), Pos2::new(header_rect.right(), divider_y)],
1609 Stroke::new(1.0, ui.visuals().weak_text_color().gamma_multiply(0.5)),
1610 );
1611}
1612
1613fn format_address(offset: ByteOffset, width: usize) -> String {
1614 format!("{:0width$X}", offset.get(), width = width)
1615}
1616
1617#[cfg(test)]
1618mod tests {
1619 use super::*;
1620
1621 #[test]
1622 fn address_width_scales_with_length() {
1623 assert_eq!(address_hex_width(ByteLen::new(0)), 8);
1624 assert_eq!(address_hex_width(ByteLen::new(256)), 8);
1625 assert_eq!(address_hex_width(ByteLen::new(1u64 << 32)), 8);
1626 assert_eq!(address_hex_width(ByteLen::new((1u64 << 32) + 1)), 9);
1627 }
1628
1629 #[test]
1630 fn row_count_handles_partial_row() {
1631 let cols = ColumnCount::new(16).unwrap();
1632 assert_eq!(row_count(ByteLen::new(0), cols), 0);
1633 assert_eq!(row_count(ByteLen::new(1), cols), 1);
1634 assert_eq!(row_count(ByteLen::new(16), cols), 1);
1635 assert_eq!(row_count(ByteLen::new(17), cols), 2);
1636 }
1637
1638 #[test]
1639 fn format_address_zero_pads() {
1640 assert_eq!(format_address(ByteOffset::new(0x1a), 8), "0000001A");
1641 }
1642
1643 #[test]
1644 fn hex_span_covers_contiguous_columns() {
1645 let cols = ColumnCount::new(16).unwrap();
1646 let layout = RowLayout::compute(10.0, 8, cols);
1647 let origin = Pos2::ZERO;
1648 let span = layout.hex_span_rect(origin, 3, 7, 20.0);
1649 let c3 = layout.hex_cell_rect(origin, 3, 20.0);
1650 let c7 = layout.hex_cell_rect(origin, 7, 20.0);
1651 assert_eq!(span.left(), c3.left());
1652 assert_eq!(span.right(), c7.right());
1653 }
1654
1655 #[test]
1656 fn hit_test_clamps_row_past_last_visible() {
1657 let cols = ColumnCount::new(16).unwrap();
1658 let layout = RowLayout::compute(10.0, 8, cols);
1659 let block = Rect::from_min_size(Pos2::ZERO, Vec2::new(layout.total_width, 60.0));
1660 let below = Pos2::new(layout.hex_start_x + 5.0, 1000.0);
1662 let hit = layout.hit_test(block, below, 20.0, 3).unwrap();
1663 assert_eq!(hit.row, 2);
1664 }
1665}