use eframe::egui::{self, Color32, Pos2, Rect, Response, Sense, Ui, Vec2};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use crate::cache::Cache;
use crate::frame::FrameStatus;
#[derive(Clone, Debug)]
struct LoadIndicatorCache {
statuses: Vec<FrameStatus>,
cached_count: usize, loaded_events: usize, sequences_version: usize, }
#[derive(Clone, Debug)]
pub struct SequenceRange {
pub start_frame: usize,
pub end_frame: usize,
pub pattern: String,
}
#[derive(Clone, Debug)]
pub struct TimeSliderConfig {
pub height: f32,
pub show_labels: bool,
pub show_dividers: bool,
pub label_min_width: f32,
pub show_load_indicator: bool,
pub load_indicator_height: f32,
pub show_frame_numbers: bool,
}
impl Default for TimeSliderConfig {
fn default() -> Self {
Self {
height: 24.0,
show_labels: true,
show_dividers: true,
label_min_width: 60.0,
show_load_indicator: true,
load_indicator_height: 4.0,
show_frame_numbers: false,
}
}
}
pub fn time_slider(
ui: &mut Ui,
current_frame: usize,
total_frames: usize,
sequences: &[SequenceRange],
config: &TimeSliderConfig,
cache: &Cache,
) -> Option<usize> {
if total_frames == 0 {
return None;
}
let cache_id = ui.id().with("load_indicator_cache");
let current_cached_count = cache.cached_frames_count();
let current_loaded_events = cache.loaded_events_counter();
let current_seq_ver = cache.sequences_version();
let cached_statuses = ui.ctx().memory_mut(|mem| {
let stored: Option<LoadIndicatorCache> = mem.data.get_temp(cache_id);
match stored {
Some(cached)
if cached.cached_count == current_cached_count
&& cached.loaded_events == current_loaded_events
&& cached.sequences_version == current_seq_ver =>
{
cached.statuses
}
_ => {
let statuses = cache.get_frame_stats();
mem.data.insert_temp(
cache_id,
LoadIndicatorCache {
statuses: statuses.clone(),
cached_count: current_cached_count,
loaded_events: current_loaded_events,
sequences_version: current_seq_ver,
},
);
statuses
}
}
});
let total_height = if config.show_load_indicator {
config.height + config.load_indicator_height
} else {
config.height
};
let desired_size = Vec2::new(ui.available_width(), total_height);
let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click_and_drag());
if ui.is_rect_visible(rect) {
let painter = ui.painter();
draw_seq_backgrounds(painter, rect, sequences, total_frames);
draw_play_range(painter, rect, cache.get_play_range(), total_frames);
if config.show_dividers {
draw_seq_dividers(painter, rect, sequences, total_frames);
}
if config.show_labels {
draw_seq_labels(
painter,
rect,
sequences,
total_frames,
config.label_min_width,
);
}
draw_playhead(painter, rect, current_frame, total_frames);
if config.show_frame_numbers {
draw_frame_numbers(painter, rect, sequences, total_frames, cache.get_play_range());
}
if config.show_load_indicator {
draw_load_indicator(
painter,
rect,
&cached_statuses,
config.load_indicator_height,
);
}
}
let slider_rect =
Rect::from_min_max(rect.min, Pos2::new(rect.max.x, rect.min.y + config.height));
handle_interaction(&response, slider_rect, total_frames)
}
fn draw_seq_backgrounds(
painter: &egui::Painter,
rect: Rect,
sequences: &[SequenceRange],
total_frames: usize,
) {
let frame_to_x =
|frame: usize| -> f32 { rect.min.x + (frame as f32 / total_frames as f32) * rect.width() };
for seq in sequences {
let x_start = frame_to_x(seq.start_frame);
let x_end = frame_to_x(seq.end_frame + 1);
let seq_rect =
Rect::from_min_max(Pos2::new(x_start, rect.min.y), Pos2::new(x_end, rect.max.y));
let color = hash_color(&seq.pattern);
painter.rect_filled(seq_rect, 0.0, color);
}
}
fn draw_play_range(
painter: &egui::Painter,
rect: Rect,
play_range: (usize, usize),
total_frames: usize,
) {
if total_frames == 0 {
return;
}
let (start, end) = play_range;
let frame_to_x =
|frame: usize| -> f32 { rect.min.x + (frame as f32 / total_frames as f32) * rect.width() };
let x_start = frame_to_x(start);
let x_end = frame_to_x(end + 1);
let play_rect = Rect::from_min_max(
Pos2::new(x_start, rect.min.y),
Pos2::new(x_end, rect.min.y + 4.0),
);
painter.rect_filled(
play_rect,
0.0,
Color32::from_rgba_premultiplied(90, 90, 90, 191),
);
}
fn draw_seq_dividers(
painter: &egui::Painter,
rect: Rect,
sequences: &[SequenceRange],
total_frames: usize,
) {
let frame_to_x =
|frame: usize| -> f32 { rect.min.x + (frame as f32 / total_frames as f32) * rect.width() };
for seq in sequences.iter().skip(1) {
let x = frame_to_x(seq.start_frame);
painter.line_segment(
[Pos2::new(x, rect.min.y), Pos2::new(x, rect.max.y)],
(1.5, Color32::from_gray(200)),
);
}
}
fn draw_seq_labels(
painter: &egui::Painter,
rect: Rect,
sequences: &[SequenceRange],
total_frames: usize,
min_width: f32,
) {
let frame_to_x =
|frame: usize| -> f32 { rect.min.x + (frame as f32 / total_frames as f32) * rect.width() };
for (idx, seq) in sequences.iter().enumerate() {
let x_start = frame_to_x(seq.start_frame);
let x_end = frame_to_x(seq.end_frame + 1);
let zone_width = x_end - x_start;
let label = if zone_width > min_width {
extract_filename(&seq.pattern)
} else if zone_width > 20.0 {
format!("{}", idx)
} else {
continue;
};
let center_x = (x_start + x_end) / 2.0;
let center_y = (rect.min.y + rect.max.y) / 2.0;
painter.text(
Pos2::new(center_x, center_y),
egui::Align2::CENTER_CENTER,
label,
egui::FontId::proportional(11.0),
Color32::from_gray(240),
);
}
}
fn draw_playhead(painter: &egui::Painter, rect: Rect, current_frame: usize, total_frames: usize) {
let x = rect.min.x + (current_frame as f32 / total_frames as f32) * rect.width();
painter.line_segment(
[Pos2::new(x, rect.min.y), Pos2::new(x, rect.max.y)],
(2.0, Color32::from_rgb(255, 220, 100)),
);
let frame_text = format!("{}", current_frame);
let text_pos = Pos2::new(x + 4.0, rect.min.y + 2.0);
let galley = painter.layout_no_wrap(
frame_text.clone(),
egui::FontId::proportional(11.0),
Color32::WHITE,
);
let text_rect = egui::Rect::from_min_size(text_pos, galley.size());
painter.rect_filled(text_rect.expand(2.0), 2.0, Color32::from_black_alpha(180));
painter.text(
text_pos,
egui::Align2::LEFT_TOP,
frame_text,
egui::FontId::proportional(11.0),
Color32::from_rgba_unmultiplied(255, 255, 255, 128),
);
}
fn handle_interaction(response: &Response, rect: Rect, total_frames: usize) -> Option<usize> {
if (response.dragged() || response.clicked())
&& let Some(pos) = response.interact_pointer_pos() {
let ratio = ((pos.x - rect.min.x) / rect.width()).clamp(0.0, 1.0);
let new_frame = (ratio * total_frames as f32) as usize;
return Some(new_frame.min(total_frames.saturating_sub(1)));
}
None
}
fn hash_color(pattern: &str) -> Color32 {
let mut hasher = DefaultHasher::new();
pattern.hash(&mut hasher);
let hash = hasher.finish();
let hue = (hash % 360) as f32;
let saturation = 0.65;
let value = 0.55;
hsv_to_rgb(hue, saturation, value)
}
fn hsv_to_rgb(h: f32, s: f32, v: f32) -> Color32 {
let c = v * s;
let h_prime = h / 60.0;
let x = c * (1.0 - ((h_prime % 2.0) - 1.0).abs());
let m = v - c;
let (r, g, b) = if h_prime < 1.0 {
(c, x, 0.0)
} else if h_prime < 2.0 {
(x, c, 0.0)
} else if h_prime < 3.0 {
(0.0, c, x)
} else if h_prime < 4.0 {
(0.0, x, c)
} else if h_prime < 5.0 {
(x, 0.0, c)
} else {
(c, 0.0, x)
};
Color32::from_rgb(
((r + m) * 255.0) as u8,
((g + m) * 255.0) as u8,
((b + m) * 255.0) as u8,
)
}
fn extract_filename(pattern: &str) -> String {
let normalized = pattern.replace('\\', "/");
let filename = normalized.split('/').next_back().unwrap_or(pattern);
filename.split('.').next().unwrap_or(filename).to_string()
}
fn draw_load_indicator(painter: &egui::Painter, rect: Rect, statuses: &[FrameStatus], height: f32) {
let total = statuses.len();
if total == 0 {
return;
}
let indicator_rect = Rect::from_min_max(
Pos2::new(rect.min.x, rect.max.y),
Pos2::new(rect.max.x, rect.max.y + height),
);
let block_width = indicator_rect.width() / total as f32;
for (idx, status) in statuses.iter().enumerate() {
let x_start = indicator_rect.min.x + (idx as f32 * block_width);
let x_end = x_start + block_width;
let color = status.color();
let block_rect = Rect::from_min_max(
Pos2::new(x_start, indicator_rect.min.y),
Pos2::new(x_end, indicator_rect.max.y),
);
painter.rect_filled(block_rect, 0.0, color);
}
}
fn draw_frame_numbers(
painter: &egui::Painter,
rect: Rect,
sequences: &[SequenceRange],
total_frames: usize,
play_range: (usize, usize),
) {
if total_frames == 0 {
return;
}
let frame_to_x = |frame: usize| -> f32 {
rect.min.x + (frame as f32 / total_frames as f32) * rect.width()
};
let text_color = Color32::from_rgba_unmultiplied(255, 255, 255, 200);
let font_id = egui::FontId::monospace(8.0);
let offset = 3.0;
let global_start_x = frame_to_x(0) + offset;
painter.text(
Pos2::new(global_start_x, rect.min.y + 2.0),
egui::Align2::LEFT_TOP,
"0",
font_id.clone(),
text_color,
);
let global_end_frame = total_frames.saturating_sub(1);
let global_end_x = frame_to_x(global_end_frame) - offset;
painter.text(
Pos2::new(global_end_x, rect.min.y + 2.0),
egui::Align2::RIGHT_TOP,
format!("{}", global_end_frame),
font_id.clone(),
text_color,
);
let mut current_offset = 0;
for (idx, seq) in sequences.iter().enumerate() {
if idx > 0 {
let seq_start_x = frame_to_x(current_offset) + offset;
painter.text(
Pos2::new(seq_start_x, rect.min.y + 2.0),
egui::Align2::LEFT_TOP,
format!("{}", current_offset),
font_id.clone(),
text_color,
);
}
current_offset += seq.end_frame - seq.start_frame + 1;
}
let (play_start, play_end) = play_range;
if play_start > 0 {
let play_start_x = frame_to_x(play_start) + offset;
painter.text(
Pos2::new(play_start_x, rect.max.y - 2.0),
egui::Align2::LEFT_BOTTOM,
format!("{}", play_start),
font_id.clone(),
Color32::from_rgba_unmultiplied(255, 255, 0, 200),
);
}
if play_end < global_end_frame {
let play_end_x = frame_to_x(play_end) - offset;
painter.text(
Pos2::new(play_end_x, rect.max.y - 2.0),
egui::Align2::RIGHT_BOTTOM,
format!("{}", play_end),
font_id.clone(),
Color32::from_rgba_unmultiplied(255, 255, 0, 200),
);
}
}