use eframe::egui;
use log::{info, trace};
use super::coords;
const SCRUB_NORMAL: (f32, f32, f32, f32) = (1.0, 1.0, 1.0, 0.5);
const SCRUB_OUTSIDE: (f32, f32, f32, f32) = (0.75, 0.0, 0.0, 0.5);
const ZOOM_STEP: f32 = 0.025;
const ZOOM_IN_FACTOR: f32 = 1.0 + ZOOM_STEP;
const ZOOM_OUT_FACTOR: f32 = 1.0 / ZOOM_IN_FACTOR;
pub fn fit(value: f32, old_min: f32, old_max: f32, new_min: f32, new_max: f32) -> f32 {
if (old_max - old_min).abs() < f32::EPSILON {
return new_min;
}
let t = (value - old_min) / (old_max - old_min);
new_min + t * (new_max - new_min)
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum ViewportMode {
Manual,
AutoFit,
Auto100,
}
#[derive(Clone, serde::Deserialize, serde::Serialize)]
pub struct ViewportState {
pub zoom: f32,
pub pan: egui::Vec2,
pub mode: ViewportMode,
#[serde(skip)]
pub image_size: egui::Vec2,
#[serde(skip)]
pub viewport_size: egui::Vec2,
#[serde(skip)]
pub scrubber: ViewportScrubber,
#[serde(skip)]
pub rmb_tool_drag_active: bool,
#[serde(skip)]
pub last_rendered_epoch: u64,
#[serde(skip)]
pub last_rendered_frame: Option<i32>,
}
#[derive(Clone, Copy)]
pub struct ViewportRenderState {
pub model_matrix: [[f32; 4]; 4],
pub view_matrix: [[f32; 4]; 4],
pub projection_matrix: [[f32; 4]; 4],
}
impl Default for ViewportState {
fn default() -> Self {
Self {
zoom: 1.0,
pan: egui::Vec2::ZERO,
mode: ViewportMode::AutoFit,
image_size: egui::Vec2::new(1920.0, 1080.0),
viewport_size: egui::Vec2::new(1920.0, 1080.0),
scrubber: ViewportScrubber::new(),
rmb_tool_drag_active: false,
last_rendered_epoch: 0,
last_rendered_frame: None,
}
}
}
impl ViewportState {
pub fn new() -> Self {
Self::default()
}
pub fn reset(&mut self) {
self.zoom = 1.0;
self.pan = egui::Vec2::ZERO;
self.mode = ViewportMode::AutoFit;
}
pub fn request_refresh(&mut self) {
self.last_rendered_epoch = 0;
self.last_rendered_frame = None;
}
pub fn draw(&self, ui: &egui::Ui, panel_rect: egui::Rect) {
self.scrubber.draw(ui, panel_rect);
}
pub fn set_viewport_size(&mut self, size: egui::Vec2) {
self.viewport_size = size;
if self.mode == ViewportMode::AutoFit {
self.apply_fit();
}
}
pub fn set_image_size(&mut self, size: egui::Vec2) {
self.image_size = size;
if self.mode == ViewportMode::AutoFit {
self.apply_fit();
}
}
pub fn set_mode_fit(&mut self) {
info!("Viewport mode: AutoFit");
self.mode = ViewportMode::AutoFit;
self.apply_fit();
}
pub fn set_mode_100(&mut self) {
info!("Viewport mode: Auto100");
self.mode = ViewportMode::Auto100;
self.apply_100();
}
fn apply_fit(&mut self) {
if self.image_size.x <= 0.0 || self.image_size.y <= 0.0 {
return;
}
let scale_x = self.viewport_size.x / self.image_size.x;
let scale_y = self.viewport_size.y / self.image_size.y;
self.zoom = scale_x.min(scale_y);
self.pan = egui::Vec2::ZERO;
}
fn apply_100(&mut self) {
self.zoom = 1.0;
self.pan = egui::Vec2::ZERO;
}
pub fn handle_zoom(&mut self, zoom_delta: f32, cursor_pos: egui::Vec2) {
if zoom_delta.abs() < 0.001 {
return;
}
self.mode = ViewportMode::Manual;
let old_zoom = self.zoom;
let zoom_factor = if zoom_delta > 0.0 {
ZOOM_IN_FACTOR
} else {
ZOOM_OUT_FACTOR
};
self.zoom = (self.zoom * zoom_factor).clamp(0.01, 100.0);
let zoom_ratio = self.zoom / old_zoom;
let cursor_to_center = coords::screen_to_viewport_centered(cursor_pos, self.viewport_size);
self.pan = cursor_to_center - (cursor_to_center - self.pan) * zoom_ratio;
trace!(
"Zoom: {:.2}x, Pan: ({:.1}, {:.1})",
self.zoom, self.pan.x, self.pan.y
);
}
pub fn handle_pan(&mut self, delta: egui::Vec2) {
self.mode = ViewportMode::Manual;
self.pan += coords::screen_delta_to_viewport(delta);
trace!("Pan: ({:.1}, {:.1})", self.pan.x, self.pan.y);
}
pub fn get_image_screen_bounds(&self) -> egui::Rect {
let min = self.image_to_screen(egui::vec2(0.0, 0.0));
let max = self.image_to_screen(self.image_size);
egui::Rect::from_min_max(min.to_pos2(), max.to_pos2())
}
#[allow(dead_code)]
pub fn is_point_over_image(&self, screen_pos: egui::Vec2) -> bool {
self.screen_to_image(screen_pos).is_some()
}
pub fn image_to_screen(&self, image_pos: egui::Vec2) -> egui::Vec2 {
let frame = egui::vec2(
image_pos.x - self.image_size.x * 0.5,
image_pos.y - self.image_size.y * 0.5,
);
let viewport = egui::vec2(
frame.x * self.zoom + self.pan.x,
frame.y * self.zoom + self.pan.y,
);
egui::vec2(
viewport.x + self.viewport_size.x * 0.5,
viewport.y + self.viewport_size.y * 0.5,
)
}
#[allow(dead_code)]
pub fn screen_to_image(&self, screen_pos: egui::Vec2) -> Option<egui::Vec2> {
let viewport = egui::vec2(
screen_pos.x - self.viewport_size.x * 0.5,
screen_pos.y - self.viewport_size.y * 0.5,
);
let frame = egui::vec2(
(viewport.x - self.pan.x) / self.zoom,
(viewport.y - self.pan.y) / self.zoom,
);
let image = egui::vec2(
frame.x + self.image_size.x * 0.5,
frame.y + self.image_size.y * 0.5,
);
if image.x >= 0.0
&& image.x <= self.image_size.x
&& image.y >= 0.0
&& image.y <= self.image_size.y
{
Some(image)
} else {
None
}
}
pub fn handle_scrubbing(
&mut self,
response: &egui::Response,
panel_rect: egui::Rect,
double_clicked: bool,
play_start: i32,
play_end: i32,
) -> Option<i32> {
if double_clicked || play_end < play_start {
return None;
}
let current_bounds = self.get_image_screen_bounds();
let current_size = self.image_size;
let scrubber = &mut self.scrubber;
if (response.clicked_by(egui::PointerButton::Primary)
|| response.dragged_by(egui::PointerButton::Primary))
&& let Some(screen_pos) = response.interact_pointer_pos()
{
let local_x = screen_pos.x - panel_rect.min.x;
if !scrubber.is_active() {
scrubber.start_scrubbing(current_bounds, current_size, 0.5);
scrubber.set_last_mouse_x(local_x);
}
let image_bounds = scrubber.frozen_bounds().unwrap_or(current_bounds);
let frame = fit(
local_x,
image_bounds.min.x, image_bounds.max.x,
play_start as f32, play_end as f32,
).round() as i32;
let frame_clamped = frame.clamp(play_start, play_end);
let is_clamped = frame != frame_clamped;
scrubber.set_clamped(is_clamped);
scrubber.set_current_frame(frame_clamped);
scrubber.set_visual_x(local_x);
scrubber.set_last_mouse_x(local_x);
Some(frame_clamped)
} else if response.drag_stopped() || response.clicked() {
scrubber.stop_scrubbing();
None
} else {
None
}
}
pub fn render_state(&self) -> ViewportRenderState {
ViewportRenderState {
model_matrix: self.get_model_matrix(),
view_matrix: self.get_view_matrix(),
projection_matrix: self.get_projection_matrix(),
}
}
pub fn get_model_matrix(&self) -> [[f32; 4]; 4] {
[
[self.image_size.x, 0.0, 0.0, 0.0],
[0.0, self.image_size.y, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
]
}
pub fn get_view_matrix(&self) -> [[f32; 4]; 4] {
[
[self.zoom, 0.0, 0.0, 0.0],
[0.0, self.zoom, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[self.pan.x, self.pan.y, 0.0, 1.0],
]
}
pub fn get_projection_matrix(&self) -> [[f32; 4]; 4] {
let w = self.viewport_size.x;
let h = self.viewport_size.y;
if w <= 0.0 || h <= 0.0 {
return Self::identity_matrix();
}
let left = -w / 2.0;
let right = w / 2.0;
let bottom = -h / 2.0;
let top = h / 2.0;
[
[2.0 / (right - left), 0.0, 0.0, 0.0],
[0.0, 2.0 / (top - bottom), 0.0, 0.0],
[0.0, 0.0, -1.0, 0.0],
[
-(right + left) / (right - left),
-(top + bottom) / (top - bottom),
0.0,
1.0,
],
]
}
fn identity_matrix() -> [[f32; 4]; 4] {
[
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
]
}
}
#[derive(Clone, Default)]
pub struct ViewportScrubber {
is_active: bool,
normalized_position: Option<f32>, visual_x: Option<f32>, current_frame: Option<usize>,
is_clamped: bool, frozen_bounds: Option<egui::Rect>, last_mouse_x: Option<f32>, }
impl ViewportScrubber {
pub fn new() -> Self {
Self {
is_active: false,
normalized_position: None,
visual_x: None,
current_frame: None,
is_clamped: false,
frozen_bounds: None,
last_mouse_x: None,
}
}
pub fn draw(&self, ui: &egui::Ui, panel_rect: egui::Rect) {
if !self.is_active {
return;
}
if let Some(visual_x) = self.visual_x {
let painter = ui.painter();
let (r, g, b, a) = if self.is_clamped {
SCRUB_OUTSIDE
} else {
SCRUB_NORMAL
};
let line_color = egui::Color32::from_rgba_unmultiplied(
(r * 255.0) as u8,
(g * 255.0) as u8,
(b * 255.0) as u8,
(a * 255.0) as u8,
);
let screen_x = panel_rect.min.x + visual_x;
let line_top = egui::pos2(screen_x, panel_rect.top());
let line_bottom = egui::pos2(screen_x, panel_rect.bottom());
painter.line_segment([line_top, line_bottom], egui::Stroke::new(1.0, line_color));
if let Some(frame) = self.current_frame {
let text = format!("{}", frame);
let text_pos = egui::pos2(screen_x + 10.0, panel_rect.top() + 10.0);
painter.text(
text_pos,
egui::Align2::LEFT_TOP,
text,
egui::FontId::proportional(12.0),
line_color,
);
}
}
}
pub fn is_active(&self) -> bool {
self.is_active
}
pub fn start_scrubbing(
&mut self,
image_bounds: egui::Rect,
image_size: egui::Vec2,
normalized: f32,
) {
self.is_active = true;
self.frozen_bounds = Some(image_bounds);
let _ = image_size; self.normalized_position = Some(normalized);
}
pub fn stop_scrubbing(&mut self) {
self.is_active = false;
self.normalized_position = None;
self.visual_x = None;
self.current_frame = None;
self.is_clamped = false;
self.frozen_bounds = None;
self.last_mouse_x = None;
}
pub fn frozen_bounds(&self) -> Option<egui::Rect> {
self.frozen_bounds
}
pub fn set_normalized_position(&mut self, normalized: f32) {
self.normalized_position = Some(normalized);
}
pub fn set_visual_x(&mut self, x: f32) {
self.visual_x = Some(x);
}
pub fn set_current_frame(&mut self, frame: i32) {
self.current_frame = Some(frame.max(0) as usize);
}
pub fn set_clamped(&mut self, clamped: bool) {
self.is_clamped = clamped;
}
pub fn normalized_position(&self) -> Option<f32> {
self.normalized_position
}
pub fn set_last_mouse_x(&mut self, mouse_x: f32) {
self.last_mouse_x = Some(mouse_x);
}
pub fn mouse_moved(&self, current_mouse_x: f32) -> bool {
if let Some(last_x) = self.last_mouse_x {
(current_mouse_x - last_x).abs() > 0.1
} else {
true
}
}
pub fn mouse_to_normalized(mouse_x: f32, bounds: egui::Rect) -> f32 {
let left = bounds.min.x;
let right = bounds.max.x;
if right > left {
(mouse_x - left) / (right - left)
} else {
0.5
}
}
pub fn normalized_to_pixel(normalized: f32, bounds: egui::Rect) -> f32 {
let left = bounds.min.x;
let right = bounds.max.x;
left + normalized * (right - left)
}
pub fn normalized_to_frame(normalized: f32, total_frames: i32) -> i32 {
if total_frames > 1 {
let clamped = normalized.clamp(0.0, 1.0);
(clamped * (total_frames - 1) as f32).round() as i32
} else {
0
}
}
}