hex_patch/app/
cursor_position.rs

1use super::{data::Data, info_mode::InfoMode, log::NotificationLevel, pane::Pane, App};
2
3pub struct CursorPosition {
4    pub cursor: Option<(u16, u16)>,
5    pub local_x: usize,
6    pub local_byte_index: usize,
7    pub block_index: usize,
8    pub local_block_index: usize,
9    pub line_index: usize,
10    pub line_byte_index: usize,
11    pub global_byte_index: usize,
12    pub high_byte: bool,
13}
14
15impl CursorPosition {
16    pub fn get_high_byte_offset(&self) -> usize {
17        match &self.high_byte {
18            true => 0,
19            false => 1,
20        }
21    }
22}
23
24impl App {
25    pub(super) fn get_cursor_position_no_self(
26        data: &Data,
27        blocks_per_row: usize,
28        block_size: usize,
29        cursor: (u16, u16),
30        scroll: usize,
31    ) -> CursorPosition {
32        if data.is_empty() || blocks_per_row == 0 {
33            return CursorPosition {
34                cursor: Some((0, 0)),
35                local_x: 0,
36                local_byte_index: 0,
37                block_index: 0,
38                local_block_index: 0,
39                line_index: 0,
40                line_byte_index: 0,
41                global_byte_index: 0,
42                high_byte: false,
43            };
44        }
45        let local_x = cursor.0 as usize % (block_size * 3 + 1);
46        let high_byte = local_x % 3 == 0;
47        let local_byte_index = local_x / 3;
48        let block_index = cursor.0 as usize / (block_size * 3 + 1)
49            + (scroll + cursor.1 as usize) * blocks_per_row;
50        let local_block_index = block_index % blocks_per_row;
51        let line_index = block_index / blocks_per_row;
52        let line_byte_index = local_byte_index + block_size * local_block_index;
53        let global_byte_index = line_byte_index + line_index * block_size * blocks_per_row;
54
55        CursorPosition {
56            cursor: Some(cursor),
57            local_x,
58            local_byte_index,
59            block_index,
60            local_block_index,
61            line_index,
62            line_byte_index,
63            global_byte_index,
64            high_byte,
65        }
66    }
67    pub(super) fn get_cursor_position(&self) -> CursorPosition {
68        Self::get_cursor_position_no_self(
69            &self.data,
70            self.blocks_per_row,
71            self.block_size,
72            self.cursor,
73            self.scroll,
74        )
75    }
76
77    pub(super) fn get_expected_cursor_position_no_self(
78        global_byte_index: usize,
79        high_byte: bool,
80        block_size: usize,
81        blocks_per_row: usize,
82        screen_size: (u16, u16),
83        vertical_margin: u16,
84        scroll: usize,
85    ) -> CursorPosition {
86        let block_index = global_byte_index / block_size;
87        let line_index = block_index / blocks_per_row;
88        let local_block_index = block_index % blocks_per_row;
89        let local_byte_index = global_byte_index % block_size;
90        let local_x = (local_byte_index + local_block_index * block_size) * 3 + local_block_index;
91        let cursor_x = local_x as u16 + if high_byte { 0 } else { 1 };
92        let cursor_y = line_index as isize - scroll as isize;
93        let cursor =
94            if cursor_y < 0 || cursor_y >= screen_size.1 as isize - vertical_margin as isize {
95                None
96            } else {
97                Some((cursor_x, cursor_y as u16))
98            };
99
100        CursorPosition {
101            cursor,
102            local_x,
103            local_byte_index,
104            block_index,
105            local_block_index,
106            line_index,
107            line_byte_index: local_byte_index + local_block_index * block_size,
108            global_byte_index,
109            high_byte,
110        }
111    }
112
113    pub(super) fn get_expected_cursor_position(
114        &self,
115        global_byte_index: usize,
116        high_byte: bool,
117    ) -> CursorPosition {
118        Self::get_expected_cursor_position_no_self(
119            global_byte_index,
120            high_byte,
121            self.block_size,
122            self.blocks_per_row,
123            self.screen_size,
124            self.vertical_margin,
125            self.scroll,
126        )
127    }
128
129    pub(super) fn jump_to_fuzzy_symbol(
130        &mut self,
131        symbol: &str,
132        symbols: &[(u64, String)],
133        scroll: usize,
134    ) {
135        if symbol.is_empty() {
136            if let Some(symbols) = self.header.get_symbols() {
137                if let Some(symbol) = symbols.iter().nth(scroll) {
138                    let (address, name) = symbol;
139                    let log_message = format!("Jumping to symbol {} at {:#X}", name, address);
140                    self.jump_to(*address as usize, true);
141                    self.log(NotificationLevel::Debug, &log_message);
142                } else {
143                    unreachable!("The scroll should not be greater than the number of symbols")
144                }
145            } else {
146                self.log(NotificationLevel::Error, "No symbols found");
147            }
148            return;
149        } else if symbols.is_empty() {
150            self.log(
151                NotificationLevel::Error,
152                "No symbols matching the search pattern found",
153            );
154            return;
155        }
156
157        let mut find_iter = symbols.iter().skip(scroll);
158        if let Some(symbol) = find_iter.next() {
159            let (address, name) = symbol;
160            self.log(
161                NotificationLevel::Debug,
162                &format!("Jumping to symbol {} at {:#X}", name, address),
163            );
164            self.jump_to(*address as usize, true);
165        } else {
166            unreachable!("The scroll should not be greater than the number of symbols");
167        }
168    }
169
170    pub(super) fn jump_to_fuzzy_comment(
171        &mut self,
172        comment: &str,
173        comments: &[(u64, String)],
174        scroll: usize,
175    ) {
176        if comment.is_empty() {
177            if let Some(comment) = self.comments.iter().nth(scroll) {
178                let (address, _comment) = comment;
179                let log_message = format!("Jumping to comment at {:#X}", address);
180                self.jump_to(*address as usize, false);
181                self.log(NotificationLevel::Debug, &log_message);
182            } else {
183                unreachable!("The scroll should not be greater than the number of comments")
184            }
185            return;
186        } else if comments.is_empty() {
187            self.log(
188                NotificationLevel::Error,
189                "No comments matching the search pattern found",
190            );
191            return;
192        }
193
194        let mut find_iter = comments.iter().skip(scroll);
195        if let Some(comment) = find_iter.next() {
196            let (address, _comment) = comment;
197            self.log(
198                NotificationLevel::Debug,
199                &format!("Jumping to comment at {:#X}", address),
200            );
201            self.jump_to(*address as usize, false);
202        } else {
203            unreachable!("The scroll should not be greater than the number of comments");
204        }
205    }
206
207    pub(super) fn jump_to_symbol(&mut self, symbol: &str) {
208        if let Some(address) = symbol.strip_prefix("0x") {
209            if let Ok(address) = usize::from_str_radix(address, 16) {
210                self.log(
211                    NotificationLevel::Debug,
212                    &format!("Jumping to address: {:#X}", address),
213                );
214                self.jump_to(address, false);
215            } else {
216                self.log(
217                    NotificationLevel::Error,
218                    &format!("Invalid address: {}", symbol),
219                );
220            }
221        } else if let Some(address) = symbol.strip_prefix("v0x") {
222            if let Ok(address) = usize::from_str_radix(address, 16) {
223                self.log(
224                    NotificationLevel::Debug,
225                    &format!("Jumping to virtual address: {:#X}", address),
226                );
227                self.jump_to(address, true);
228            } else {
229                self.log(
230                    NotificationLevel::Error,
231                    &format!("Invalid virtual address: {}", symbol),
232                );
233            }
234        } else if let Some(address) = self.header.symbol_to_address(symbol) {
235            self.log(
236                NotificationLevel::Debug,
237                &format!("Jumping to symbol {} at {:#X}", symbol, address),
238            );
239            self.jump_to(address as usize, true);
240        } else if let Some(address) = self
241            .header
242            .get_sections()
243            .iter()
244            .find(|x| x.name == symbol)
245            .map(|x| x.file_offset)
246        {
247            self.log(
248                NotificationLevel::Debug,
249                &format!("Jumping to section {} at {:#X}", symbol, address),
250            );
251            self.jump_to(address as usize, false);
252        } else {
253            self.log(
254                NotificationLevel::Error,
255                &format!("Symbol not found: {}", symbol),
256            );
257        }
258    }
259
260    #[allow(clippy::too_many_arguments)]
261    pub(super) fn jump_to_no_self(
262        mut address: usize,
263        data: &Data,
264        screen_size: (u16, u16),
265        vertical_margin: u16,
266        scroll: &mut usize,
267        cursor: &mut (u16, u16),
268        block_size: usize,
269        blocks_per_row: usize,
270    ) {
271        if address >= data.len() {
272            address = data.len().saturating_sub(1);
273        }
274        if screen_size.1 <= vertical_margin {
275            *scroll = 0;
276            *cursor = (0, 0);
277            return;
278        }
279
280        let expected_cursor_position = Self::get_expected_cursor_position_no_self(
281            address,
282            false,
283            block_size,
284            blocks_per_row,
285            screen_size,
286            vertical_margin,
287            *scroll,
288        );
289        let CursorPosition {
290            local_x,
291            line_index,
292            ..
293        } = expected_cursor_position;
294        let y = line_index as isize - *scroll as isize;
295
296        if y < 0 {
297            *scroll = line_index;
298            *cursor = (local_x as u16, 0);
299        } else if y < screen_size.1 as isize - vertical_margin as isize {
300            *cursor = (local_x as u16, y as u16);
301        } else {
302            *scroll = line_index - (screen_size.1 - vertical_margin - 1) as usize;
303            *cursor = (local_x as u16, (screen_size.1 - vertical_margin - 1));
304        }
305    }
306
307    pub(super) fn jump_to(&mut self, mut address: usize, is_virtual: bool) {
308        if is_virtual {
309            if let Some(physical_address) = self.header.virtual_to_physical_address(address as u64)
310            {
311                address = physical_address as usize;
312            } else {
313                self.log(
314                    NotificationLevel::Error,
315                    &format!("Virtual address {:#X} not found", address),
316                );
317                return;
318            }
319        }
320        Self::jump_to_no_self(
321            address,
322            &self.data,
323            self.screen_size,
324            self.vertical_margin,
325            &mut self.scroll,
326            &mut self.cursor,
327            self.block_size,
328            self.blocks_per_row,
329        )
330    }
331
332    pub(super) fn move_cursor_in_selected_panel(&mut self, dx: isize, dy: isize) {
333        match self.selected_pane {
334            Pane::Hex => self.move_cursor(dx, dy, false),
335            Pane::View => match self.info_mode {
336                InfoMode::Text => self.move_cursor(dx * 2, dy, false),
337                InfoMode::Assembly => self.move_cursor_to_near_instruction(dy),
338            },
339        }
340    }
341
342    pub(super) fn move_cursor(&mut self, dx: isize, dy: isize, best_effort: bool) {
343        if self.screen_size.1 <= self.vertical_margin {
344            return;
345        }
346        let current_position = self.get_cursor_position();
347        let half_byte_delta =
348            dx + (dy * self.block_size as isize * self.blocks_per_row as isize * 2);
349        let half_byte_position =
350            current_position.global_byte_index * 2 + if current_position.high_byte { 0 } else { 1 };
351
352        let mut new_half_byte_position =
353            (half_byte_position as isize).saturating_add(half_byte_delta);
354        if !best_effort
355            && (new_half_byte_position < 0
356                || new_half_byte_position >= self.data.len() as isize * 2)
357        {
358            return;
359        } else if best_effort {
360            let max_half_byte_position = (self.data.len() as isize * 2 - 1).max(0);
361            new_half_byte_position = new_half_byte_position.clamp(0, max_half_byte_position);
362        }
363        let new_global_byte_index = new_half_byte_position as usize / 2;
364        let new_high_byte = new_half_byte_position % 2 == 0;
365
366        let new_selected_row = new_global_byte_index / (self.block_size * self.blocks_per_row);
367        let min_visible_row = self.scroll;
368        let max_visible_row =
369            self.scroll + (self.screen_size.1 - self.vertical_margin) as usize - 1;
370        let new_scroll = if new_selected_row < min_visible_row {
371            new_selected_row
372        } else if new_selected_row > max_visible_row {
373            new_selected_row - (self.screen_size.1 - self.vertical_margin) as usize + 1
374        } else {
375            self.scroll
376        };
377
378        self.scroll = new_scroll;
379
380        self.cursor = self
381            .get_expected_cursor_position(new_global_byte_index, new_high_byte)
382            .cursor
383            .expect("The scroll should be adequate for the cursor to be visible");
384    }
385
386    pub(super) fn move_cursor_page_up(&mut self) {
387        let screen_size_y = self.screen_size.1 as isize - self.vertical_margin as isize;
388        if screen_size_y > 0 {
389            self.move_cursor(0, -screen_size_y, true);
390        }
391    }
392
393    pub(super) fn move_cursor_page_down(&mut self) {
394        let screen_size_y = self.screen_size.1 as isize - self.vertical_margin as isize;
395        if screen_size_y > 0 {
396            self.move_cursor(0, screen_size_y, true);
397        }
398    }
399
400    pub(super) fn move_cursor_to_end(&mut self) {
401        let bytes = self.data.len();
402
403        self.move_cursor(bytes as isize * 2, 0, true);
404    }
405
406    pub(super) fn move_cursor_to_start(&mut self) {
407        let bytes = self.data.len();
408        self.move_cursor(bytes as isize * -2, 0, true);
409    }
410
411    pub(super) fn move_cursor_to_near_instruction(&mut self, instruction_count: isize) {
412        let current_offset = self.get_cursor_position().global_byte_index;
413        if current_offset >= self.assembly_offsets.len() {
414            return;
415        }
416        let current_instruction_index = self.assembly_offsets[current_offset];
417        let mut next_instruction_index = (current_instruction_index as isize + instruction_count)
418            .clamp(
419                0,
420                self.assembly_instructions.len().saturating_sub(1) as isize,
421            ) as usize;
422        while instruction_count < 0
423            && next_instruction_index != 0
424            && next_instruction_index != current_instruction_index
425            && self.assembly_instructions[next_instruction_index].file_address()
426                == self.assembly_instructions[current_instruction_index].file_address()
427        {
428            next_instruction_index = next_instruction_index.saturating_sub(1);
429        }
430
431        let target_address = self.assembly_instructions[next_instruction_index].file_address();
432        self.jump_to(target_address as usize, false);
433    }
434
435    pub(super) fn switch_selected_pane(&mut self) {
436        match self.selected_pane {
437            Pane::Hex => self.selected_pane = Pane::View,
438            Pane::View => self.selected_pane = Pane::Hex,
439        }
440    }
441}
442
443#[cfg(test)]
444mod test {
445    use super::*;
446    #[test]
447    fn test_move_cursor() {
448        let data = vec![0; 0x100];
449        let mut app = App::mockup(data);
450        app.resize_to_size(80, 24);
451
452        app.move_cursor(1, 0, false);
453        assert_eq!(app.cursor, (1, 0));
454        app.move_cursor(0, 1, false);
455        assert_eq!(app.cursor, (1, 1));
456
457        app.move_cursor(0, 0, false);
458        assert_eq!(app.cursor, (1, 1));
459
460        app.move_cursor(0, -1, false);
461        assert_eq!(app.cursor, (1, 0));
462        app.move_cursor(-1, 0, false);
463        assert_eq!(app.cursor, (0, 0));
464
465        app.move_cursor(0, -1, false);
466        assert_eq!(app.cursor, (0, 0));
467        app.move_cursor(-1, 0, false);
468        assert_eq!(app.cursor, (0, 0));
469
470        let current_position = app.get_cursor_position();
471        assert_eq!(current_position.global_byte_index, 0);
472        assert!(current_position.high_byte);
473
474        app.move_cursor(81, 0, false);
475        let current_position = app.get_cursor_position();
476        assert_eq!(current_position.global_byte_index, 40);
477        assert!(!current_position.high_byte);
478
479        app.move_cursor(-1, -1, false);
480        let bytes_per_line = app.block_size * app.blocks_per_row;
481        let current_position = app.get_cursor_position();
482        assert_eq!(current_position.global_byte_index, 40 - bytes_per_line);
483        assert!(current_position.high_byte);
484    }
485
486    #[test]
487    fn test_move_with_no_screen() {
488        let data = vec![0; 0x100];
489        let mut app = App::mockup(data);
490        app.resize_to_size(0, 0);
491
492        app.move_cursor(1, 0, false);
493        assert_eq!(app.cursor, (0, 0));
494        app.move_cursor(0, 1, false);
495        assert_eq!(app.cursor, (0, 0));
496        app.move_cursor(0, -1, false);
497        assert_eq!(app.cursor, (0, 0));
498        app.move_cursor(-1, 0, false);
499        assert_eq!(app.cursor, (0, 0));
500    }
501
502    #[test]
503    fn test_move_with_small_screen() {
504        let data = vec![0; 0x100];
505        let mut app = App::mockup(data);
506        app.resize_to_size(1, 1);
507
508        app.move_cursor(1, 0, false);
509        assert_eq!(app.cursor, (0, 0));
510        app.move_cursor(0, 1, false);
511        assert_eq!(app.cursor, (0, 0));
512        app.move_cursor(0, -1, false);
513        assert_eq!(app.cursor, (0, 0));
514        app.move_cursor(-1, 0, false);
515        assert_eq!(app.cursor, (0, 0));
516    }
517
518    #[test]
519    fn test_move_to_near_instruction() {
520        let data = vec![
521            0x90, 0x90, 0x90, 0x48, 0x89, 0xd8, 0xeb, 0xfe, 0x90, 0x90, 0x90,
522        ];
523        let mut app = App::mockup(data);
524        app.resize_to_size(80, 24);
525
526        app.move_cursor_to_near_instruction(1);
527        let current_position = app.get_cursor_position().global_byte_index;
528        assert_eq!(current_position, 1);
529
530        app.move_cursor_to_near_instruction(2);
531        let current_position = app.get_cursor_position().global_byte_index;
532        assert_eq!(current_position, 3);
533
534        app.move_cursor_to_near_instruction(-1);
535        let current_position = app.get_cursor_position().global_byte_index;
536        assert_eq!(current_position, 2);
537
538        app.move_cursor_to_near_instruction(4);
539        let current_position = app.get_cursor_position().global_byte_index;
540        assert_eq!(current_position, 9);
541
542        app.move_cursor_to_near_instruction(2);
543        let current_position = app.get_cursor_position().global_byte_index;
544        assert_eq!(current_position, 10);
545
546        app.move_cursor_to_near_instruction(100);
547        let current_position = app.get_cursor_position().global_byte_index;
548        assert_eq!(current_position, 10);
549
550        app.move_cursor_to_near_instruction(-100);
551        let current_position = app.get_cursor_position().global_byte_index;
552        assert_eq!(current_position, 0);
553    }
554}