Created
March 6, 2025 01:07
-
-
Save DrBrad/ea4ba951968bb15bc131f558453df821 to your computer and use it in GitHub Desktop.
HexEditor GTK-3 for Rust
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::cell::RefCell; | |
use std::cmp::max; | |
use gtk::gdk::{EventMask, EventMotion, WindowAttr, WindowType, WindowWindowClass, RGBA}; | |
use gtk::{gdk, glib, pango, Allocation, Buildable, Misc, StateFlags, Widget}; | |
use gtk::cairo::{Context, FontSlant, FontWeight}; | |
use gtk::glib::Propagation; | |
use gtk::glib::Propagation::Proceed; | |
use gtk::pango::Weight; | |
use gtk::prelude::{StyleContextExt, StyleContextExtManual, WidgetExt}; | |
use gtk::subclass::prelude::{ObjectImpl, ObjectSubclass, ObjectSubclassExt, ObjectSubclassIsExt, WidgetClassSubclassExt, WidgetImpl}; | |
const BYTES_PER_ROW: usize = 16; | |
pub struct HexEditorImpl { | |
data: RefCell<Vec<u8>>, | |
cursor: RefCell<Option<usize>>, | |
selection: RefCell<Option<(usize, usize)>>, | |
line_number_color: RefCell<RGBA>, | |
cursor_color: RefCell<RGBA>, | |
selection_color: RefCell<RGBA> | |
} | |
impl Default for HexEditorImpl { | |
fn default() -> Self { | |
Self { | |
data: RefCell::new(Vec::new()), | |
cursor: RefCell::new(None), | |
selection: RefCell::new(None), | |
line_number_color: RefCell::new(RGBA::new(0.7, 0.7, 0.7, 1.0)), | |
cursor_color: RefCell::new(RGBA::new(0.8, 0.8, 0.8, 1.0)), | |
selection_color: RefCell::new(RGBA::new(0.4, 0.0, 0.4, 1.0)) | |
} | |
} | |
} | |
#[glib::object_subclass] | |
impl ObjectSubclass for HexEditorImpl { | |
const NAME: &'static str = "HexEditor"; | |
type ParentType = Widget; | |
type Type = HexEditor; | |
fn class_init(class: &mut Self::Class) { | |
class.set_css_name("hexeditor"); | |
} | |
} | |
impl ObjectImpl for HexEditorImpl {} | |
impl WidgetImpl for HexEditorImpl { | |
fn draw(&self, cr: &Context) -> Propagation { | |
let widget = self.obj(); | |
let style_context = widget.style_context(); | |
let pango_context = widget.create_pango_context(); | |
style_context.set_state(StateFlags::NORMAL); | |
let text_color = style_context.color(StateFlags::NORMAL); | |
let font_desc = style_context.font(StateFlags::NORMAL); | |
let metrics = pango_context.metrics(Some(&font_desc), None); | |
let ascent = metrics.ascent() as f64 / pango::SCALE as f64; | |
let decent = metrics.descent() as f64 / pango::SCALE as f64; | |
let font_size = if font_desc.is_size_absolute() { | |
font_desc.size() as f64 | |
} else { | |
font_desc.size() as f64 / pango::SCALE as f64 | |
}; | |
let font_weight = match font_desc.weight() { | |
Weight::Bold => FontWeight::Bold, | |
_ => { | |
FontWeight::Normal | |
} | |
}; | |
let padding = style_context.padding(StateFlags::NORMAL); | |
if let Ok(background) = style_context.style_property_for_state("background-color", StateFlags::NORMAL).get::<RGBA>() { | |
cr.set_source_rgba(background.red(), background.green(), background.blue(), background.alpha()); | |
cr.paint(); | |
} | |
let char_width = metrics.approximate_char_width() as f64 / pango::SCALE as f64; | |
let row_height = ascent + decent; | |
let ascii_offset = (BYTES_PER_ROW as f64) * (char_width * 2.0) + 9.0; | |
let line_numbers_width = padding.left as f64 + 8.0 * char_width + 15.0; | |
cr.select_font_face(font_desc.family().unwrap().as_str(), FontSlant::Normal, font_weight); | |
cr.set_font_size(font_size); | |
for (i, &byte) in self.data.borrow().iter().enumerate() { | |
let row = i / BYTES_PER_ROW; | |
let col = i % BYTES_PER_ROW; | |
let hex_x = col as f64 * (char_width * 2.0) + line_numbers_width; | |
let y = padding.top as f64 + (row as f64 * row_height); | |
let ascii_x = ascii_offset + col as f64 * char_width + line_numbers_width; | |
if col == 0 { | |
let color = match *self.cursor.borrow() { | |
Some(cursor) => { | |
if cursor/BYTES_PER_ROW == row { | |
text_color | |
} else { | |
*self.line_number_color.borrow() | |
} | |
} | |
None => { | |
*self.line_number_color.borrow() | |
} | |
}; | |
cr.set_source_rgba(color.red(), color.green(), color.blue(), color.alpha()); | |
let line_number = format!("{:08X}", row * BYTES_PER_ROW); | |
cr.move_to(padding.left as f64, y + row_height - decent); | |
cr.show_text(&line_number); | |
} | |
match *self.selection.borrow() { | |
Some((x, x2)) => { | |
if i >= x && i <= x+x2-1 { | |
let color = self.selection_color.borrow(); | |
cr.set_source_rgba(color.red(), color.green(), color.blue(), color.alpha()); | |
cr.rectangle(hex_x - 2.0, y, char_width * 2.0, row_height); | |
cr.fill().unwrap(); | |
cr.rectangle(ascii_x - 1.0, y, char_width - 2.0 + 2.0, row_height); | |
cr.fill().unwrap(); | |
} | |
} | |
None => {} | |
} | |
if Some(i) == *self.cursor.borrow() { | |
let color = self.cursor_color.borrow(); | |
cr.set_source_rgba(color.red(), color.green(), color.blue(), color.alpha()); | |
cr.rectangle(hex_x - 1.0, y + 1.0, char_width * 2.0 - 2.0, row_height - 2.0); | |
cr.stroke().unwrap(); | |
cr.rectangle(ascii_x, y + 1.0, char_width - 2.0, row_height - 2.0); | |
cr.stroke().unwrap(); | |
} | |
cr.set_source_rgba(text_color.red(), text_color.green(), text_color.blue(), text_color.alpha()); | |
let hex = format!("{:02X}", byte); | |
cr.move_to(hex_x, y + row_height - decent); | |
cr.show_text(&hex); | |
let ascii_char = if byte.is_ascii_graphic() { byte as char } else { '.' }; | |
cr.move_to(ascii_x, y + row_height - decent); | |
cr.show_text(&ascii_char.to_string()); | |
} | |
Proceed | |
} | |
fn realize(&self) { | |
let widget = self.obj(); | |
let allocation = widget.allocation(); | |
let attr = WindowAttr { | |
title: None, | |
event_mask: EventMask::POINTER_MOTION_MASK, | |
x: Some(allocation.x()), | |
y: Some(allocation.y()), | |
width: allocation.width(), | |
height: allocation.height(), | |
wclass: WindowWindowClass::InputOutput, | |
visual: None, | |
window_type: WindowType::Child, | |
cursor: None, | |
override_redirect: false, | |
type_hint: None | |
}; | |
let parent_window = widget.parent_window().unwrap(); | |
let window = gdk::Window::new(Some(&parent_window), &attr); | |
widget.register_window(&window); | |
widget.set_window(window); | |
widget.set_realized(true); | |
let (calculated_width, calculated_height) = widget.calculate_size(); | |
let width = max(calculated_width, allocation.width()); | |
let height = max(calculated_height, allocation.height()); | |
widget.set_size_request(width, height); | |
self.size_allocate(&Allocation::new(allocation.x(), allocation.y(), width, height)); | |
} | |
fn motion_notify_event(&self, event: &EventMotion) -> Propagation { | |
let widget = self.obj(); | |
let style_context = widget.style_context(); | |
let pango_context = widget.create_pango_context(); | |
style_context.set_state(StateFlags::NORMAL); | |
let font_desc = style_context.font(StateFlags::NORMAL); | |
let metrics = pango_context.metrics(Some(&font_desc), None); | |
let ascent = metrics.ascent() as f64 / pango::SCALE as f64; | |
let decent = metrics.descent() as f64 / pango::SCALE as f64; | |
let padding = style_context.padding(StateFlags::NORMAL); | |
let char_width = metrics.approximate_char_width() as f64 / pango::SCALE as f64; | |
let row_height = ascent + decent; | |
let ascii_offset = (BYTES_PER_ROW as f64) * (char_width * 2.0) + 10.0; | |
let line_numbers_width = padding.left as f64 + 8.0 * char_width + 13.0; | |
let (x, y) = event.position(); | |
let mut col = ((x - line_numbers_width) / (char_width * 2.0)).floor() as isize; | |
let row = ((y - padding.top as f64) / row_height).floor() as isize; | |
if x-line_numbers_width >= ascii_offset { | |
let ascii_col = ((x - line_numbers_width - ascii_offset) / char_width).floor() as isize; | |
col = ascii_col; | |
} | |
if col >= BYTES_PER_ROW as isize || row < 0 { | |
*self.cursor.borrow_mut() = None; | |
return Proceed; | |
} | |
if row >= 0 && col >= 0 { | |
let index = (row * BYTES_PER_ROW as isize + col) as usize; | |
if index < self.data.borrow().len() { | |
*self.cursor.borrow_mut() = Some(index); | |
} else { | |
*self.cursor.borrow_mut() = None; | |
} | |
} else { | |
*self.cursor.borrow_mut() = None; | |
} | |
self.obj().queue_draw(); | |
Proceed | |
} | |
} | |
glib::wrapper! { | |
pub struct HexEditor(ObjectSubclass<HexEditorImpl>) | |
@extends Misc, Widget, @implements Buildable; | |
} | |
impl HexEditor { | |
pub fn new(data: Vec<u8>) -> Self { | |
let _self = glib::Object::builder::<HexEditor>().build(); | |
*_self.imp().data.borrow_mut() = data.to_vec(); | |
_self | |
} | |
pub fn set_data(&self, data: Vec<u8>) { | |
*self.imp().data.borrow_mut() = data.to_vec(); | |
} | |
pub fn set_line_number_color(&self, color: RGBA) { | |
*self.imp().line_number_color.borrow_mut() = color; | |
} | |
pub fn get_line_number_color(&self) -> RGBA { | |
self.imp().line_number_color.borrow().clone() | |
} | |
pub fn set_cursor_color(&self, color: RGBA) { | |
*self.imp().cursor_color.borrow_mut() = color; | |
} | |
pub fn get_cursor_color(&self) -> RGBA { | |
self.imp().cursor_color.borrow().clone() | |
} | |
pub fn set_selection_color(&self, color: RGBA) { | |
*self.imp().selection_color.borrow_mut() = color; | |
} | |
pub fn get_selection_color(&self) -> RGBA { | |
self.imp().selection_color.borrow().clone() | |
} | |
pub fn set_selection(&self, a: usize, b: usize) { | |
*self.imp().selection.borrow_mut() = Some((a, b)); | |
self.queue_draw(); | |
} | |
pub fn get_selection(&self) -> Option<(usize, usize)> { | |
self.imp().selection.borrow().clone() | |
} | |
fn calculate_size(&self) -> (i32, i32) { | |
let style_context = self.style_context(); | |
let pango_context = self.create_pango_context(); | |
style_context.set_state(StateFlags::NORMAL); | |
let font_desc = style_context.font(StateFlags::NORMAL); | |
let metrics = pango_context.metrics(Some(&font_desc), None); | |
let ascent = metrics.ascent() as f64 / pango::SCALE as f64; | |
let decent = metrics.descent() as f64 / pango::SCALE as f64; | |
let padding = style_context.padding(StateFlags::NORMAL); | |
let char_width = metrics.approximate_char_width() as f64 / pango::SCALE as f64; | |
let row_height = ascent + decent; | |
let ascii_offset = (BYTES_PER_ROW as f64) * (char_width * 2.0) + 9.0; | |
let line_numbers_width = padding.left as f64 + 8.0 * char_width + 15.0; | |
let width = padding.right as f64 + (char_width * BYTES_PER_ROW as f64) + line_numbers_width + ascii_offset; | |
let height = padding.top as f64 + padding.bottom as f64 + ((self.imp().data.borrow().len() % BYTES_PER_ROW) as f64 * row_height); | |
(width as i32, height as i32) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment