1use egui::{FontFamily, Grid, RichText, TextEdit, Ui, Window}; 2use std::ops::{BitOr, BitOrAssign}; 3 4#[derive(Debug, Clone, Copy, PartialEq, Eq)] 5pub struct GenericBreakpoint<T> { 6 pub start_address: T, 7 pub end_address: T, 8 pub read: bool, 9 pub write: bool, 10 pub execute: bool, 11} 12 13#[derive(Debug, Clone)] 14pub struct BreakpointsWidget<T> { 15 id: String, 16 breakpoints: Vec<GenericBreakpoint<T>>, 17 breakpoints_changed: bool, 18 start_address: String, 19 end_address: String, 20 read: bool, 21 write: bool, 22 execute: bool, 23} 24 25impl<T> BreakpointsWidget<T> { 26 pub fn new(id: impl Into<String>) -> Self { 27 Self { 28 id: id.into(), 29 breakpoints: Vec::new(), 30 breakpoints_changed: false, 31 start_address: String::new(), 32 end_address: String::new(), 33 read: true, 34 write: false, 35 execute: false, 36 } 37 } 38} 39 40pub trait BreakpointAddress: Sized + Copy + Eq + Ord { 41 fn from_hex_str(value: &str) -> Option<Self>; 42 43 fn format_hex(self) -> String; 44} 45 46impl BreakpointAddress for u16 { 47 fn from_hex_str(value: &str) -> Option<Self> { 48 Self::from_str_radix(value, 16).ok() 49 } 50 51 fn format_hex(self) -> String { 52 format!("{self:04X}") 53 } 54} 55 56impl BreakpointAddress for u32 { 57 fn from_hex_str(value: &str) -> Option<Self> { 58 Self::from_str_radix(value, 16).ok() 59 } 60 61 fn format_hex(self) -> String { 62 format!("{self:08X}") 63 } 64} 65 66#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 67pub struct U24(u32); 68 69impl U24 { 70 pub fn new(value: u32) -> Self { 71 Self(value & 0xFFFFFF) 72 } 73 74 pub fn get(self) -> u32 { 75 self.0 76 } 77} 78 79impl BreakpointAddress for U24 { 80 fn from_hex_str(value: &str) -> Option<Self> { 81 u32::from_hex_str(value).map(Self::new) 82 } 83 84 fn format_hex(self) -> String { 85 format!("{:06X}", self.0) 86 } 87} 88 89#[derive(Debug, Clone, Copy, PartialEq, Eq)] 90pub enum BreakpointWindowResponse { 91 NotChanged, 92 Changed, 93} 94 95impl BreakpointWindowResponse { 96 pub fn from_changed(changed: bool) -> Self { 97 if changed { Self::Changed } else { Self::NotChanged } 98 } 99} 100 101impl BitOr for BreakpointWindowResponse { 102 type Output = Self; 103 104 fn bitor(self, rhs: Self) -> Self::Output { 105 match (self, rhs) { 106 (Self::Changed, _) | (_, Self::Changed) => Self::Changed, 107 (Self::NotChanged, Self::NotChanged) => Self::NotChanged, 108 } 109 } 110} 111 112impl BitOrAssign for BreakpointWindowResponse { 113 fn bitor_assign(&mut self, rhs: Self) { 114 *self = *self | rhs; 115 } 116} 117 118impl<T> BreakpointsWidget<T> 119where 120 T: BreakpointAddress, 121{ 122 #[must_use] 123 pub fn show_window( 124 &mut self, 125 ctx: &egui::Context, 126 window_title: impl Into<egui::WidgetText>, 127 window_open: &mut bool, 128 additional_breakpoints: impl FnOnce(&mut Ui) -> BreakpointWindowResponse, 129 ) -> BreakpointWindowResponse { 130 let mut response = BreakpointWindowResponse::from_changed(self.breakpoints_changed); 131 self.breakpoints_changed = false; 132 133 Window::new(window_title) 134 .open(window_open) 135 .constrain(false) 136 .resizable([true, true]) 137 .default_pos(crate::rand_window_pos()) 138 .show(ctx, |ui| { 139 response |= self.render(ui); 140 141 response |= additional_breakpoints(ui); 142 }); 143 144 response 145 } 146 147 #[must_use] 148 pub fn breakpoints(&self) -> &[GenericBreakpoint<T>] { 149 &self.breakpoints 150 } 151 152 fn render(&mut self, ui: &mut Ui) -> BreakpointWindowResponse { 153 let initial_breakpoints = self.breakpoints.clone(); 154 155 if !self.breakpoints.is_empty() { 156 Grid::new(format!("{}_breakpoints", self.id)).show(ui, |ui| { 157 ui.heading("Addresses"); 158 ui.heading("R"); 159 ui.heading("W"); 160 ui.heading("X"); 161 ui.label(""); 162 ui.end_row(); 163 164 let mut remove_idx: Option<usize> = None; 165 for (i, breakpoint) in self.breakpoints.iter_mut().enumerate() { 166 if breakpoint.start_address == breakpoint.end_address { 167 ui.label( 168 RichText::new(format!("${}", breakpoint.start_address.format_hex())) 169 .family(FontFamily::Monospace), 170 ); 171 } else { 172 ui.label( 173 RichText::new(format!( 174 "${}-${}", 175 breakpoint.start_address.format_hex(), 176 breakpoint.end_address.format_hex() 177 )) 178 .family(FontFamily::Monospace), 179 ); 180 } 181 182 for value in 183 [&mut breakpoint.read, &mut breakpoint.write, &mut breakpoint.execute] 184 { 185 ui.checkbox(value, ""); 186 } 187 188 if ui.button("Remove").clicked() { 189 remove_idx = Some(i); 190 } 191 192 ui.end_row(); 193 } 194 195 if let Some(remove_idx) = remove_idx 196 && remove_idx < self.breakpoints.len() 197 { 198 self.breakpoints.remove(remove_idx); 199 } 200 201 macro_rules! check_all_button { 202 ($label:literal, $field:ident = $value:literal) => { 203 if ui.button($label).clicked() { 204 for breakpoint in &mut self.breakpoints { 205 breakpoint.$field = $value; 206 } 207 } 208 }; 209 } 210 211 ui.label(""); 212 check_all_button!("Check all", read = true); 213 check_all_button!("Check all", write = true); 214 check_all_button!("Check all", execute = true); 215 ui.end_row(); 216 217 ui.label(""); 218 check_all_button!("Uncheck all", read = false); 219 check_all_button!("Uncheck all", write = false); 220 check_all_button!("Uncheck all", execute = false); 221 ui.end_row(); 222 }); 223 224 ui.separator(); 225 } 226 227 let mut enter_pressed = false; 228 229 ui.heading("Add Breakpoint"); 230 ui.horizontal(|ui| { 231 ui.label("$"); 232 let start_resp = 233 ui.add(TextEdit::singleline(&mut self.start_address).desired_width(80.0)); 234 ui.label("-"); 235 ui.label("$"); 236 let end_resp = ui.add(TextEdit::singleline(&mut self.end_address).desired_width(80.0)); 237 238 ui.checkbox(&mut self.read, "Read"); 239 ui.checkbox(&mut self.write, "Write"); 240 ui.checkbox(&mut self.execute, "Execute"); 241 242 enter_pressed = (start_resp.lost_focus() || end_resp.lost_focus()) 243 && ui.input(|i| i.key_pressed(egui::Key::Enter)); 244 }); 245 246 let button_resp = ui.button("Add"); 247 if (button_resp.clicked() || enter_pressed) 248 && let Some(start_address) = T::from_hex_str(&self.start_address) 249 { 250 if self.end_address.is_empty() { 251 self.breakpoints.push(GenericBreakpoint { 252 start_address, 253 end_address: start_address, 254 read: self.read, 255 write: self.write, 256 execute: self.execute, 257 }); 258 259 self.start_address.clear(); 260 } else if let Some(end_address) = T::from_hex_str(&self.end_address) 261 && end_address >= start_address 262 { 263 self.breakpoints.push(GenericBreakpoint { 264 start_address, 265 end_address, 266 read: self.read, 267 write: self.write, 268 execute: self.execute, 269 }); 270 271 self.start_address.clear(); 272 self.end_address.clear(); 273 } 274 } 275 276 if initial_breakpoints != self.breakpoints { 277 BreakpointWindowResponse::Changed 278 } else { 279 BreakpointWindowResponse::NotChanged 280 } 281 } 282 283 pub fn has_execute_breakpoint(&self, address: T) -> bool { 284 self.breakpoints.iter().any(|breakpoint| { 285 breakpoint.execute 286 && breakpoint.start_address == address 287 && breakpoint.end_address == address 288 }) 289 } 290 291 pub fn toggle_execute_breakpoint(&mut self, address: T) { 292 let mut value: Option<bool> = None; 293 let mut to_remove = Vec::new(); 294 for (i, breakpoint) in self.breakpoints.iter_mut().enumerate() { 295 if breakpoint.start_address != address || breakpoint.end_address != address { 296 continue; 297 } 298 299 let value = *value.get_or_insert(!breakpoint.execute); 300 breakpoint.execute = value; 301 302 if !breakpoint.read && !breakpoint.write && !breakpoint.execute { 303 to_remove.push(i); 304 } 305 } 306 307 if value.is_none() { 308 self.breakpoints.push(GenericBreakpoint { 309 start_address: address, 310 end_address: address, 311 read: false, 312 write: false, 313 execute: true, 314 }); 315 } 316 317 for remove_idx in to_remove.into_iter().rev() { 318 if remove_idx < self.breakpoints.len() { 319 self.breakpoints.remove(remove_idx); 320 } 321 } 322 323 self.breakpoints_changed = true; 324 } 325 326 // Render a widget that can be clicked on to toggle execute breakpoints at a specific address. 327 // 328 // Draws a red circle if there exists a breakpoint at the specified address, otherwise draws 329 // an empty circle whenever the widget is hovered over. 330 pub fn render_clickable_widget( 331 &mut self, 332 address: T, 333 interact_id: impl Into<egui::Id>, 334 ui: &mut Ui, 335 ) { 336 let max_rect = ui.max_rect(); 337 338 let interact_resp = ui.interact(max_rect, interact_id.into(), egui::Sense::click()); 339 340 let has_execute_breakpoint = self.has_execute_breakpoint(address); 341 if interact_resp.clicked() { 342 self.toggle_execute_breakpoint(address); 343 } else if interact_resp.hovered() || has_execute_breakpoint { 344 let fill_color = if interact_resp.hovered() && has_execute_breakpoint { 345 egui::Color32::LIGHT_RED 346 } else if has_execute_breakpoint { 347 egui::Color32::RED 348 } else { 349 ui.visuals().panel_fill 350 }; 351 let stroke_color = ui.visuals().text_color(); 352 353 let radius = if max_rect.width() < max_rect.height() { 354 0.5 * max_rect.width() 355 } else { 356 0.5 * max_rect.height() 357 }; 358 359 ui.painter().circle( 360 [ 361 0.5 * max_rect.width() + max_rect.left(), 362 0.5 * max_rect.height() + max_rect.top(), 363 ] 364 .into(), 365 radius, 366 fill_color, 367 egui::Stroke::new(1.0_f32, stroke_color), 368 ); 369 } 370 } 371}