use crate::layout::subpixel_snap_rect;
use crate::motion::CASpring;
use crate::theme::MacTheme;
use egui::{
CornerRadius, Id, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Ui, Vec2, Widget,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MacPlayerControlsStyle {
#[default]
Floating,
Inline,
Minimal,
}
pub struct MacPlayerView<'a> {
controls_style: MacPlayerControlsStyle,
is_playing: bool,
progress: f32,
theme: Option<&'a MacTheme>,
}
impl<'a> Default for MacPlayerView<'a> {
fn default() -> Self {
Self::new()
}
}
impl<'a> MacPlayerView<'a> {
pub fn new() -> Self {
Self {
controls_style: MacPlayerControlsStyle::Floating,
is_playing: false,
progress: 0.0,
theme: None,
}
}
pub fn controls_style(mut self, style: MacPlayerControlsStyle) -> Self {
self.controls_style = style;
self
}
pub fn is_playing(mut self, playing: bool) -> Self {
self.is_playing = playing;
self
}
pub fn progress(mut self, progress: f32) -> Self {
self.progress = progress;
self
}
pub fn theme(mut self, theme: &'a MacTheme) -> Self {
self.theme = Some(theme);
self
}
pub fn hud_pod_size(&self) -> Vec2 {
let theme_cow = MacTheme::resolve(self.theme, &egui::Context::default());
let m = theme_cow.player_view();
Vec2::new(m.hud_width.points(), m.hud_height.points())
}
pub fn hud_corner_radius(&self) -> f32 {
let theme_cow = MacTheme::resolve(self.theme, &egui::Context::default());
theme_cow.player_view().hud_corner_radius.points()
}
pub fn auto_hide_seconds(&self) -> f32 {
let theme_cow = MacTheme::resolve(self.theme, &egui::Context::default());
theme_cow.player_view().auto_hide_seconds.points()
}
pub fn scrubber_height(&self) -> f32 {
let theme_cow = MacTheme::resolve(self.theme, &egui::Context::default());
theme_cow.player_view().scrubber_height.points()
}
pub fn scrubber_track_height(&self) -> f32 {
let theme_cow = MacTheme::resolve(self.theme, &egui::Context::default());
theme_cow.player_view().scrubber_track_height.points()
}
pub fn scrubber_knob_size(&self) -> Vec2 {
let theme_cow = MacTheme::resolve(self.theme, &egui::Context::default());
let sz = theme_cow.player_view().scrubber_knob_size.points();
Vec2::new(sz, sz)
}
pub fn inline_bar_height(&self) -> f32 {
let theme_cow = MacTheme::resolve(self.theme, &egui::Context::default());
theme_cow.player_view().inline_height.points()
}
pub fn inline_corner_radius(&self) -> f32 {
let theme_cow = MacTheme::resolve(self.theme, &egui::Context::default());
theme_cow.player_view().inline_corner_radius.points()
}
}
impl<'a> Widget for MacPlayerView<'a> {
fn ui(mut self, ui: &mut Ui) -> Response {
let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
let theme: &MacTheme = &theme_cow;
let metrics = theme.player_view();
let available_w = ui.available_width();
let total_size = match self.controls_style {
MacPlayerControlsStyle::Floating => {
let pod = self.hud_pod_size();
Vec2::new(available_w.max(pod.x), pod.y + metrics.hud_bottom_margin.points() * 2.0)
}
MacPlayerControlsStyle::Inline => {
Vec2::new(available_w, self.inline_bar_height())
}
MacPlayerControlsStyle::Minimal => {
let pod = self.hud_pod_size();
Vec2::new(available_w, pod.y)
}
};
let (rect, mut response) = ui.allocate_exact_size(total_size, Sense::click_and_drag());
let rect = subpixel_snap_rect(rect, ui.ctx().pixels_per_point());
let painter = ui.painter_at(rect);
match self.controls_style {
MacPlayerControlsStyle::Floating => {
let pod_size = self.hud_pod_size();
let corner_r = self.hud_corner_radius();
let pod_rect = Rect::from_center_size(
Pos2::new(rect.center().x, rect.max.y - metrics.hud_bottom_margin.points() - pod_size.y * 0.5),
pod_size,
);
let now = ui.ctx().input(|i| i.time);
let last_active_id = Id::NULL.with("hud_last_active");
let mut last_active = ui.ctx().data(|d| d.get_temp::<f64>(last_active_id)).unwrap_or(now);
let pointer_pos = ui.input(|i| i.pointer.hover_pos());
let pointer_in_video = pointer_pos.is_some_and(|p| rect.contains(p));
if pointer_in_video {
last_active = now;
ui.ctx().data_mut(|d| d.insert_temp(last_active_id, now));
}
let idle_time = (now - last_active) as f32;
let auto_hide_thresh = metrics.auto_hide_seconds.points();
let is_hovered = pointer_pos.is_some_and(|p| pod_rect.contains(p));
let target_alpha = if is_hovered || !self.is_playing || idle_time < auto_hide_thresh {
1.0
} else {
0.0
};
let spring = CASpring::new(1.0, 1.0);
let alpha = spring.animate_f32(ui.ctx(), Id::NULL.with("hud_alpha"), target_alpha);
if alpha > 0.0 {
let hud_bg = theme.control_background().gamma_multiply(alpha);
let hud_stroke = Stroke::new(1.0, theme.separator().gamma_multiply(alpha));
painter.rect(
pod_rect,
CornerRadius::same(corner_r as u8),
hud_bg,
hud_stroke,
StrokeKind::Inside,
);
let scrub_h = self.scrubber_height();
let track_h = self.scrubber_track_height();
let knob_sz = self.scrubber_knob_size();
let scrub_center_y = pod_rect.max.y - scrub_h * 0.5;
let track_margin = corner_r * 2.0;
let track_rect = Rect::from_center_size(
Pos2::new(pod_rect.center().x, scrub_center_y),
Vec2::new(pod_rect.width() - track_margin * 2.0, track_h),
);
let scrub_hit_rect = Rect::from_center_size(
track_rect.center(),
Vec2::new(track_rect.width(), scrub_h),
);
let interact_pos = ui.input(|i| i.pointer.interact_pos());
if interact_pos.is_some_and(|p| scrub_hit_rect.contains(p)) && ui.input(|i| i.pointer.primary_down()) {
if let Some(pos) = interact_pos {
let prog = ((pos.x - track_rect.min.x) / track_rect.width()).clamp(0.0, 1.0);
self.progress = prog;
response.mark_changed();
}
}
painter.rect_filled(
track_rect,
CornerRadius::same(metrics.scrubber_track_corner_radius.points() as u8),
theme.separator().gamma_multiply(alpha),
);
let knob_x = track_rect.min.x + track_rect.width() * self.progress.clamp(0.0, 1.0);
let knob_pos = Pos2::new(knob_x, scrub_center_y);
painter.circle_filled(
knob_pos,
knob_sz.x * 0.5,
theme.control_accent().gamma_multiply(alpha),
);
let total_secs = 100;
let elapsed_secs = (self.progress * (total_secs as f32)) as u32;
let remain_secs = ((1.0 - self.progress) * (total_secs as f32)) as u32;
let elapsed_str = format!("{:02}:{:02}", elapsed_secs / 60, elapsed_secs % 60);
let remain_str = format!("-{:02}:{:02}", remain_secs / 60, remain_secs % 60);
let text_style = ui.style().text_styles.get(&egui::TextStyle::Small).cloned().unwrap_or_default();
let el_galley = painter.layout_no_wrap(elapsed_str, text_style.clone(), theme.label().gamma_multiply(alpha));
let rem_galley = painter.layout_no_wrap(remain_str, text_style, theme.label().gamma_multiply(alpha));
painter.galley(
Pos2::new(pod_rect.min.x + corner_r, scrub_center_y - el_galley.size().y * 0.5),
el_galley,
theme.label().gamma_multiply(alpha),
);
painter.galley(
Pos2::new(pod_rect.max.x - corner_r - rem_galley.size().x, scrub_center_y - rem_galley.size().y * 0.5),
rem_galley,
theme.label().gamma_multiply(alpha),
);
let play_center = Pos2::new(pod_rect.center().x, pod_rect.min.y + metrics.top_row_height.points() * 0.5);
let play_hit = Rect::from_center_size(play_center, Vec2::new(knob_sz.x * 2.0, knob_sz.x * 2.0));
if interact_pos.is_some_and(|p| play_hit.contains(p)) && ui.input(|i| i.pointer.primary_clicked()) {
self.is_playing = !self.is_playing;
response.mark_changed();
}
if self.is_playing {
let bar_w = 2.0;
let bar_h = knob_sz.x;
let bar1 = Rect::from_center_size(Pos2::new(play_center.x - 2.0, play_center.y), Vec2::new(bar_w, bar_h));
let bar2 = Rect::from_center_size(Pos2::new(play_center.x + 2.0, play_center.y), Vec2::new(bar_w, bar_h));
painter.rect_filled(bar1, CornerRadius::same(0), theme.label().gamma_multiply(alpha));
painter.rect_filled(bar2, CornerRadius::same(0), theme.label().gamma_multiply(alpha));
} else {
let r = knob_sz.x * 0.5;
let p1 = Pos2::new(play_center.x - r * 0.5, play_center.y - r);
let p2 = Pos2::new(play_center.x - r * 0.5, play_center.y + r);
let p3 = Pos2::new(play_center.x + r, play_center.y);
painter.add(egui::Shape::convex_polygon(vec![p1, p2, p3], theme.label().gamma_multiply(alpha), Stroke::NONE));
}
}
}
MacPlayerControlsStyle::Inline => {
let bar_h = self.inline_bar_height();
let bar_rect = Rect::from_min_size(
Pos2::new(rect.min.x, rect.max.y - bar_h),
Vec2::new(rect.width(), bar_h),
);
painter.rect_filled(
bar_rect,
CornerRadius::same(0),
theme.control_background(),
);
painter.line_segment(
[bar_rect.left_top(), bar_rect.right_top()],
Stroke::new(1.0, theme.separator()),
);
let play_center = bar_rect.center();
let icon_radius = metrics.scrubber_knob_size.points() * 0.5;
painter.circle_filled(play_center, icon_radius, theme.label());
}
MacPlayerControlsStyle::Minimal => {
let center = rect.center();
let min_radius = metrics.scrubber_knob_size.points();
painter.circle_filled(center, min_radius, theme.control_background());
painter.circle_stroke(center, min_radius, Stroke::new(1.0, theme.control_accent()));
}
}
if response.clicked() {
response.mark_changed();
}
response
}
}