VIP inspection tooling #4
			
				
			
		
		
		
	| 
						 | 
					@ -1,8 +1,8 @@
 | 
				
			||||||
use std::{ops::Range, str::FromStr};
 | 
					use std::ops::{Bound, RangeBounds};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use egui::{
 | 
					use egui::{
 | 
				
			||||||
    ecolor::HexColor, Align, Color32, Frame, Layout, Response, RichText, Sense, TextEdit, Ui,
 | 
					    ecolor::HexColor, Align, Color32, CursorIcon, Frame, Layout, Margin, Rect, Response, RichText,
 | 
				
			||||||
    UiBuilder, Vec2, WidgetText,
 | 
					    Rounding, Sense, Shape, Stroke, TextEdit, Ui, UiBuilder, Vec2, Widget, WidgetText,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub trait UiExt {
 | 
					pub trait UiExt {
 | 
				
			||||||
| 
						 | 
					@ -24,13 +24,6 @@ pub trait UiExt {
 | 
				
			||||||
    fn color_pair_button(&mut self, left: Color32, right: Color32) -> Response;
 | 
					    fn color_pair_button(&mut self, left: Color32, right: Color32) -> Response;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fn color_picker(&mut self, color: &mut Color32, hex: &mut String) -> Response;
 | 
					    fn color_picker(&mut self, color: &mut Color32, hex: &mut String) -> Response;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn number_picker<T: FromStr + PartialOrd>(
 | 
					 | 
				
			||||||
        &mut self,
 | 
					 | 
				
			||||||
        text: &mut String,
 | 
					 | 
				
			||||||
        value: &mut T,
 | 
					 | 
				
			||||||
        range: Range<T>,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl UiExt for Ui {
 | 
					impl UiExt for Ui {
 | 
				
			||||||
| 
						 | 
					@ -90,18 +83,174 @@ impl UiExt for Ui {
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .inner
 | 
					        .inner
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fn number_picker<T: FromStr + PartialOrd>(
 | 
					pub struct NumberEdit<'a> {
 | 
				
			||||||
        &mut self,
 | 
					    value: &'a mut usize,
 | 
				
			||||||
        text: &mut String,
 | 
					    min: Option<usize>,
 | 
				
			||||||
        value: &mut T,
 | 
					    max: Option<usize>,
 | 
				
			||||||
        range: Range<T>,
 | 
					}
 | 
				
			||||||
    ) {
 | 
					
 | 
				
			||||||
        let res = self.add(TextEdit::singleline(text).horizontal_align(Align::Max));
 | 
					impl<'a> NumberEdit<'a> {
 | 
				
			||||||
        if res.changed() {
 | 
					    pub fn new(value: &'a mut usize) -> Self {
 | 
				
			||||||
            if let Some(new_value) = text.parse().ok().filter(|v| range.contains(v)) {
 | 
					        Self {
 | 
				
			||||||
                *value = new_value;
 | 
					            value,
 | 
				
			||||||
            }
 | 
					            min: None,
 | 
				
			||||||
 | 
					            max: None,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn range(self, range: impl RangeBounds<usize>) -> Self {
 | 
				
			||||||
 | 
					        let min = match range.start_bound() {
 | 
				
			||||||
 | 
					            Bound::Unbounded => None,
 | 
				
			||||||
 | 
					            Bound::Included(t) => Some(*t),
 | 
				
			||||||
 | 
					            Bound::Excluded(t) => t.checked_add(1),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let max = match range.end_bound() {
 | 
				
			||||||
 | 
					            Bound::Unbounded => None,
 | 
				
			||||||
 | 
					            Bound::Included(t) => Some(*t),
 | 
				
			||||||
 | 
					            Bound::Excluded(t) => t.checked_sub(1),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        Self { min, max, ..self }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Widget for NumberEdit<'_> {
 | 
				
			||||||
 | 
					    fn ui(self, ui: &mut Ui) -> Response {
 | 
				
			||||||
 | 
					        let (last_value, mut str) = ui.memory(|m| {
 | 
				
			||||||
 | 
					            m.data
 | 
				
			||||||
 | 
					                .get_temp(ui.id())
 | 
				
			||||||
 | 
					                .unwrap_or((*self.value, self.value.to_string()))
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        let mut stale = false;
 | 
				
			||||||
 | 
					        if *self.value != last_value {
 | 
				
			||||||
 | 
					            str = self.value.to_string();
 | 
				
			||||||
 | 
					            stale = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        let valid = str.parse().is_ok_and(|v: usize| v == *self.value);
 | 
				
			||||||
 | 
					        let text = TextEdit::singleline(&mut str)
 | 
				
			||||||
 | 
					            .horizontal_align(Align::Max)
 | 
				
			||||||
 | 
					            .margin(Margin {
 | 
				
			||||||
 | 
					                left: 4.0,
 | 
				
			||||||
 | 
					                right: 20.0,
 | 
				
			||||||
 | 
					                top: 2.0,
 | 
				
			||||||
 | 
					                bottom: 2.0,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        let res = if valid {
 | 
				
			||||||
 | 
					            ui.add(text)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            let message = match (self.min, self.max) {
 | 
				
			||||||
 | 
					                (Some(min), Some(max)) => format!("Please enter a number between {min} and {max}."),
 | 
				
			||||||
 | 
					                (Some(min), None) => format!("Please enter a number greater than {min}."),
 | 
				
			||||||
 | 
					                (None, Some(max)) => format!("Please enter a number less than {max}."),
 | 
				
			||||||
 | 
					                (None, None) => "Please enter a number.".to_string(),
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            ui.scope(|ui| {
 | 
				
			||||||
 | 
					                let style = ui.style_mut();
 | 
				
			||||||
 | 
					                style.visuals.selection.stroke.color = style.visuals.error_fg_color;
 | 
				
			||||||
 | 
					                style.visuals.widgets.hovered.bg_stroke.color =
 | 
				
			||||||
 | 
					                    style.visuals.error_fg_color.gamma_multiply(0.60);
 | 
				
			||||||
 | 
					                ui.add(text)
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .inner
 | 
				
			||||||
 | 
					            .on_hover_text(message)
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let arrow_left = res.rect.max.x + 4.0;
 | 
				
			||||||
 | 
					        let arrow_right = res.rect.max.x + 20.0;
 | 
				
			||||||
 | 
					        let arrow_top = res.rect.min.y - 2.0;
 | 
				
			||||||
 | 
					        let arrow_middle = (res.rect.min.y + res.rect.max.y) / 2.0;
 | 
				
			||||||
 | 
					        let arrow_bottom = res.rect.max.y + 2.0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut delta = 0;
 | 
				
			||||||
 | 
					        let top_arrow_rect = Rect {
 | 
				
			||||||
 | 
					            min: (arrow_left, arrow_top).into(),
 | 
				
			||||||
 | 
					            max: (arrow_right, arrow_middle).into(),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        if draw_arrow(ui, top_arrow_rect, true).clicked_or_dragged() {
 | 
				
			||||||
 | 
					            delta = 1;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        let bottom_arrow_rect = Rect {
 | 
				
			||||||
 | 
					            min: (arrow_left, arrow_middle).into(),
 | 
				
			||||||
 | 
					            max: (arrow_right, arrow_bottom).into(),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        if draw_arrow(ui, bottom_arrow_rect, false).clicked_or_dragged() {
 | 
				
			||||||
 | 
					            delta = -1;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let in_range =
 | 
				
			||||||
 | 
					            |&val: &usize| self.min.is_none_or(|m| m <= val) && self.max.is_none_or(|m| m >= val);
 | 
				
			||||||
 | 
					        if delta != 0 {
 | 
				
			||||||
 | 
					            if let Some(new_value) = self.value.checked_add_signed(delta).filter(in_range) {
 | 
				
			||||||
 | 
					                *self.value = new_value;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            str = self.value.to_string();
 | 
				
			||||||
 | 
					            stale = true;
 | 
				
			||||||
 | 
					        } else if res.changed {
 | 
				
			||||||
 | 
					            if let Some(new_value) = str.parse().ok().filter(in_range) {
 | 
				
			||||||
 | 
					                *self.value = new_value;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            stale = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if stale {
 | 
				
			||||||
 | 
					            ui.memory_mut(|m| m.data.insert_temp(ui.id(), (*self.value, str)));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        res
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn draw_arrow(ui: &mut Ui, rect: Rect, up: bool) -> Response {
 | 
				
			||||||
 | 
					    let arrow_res = ui
 | 
				
			||||||
 | 
					        .allocate_rect(rect, Sense::click_and_drag())
 | 
				
			||||||
 | 
					        .on_hover_cursor(CursorIcon::Default);
 | 
				
			||||||
 | 
					    let visuals = ui.style().visuals.widgets.style(&arrow_res);
 | 
				
			||||||
 | 
					    let painter = ui.painter_at(arrow_res.rect);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let rounding = if up {
 | 
				
			||||||
 | 
					        Rounding {
 | 
				
			||||||
 | 
					            ne: 2.0,
 | 
				
			||||||
 | 
					            ..Rounding::ZERO
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        Rounding {
 | 
				
			||||||
 | 
					            se: 2.0,
 | 
				
			||||||
 | 
					            ..Rounding::ZERO
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    painter.rect_filled(arrow_res.rect, rounding, visuals.bg_fill);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let left = rect.left() + 4.0;
 | 
				
			||||||
 | 
					    let center = (rect.left() + rect.right()) / 2.0;
 | 
				
			||||||
 | 
					    let right = rect.right() - 4.0;
 | 
				
			||||||
 | 
					    let top = rect.top() + 3.0;
 | 
				
			||||||
 | 
					    let bottom = rect.bottom() - 3.0;
 | 
				
			||||||
 | 
					    let points = if up {
 | 
				
			||||||
 | 
					        vec![
 | 
				
			||||||
 | 
					            (left, bottom).into(),
 | 
				
			||||||
 | 
					            (center, top).into(),
 | 
				
			||||||
 | 
					            (right, bottom).into(),
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        vec![
 | 
				
			||||||
 | 
					            (right, top).into(),
 | 
				
			||||||
 | 
					            (center, bottom).into(),
 | 
				
			||||||
 | 
					            (left, top).into(),
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    painter.add(Shape::convex_polygon(
 | 
				
			||||||
 | 
					        points,
 | 
				
			||||||
 | 
					        visuals.fg_stroke.color,
 | 
				
			||||||
 | 
					        Stroke::NONE,
 | 
				
			||||||
 | 
					    ));
 | 
				
			||||||
 | 
					    arrow_res
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					trait ResponseExt {
 | 
				
			||||||
 | 
					    fn clicked_or_dragged(&self) -> bool;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ResponseExt for Response {
 | 
				
			||||||
 | 
					    fn clicked_or_dragged(&self) -> bool {
 | 
				
			||||||
 | 
					        self.clicked() || self.dragged()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,10 @@ use crate::{
 | 
				
			||||||
    emulator::SimId,
 | 
					    emulator::SimId,
 | 
				
			||||||
    memory::{MemoryMonitor, MemoryView},
 | 
					    memory::{MemoryMonitor, MemoryView},
 | 
				
			||||||
    vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader},
 | 
					    vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader},
 | 
				
			||||||
    window::{utils::UiExt, AppWindow},
 | 
					    window::{
 | 
				
			||||||
 | 
					        utils::{NumberEdit, UiExt},
 | 
				
			||||||
 | 
					        AppWindow,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use super::utils::{parse_palette, CharacterGrid, GENERIC_PALETTE};
 | 
					use super::utils::{parse_palette, CharacterGrid, GENERIC_PALETTE};
 | 
				
			||||||
| 
						 | 
					@ -20,7 +23,6 @@ pub struct BgMapWindow {
 | 
				
			||||||
    loader: Arc<VramTextureLoader>,
 | 
					    loader: Arc<VramTextureLoader>,
 | 
				
			||||||
    bgmaps: MemoryView,
 | 
					    bgmaps: MemoryView,
 | 
				
			||||||
    cell_index: usize,
 | 
					    cell_index: usize,
 | 
				
			||||||
    cell_index_str: String,
 | 
					 | 
				
			||||||
    generic_palette: bool,
 | 
					    generic_palette: bool,
 | 
				
			||||||
    params: VramParams<BgMapParams>,
 | 
					    params: VramParams<BgMapParams>,
 | 
				
			||||||
    scale: f32,
 | 
					    scale: f32,
 | 
				
			||||||
| 
						 | 
					@ -38,7 +40,6 @@ impl BgMapWindow {
 | 
				
			||||||
            loader: Arc::new(loader),
 | 
					            loader: Arc::new(loader),
 | 
				
			||||||
            bgmaps: memory.view(sim_id, 0x00020000, 0x1d800),
 | 
					            bgmaps: memory.view(sim_id, 0x00020000, 0x1d800),
 | 
				
			||||||
            cell_index: 0,
 | 
					            cell_index: 0,
 | 
				
			||||||
            cell_index_str: "0".into(),
 | 
					 | 
				
			||||||
            generic_palette: false,
 | 
					            generic_palette: false,
 | 
				
			||||||
            params,
 | 
					            params,
 | 
				
			||||||
            scale: 1.0,
 | 
					            scale: 1.0,
 | 
				
			||||||
| 
						 | 
					@ -59,11 +60,9 @@ impl BgMapWindow {
 | 
				
			||||||
                        });
 | 
					                        });
 | 
				
			||||||
                        row.col(|ui| {
 | 
					                        row.col(|ui| {
 | 
				
			||||||
                            let mut bgmap_index = self.cell_index / 4096;
 | 
					                            let mut bgmap_index = self.cell_index / 4096;
 | 
				
			||||||
                            let mut bgmap_index_str = bgmap_index.to_string();
 | 
					                            ui.add(NumberEdit::new(&mut bgmap_index).range(0..14));
 | 
				
			||||||
                            ui.number_picker(&mut bgmap_index_str, &mut bgmap_index, 0..14);
 | 
					 | 
				
			||||||
                            if bgmap_index != self.cell_index / 4096 {
 | 
					                            if bgmap_index != self.cell_index / 4096 {
 | 
				
			||||||
                                self.cell_index = (bgmap_index * 4096) + (self.cell_index % 4096);
 | 
					                                self.cell_index = (bgmap_index * 4096) + (self.cell_index % 4096);
 | 
				
			||||||
                                self.cell_index_str = self.cell_index.to_string();
 | 
					 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        });
 | 
					                        });
 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
| 
						 | 
					@ -72,11 +71,7 @@ impl BgMapWindow {
 | 
				
			||||||
                            ui.label("Cell");
 | 
					                            ui.label("Cell");
 | 
				
			||||||
                        });
 | 
					                        });
 | 
				
			||||||
                        row.col(|ui| {
 | 
					                        row.col(|ui| {
 | 
				
			||||||
                            ui.number_picker(
 | 
					                            ui.add(NumberEdit::new(&mut self.cell_index).range(0..16 * 4096));
 | 
				
			||||||
                                &mut self.cell_index_str,
 | 
					 | 
				
			||||||
                                &mut self.cell_index,
 | 
					 | 
				
			||||||
                                0..16 * 4096,
 | 
					 | 
				
			||||||
                            );
 | 
					 | 
				
			||||||
                        });
 | 
					                        });
 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
                    body.row(row_height, |mut row| {
 | 
					                    body.row(row_height, |mut row| {
 | 
				
			||||||
| 
						 | 
					@ -168,7 +163,6 @@ impl BgMapWindow {
 | 
				
			||||||
            .with_selected(self.cell_index % 4096);
 | 
					            .with_selected(self.cell_index % 4096);
 | 
				
			||||||
        if let Some(selected) = grid.show(ui) {
 | 
					        if let Some(selected) = grid.show(ui) {
 | 
				
			||||||
            self.cell_index = (self.cell_index / 4096 * 4096) + selected;
 | 
					            self.cell_index = (self.cell_index / 4096 * 4096) + selected;
 | 
				
			||||||
            self.cell_index_str = self.cell_index.to_string();
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,10 @@ use crate::{
 | 
				
			||||||
    emulator::SimId,
 | 
					    emulator::SimId,
 | 
				
			||||||
    memory::{MemoryMonitor, MemoryView},
 | 
					    memory::{MemoryMonitor, MemoryView},
 | 
				
			||||||
    vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader},
 | 
					    vram::{VramImage, VramParams, VramProcessor, VramRenderer, VramTextureLoader},
 | 
				
			||||||
    window::{utils::UiExt as _, AppWindow},
 | 
					    window::{
 | 
				
			||||||
 | 
					        utils::{NumberEdit, UiExt as _},
 | 
				
			||||||
 | 
					        AppWindow,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use super::utils::{self, CharacterGrid};
 | 
					use super::utils::{self, CharacterGrid};
 | 
				
			||||||
| 
						 | 
					@ -83,7 +86,6 @@ pub struct CharacterDataWindow {
 | 
				
			||||||
    palettes: MemoryView,
 | 
					    palettes: MemoryView,
 | 
				
			||||||
    palette: VramPalette,
 | 
					    palette: VramPalette,
 | 
				
			||||||
    index: usize,
 | 
					    index: usize,
 | 
				
			||||||
    index_str: String,
 | 
					 | 
				
			||||||
    params: VramParams<CharDataParams>,
 | 
					    params: VramParams<CharDataParams>,
 | 
				
			||||||
    scale: f32,
 | 
					    scale: f32,
 | 
				
			||||||
    show_grid: bool,
 | 
					    show_grid: bool,
 | 
				
			||||||
| 
						 | 
					@ -104,7 +106,6 @@ impl CharacterDataWindow {
 | 
				
			||||||
            palettes: memory.view(sim_id, 0x0005f860, 16),
 | 
					            palettes: memory.view(sim_id, 0x0005f860, 16),
 | 
				
			||||||
            palette: params.palette,
 | 
					            palette: params.palette,
 | 
				
			||||||
            index: params.index,
 | 
					            index: params.index,
 | 
				
			||||||
            index_str: params.index.to_string(),
 | 
					 | 
				
			||||||
            params,
 | 
					            params,
 | 
				
			||||||
            scale: 4.0,
 | 
					            scale: 4.0,
 | 
				
			||||||
            show_grid: true,
 | 
					            show_grid: true,
 | 
				
			||||||
| 
						 | 
					@ -123,7 +124,7 @@ impl CharacterDataWindow {
 | 
				
			||||||
                            ui.label("Index");
 | 
					                            ui.label("Index");
 | 
				
			||||||
                        });
 | 
					                        });
 | 
				
			||||||
                        row.col(|ui| {
 | 
					                        row.col(|ui| {
 | 
				
			||||||
                            ui.number_picker(&mut self.index_str, &mut self.index, 0..2048);
 | 
					                            ui.add(NumberEdit::new(&mut self.index).range(0..2048));
 | 
				
			||||||
                        });
 | 
					                        });
 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
                    body.row(row_height, |mut row| {
 | 
					                    body.row(row_height, |mut row| {
 | 
				
			||||||
| 
						 | 
					@ -236,7 +237,6 @@ impl CharacterDataWindow {
 | 
				
			||||||
            .with_selected(self.index);
 | 
					            .with_selected(self.index);
 | 
				
			||||||
        if let Some(selected) = grid.show(ui) {
 | 
					        if let Some(selected) = grid.show(ui) {
 | 
				
			||||||
            self.index = selected;
 | 
					            self.index = selected;
 | 
				
			||||||
            self.index_str = selected.to_string();
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue