177 lines
5.0 KiB
Rust
177 lines
5.0 KiB
Rust
use std::cmp;
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq)]
|
|
struct State {
|
|
num_videos: u32,
|
|
}
|
|
|
|
impl State {
|
|
pub fn load(ctx: &egui::Context, id: egui::Id) -> Option<Self> {
|
|
ctx.data(|i| i.get_temp(id))
|
|
}
|
|
|
|
pub fn store(self, ctx: &egui::Context, id: egui::Id) {
|
|
ctx.data_mut(|i| i.insert_temp(id, self))
|
|
}
|
|
}
|
|
|
|
pub const DEFAULT_VIDEO_SIZE: egui::Vec2 = egui::vec2(320.0, 180.0);
|
|
pub const DEFAULT_MAX_COLUMNS: u32 = 4;
|
|
pub const DEFAULT_SPACING: f32 = 16.0;
|
|
|
|
pub struct VideoGrid {
|
|
id: egui::Id,
|
|
|
|
// Current frame
|
|
available_rect: egui::Rect,
|
|
prev_state: State,
|
|
curr_state: State,
|
|
video_index: u32, // Kinda "cursor"
|
|
|
|
// Options
|
|
min_video_size: egui::Vec2,
|
|
max_columns: u32,
|
|
spacing: f32,
|
|
}
|
|
|
|
impl VideoGrid {
|
|
pub fn new(id_source: impl std::hash::Hash) -> Self {
|
|
Self {
|
|
id: egui::Id::new(id_source),
|
|
available_rect: egui::Rect::NAN,
|
|
prev_state: State::default(),
|
|
curr_state: State::default(),
|
|
video_index: 0,
|
|
min_video_size: DEFAULT_VIDEO_SIZE,
|
|
max_columns: DEFAULT_MAX_COLUMNS,
|
|
spacing: DEFAULT_SPACING,
|
|
}
|
|
}
|
|
|
|
pub fn show<R>(
|
|
mut self,
|
|
ui: &mut egui::Ui,
|
|
grid: impl FnOnce(&mut VideoGridContext) -> R,
|
|
) -> egui::InnerResponse<R> {
|
|
// TODO(theomonnom): Should I care about the current egui layout?
|
|
|
|
let prev_state = State::load(ui.ctx(), self.id);
|
|
let is_first_frame = prev_state.is_none();
|
|
|
|
self.prev_state = prev_state.unwrap_or_default();
|
|
self.available_rect = ui.available_rect_before_wrap();
|
|
|
|
ui.ctx()
|
|
.check_for_id_clash(self.id, self.available_rect, "VideoGrid");
|
|
|
|
ui.allocate_ui_at_rect(self.available_rect, |ui| {
|
|
ui.set_visible(!is_first_frame);
|
|
|
|
let mut ctx = VideoGridContext {
|
|
layout: &mut self,
|
|
ui,
|
|
};
|
|
let res = grid(&mut ctx);
|
|
|
|
// Save the new state
|
|
if self.curr_state != self.prev_state {
|
|
self.curr_state.clone().store(ui.ctx(), self.id);
|
|
ui.ctx().request_repaint();
|
|
}
|
|
|
|
res
|
|
})
|
|
}
|
|
|
|
fn next_frame_rect(&mut self) -> egui::Rect {
|
|
assert!(self.available_rect.is_finite());
|
|
assert!(self.spacing <= self.min_video_size.x);
|
|
|
|
// increment the amount of videos for the next frame
|
|
self.curr_state.num_videos += 1;
|
|
|
|
let num_videos = self.prev_state.num_videos;
|
|
if num_videos == 0 {
|
|
return egui::Rect::NOTHING;
|
|
}
|
|
|
|
let max_columns = self.max_columns;
|
|
let minimum_size = self.min_video_size;
|
|
let available_size = self.available_rect.size();
|
|
|
|
let calc_min_width =
|
|
|columns: u32| columns as f32 * minimum_size.x + (columns - 1) as f32 * self.spacing;
|
|
|
|
let total_columns = {
|
|
let mut est = (available_size.x / minimum_size.x) as u32 + 1;
|
|
if available_size.x < calc_min_width(est) {
|
|
est -= 1;
|
|
}
|
|
cmp::max(1, cmp::min(est, max_columns))
|
|
};
|
|
|
|
let aspect_ratio = minimum_size.x / minimum_size.y;
|
|
let remaining_width = available_size.x - calc_min_width(total_columns);
|
|
let w = minimum_size.x + remaining_width / total_columns as f32;
|
|
let h = w / aspect_ratio;
|
|
|
|
let x_index = self.video_index % total_columns;
|
|
let y_index = self.video_index / total_columns;
|
|
|
|
let x = {
|
|
let mut x = x_index as f32 * (w + self.spacing);
|
|
|
|
// vertically center the last row
|
|
let total_rows = num_videos / total_columns + 1;
|
|
if (y_index + 1) == total_rows {
|
|
let nb_items = num_videos - (total_rows - 1) * total_columns; // nb. of items on the last row
|
|
x += (total_columns - nb_items) as f32 * (w + self.spacing) / 2.0;
|
|
}
|
|
|
|
x
|
|
};
|
|
let y = y_index as f32 * (h + self.spacing);
|
|
|
|
let min = egui::pos2(x, y) + self.available_rect.left_top().to_vec2();
|
|
let max = egui::pos2(w, h) + min.to_vec2();
|
|
|
|
self.video_index += 1;
|
|
|
|
egui::Rect { min, max }
|
|
}
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
impl VideoGrid {
|
|
pub fn min_video_size(mut self, min_video_size: egui::Vec2) -> Self {
|
|
self.min_video_size = min_video_size;
|
|
self
|
|
}
|
|
|
|
pub fn max_columns(mut self, max_columns: u32) -> Self {
|
|
self.max_columns = max_columns;
|
|
self
|
|
}
|
|
|
|
pub fn spacing(mut self, spacing: f32) -> Self {
|
|
self.spacing = spacing;
|
|
self
|
|
}
|
|
}
|
|
|
|
pub struct VideoGridContext<'a> {
|
|
layout: &'a mut VideoGrid,
|
|
ui: &'a mut egui::Ui,
|
|
}
|
|
|
|
impl<'a> VideoGridContext<'a> {
|
|
pub fn video_frame(&mut self, add_contents: impl FnOnce(&mut egui::Ui)) -> egui::Response {
|
|
let frame_rect = self.layout.next_frame_rect();
|
|
|
|
let mut child_ui = self.ui.child_ui(frame_rect, egui::Layout::default());
|
|
add_contents(&mut child_ui);
|
|
|
|
self.ui.allocate_rect(frame_rect, egui::Sense::hover())
|
|
}
|
|
}
|