diff --git a/src/app.rs b/src/app.rs index 1e521c2..cd63da4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -28,6 +28,18 @@ pub struct TemplateApp { #[serde(skip)] fps_ema: f32, + /// Whether we've auto-fitted the start viewport. + #[serde(skip)] + did_fit_start: bool, + + /// Last known cursor position (for edge scrolling even without movement). + #[serde(skip)] + last_cursor_pos: Option, + + /// Whether a reset-to-start was requested (from UI) + #[serde(skip)] + reset_view_requested: bool, + /// Last pointer position for manual drag tracking (smoother than `Sense::drag`). #[serde(skip)] last_pointer_pos: Option, @@ -55,6 +67,9 @@ impl Default for TemplateApp { last_pointer_pos: None, is_dragging: false, text_cache: None, + did_fit_start: false, + last_cursor_pos: None, + reset_view_requested: false, } } } @@ -180,9 +195,7 @@ impl eframe::App for TemplateApp { ui.label(format!("Zoom: {:.0}%", self.zoom * 100.0)); if ui.button("Reset View").clicked() { - self.pan_x = 0.0; - self.pan_y = 0.0; - self.zoom = 1.0; + self.reset_view_requested = true; } ui.separator(); @@ -214,6 +227,40 @@ impl eframe::App for TemplateApp { let canvas_rect = response.rect; painter.rect_filled(canvas_rect, 0.0, background_color); + if !self.did_fit_start { + self.reset_view_requested = true; + self.did_fit_start = true; + } + + let mut maybe_fit_view = |rect: &egui::Rect| { + if let Some(ref content) = self.svg_content { + if let Some((sx, sy, sw, sh)) = content.start_rect.or(content.viewbox) { + if sw > 0.0 && sh > 0.0 { + let scale_x = rect.width() / sw; + let scale_y = rect.height() / sh; + let fit_zoom = scale_x.min(scale_y).clamp(0.01, 100.0); + let visible_w = rect.width() / fit_zoom; + let visible_h = rect.height() / fit_zoom; + + self.zoom = fit_zoom; + self.pan_x = -sx + (visible_w - sw) * 0.5; + self.pan_y = -sy + (visible_h - sh) * 0.5; + return true; + } + } + } + false + }; + + if self.reset_view_requested { + if maybe_fit_view(&canvas_rect) { + self.reset_view_requested = false; + } else { + // Nothing to fit, still clear the flag to avoid loops + self.reset_view_requested = false; + } + } + // Drag handling if primary_pressed && response.hovered() { self.is_dragging = true; @@ -257,8 +304,7 @@ impl eframe::App for TemplateApp { let internal_alpha = (0.3_f32 * 255.0).round() as u8; let video_scroll_color = egui::Color32::from_rgba_unmultiplied(255, 0, 0, internal_alpha); - let audio_area_color = - egui::Color32::from_rgba_unmultiplied(0, 0, 255, internal_alpha); + let audio_area_color = egui::Color32::from_rgba_unmultiplied(0, 0, 255, internal_alpha); let anchor_color = egui::Color32::from_rgba_unmultiplied(64, 255, 64, internal_alpha); let text_color = egui::Color32::from_rgb(255, 255, 255); diff --git a/src/svg.rs b/src/svg.rs index 8a00006..d31eb16 100644 --- a/src/svg.rs +++ b/src/svg.rs @@ -1,7 +1,7 @@ //! SVG parsing module for extracting special elements from SVG files. -use quick_xml::Reader; use quick_xml::events::Event; +use quick_xml::Reader; use std::fs; /// Trait for elements that can be rendered with a bounding box. @@ -114,6 +114,7 @@ pub struct SvgContent { pub texts: Vec, pub viewbox: Option<(f32, f32, f32, f32)>, // (min_x, min_y, width, height) pub background_color: Option<[u8; 3]>, + pub start_rect: Option<(f32, f32, f32, f32)>, } /// State for tracking current element during parsing. @@ -287,6 +288,8 @@ impl SvgContent { width, height, }); + } else if id == "start" { + svg_content.start_rect = Some((x, y, width, height)); } } "text" => { @@ -364,6 +367,8 @@ impl SvgContent { width, height, }); + } else if id == "start" { + svg_content.start_rect = Some((x, y, width, height)); } } }