Fullscreen MediaViewer refactor
- Moved media related logic into notedeck instead of the ui crate, since they pertain to Images/ImageCache based systems - Made RenderableMedia owned to make it less of a nightmware to work with and the perf should be negligible - Added a ImageMetadata cache to Images. This is referenced whenever we encounter an image so we don't have to redo the work all of the time - Relpaced our ad-hoc, hand(vibe?)-coded panning and zoom logic with the Scene widget, which is explicitly designed for this use case - Extracted and detangle fullscreen media rendering from inside of note rendering. We instead let the application decide what action they want to perform when note media is clicked on. - We add an on_view_media action to MediaAction for the application to handle. The Columns app uses this toggle a FullscreenMedia app option bits whenever we get a MediaAction::ViewMedis(urls). Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
3
crates/notedeck_ui/src/media/mod.rs
Normal file
3
crates/notedeck_ui/src/media/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod viewer;
|
||||
|
||||
pub use viewer::{MediaViewer, MediaViewerState};
|
||||
232
crates/notedeck_ui/src/media/spiral.rs
Normal file
232
crates/notedeck_ui/src/media/spiral.rs
Normal file
@@ -0,0 +1,232 @@
|
||||
/// Spiral layout for media galleries
|
||||
|
||||
use egui::{pos2, vec2, Color32, Rect, Sense, TextureId, Vec2};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ImageItem {
|
||||
pub texture: TextureId,
|
||||
pub ar: f32, // width / height (must be > 0)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Placed {
|
||||
texture: TextureId,
|
||||
rect: Rect,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct LayoutParams {
|
||||
pub gutter: f32,
|
||||
pub h_min: f32,
|
||||
pub h_max: f32,
|
||||
pub w_min: f32,
|
||||
pub w_max: f32,
|
||||
pub seed_center: bool,
|
||||
}
|
||||
|
||||
pub fn layout_spiral(images: &[ImageItem], params: LayoutParams) -> (Vec<Placed>, Vec2) {
|
||||
if images.is_empty() {
|
||||
return (Vec::new(), vec2(0.0, 0.0));
|
||||
}
|
||||
|
||||
let eps = f32::EPSILON;
|
||||
let g = params.gutter.max(0.0);
|
||||
let h_min = params.h_min.max(1.0);
|
||||
let h_max = params.h_max.max(h_min);
|
||||
let w_min = params.w_min.max(1.0);
|
||||
let w_max = params.w_max.max(w_min);
|
||||
|
||||
let mut placed = Vec::with_capacity(images.len());
|
||||
|
||||
// Build around origin; normalize at the end.
|
||||
let mut x_min = 0.0f32;
|
||||
let mut x_max = 0.0f32;
|
||||
let mut y_min = 0.0f32;
|
||||
let mut y_max = 0.0f32;
|
||||
|
||||
// dir: 0 right-col, 1 top-row, 2 left-col, 3 bottom-row
|
||||
let mut dir = 0usize;
|
||||
let mut i = 0usize;
|
||||
|
||||
// Optional seed: center a single image
|
||||
if params.seed_center && i < images.len() {
|
||||
let ar = images[i].ar.max(eps);
|
||||
let h = ((h_min + h_max) * 0.5).clamp(h_min, h_max);
|
||||
let w = ar * h;
|
||||
|
||||
let rect = Rect::from_center_size(pos2(0.0, 0.0), vec2(w, h));
|
||||
placed.push(Placed { texture: images[i].texture, rect });
|
||||
|
||||
x_min = rect.min.x;
|
||||
x_max = rect.max.x;
|
||||
y_min = rect.min.y;
|
||||
y_max = rect.max.y;
|
||||
|
||||
i += 1;
|
||||
dir = 1; // start by adding a row above
|
||||
} else {
|
||||
// ensure non-empty bbox for the first strip
|
||||
x_min = 0.0; x_max = 1.0; y_min = 0.0; y_max = 1.0;
|
||||
}
|
||||
|
||||
// --- helpers -------------------------------------------------------------
|
||||
|
||||
// Choose how many items fit and the strip size S (W for column, H for row).
|
||||
fn choose_k<F: Fn(&ImageItem) -> f32>(
|
||||
images: &[ImageItem],
|
||||
L: f32,
|
||||
g: f32,
|
||||
s_min: f32,
|
||||
s_max: f32,
|
||||
weight: F,
|
||||
) -> (usize, f32) {
|
||||
// prefix sums of weights (sum over first k items)
|
||||
let mut pref = Vec::with_capacity(images.len() + 1);
|
||||
pref.push(0.0);
|
||||
for im in images {
|
||||
pref.push(pref.last().copied().unwrap_or(0.0) + weight(im));
|
||||
}
|
||||
|
||||
let k_max = images.len().max(1);
|
||||
let mut chosen_k = 1usize;
|
||||
let mut chosen_s = f32::NAN;
|
||||
|
||||
for k in 1..=k_max {
|
||||
let L_eff = (L - g * (k as f32 - 1.0)).max(1.0);
|
||||
let sum_w = pref[k].max(f32::EPSILON);
|
||||
let s = (L_eff / sum_w).max(1.0);
|
||||
|
||||
if s > s_max && k < k_max {
|
||||
continue; // too big; add one more to thin the strip
|
||||
}
|
||||
if s < s_min {
|
||||
// prefer one fewer if possible
|
||||
if k > 1 {
|
||||
let k2 = k - 1;
|
||||
let L_eff2 = (L - g * (k2 as f32 - 1.0)).max(1.0);
|
||||
let sum_w2 = pref[k2].max(f32::EPSILON);
|
||||
chosen_k = k2;
|
||||
chosen_s = (L_eff2 / sum_w2).max(1.0);
|
||||
} else {
|
||||
chosen_k = 1;
|
||||
chosen_s = s_min;
|
||||
}
|
||||
return (chosen_k, chosen_s);
|
||||
}
|
||||
return (k, s); // within bounds
|
||||
}
|
||||
|
||||
// Fell through: use k_max and clamp
|
||||
let L_eff = (L - g * (k_max as f32 - 1.0)).max(1.0);
|
||||
let sum_w = pref[k_max].max(f32::EPSILON);
|
||||
let s = (L_eff / sum_w).clamp(s_min, s_max);
|
||||
(k_max, s)
|
||||
}
|
||||
|
||||
// Place a column (top→bottom). Returns the new right/left edge.
|
||||
fn place_column(
|
||||
placed: &mut Vec<Placed>,
|
||||
strip: &[ImageItem],
|
||||
W: f32,
|
||||
x: f32,
|
||||
y_top: f32,
|
||||
g: f32,
|
||||
) -> f32 {
|
||||
let mut y = y_top;
|
||||
for (idx, im) in strip.iter().enumerate() {
|
||||
let h = (W / im.ar.max(f32::EPSILON)).max(1.0);
|
||||
let rect = Rect::from_min_size(pos2(x, y), vec2(W, h));
|
||||
placed.push(Placed { texture: im.texture, rect });
|
||||
y += h;
|
||||
if idx + 1 != strip.len() { y += g; }
|
||||
}
|
||||
x + W
|
||||
}
|
||||
|
||||
// Place a row (left→right). Returns the new top/bottom edge.
|
||||
fn place_row(
|
||||
placed: &mut Vec<Placed>,
|
||||
strip: &[ImageItem],
|
||||
H: f32,
|
||||
x_left: f32,
|
||||
y: f32,
|
||||
g: f32,
|
||||
) -> f32 {
|
||||
let mut x = x_left;
|
||||
for (idx, im) in strip.iter().enumerate() {
|
||||
let w = (im.ar.max(f32::EPSILON) * H).max(1.0);
|
||||
let rect = Rect::from_min_size(pos2(x, y), vec2(w, H));
|
||||
placed.push(Placed { texture: im.texture, rect });
|
||||
x += w;
|
||||
if idx + 1 != strip.len() { x += g; }
|
||||
}
|
||||
y + H
|
||||
}
|
||||
|
||||
// --- main loop -----------------------------------------------------------
|
||||
|
||||
while i < images.len() {
|
||||
let remaining = &images[i..];
|
||||
|
||||
if dir % 2 == 0 {
|
||||
// COLUMN (dir 0: right, 2: left)
|
||||
let L = (y_max - y_min).max(1.0);
|
||||
let (k, W) = choose_k(
|
||||
remaining,
|
||||
L, g, w_min, w_max,
|
||||
|im| 1.0 / im.ar.max(f32::EPSILON),
|
||||
);
|
||||
|
||||
let x = if dir == 0 { x_max + g } else { x_min - g - W };
|
||||
let new_edge = place_column(&mut placed, &remaining[..k], W, x, y_min, g);
|
||||
if dir == 0 { x_max = new_edge; } else { x_min = x; }
|
||||
i += k;
|
||||
} else {
|
||||
// ROW (dir 1: top, 3: bottom)
|
||||
let L = (x_max - x_min).max(1.0);
|
||||
let (k, H) = choose_k(
|
||||
remaining,
|
||||
L, g, h_min, h_max,
|
||||
|im| im.ar.max(f32::EPSILON),
|
||||
);
|
||||
|
||||
let y = if dir == 1 { y_max + g } else { y_min - g - H };
|
||||
let new_edge = place_row(&mut placed, &remaining[..k], H, x_min, y, g);
|
||||
if dir == 1 { y_max = new_edge; } else { y_min = y; }
|
||||
i += k;
|
||||
}
|
||||
|
||||
dir = (dir + 1) % 4;
|
||||
}
|
||||
|
||||
// Normalize so bbox top-left is (0,0)
|
||||
let shift = vec2(-x_min, -y_min);
|
||||
for p in &mut placed {
|
||||
p.rect = p.rect.translate(shift);
|
||||
}
|
||||
let total_size = vec2(x_max - x_min, y_max - y_min);
|
||||
(placed, total_size)
|
||||
}
|
||||
|
||||
pub fn spiral_gallery(ui: &mut egui::Ui, images: &[ImageItem], params: LayoutParams) {
|
||||
use egui::{ScrollArea, Stroke};
|
||||
|
||||
let (placed, size) = layout_spiral(images, params);
|
||||
|
||||
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
|
||||
let (rect, _resp) = ui.allocate_exact_size(size, Sense::hover());
|
||||
let painter = ui.painter_at(rect);
|
||||
painter.rect_stroke(
|
||||
Rect::from_min_size(rect.min, size),
|
||||
0.0,
|
||||
Stroke::new(1.0, Color32::DARK_GRAY),
|
||||
);
|
||||
|
||||
let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0));
|
||||
for p in &placed {
|
||||
let r = Rect::from_min_max(rect.min + p.rect.min.to_vec2(),
|
||||
rect.min + p.rect.max.to_vec2());
|
||||
painter.image(p.texture, r, uv, Color32::WHITE);
|
||||
}
|
||||
});
|
||||
}
|
||||
118
crates/notedeck_ui/src/media/viewer.rs
Normal file
118
crates/notedeck_ui/src/media/viewer.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use egui::{pos2, Color32, Rect};
|
||||
use notedeck::{ImageType, Images};
|
||||
|
||||
/// State used in the MediaViewer ui widget.
|
||||
///
|
||||
#[derive(Default)]
|
||||
pub struct MediaViewerState {
|
||||
pub urls: Vec<String>,
|
||||
}
|
||||
|
||||
/// A panning, scrolling, optionally fullscreen, and tiling media viewer
|
||||
pub struct MediaViewer<'a> {
|
||||
state: &'a MediaViewerState,
|
||||
fullscreen: bool,
|
||||
}
|
||||
|
||||
impl<'a> MediaViewer<'a> {
|
||||
pub fn new(state: &'a MediaViewerState) -> Self {
|
||||
let fullscreen = false;
|
||||
Self { state, fullscreen }
|
||||
}
|
||||
|
||||
pub fn fullscreen(mut self, enable: bool) -> Self {
|
||||
self.fullscreen = enable;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn ui(&self, images: &mut Images, ui: &mut egui::Ui) {
|
||||
if self.fullscreen {
|
||||
egui::Window::new("Media Viewer")
|
||||
.title_bar(false)
|
||||
.fixed_size(ui.ctx().screen_rect().size())
|
||||
.fixed_pos(ui.ctx().screen_rect().min)
|
||||
.frame(egui::Frame::NONE)
|
||||
.show(ui.ctx(), |ui| self.ui_content(images, ui));
|
||||
} else {
|
||||
self.ui_content(images, ui);
|
||||
}
|
||||
}
|
||||
|
||||
fn ui_content(&self, images: &mut Images, ui: &mut egui::Ui) {
|
||||
let avail_rect = ui.available_rect_before_wrap();
|
||||
|
||||
// TODO: id_salt
|
||||
let id = ui.id().with("media_viewer");
|
||||
let mut scene_rect = ui.ctx().data(|d| d.get_temp(id)).unwrap_or(avail_rect);
|
||||
let prev = scene_rect;
|
||||
|
||||
// Draw background
|
||||
ui.painter()
|
||||
.rect_filled(avail_rect, 0.0, egui::Color32::from_black_alpha(128));
|
||||
|
||||
egui::Scene::new()
|
||||
.zoom_range(0.0..=10.0) // enhance 🔬
|
||||
.show(ui, &mut scene_rect, |ui| {
|
||||
self.render_image_tiles(images, ui);
|
||||
});
|
||||
|
||||
if scene_rect != prev {
|
||||
ui.ctx().data_mut(|d| d.insert_temp(id, scene_rect));
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Tile a scene with images.
|
||||
///
|
||||
/// TODO(jb55): Let's improve image tiling over time, spiraling outward. We
|
||||
/// should have a way to click "next" and have the scene smoothly transition and
|
||||
/// focus on the next image
|
||||
fn render_image_tiles(&self, images: &mut Images, ui: &mut egui::Ui) {
|
||||
for url in &self.state.urls {
|
||||
// fetch image texture
|
||||
let Some(texture) = images.latest_texture(ui, url, ImageType::Content(None)) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// the area the next image will be put in.
|
||||
let mut img_rect = ui.available_rect_before_wrap();
|
||||
if !ui.is_rect_visible(img_rect) {
|
||||
// just stop rendering images if we're going out of the scene
|
||||
// basic culling when we have lots of images
|
||||
break;
|
||||
}
|
||||
|
||||
{
|
||||
let size = texture.size_vec2();
|
||||
img_rect.set_height(size.y);
|
||||
img_rect.set_width(size.x);
|
||||
let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0));
|
||||
|
||||
// image actions
|
||||
/*
|
||||
let response = ui.interact(
|
||||
render_rect,
|
||||
carousel_id.with("img"),
|
||||
Sense::click(),
|
||||
);
|
||||
|
||||
if response.clicked() {
|
||||
ui.data_mut(|data| {
|
||||
data.insert_temp(carousel_id.with("show_popup"), true);
|
||||
});
|
||||
} else if background_response.clicked() || response.clicked_elsewhere() {
|
||||
ui.data_mut(|data| {
|
||||
data.insert_temp(carousel_id.with("show_popup"), false);
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
// Paint image
|
||||
ui.painter()
|
||||
.image(texture.id(), img_rect, uv, Color32::WHITE);
|
||||
|
||||
ui.advance_cursor_after_rect(img_rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user