use bladvak::eframe::egui::{
self, Color32, FontFamily, FontId, Painter, ScrollArea, Stroke, TextStyle, Theme, Vec2,
};
use bladvak::errors::ErrorManager;
use crate::WombatApp;
use crate::app::Accent;
impl WombatApp {
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
#[allow(clippy::cast_precision_loss)]
pub(crate) fn app_central_panel(
&mut self,
ui: &mut egui::Ui,
_error_manager: &mut ErrorManager,
) {
ScrollArea::vertical().show_viewport(ui, |ui: &mut egui::Ui, viewport: egui::Rect| {
let text_style = TextStyle::Monospace;
let font_size = ui
.style()
.text_styles
.get(&text_style)
.map_or(14.0, |s| s.size);
let row_height = ui.text_style_height(&text_style).max(14.0) + 1.0; let lines_total = self
.binary_file
.len()
.div_ceil(self.display_settings.bytes_per_line);
let total_height = (lines_total as f32) * row_height;
let _rect = ui.allocate_space(egui::vec2(viewport.width(), total_height));
let top_y = viewport.top(); let bottom_y = viewport.bottom();
let first_line = (top_y / row_height).floor().max(0.0) as usize;
let last_line = (bottom_y / row_height).ceil().max(0.0) as usize;
let first_line = first_line.min(lines_total);
let last_line = last_line.min(lines_total);
let left = viewport.left() + 4.0;
self.show_lines(ui, left, font_size, row_height, (first_line, last_line));
});
}
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::too_many_lines)]
fn show_lines(
&mut self,
ui: &mut egui::Ui,
left: f32,
font_size: f32,
row_height: f32,
(first_line, last_line): (usize, usize),
) {
let mut mark_selection_stale = false;
let bytes_per_line = self.display_settings.bytes_per_line;
let font_id = FontId::new(font_size, FontFamily::Monospace);
let painter = ui.painter();
let mut y = first_line as f32 * row_height;
let offset_col_nb: usize = 8;
let offset_col_width = 80.0; let hex_col_x = left + offset_col_width;
let hex_col_width = (bytes_per_line as f32) * 3.0 * (font_size * 0.6); let ascii_col_x = hex_col_x + hex_col_width + 8.0;
for line in first_line..last_line {
let offset = line * bytes_per_line;
let slice_end = (offset + bytes_per_line).min(self.binary_file.len());
let slice = &self.binary_file[offset..slice_end];
let offset_text = format!("{offset:0offset_col_nb$X}:");
let mut hex_buf = Vec::with_capacity(bytes_per_line);
for b in slice {
if self.display_settings.display_lsb {
hex_buf.push(format!("{:02X} ", b.reverse_bits()));
} else {
hex_buf.push(format!("{b:02X} "));
}
}
let mut ascii_buf = Vec::with_capacity(bytes_per_line);
for b in slice {
let b = if self.display_settings.display_lsb {
b.reverse_bits()
} else {
*b
};
let c = match b {
x if Self::RANGE_ASCII_PRINTABLE.contains(&x) => x as char,
c => {
if self.display_settings.limit_to_base_ascii {
'.'
} else {
c as char
}
}
};
ascii_buf.push(c);
}
let origin = ui.min_rect().min;
painter.text(
origin + Vec2::new(left, y),
egui::Align2::LEFT_TOP,
offset_text,
font_id.clone(),
ui.visuals().text_color(),
);
for (idx, (hex, ascii)) in std::iter::zip(&hex_buf, &ascii_buf).enumerate() {
let x_pos = (idx as f32) * 3.0 * (font_size * 0.6);
let color = if self
.selection
.range
.is_some_and(|s| (s.0..=s.1).contains(&(offset + idx)))
{
if ui.ctx().theme() == Theme::Light {
self.selection.color.0
} else {
self.selection.color.1
}
} else {
ui.visuals().text_color()
};
painter.text(
origin + Vec2::new(hex_col_x + x_pos, y),
egui::Align2::LEFT_TOP,
hex,
font_id.clone(),
color,
);
let hex_pos = (idx as f32) * (font_size * 0.6);
painter.text(
origin + Vec2::new(ascii_col_x + hex_pos, y),
egui::Align2::LEFT_TOP,
ascii,
font_id.clone(),
color,
);
}
let char_width = ui.fonts_mut(|f| f.glyph_width(&font_id, '0'));
Self::interact_offset(
ui,
if self.visual_debug {
Some(painter)
} else {
None
},
origin,
bytes_per_line,
char_width,
offset_col_nb,
row_height,
line,
y,
);
let hex_group_width = char_width * 2.0;
for (idx, b) in slice.iter().enumerate() {
let bx = hex_col_x + (idx as f32) * (hex_group_width + char_width);
let byte_rect = egui::Rect::from_min_size(
origin + Vec2::new(bx, y),
egui::vec2(hex_group_width, row_height),
);
let resp = ui.interact(
byte_rect,
ui.id().with(("hex", line, idx)),
egui::Sense::click(),
);
if self.visual_debug {
painter.rect(
byte_rect,
1.0,
Color32::TRANSPARENT,
Stroke::new(0.5, Color32::BLACK),
egui::StrokeKind::Middle,
);
}
let is_clicked = resp.clicked();
if resp.hovered() {
resp.on_hover_ui(|ui| {
let position = line * bytes_per_line + idx;
ui.label(format!("Bytes at index 0x{position:X} ({position})"));
self.ui_table_u8(ui, *b, &Accent::Hex);
});
}
if is_clicked {
let is_alt = ui.ctx().input(|i| i.modifiers.shift);
self.selection.range = self.handle_selection_click(offset, idx, is_alt);
mark_selection_stale = true;
}
let bx = ascii_col_x + (idx as f32) * (font_size * 0.6);
let width = 1.0 * char_width;
let byte_rect = egui::Rect::from_min_size(
origin + Vec2::new(bx, y),
egui::vec2(width, row_height),
);
let resp = ui.interact(
byte_rect,
ui.id().with(("ascii", line, idx)),
egui::Sense::click(),
);
if self.visual_debug {
painter.rect(
byte_rect,
1.0,
Color32::TRANSPARENT,
Stroke::new(1.0, Color32::BLACK),
egui::StrokeKind::Outside,
);
}
let is_clicked = resp.clicked();
if resp.hovered() {
resp.on_hover_ui(|ui| {
let position = line * bytes_per_line + idx;
ui.label(format!("Bytes at index 0x{position:X} ({position})"));
self.ui_table_u8(ui, *b, &Accent::Ascii);
});
}
if is_clicked {
let is_alt = ui.ctx().input(|i| i.modifiers.shift);
self.selection.range = self.handle_selection_click(offset, idx, is_alt);
mark_selection_stale = true;
}
}
y += row_height;
}
if mark_selection_stale {
self.stale_selection();
}
}
fn handle_selection_click(
&self,
offset: usize,
idx: usize,
is_alt: bool,
) -> Option<(usize, usize)> {
let current_idx = offset + idx;
if let Some((select1, select2)) = self.selection.range {
if is_alt {
if select1 == current_idx {
return Some((current_idx, current_idx));
} else if current_idx < select1 {
return Some((current_idx, select2));
} else if select1 > current_idx {
return Some((current_idx, select1));
} else if current_idx > select2 || (select1 < current_idx && current_idx < select2)
{
return Some((select1, current_idx));
}
} else if select1 == current_idx {
return None;
} else {
return Some((current_idx, current_idx));
}
}
Some((current_idx, current_idx))
}
#[inline]
#[allow(clippy::too_many_arguments)]
fn interact_offset(
ui: &egui::Ui,
painter: Option<&Painter>,
origin: egui::Pos2,
bytes_per_line: usize,
char_width: f32,
offset_col_nb: usize,
row_height: f32,
line: usize,
y: f32,
) {
#[allow(clippy::cast_precision_loss)]
let byte_rect = egui::Rect::from_min_size(
origin + Vec2::new(0.0, y),
egui::vec2(char_width * offset_col_nb as f32 + char_width, row_height),
);
let resp = ui.interact(
byte_rect,
ui.id().with(("offset_hex", line, 0)),
egui::Sense::click(),
);
if let Some(painter) = painter {
painter.rect(
byte_rect,
1.0,
Color32::TRANSPARENT,
Stroke::new(0.5, Color32::RED),
egui::StrokeKind::Middle,
);
}
if resp.hovered() {
resp.on_hover_ui(|ui| {
let position_start = line * bytes_per_line;
let position_end = line * bytes_per_line + bytes_per_line - 1;
ui.label(format!("Position from 0x{position_start:X} ({position_start}) to 0x{position_end:X} ({position_end})"));
});
}
}
}