pub mod painter;
pub use painter::MacTimelineZone;
use crate::layout::subpixel_snap_rect;
use crate::theme::MacTheme;
use egui::{Id, Pos2, Rect, Response, Sense, Ui, Vec2};
use std::fmt::Debug;
use std::hash::Hash;
#[derive(Debug, Clone)]
pub struct MacTrimScrubberResponse {
pub response: Response,
pub in_point_dragged: bool,
pub out_point_dragged: bool,
pub playhead_dragged: bool,
pub changed: bool,
}
pub struct MacTrimScrubber<'a> {
id: Id,
duration: f64,
in_point: &'a mut f64,
out_point: &'a mut f64,
playhead: &'a mut f64,
waveform: &'a [f32],
zones: &'a [MacTimelineZone<'a>],
width: Option<f32>,
height: Option<f32>,
enabled: bool,
clamp_playhead_to_trim: bool,
theme: Option<&'a MacTheme>,
}
impl<'a> MacTrimScrubber<'a> {
pub fn new(
id_source: impl Hash + Debug,
duration: f64,
in_point: &'a mut f64,
out_point: &'a mut f64,
playhead: &'a mut f64,
) -> Self {
#[allow(deprecated)]
let id = Id::new(id_source);
Self {
id,
duration,
in_point,
out_point,
playhead,
waveform: &[],
zones: &[],
width: None,
height: None,
enabled: true,
clamp_playhead_to_trim: true,
theme: None,
}
}
pub fn waveform(mut self, waveform: &'a [f32]) -> Self {
self.waveform = waveform;
self
}
pub fn zones(mut self, zones: &'a [MacTimelineZone<'a>]) -> Self {
self.zones = zones;
self
}
pub fn width(mut self, width: f32) -> Self {
self.width = Some(width);
self
}
pub fn height(mut self, height: f32) -> Self {
self.height = Some(height);
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn clamp_playhead_to_trim(mut self, clamp: bool) -> Self {
self.clamp_playhead_to_trim = clamp;
self
}
pub fn theme(mut self, theme: &'a MacTheme) -> Self {
self.theme = Some(theme);
self
}
pub fn format_timecode(theme: &MacTheme, seconds: f64) -> String {
let sm = theme.trim_scrubber();
let sec_per_min = sm.seconds_per_minute.points() as f64;
let tenths_mul = sm.tenths_per_second.points() as f64;
let clamped = seconds.max(0.0);
let total_tenths = (clamped * tenths_mul).round() as u64;
let tenths_u64 = (tenths_mul.round() as u64).max(1);
let sec_min_u64 = (sec_per_min.round() as u64).max(1);
let total_secs = total_tenths / tenths_u64;
let tenths = total_tenths % tenths_u64;
let mins = total_secs / sec_min_u64;
let secs = total_secs % sec_min_u64;
format!("{mins:02}:{secs:02}.{tenths}")
}
pub fn show(self, ui: &mut Ui) -> MacTrimScrubberResponse {
let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
let theme: &MacTheme = &theme_cow;
let sm = theme.trim_scrubber();
let width = self.width.unwrap_or_else(|| sm.default_width.points());
let height = self.height.unwrap_or_else(|| sm.height.points());
let dur = self.duration.max(f64::EPSILON);
let mut changed = false;
let orig_in = *self.in_point;
let orig_out = *self.out_point;
let orig_play = *self.playhead;
let clamped_in = orig_in.clamp(0.0, dur);
let clamped_out = orig_out.clamp(clamped_in, dur);
let clamped_play = if self.clamp_playhead_to_trim {
orig_play.clamp(clamped_in, clamped_out)
} else {
orig_play.clamp(0.0, dur)
};
if (clamped_in - orig_in).abs() > f64::EPSILON
|| (clamped_out - orig_out).abs() > f64::EPSILON
|| (clamped_play - orig_play).abs() > f64::EPSILON
{
*self.in_point = clamped_in;
*self.out_point = clamped_out;
*self.playhead = clamped_play;
changed = true;
}
let (raw_rect, mut response) =
ui.allocate_exact_size(Vec2::new(width, height), Sense::click_and_drag());
let rect = subpixel_snap_rect(raw_rect, ui.ctx().pixels_per_point());
let v_inset = sm.track_inset_vertical.points();
let badge_h = sm.badge_height.points();
let track_rect = Rect::from_min_max(
Pos2::new(rect.left(), rect.top() + badge_h),
Pos2::new(
rect.right(),
(rect.bottom() - v_inset * 0.5).max(rect.top() + badge_h),
),
);
let handle_w = sm.handle_width.points();
let track_w = track_rect.width().max(1.0);
let x_to_time = |x: f32| -> f64 {
let frac = ((x - track_rect.left()) / track_w).clamp(0.0, 1.0) as f64;
frac * dur
};
let time_to_x = |t: f64| -> f32 {
let frac = (t / dur).clamp(0.0, 1.0) as f32;
track_rect.left() + frac * track_w
};
let in_x = time_to_x(*self.in_point);
let out_x = time_to_x(*self.out_point);
let (in_rect, out_rect) = painter::compute_handle_rects(track_rect, in_x, out_x, handle_w);
let sense = if self.enabled {
Sense::click_and_drag()
} else {
Sense::hover()
};
let in_resp = ui.interact(in_rect, self.id.with("in_handle"), sense);
let out_resp = ui.interact(out_rect, self.id.with("out_handle"), sense);
let mut in_point_dragged = false;
let mut out_point_dragged = false;
let mut playhead_dragged = false;
if self.enabled {
let drag_id = self.id.with("active_drag_target");
let (any_pressed, primary_down, any_released, ptr_pos) = ui.input(|i| {
(
i.pointer.any_pressed(),
i.pointer.primary_down(),
i.pointer.any_released(),
i.pointer.interact_pos().or_else(|| i.pointer.latest_pos()),
)
});
let prev_target = ui
.data(|d| d.get_temp::<(bool, bool, bool)>(drag_id))
.unwrap_or((false, false, false));
let mut target = prev_target;
if any_pressed {
if let Some(pos) = ptr_pos {
if in_rect.contains(pos) {
target = (true, false, false);
} else if out_rect.contains(pos) {
target = (false, true, false);
} else if rect.contains(pos) {
target = (false, false, true);
} else {
target = (false, false, false);
}
}
}
let active_in = target.0 || in_resp.dragged() || in_resp.drag_stopped();
let active_out = target.1 || out_resp.dragged() || out_resp.drag_stopped();
let active_play = target.2 || response.clicked() || response.dragged();
if (primary_down || any_released || any_pressed)
&& (active_in || active_out || active_play)
{
if let Some(pos) = ptr_pos {
if active_in {
let next_in = x_to_time(pos.x).clamp(0.0, *self.out_point);
*self.in_point = next_in;
if self.clamp_playhead_to_trim && *self.playhead < next_in {
*self.playhead = next_in;
}
in_point_dragged = true;
changed = true;
} else if active_out {
let next_out = x_to_time(pos.x).clamp(*self.in_point, dur);
*self.out_point = next_out;
if self.clamp_playhead_to_trim && *self.playhead > next_out {
*self.playhead = next_out;
}
out_point_dragged = true;
changed = true;
} else if active_play {
let next_play = if self.clamp_playhead_to_trim {
x_to_time(pos.x).clamp(*self.in_point, *self.out_point)
} else {
x_to_time(pos.x).clamp(0.0, dur)
};
*self.playhead = next_play;
playhead_dragged = true;
changed = true;
}
}
}
if !primary_down || any_released {
target = (false, false, false);
}
if prev_target != target {
ui.data_mut(|d| d.insert_temp(drag_id, target));
}
}
let tc = Self::format_timecode(theme, *self.playhead);
let painter = ui.painter_at(rect);
painter::paint_trim_scrubber(
&painter,
theme,
rect,
track_rect,
dur,
*self.in_point,
*self.out_point,
*self.playhead,
self.waveform,
self.zones,
&tc,
);
if changed {
response.mark_changed();
}
MacTrimScrubberResponse {
response,
in_point_dragged,
out_point_dragged,
playhead_dragged,
changed,
}
}
}