use crate::entities::Attrs;
use eframe::egui::{self, Pos2};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use std::collections::HashMap;
#[derive(Clone, Debug)]
pub struct ClipboardLayer {
pub source_uuid: Uuid,
pub attrs: Attrs,
pub original_start: i32,
}
#[derive(Default)]
pub struct TimelineActions {
pub hovered: bool,
}
#[derive(Clone, Debug)]
pub struct TimelineConfig {
pub layer_height: f32,
pub name_column_width: f32,
pub pixels_per_frame: f32, }
impl Default for TimelineConfig {
fn default() -> Self {
Self {
layer_height: 30.0,
name_column_width: 80.0,
pixels_per_frame: 2.0, }
}
}
impl TimelineConfig {
pub fn new(layer_height: f32, name_column_width: f32) -> Self {
Self {
layer_height,
name_column_width,
..Default::default()
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct TimelineState {
pub zoom: f32, pub pan_offset: f32, #[serde(skip)]
pub drag_state: Option<GlobalDragState>, pub snap_enabled: bool,
pub lock_work_area: bool,
pub last_comp_uuid: Option<Uuid>, pub view_mode: TimelineViewMode,
#[serde(skip)]
pub last_canvas_width: f32, pub outline_width: f32, #[serde(skip)]
pub hatch_texture: Option<egui::TextureHandle>, #[serde(skip)]
pub clipboard: Vec<ClipboardLayer>, #[serde(skip)]
pub(super) geom_cache: HashMap<usize, LayerGeom>,
#[serde(skip)]
pub rename_dialog_open: bool,
#[serde(skip)]
pub rename_dialog_name: String,
#[serde(skip)]
pub rename_dialog_old_name: String,
}
impl std::fmt::Debug for TimelineState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TimelineState")
.field("zoom", &self.zoom)
.field("pan_offset", &self.pan_offset)
.field("drag_state", &self.drag_state)
.field("snap_enabled", &self.snap_enabled)
.field("lock_work_area", &self.lock_work_area)
.field("last_comp_uuid", &self.last_comp_uuid)
.field("view_mode", &self.view_mode)
.field("last_canvas_width", &self.last_canvas_width)
.field("outline_width", &self.outline_width)
.field("hatch_texture", &self.hatch_texture.as_ref().map(|_| "TextureHandle"))
.field("clipboard", &format!("{} layers", self.clipboard.len()))
.field("geom_cache", &format!("{} entries", self.geom_cache.len()))
.field("rename_dialog_open", &self.rename_dialog_open)
.finish()
}
}
impl Default for TimelineState {
fn default() -> Self {
Self {
zoom: 1.0,
pan_offset: 0.0,
drag_state: None,
snap_enabled: true,
lock_work_area: false,
last_comp_uuid: None,
view_mode: TimelineViewMode::Split,
last_canvas_width: 800.0, outline_width: 400.0, hatch_texture: None,
clipboard: Vec::new(),
geom_cache: HashMap::new(),
rename_dialog_open: false,
rename_dialog_name: String::new(),
rename_dialog_old_name: String::new(),
}
}
}
impl TimelineState {
pub fn get_hatch_texture(&mut self, ctx: &egui::Context) -> egui::TextureId {
if self.hatch_texture.is_none() {
self.hatch_texture = Some(create_hatch_texture(ctx));
}
self.hatch_texture.as_ref().unwrap().id()
}
}
fn create_hatch_texture(ctx: &egui::Context) -> egui::TextureHandle {
const SIZE: usize = 64;
const LINE_WIDTH: usize = 14;
const SPACING: usize = 32;
let mut pixels = vec![egui::Color32::WHITE; SIZE * SIZE];
for y in 0..SIZE {
for x in 0..SIZE {
let diag = (x + y) % SPACING;
if diag < LINE_WIDTH {
pixels[y * SIZE + x] = egui::Color32::from_gray(243);
}
}
}
let image = egui::ColorImage::from_rgba_unmultiplied(
[SIZE, SIZE],
&pixels.iter().flat_map(|c| c.to_array()).collect::<Vec<_>>(),
);
ctx.load_texture(
"hatch_pattern",
image,
egui::TextureOptions {
magnification: egui::TextureFilter::Linear,
minification: egui::TextureFilter::Linear,
wrap_mode: egui::TextureWrapMode::Repeat,
..Default::default()
},
)
}
#[derive(Clone, Debug)]
pub enum GlobalDragState {
ProjectItem {
source_uuid: Uuid,
duration: Option<i32>,
},
TimelinePan {
drag_start_pos: Pos2,
initial_pan_offset: f32,
},
MovingLayer {
layer_idx: usize,
initial_start: i32, drag_start_x: f32,
drag_start_y: f32,
},
AdjustPlayStart {
layer_idx: usize,
initial_play_start: i32,
drag_start_x: f32,
},
AdjustPlayEnd {
layer_idx: usize,
initial_play_end: i32,
drag_start_x: f32,
},
SlidingLayer {
layer_idx: usize,
initial_in: i32,
initial_trim_in: i32,
initial_trim_out: i32,
speed: f32,
drag_start_x: f32,
},
}
#[derive(Clone, Debug, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TimelineViewMode {
Split,
CanvasOnly,
OutlineOnly,
}
#[derive(Clone, Copy)]
pub(super) struct LayerGeom {
pub full_bar_rect: eframe::egui::Rect,
pub visible_bar_rect: Option<eframe::egui::Rect>,
}
impl LayerGeom {
pub fn calc(
child_start: i32,
child_end: i32,
play_start: i32,
play_end: i32,
child_y: f32,
timeline_rect: eframe::egui::Rect,
config: &TimelineConfig,
pan_offset: f32,
zoom: f32,
) -> Self {
use eframe::egui::{Pos2, Rect};
let frame_to_screen_x = |frame: f32, timeline_min_x: f32| -> f32 {
let frame_offset = frame - pan_offset;
timeline_min_x + (frame_offset * config.pixels_per_frame * zoom)
};
let full_bar_x_start = frame_to_screen_x(child_start as f32, timeline_rect.min.x);
let full_bar_x_end = frame_to_screen_x((child_end + 1) as f32, timeline_rect.min.x);
let full_bar_rect = Rect::from_min_max(
Pos2::new(full_bar_x_start, child_y + 4.0),
Pos2::new(full_bar_x_end, child_y + config.layer_height - 4.0),
);
let visible_bar_rect = if play_start <= play_end {
let visible_bar_x_start = frame_to_screen_x(play_start as f32, timeline_rect.min.x);
let visible_bar_x_end =
frame_to_screen_x((play_end + 1) as f32, timeline_rect.min.x);
Some(Rect::from_min_max(
Pos2::new(visible_bar_x_start, child_y + 4.0),
Pos2::new(visible_bar_x_end, child_y + config.layer_height - 4.0),
))
} else {
None
};
Self {
full_bar_rect,
visible_bar_rect,
}
}
}