Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
135 lines
3.6 KiB
Rust
135 lines
3.6 KiB
Rust
use crate::app::{DisplayOptions, SearchState};
|
|
use crate::buffers::{HexBuffer, HexLine, LineDirection};
|
|
use egui::{
|
|
Color32, Label, ScrollArea, TextStyle, TextWrapMode, Ui,
|
|
text::{LayoutJob, TextFormat},
|
|
};
|
|
|
|
pub fn render(
|
|
ui: &mut Ui,
|
|
buf: &HexBuffer,
|
|
display: DisplayOptions,
|
|
search: Option<&SearchState>,
|
|
) -> usize {
|
|
let line_count = buf.len();
|
|
if line_count == 0 {
|
|
ui.label("no hex data");
|
|
return 0;
|
|
}
|
|
let row_height = ui.text_style_height(&TextStyle::Monospace);
|
|
ScrollArea::both().stick_to_bottom(true).show_rows(
|
|
ui,
|
|
row_height,
|
|
line_count,
|
|
|ui, row_range| {
|
|
for row in row_range {
|
|
if let Some(line) = buf.get(row) {
|
|
ui.add(
|
|
Label::new(format_hex_line(line, display, search, ui.style()))
|
|
.wrap_mode(TextWrapMode::Extend),
|
|
);
|
|
}
|
|
}
|
|
},
|
|
);
|
|
line_count
|
|
}
|
|
|
|
fn highlight_matches(
|
|
text: &str,
|
|
query: &str,
|
|
case_sensitive: bool,
|
|
default_format: TextFormat,
|
|
highlight_format: TextFormat,
|
|
) -> LayoutJob {
|
|
let search_text = if case_sensitive {
|
|
text.to_string()
|
|
} else {
|
|
text.to_lowercase()
|
|
};
|
|
let query = if case_sensitive {
|
|
query.to_string()
|
|
} else {
|
|
query.to_lowercase()
|
|
};
|
|
let mut job = LayoutJob::default();
|
|
let mut last_end = 0;
|
|
let mut idx = 0;
|
|
while let Some(pos) = search_text[idx..].find(&query) {
|
|
let start = idx + pos;
|
|
let end = start + query.len();
|
|
if last_end < start {
|
|
job.append(&text[last_end..start], 0.0, default_format.clone());
|
|
}
|
|
job.append(&text[start..end], 0.0, highlight_format.clone());
|
|
last_end = end;
|
|
idx = end;
|
|
}
|
|
if last_end < text.len() {
|
|
job.append(&text[last_end..], 0.0, default_format);
|
|
}
|
|
job
|
|
}
|
|
|
|
fn monospace_format(style: &egui::Style) -> TextFormat {
|
|
TextFormat {
|
|
font_id: style
|
|
.text_styles
|
|
.get(&TextStyle::Monospace)
|
|
.cloned()
|
|
.unwrap_or_default(),
|
|
color: Color32::WHITE,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn format_hex_line(
|
|
line: &HexLine,
|
|
display: DisplayOptions,
|
|
search: Option<&SearchState>,
|
|
style: &egui::Style,
|
|
) -> LayoutJob {
|
|
let mut prefix = Vec::new();
|
|
if display.show_timestamp {
|
|
let ts = line.elapsed.as_secs_f64();
|
|
let time = if ts < 60.0 {
|
|
format!("{:05.2}", ts)
|
|
} else {
|
|
format!("{:02}:{:02}", (ts / 60.0) as u64, (ts % 60.0) as u64)
|
|
};
|
|
prefix.push(format!("[{time}]"));
|
|
}
|
|
if display.show_direction {
|
|
let dir = match line.direction {
|
|
LineDirection::In => "IN",
|
|
LineDirection::Out => "OUT",
|
|
};
|
|
prefix.push(format!("[{dir}]"));
|
|
}
|
|
if display.show_pipeline {
|
|
prefix.push(format!("[{}]", line.pipeline));
|
|
}
|
|
let prefix = if prefix.is_empty() {
|
|
String::new()
|
|
} else {
|
|
format!("{} ", prefix.join(" "))
|
|
};
|
|
let full_text = format!("{prefix}{} |{}|", line.hex, line.ascii);
|
|
|
|
let default = monospace_format(style);
|
|
let highlight = TextFormat {
|
|
font_id: default.font_id.clone(),
|
|
color: Color32::BLACK,
|
|
background: Color32::from_rgb(255, 255, 0),
|
|
..Default::default()
|
|
};
|
|
|
|
match search {
|
|
Some(state) if state.active && !state.query.is_empty() => {
|
|
highlight_matches(&full_text, &state.query, state.case_sensitive, default, highlight)
|
|
}
|
|
_ => LayoutJob::single_section(full_text, default),
|
|
}
|
|
}
|
|
|