| use anyhow::anyhow; |
| use egui::{pos2, vec2, Align2, Color32, PointerButton, Rect, Rounding, Shape, Stroke, TextStyle, Ui, Vec2}; |
| |
| use crate::app_state::AppState; |
| use crate::views::RenderView; |
| use crate::views::ScreenBounds; |
| use crate::views::{MIN_BLOCK_HEIGHT_FOR_TEXT, MIN_BLOCK_WIDTH_FOR_TEXT}; |
| |
| pub const MIN_BLOCK_SIZE_FOR_GRID: f32 = 4.0; |
| pub struct DetailedPixelViewer; |
| |
| impl RenderView for DetailedPixelViewer { |
| fn title(&self) -> String { |
| "Pixel View".into() |
| } |
| |
| fn render(&self, ui: &mut Ui, state: &mut AppState) -> anyhow::Result<()> { |
| let Some(stream) = &state.stream else { |
| return Ok(()); |
| }; |
| |
| let Some(frame) = stream.current_frame() else { |
| return Ok(()); |
| }; |
| |
| let Some(selected_object) = &state.settings.selected_object else { |
| return Ok(()); |
| }; |
| |
| let plane = state.settings.sharable.current_plane.to_plane(); |
| let Ok(pixel_data) = stream.pixel_data.get_or_create_pixels( |
| frame, |
| plane, |
| state.settings.sharable.view_mode.view_settings().pixel_type, |
| ) else { |
| return Ok(()); |
| }; |
| let mut object_rect = selected_object.rect(frame).ok_or(anyhow!("Invalid selected object"))?; |
| if plane.is_chroma() { |
| object_rect = object_rect / 2.0; |
| } |
| |
| let size = ui.available_size_before_wrap(); |
| let (screen_bounds, response) = ui.allocate_exact_size(size, egui::Sense::drag()); |
| let mut world_bounds = state.settings.sharable.pixel_viewer_bounds; |
| let scale = world_bounds.calc_scale(screen_bounds); |
| let mut hover_pos_world = None; |
| |
| if response.dragged_by(PointerButton::Primary) { |
| let delta = response.drag_delta() / -scale; |
| world_bounds = world_bounds.translate(delta); |
| } else if response.hover_pos().is_some() && ui.input(|i| i.scroll_delta) != Vec2::ZERO { |
| let delta = ui.input(|i| i.scroll_delta); |
| |
| if let Some(mouse_pos) = ui.input(|i| i.pointer.hover_pos()) { |
| if screen_bounds.contains(mouse_pos) { |
| let zoom = if delta.y < 0.0 { |
| f32::powf(1.001, -delta.y) |
| } else { |
| 1.0 / (f32::powf(1.001, delta.y)) |
| }; |
| let zoom_center = world_bounds.screen_pos_to_world(mouse_pos, screen_bounds); |
| world_bounds.zoom_point(zoom_center, zoom); |
| } |
| } |
| } else if response.hover_pos().is_some() { |
| if let Some(mouse_pos) = ui.input(|i| i.pointer.hover_pos()) { |
| hover_pos_world = Some(world_bounds.screen_pos_to_world(mouse_pos, screen_bounds)); |
| } |
| } |
| state.settings.sharable.pixel_viewer_bounds = world_bounds; |
| |
| let painter = ui.painter().with_clip_rect(response.rect); |
| let clip_rect = painter.clip_rect(); |
| let mut shapes = Vec::new(); |
| let ui_style = ui.ctx().style(); |
| let mut hover_text = None; |
| for y in 0..object_rect.height() as usize { |
| for x in 0..object_rect.width() as usize { |
| let stride = pixel_data.width as usize; |
| let offset_x = object_rect.left_top().x as usize; |
| let offset_y = object_rect.left_top().y as usize; |
| let abs_x = x + offset_x; |
| let abs_y = y + offset_y; |
| let in_bounds = abs_x < pixel_data.width as usize && abs_y < pixel_data.height as usize; |
| let index = (y + offset_y) * stride + x + offset_x; |
| let pixel = if in_bounds { |
| pixel_data.pixels.get(index).copied() |
| } else { |
| None |
| }; |
| |
| let mut color = pixel.unwrap_or(0); |
| let pixel_max = 1 << pixel_data.bit_depth; |
| if pixel_data.pixel_type.is_delta() { |
| color = (color + pixel_max - 1) / 2; |
| } |
| if pixel_data.bit_depth > 8 { |
| color >>= pixel_data.bit_depth - 8; |
| } |
| let world_rect = Rect::from_min_size(pos2(x as f32, y as f32), vec2(1.0, 1.0)); |
| let screen_rect = world_bounds.world_rect_to_screen(world_rect, clip_rect); |
| if let Some(hover_pos_world) = hover_pos_world { |
| if world_rect.contains(hover_pos_world) { |
| if let Some(pixel) = pixel { |
| hover_text = Some(format!( |
| "{}, Relative: (x={}, y={}), Absolute: (x={}, y={})", |
| pixel, |
| x, |
| y, |
| x + offset_x, |
| y + offset_y |
| )); |
| } |
| } |
| } |
| |
| shapes.push(Shape::rect_filled( |
| screen_rect, |
| Rounding::ZERO, |
| Color32::from_gray(color as u8), |
| )); |
| if screen_rect.width() >= MIN_BLOCK_SIZE_FOR_GRID { |
| shapes.push(Shape::rect_stroke( |
| screen_rect, |
| Rounding::ZERO, |
| Stroke::new(1.0, Color32::from_gray(20)), |
| )); |
| } |
| |
| if let Some(pixel) = pixel { |
| if screen_rect.height() >= MIN_BLOCK_HEIGHT_FOR_TEXT |
| && screen_rect.width() >= MIN_BLOCK_WIDTH_FOR_TEXT |
| && clip_rect.intersects(screen_rect) |
| { |
| let overlay_style = &state.settings.persistent.style.overlay; |
| shapes.extend(painter.fonts(|f| { |
| let colors = if overlay_style.enable_text_shadows { |
| vec![Color32::BLACK, overlay_style.pixel_viewer_text_color] |
| } else { |
| vec![overlay_style.pixel_viewer_text_color] |
| }; |
| colors |
| .into_iter() |
| .enumerate() |
| .map(|(i, color)| { |
| let pos_offset = vec2(i as f32, i as f32); |
| Shape::text( |
| f, |
| screen_rect.center() + pos_offset, |
| Align2::CENTER_CENTER, |
| format!("{pixel}"), |
| TextStyle::Body.resolve(&ui_style), |
| color, |
| ) |
| }) |
| .collect::<Vec<_>>() |
| })); |
| } |
| } |
| } |
| } |
| painter.extend(shapes); |
| if let Some(hover_text) = hover_text { |
| response.on_hover_text_at_pointer(hover_text); |
| } |
| Ok(()) |
| } |
| } |