use std::sync::Arc;
use crate::backend::{FramebufferSize, PlayerState};
use eframe::glow::HasContext as _;
use eframe::{egui, glow};
use tracing::error;
pub trait ControlsIconProvider {
#[inline]
fn play(&self) -> egui::WidgetText { "▶".into() }
#[inline]
fn pause(&self) -> egui::WidgetText { "⏸".into() }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct DefaultControlsIconProvider;
impl ControlsIconProvider for DefaultControlsIconProvider {}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Sizing {
Width(f32),
Height(f32),
}
pub struct SharkPlayer<'a, P: ControlsIconProvider = DefaultControlsIconProvider> {
backend: &'a mut PlayerState,
icons: P,
sizing: Option<Sizing>,
bar_height: f32,
}
impl<'a> SharkPlayer<'a> {
#[inline]
pub fn new(backend: &'a mut PlayerState) -> Self {
SharkPlayer::new_with_icons(backend, DefaultControlsIconProvider)
}
}
impl<'a, P: ControlsIconProvider> SharkPlayer<'a, P> {
#[inline]
pub fn new_with_icons(backend: &'a mut PlayerState, icons: P) -> Self {
Self {
backend,
icons,
sizing: None,
bar_height: 40.0,
}
}
#[inline]
pub fn sizing(mut self, sizing: Sizing) -> Self {
self.sizing = Some(sizing);
self
}
#[inline]
pub fn bar_height(mut self, height: f32) -> Self {
self.bar_height = height;
self
}
fn format_time_mm_ss(time: f64) -> String {
format!(
"{m:02.0}:{s:02.02}",
m = (time / 60.).floor(),
s = (time % 60.).floor(),
)
}
fn format_time_hh_mm_ss(time: f64) -> String {
format!(
"{h:02.0}:{m:02.0}:{s:02.02}",
h = time / 3600.,
m = (time % 3600.) / 60.,
s = time % 60.,
)
}
fn player_size(&self, ui: &mut egui::Ui, aspect_ratio: f32) -> egui::Vec2 {
match self.sizing {
Some(Sizing::Width(w)) => egui::vec2(w, w / aspect_ratio),
Some(Sizing::Height(h)) => egui::vec2(h * aspect_ratio, h),
None => {
let avaliable_size = ui.available_size();
let (max_w, max_h) = (avaliable_size.x, avaliable_size.y);
let (width, height) = if max_w / aspect_ratio <= max_h {
(max_w, max_w / aspect_ratio)
} else {
(max_h * aspect_ratio, max_h)
};
egui::vec2(width, height)
}
}
}
fn player_ui(&mut self, ui: &mut egui::Ui, rect: egui::Rect) {
let ppp = ui.ctx().pixels_per_point();
let (fb, fb_size) = match self.backend.get_current_framebuffer(FramebufferSize {
width: (rect.width() * ppp) as i32,
height: (rect.height() * ppp) as i32,
}) {
Ok(res) => (res.framebuffer(), res.framebuffer_size()),
Err(e) if cfg!(debug_assertions) => {
error!("Failed to get current framebuffer: {e}");
return;
}
Err(_) => return,
};
let cb = eframe::egui_glow::CallbackFn::new(move |info, painter| {
let gl = painter.gl();
paint(gl, rect, fb, fb_size, info);
});
ui.painter().add(egui::PaintCallback {
rect,
callback: Arc::new(cb),
});
}
fn controls_ui(self, ui: &mut egui::Ui, rect: egui::Rect) {
const SECOND: f64 = 1.0;
const MINUTE: f64 = SECOND * 60.0;
const HOUR: f64 = MINUTE * 60.0;
const FINISHED_THRESHOLD: f64 = 0.2;
let is_hovered = ui.rect_contains_pointer(rect);
let is_paused = self.backend.is_paused().unwrap_or(true);
if is_hovered || is_paused {
let controls_rect =
egui::Rect::from_min_max(egui::pos2(rect.min.x, rect.max.y - self.bar_height), rect.max);
ui.scope_builder(egui::UiBuilder::new().max_rect(controls_rect), |ui| {
ui.painter()
.rect_filled(ui.max_rect(), 0.0, egui::Color32::from_black_alpha(180));
let inner_rect = ui.max_rect().shrink(8.0); ui.scope_builder(egui::UiBuilder::new().max_rect(inner_rect), |ui| {
ui.horizontal(|ui| {
let duration = self.backend.duration().ok().flatten().unwrap_or(0.0);
let mut current_time = self.backend.time_pos().ok().flatten().unwrap_or(0.0);
let icon = if is_paused {
self.icons.play()
} else {
self.icons.pause()
};
if ui.button(icon).clicked() {
if (current_time - duration).abs() < FINISHED_THRESHOLD {
if let Err(e) = self.backend.seek_to(0.0) {
error!("Failed to seek to start: {e}");
}
if let Err(e) = self.backend.play() {
error!("Failed to start playback: {e}");
}
} else {
if let Err(e) = self.backend.toggle_pause() {
error!("Failed to pause player: {e}");
}
}
}
if duration >= HOUR {
ui.label(Self::format_time_hh_mm_ss(current_time));
} else {
ui.label(Self::format_time_mm_ss(current_time));
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if duration >= HOUR {
ui.label(Self::format_time_hh_mm_ss(duration));
} else {
ui.label(Self::format_time_mm_ss(duration));
}
ui.spacing_mut().slider_width = ui.available_width();
let slider = ui.add(
egui::Slider::new(&mut current_time, 0.0..=duration)
.show_value(false)
.trailing_fill(true),
);
let slider_rect = slider.rect;
if slider.changed()
&& let Err(e) = self.backend.seek_to(current_time)
{
error!("Failed to seek to {current_time:03}s: {e}");
}
if let Some(ppos) = ui.ctx().pointer_latest_pos()
&& (slider_rect.contains(ppos) || slider.dragged())
{
let prev_time = ((ppos.x - slider_rect.min.x) / slider_rect.width())
.clamp(0., 1.) as f64
* duration;
egui::Tooltip::always_open(
ui.ctx().clone(),
ui.layer_id(),
ui.id(),
egui::PopupAnchor::Pointer,
)
.show(|ui| {
if duration >= HOUR {
ui.label(Self::format_time_hh_mm_ss(prev_time));
} else {
ui.label(Self::format_time_mm_ss(prev_time));
}
});
}
});
})
});
});
}
}
}
impl<'a, P: ControlsIconProvider> egui::Widget for SharkPlayer<'a, P> {
fn ui(mut self, ui: &mut egui::Ui) -> egui::Response {
let aspect_ratio = self.backend.aspect_ratio().ok().flatten().unwrap_or(16.0 / 9.0);
let sz = self.player_size(ui, aspect_ratio);
let (rect, response) = ui.allocate_exact_size(sz, egui::Sense::click());
self.player_ui(ui, rect);
self.controls_ui(ui, rect);
response
}
}
fn paint(
gl: &glow::Context,
rect: egui::Rect,
fb: glow::NativeFramebuffer,
fb_size: FramebufferSize,
info: egui::PaintCallbackInfo,
) {
unsafe {
let prev_read_fb = gl.get_parameter_i32(eframe::glow::READ_FRAMEBUFFER_BINDING);
gl.bind_framebuffer(eframe::glow::READ_FRAMEBUFFER, Some(fb));
let p_per_point = info.pixels_per_point;
let screen_h = info.screen_size_px[1] as f32;
gl.blit_framebuffer(
0,
0,
fb_size.width,
fb_size.height,
(rect.min.x * p_per_point) as i32,
(screen_h - rect.max.y * p_per_point) as i32,
(rect.max.x * p_per_point) as i32,
(screen_h - rect.min.y * p_per_point) as i32,
eframe::glow::COLOR_BUFFER_BIT,
eframe::glow::LINEAR,
);
let prev_fb = std::num::NonZeroU32::new(prev_read_fb as u32).map(eframe::glow::NativeFramebuffer);
gl.bind_framebuffer(eframe::glow::READ_FRAMEBUFFER, prev_fb);
}
}