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 =
140                        t!("app.messages.jump_to_symbol", symbol = name, address = address : {:#X});
141                    self.jump_to(*address as usize, true);
142                    self.log(NotificationLevel::Debug, log_message);
143                } else {
144                    unreachable!("The scroll should not be greater than the number of symbols")
145                }
146            } else {
147                self.log(NotificationLevel::Error, t!("errors.no_symbols_found"));
148            }
149            return;
150        } else if symbols.is_empty() {
151            self.log(NotificationLevel::Error, t!("errors.no_matching_symbols"));
152            return;
153        }
154
155        let mut find_iter = symbols.iter().skip(scroll);
156        if let Some(symbol) = find_iter.next() {
157            let (address, name) = symbol;
158            self.log(
159                NotificationLevel::Debug,
160                t!("app.messages.jump_to_symbol", symbol = name, address = address : {:#X}),
161            );
162            self.jump_to(*address as usize, true);
163        } else {
164            unreachable!("The scroll should not be greater than the number of symbols");
165        }
166    }
167
168    pub(super) fn jump_to_fuzzy_comment(
169        &mut self,
170        comment: &str,
171        comments: &[(u64, String)],
172        scroll: usize,
173    ) {
174        if comment.is_empty() {
175            if let Some(comment) = self.comments.iter().nth(scroll) {
176                let (address, _comment) = comment;
177                let log_message = t!("app.messages.jump_to_comment", address = address : {:#X});
178                self.jump_to(*address as usize, false);
179                self.log(NotificationLevel::Debug, log_message);
180            } else {
181                unreachable!("The scroll should not be greater than the number of comments")
182            }
183            return;
184        } else if comments.is_empty() {
185            self.log(NotificationLevel::Error, t!("errors.no_matching_comments"));
186            return;
187        }
188
189        let mut find_iter = comments.iter().skip(scroll);
190        if let Some(comment) = find_iter.next() {
191            let (address, _comment) = comment;
192            self.log(
193                NotificationLevel::Debug,
194                t!("app.messages.jump_to_comment", address = address : {:#X}),
195            );
196            self.jump_to(*address as usize, false);
197        } else {
198            unreachable!("The scroll should not be greater than the number of comments");
199        }
200    }
201
202    pub(super) fn jump_to_symbol(&mut self, symbol: &str) {
203        if let Some(address) = symbol.strip_prefix("0x") {
204            if let Ok(address) = usize::from_str_radix(address, 16) {
205                self.log(
206                    NotificationLevel::Debug,
207                    t!("app.messages.jump_to_address", address = address : {:#X}),
208                );
209                self.jump_to(address, false);
210            } else {
211                self.log(
212                    NotificationLevel::Error,
213                    t!("errors.invalid_address", address = symbol),
214                );
215            }
216        } else if let Some(address) = symbol.strip_prefix("v0x") {
217            if let Ok(address) = usize::from_str_radix(address, 16) {
218                self.log(
219                    NotificationLevel::Debug,
220                    t!("app.messages.jump_to_virtual_address", address = address : {:#X}),
221                );
222                self.jump_to(address, true);
223            } else {
224                self.log(
225                    NotificationLevel::Error,
226                    t!("errors.invalid_virtual_address", address = symbol),
227                );
228            }
229        } else if let Some(address) = self.header.symbol_to_address(symbol) {
230            self.log(
231                NotificationLevel::Debug,
232                t!("app.messages.jump_to_symbol", symbol = symbol, address = address : {:#X}),
233            );
234            self.jump_to(address as usize, true);
235        } else if let Some(address) = self
236            .header
237            .get_sections()
238            .iter()
239            .find(|x| x.name == symbol)
240            .map(|x| x.file_offset)
241        {
242            self.log(
243                NotificationLevel::Debug,
244                t!("app.messages.jump_to_section", section = symbol, address = address : {:#X}),
245            );
246            self.jump_to(address as usize, false);
247        } else {
248            self.log(
249                NotificationLevel::Error,
250                t!("errors.symbol_not_found", symbol = symbol),
251            );
252        }
253    }
254
255    #[allow(clippy::too_many_arguments)]
256    pub(super) fn jump_to_no_self(
257        mut address: usize,
258        data: &Data,
259        screen_size: (u16, u16),
260        vertical_margin: u16,
261        scroll: &mut usize,
262        cursor: &mut (u16, u16),
263        block_size: usize,
264        blocks_per_row: usize,
265    ) {
266        if address >= data.len() {
267            address = data.len().saturating_sub(1);
268        }
269        if screen_size.1 <= vertical_margin {
270            *scroll = 0;
271            *cursor = (0, 0);
272            return;
273        }
274
275        let expected_cursor_position = Self::get_expected_cursor_position_no_self(
276            address,
277            false,
278            block_size,
279            blocks_per_row,
280            screen_size,
281            vertical_margin,
282            *scroll,
283        );
284        let CursorPosition {
285            local_x,
286            line_index,
287            ..
288        } = expected_cursor_position;
289        let y = line_index as isize - *scroll as isize;
290
291        if y < 0 {
292            *scroll = line_index;
293            *cursor = (local_x as u16, 0);
294        } else if y < screen_size.1 as isize - vertical_margin as isize {
295            *cursor = (local_x as u16, y as u16);
296        } else {
297            *scroll = line_index - (screen_size.1 - vertical_margin - 1) as usize;
298            *cursor = (local_x as u16, (screen_size.1 - vertical_margin - 1));
299        }
300    }
301
302    pub(super) fn jump_to(&mut self, mut address: usize, is_virtual: bool) {
303        if is_virtual {
304            if let Some(physical_address) = self.header.virtual_to_physical_address(address as u64)
305            {
306                address = physical_address as usize;
307            } else {
308                self.log(
309                    NotificationLevel::Error,
310                    t!("errors.virtual_address_not_found", address = address : {:#X}),
311                );
312                return;
313            }
314        }
315        Self::jump_to_no_self(
316            address,
317            &self.data,
318            self.screen_size,
319            self.vertical_margin,
320            &mut self.scroll,
321            &mut self.cursor,
322            self.block_size,
323            self.blocks_per_row,
324        )
325    }
326
327    pub(super) fn move_cursor_in_selected_panel(&mut self, dx: isize, dy: isize) {
328        match self.selected_pane {
329            Pane::Hex => self.move_cursor(dx, dy, false),
330            Pane::View => match self.info_mode {
331                InfoMode::Text => self.move_cursor(dx * 2, dy, false),
332                InfoMode::Assembly => self.move_cursor_to_near_instruction(dy),
333            },
334        }
335    }
336
337    pub(super) fn move_cursor(&mut self, dx: isize, dy: isize, best_effort: bool) {
338        if self.screen_size.1 <= self.vertical_margin {
339            return;
340        }
341        let current_position = self.get_cursor_position();
342        let half_byte_delta =
343            dx + (dy * self.block_size as isize * self.blocks_per_row as isize * 2);
344        let half_byte_position =
345            current_position.global_byte_index * 2 + if current_position.high_byte { 0 } else { 1 };
346
347        let mut new_half_byte_position =
348            (half_byte_position as isize).saturating_add(half_byte_delta);
349        if !best_effort
350            && (new_half_byte_position < 0
351                || new_half_byte_position >= self.data.len() as isize * 2)
352        {
353            return;
354        } else if best_effort {
355            let max_half_byte_position = (self.data.len() as isize * 2 - 1).max(0);
356            new_half_byte_position = new_half_byte_position.clamp(0, max_half_byte_position);
357        }
358        let new_global_byte_index = new_half_byte_position as usize / 2;
359        let new_high_byte = new_half_byte_position % 2 == 0;
360
361        let new_selected_row = new_global_byte_index / (self.block_size * self.blocks_per_row);
362        let min_visible_row = self.scroll;
363        let max_visible_row =
364            self.scroll + (self.screen_size.1 - self.vertical_margin) as usize - 1;
365        let new_scroll = if new_selected_row < min_visible_row {
366            new_selected_row
367        } else if new_selected_row > max_visible_row {
368            new_selected_row - (self.screen_size.1 - self.vertical_margin) as usize + 1
369        } else {
370            self.scroll
371        };
372
373        self.scroll = new_scroll;
374
375        self.cursor = self
376            .get_expected_cursor_position(new_global_byte_index, new_high_byte)
377            .cursor
378            .expect(&t!("errors.cursor_position"));
379    }
380
381    pub(super) fn move_cursor_page_up(&mut self) {
382        let screen_size_y = self.screen_size.1 as isize - self.vertical_margin as isize;
383        if screen_size_y > 0 {
384            self.move_cursor(0, -screen_size_y, true);
385        }
386    }
387
388    pub(super) fn move_cursor_page_down(&mut self) {
389        let screen_size_y = self.screen_size.1 as isize - self.vertical_margin as isize;
390        if screen_size_y > 0 {
391            self.move_cursor(0, screen_size_y, true);
392        }
393    }
394
395    pub(super) fn move_cursor_to_end(&mut self) {
396        let bytes = self.data.len();
397
398        self.move_cursor(bytes as isize * 2, 0, true);
399    }
400
401    pub(super) fn move_cursor_to_start(&mut self) {
402        let bytes = self.data.len();
403        self.move_cursor(bytes as isize * -2, 0, true);
404    }
405
406    pub(super) fn move_cursor_to_near_instruction(&mut self, instruction_count: isize) {
407        let current_offset = self.get_cursor_position().global_byte_index;
408        if current_offset >= self.assembly_offsets.len() {
409            return;
410        }
411        let current_instruction_index = self.assembly_offsets[current_offset];
412        let mut next_instruction_index = (current_instruction_index as isize + instruction_count)
413            .clamp(
414                0,
415                self.assembly_instructions.len().saturating_sub(1) as isize,
416            ) as usize;
417        while instruction_count < 0
418            && next_instruction_index != 0
419            && next_instruction_index != current_instruction_index
420            && self.assembly_instructions[next_instruction_index].file_address()
421                == self.assembly_instructions[current_instruction_index].file_address()
422        {
423            next_instruction_index = next_instruction_index.saturating_sub(1);
424        }
425
426        let target_address = self.assembly_instructions[next_instruction_index].file_address();
427        self.jump_to(target_address as usize, false);
428    }
429
430    pub(super) fn switch_selected_pane(&mut self) {
431        match self.selected_pane {
432            Pane::Hex => self.selected_pane = Pane::View,
433            Pane::View => self.selected_pane = Pane::Hex,
434        }
435    }
436}
437
438#[cfg(test)]
439mod test {
440    use super::*;
441    #[test]
442    fn test_move_cursor() {
443        let data = vec![0; 0x100];
444        let mut app = App::mockup(data);
445        app.resize_to_size(80, 24);
446
447        app.move_cursor(1, 0, false);
448        assert_eq!(app.cursor, (1, 0));
449        app.move_cursor(0, 1, false);
450        assert_eq!(app.cursor, (1, 1));
451
452        app.move_cursor(0, 0, false);
453        assert_eq!(app.cursor, (1, 1));
454
455        app.move_cursor(0, -1, false);
456        assert_eq!(app.cursor, (1, 0));
457        app.move_cursor(-1, 0, false);
458        assert_eq!(app.cursor, (0, 0));
459
460        app.move_cursor(0, -1, false);
461        assert_eq!(app.cursor, (0, 0));
462        app.move_cursor(-1, 0, false);
463        assert_eq!(app.cursor, (0, 0));
464
465        let current_position = app.get_cursor_position();
466        assert_eq!(current_position.global_byte_index, 0);
467        assert!(current_position.high_byte);
468
469        app.move_cursor(81, 0, false);
470        let current_position = app.get_cursor_position();
471        assert_eq!(current_position.global_byte_index, 40);
472        assert!(!current_position.high_byte);
473
474        app.move_cursor(-1, -1, false);
475        let bytes_per_line = app.block_size * app.blocks_per_row;
476        let current_position = app.get_cursor_position();
477        assert_eq!(current_position.global_byte_index, 40 - bytes_per_line);
478        assert!(current_position.high_byte);
479    }
480
481    #[test]
482    fn test_move_with_no_screen() {
483        let data = vec![0; 0x100];
484        let mut app = App::mockup(data);
485        app.resize_to_size(0, 0);
486
487        app.move_cursor(1, 0, false);
488        assert_eq!(app.cursor, (0, 0));
489        app.move_cursor(0, 1, false);
490        assert_eq!(app.cursor, (0, 0));
491        app.move_cursor(0, -1, false);
492        assert_eq!(app.cursor, (0, 0));
493        app.move_cursor(-1, 0, false);
494        assert_eq!(app.cursor, (0, 0));
495    }
496
497    #[test]
498    fn test_move_with_small_screen() {
499        let data = vec![0; 0x100];
500        let mut app = App::mockup(data);
501        app.resize_to_size(1, 1);
502
503        app.move_cursor(1, 0, false);
504        assert_eq!(app.cursor, (0, 0));
505        app.move_cursor(0, 1, false);
506        assert_eq!(app.cursor, (0, 0));
507        app.move_cursor(0, -1, false);
508        assert_eq!(app.cursor, (0, 0));
509        app.move_cursor(-1, 0, false);
510        assert_eq!(app.cursor, (0, 0));
511    }
512
513    #[test]
514    fn test_move_to_near_instruction() {
515        let data = vec![
516            0x90, 0x90, 0x90, 0x48, 0x89, 0xd8, 0xeb, 0xfe, 0x90, 0x90, 0x90,
517        ];
518        let mut app = App::mockup(data);
519        app.resize_to_size(80, 24);
520
521        app.move_cursor_to_near_instruction(1);
522        let current_position = app.get_cursor_position().global_byte_index;
523        assert_eq!(current_position, 1);
524
525        app.move_cursor_to_near_instruction(2);
526        let current_position = app.get_cursor_position().global_byte_index;
527        assert_eq!(current_position, 3);
528
529        app.move_cursor_to_near_instruction(-1);
530        let current_position = app.get_cursor_position().global_byte_index;
531        assert_eq!(current_position, 2);
532
533        app.move_cursor_to_near_instruction(4);
534        let current_position = app.get_cursor_position().global_byte_index;
535        assert_eq!(current_position, 9);
536
537        app.move_cursor_to_near_instruction(2);
538        let current_position = app.get_cursor_position().global_byte_index;
539        assert_eq!(current_position, 10);
540
541        app.move_cursor_to_near_instruction(100);
542        let current_position = app.get_cursor_position().global_byte_index;
543        assert_eq!(current_position, 10);
544
545        app.move_cursor_to_near_instruction(-100);
546        let current_position = app.get_cursor_position().global_byte_index;
547        assert_eq!(current_position, 0);
548    }
549}