notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 379d6c03076be50799fd61bee051d524bdd34461
parent 258ac3de297ebc8ab50cff862ee3a53425ef2d8c
Author: kernelkind <kernelkind@gmail.com>
Date:   Tue, 29 Apr 2025 11:40:55 -0400

notedeck_ui: move carousel to `note/media.rs`

Signed-off-by: kernelkind <kernelkind@gmail.com>

Diffstat:
Mcrates/notedeck_ui/src/note/contents.rs | 272++-----------------------------------------------------------------------------
Acrates/notedeck_ui/src/note/media.rs | 271+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/notedeck_ui/src/note/mod.rs | 1+
3 files changed, 276 insertions(+), 268 deletions(-)

diff --git a/crates/notedeck_ui/src/note/contents.rs b/crates/notedeck_ui/src/note/contents.rs @@ -1,16 +1,16 @@ use crate::{ - gif::{handle_repaint, retrieve_latest_texture}, - images::{render_images, ImageType}, jobs::JobsCache, note::{NoteAction, NoteOptions, NoteResponse, NoteView}, }; -use egui::{Button, Color32, Hyperlink, Image, Response, RichText, Sense, Window}; +use egui::{Color32, Hyperlink, RichText}; use enostr::KeypairUnowned; use nostrdb::{BlockType, Mention, Note, NoteKey, Transaction}; use tracing::warn; -use notedeck::{supported_mime_hosted_at_url, Images, MediaCacheType, NoteContext}; +use notedeck::{supported_mime_hosted_at_url, MediaCacheType, NoteContext}; + +use super::media::image_carousel; pub struct NoteContents<'a, 'd> { note_context: &'a mut NoteContext<'d>, @@ -295,267 +295,3 @@ fn rot13(input: &str) -> String { }) .collect() } - -fn image_carousel( - ui: &mut egui::Ui, - img_cache: &mut Images, - images: Vec<(String, MediaCacheType)>, - carousel_id: egui::Id, -) { - // let's make sure everything is within our area - - let height = 360.0; - let width = ui.available_size().x; - let spinsz = if height > width { width } else { height }; - - let show_popup = ui.ctx().memory(|mem| { - mem.data - .get_temp(carousel_id.with("show_popup")) - .unwrap_or(false) - }); - - let current_image = show_popup.then(|| { - ui.ctx().memory(|mem| { - mem.data - .get_temp::<(String, MediaCacheType)>(carousel_id.with("current_image")) - .unwrap_or_else(|| (images[0].0.clone(), images[0].1)) - }) - }); - - ui.add_sized([width, height], |ui: &mut egui::Ui| { - egui::ScrollArea::horizontal() - .id_salt(carousel_id) - .show(ui, |ui| { - ui.horizontal(|ui| { - for (image, cache_type) in images { - render_images( - ui, - img_cache, - &image, - ImageType::Content, - cache_type, - |ui| { - ui.allocate_space(egui::vec2(spinsz, spinsz)); - }, - |ui, _| { - ui.allocate_space(egui::vec2(spinsz, spinsz)); - }, - |ui, url, renderable_media, gifs| { - let texture = handle_repaint( - ui, - retrieve_latest_texture(&image, gifs, renderable_media), - ); - let img_resp = ui.add( - Button::image( - Image::new(texture) - .max_height(height) - .corner_radius(5.0) - .fit_to_original_size(1.0), - ) - .frame(false), - ); - - if img_resp.clicked() { - ui.ctx().memory_mut(|mem| { - mem.data.insert_temp(carousel_id.with("show_popup"), true); - mem.data.insert_temp( - carousel_id.with("current_image"), - (image.clone(), cache_type), - ); - }); - } - - copy_link(url, img_resp); - }, - ); - } - }) - .response - }) - .inner - }); - - if show_popup { - let current_image = current_image - .as_ref() - .expect("the image was actually clicked"); - let image = current_image.clone().0; - let cache_type = current_image.clone().1; - - Window::new("image_popup") - .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| { - let screen_rect = ui.ctx().screen_rect(); - - // escape - if ui.input(|i| i.key_pressed(egui::Key::Escape)) { - ui.ctx().memory_mut(|mem| { - mem.data.insert_temp(carousel_id.with("show_popup"), false); - }); - } - - // background - ui.painter() - .rect_filled(screen_rect, 0.0, Color32::from_black_alpha(230)); - - // zoom init - let zoom_id = carousel_id.with("zoom_level"); - let mut zoom = ui - .ctx() - .memory(|mem| mem.data.get_temp(zoom_id).unwrap_or(1.0_f32)); - - // pan init - let pan_id = carousel_id.with("pan_offset"); - let mut pan_offset = ui - .ctx() - .memory(|mem| mem.data.get_temp(pan_id).unwrap_or(egui::Vec2::ZERO)); - - // zoom & scroll - if ui.input(|i| i.pointer.hover_pos()).is_some() { - let scroll_delta = ui.input(|i| i.smooth_scroll_delta); - if scroll_delta.y != 0.0 { - let zoom_factor = if scroll_delta.y > 0.0 { 1.05 } else { 0.95 }; - zoom *= zoom_factor; - zoom = zoom.clamp(0.1, 5.0); - - if zoom <= 1.0 { - pan_offset = egui::Vec2::ZERO; - } - - ui.ctx().memory_mut(|mem| { - mem.data.insert_temp(zoom_id, zoom); - mem.data.insert_temp(pan_id, pan_offset); - }); - } - } - - ui.centered_and_justified(|ui| { - render_images( - ui, - img_cache, - &image, - ImageType::Content, - cache_type, - |ui| { - ui.allocate_space(egui::vec2(spinsz, spinsz)); - }, - |ui, _| { - ui.allocate_space(egui::vec2(spinsz, spinsz)); - }, - |ui, url, renderable_media, gifs| { - let texture = handle_repaint( - ui, - retrieve_latest_texture(&image, gifs, renderable_media), - ); - - let texture_size = texture.size_vec2(); - let screen_size = screen_rect.size(); - let scale = (screen_size.x / texture_size.x) - .min(screen_size.y / texture_size.y) - .min(1.0); - let scaled_size = texture_size * scale * zoom; - - let visible_width = scaled_size.x.min(screen_size.x); - let visible_height = scaled_size.y.min(screen_size.y); - - let max_pan_x = ((scaled_size.x - visible_width) / 2.0).max(0.0); - let max_pan_y = ((scaled_size.y - visible_height) / 2.0).max(0.0); - - if max_pan_x > 0.0 { - pan_offset.x = pan_offset.x.clamp(-max_pan_x, max_pan_x); - } else { - pan_offset.x = 0.0; - } - - if max_pan_y > 0.0 { - pan_offset.y = pan_offset.y.clamp(-max_pan_y, max_pan_y); - } else { - pan_offset.y = 0.0; - } - - let (rect, response) = ui.allocate_exact_size( - egui::vec2(visible_width, visible_height), - egui::Sense::click_and_drag(), - ); - - let uv_min = egui::pos2( - 0.5 - (visible_width / scaled_size.x) / 2.0 - + pan_offset.x / scaled_size.x, - 0.5 - (visible_height / scaled_size.y) / 2.0 - + pan_offset.y / scaled_size.y, - ); - - let uv_max = egui::pos2( - uv_min.x + visible_width / scaled_size.x, - uv_min.y + visible_height / scaled_size.y, - ); - - let uv = egui::Rect::from_min_max(uv_min, uv_max); - - ui.painter() - .image(texture.id(), rect, uv, egui::Color32::WHITE); - let img_rect = ui.allocate_rect(rect, Sense::click()); - - if img_rect.clicked() { - ui.ctx().memory_mut(|mem| { - mem.data.insert_temp(carousel_id.with("show_popup"), true); - }); - } else if img_rect.clicked_elsewhere() { - ui.ctx().memory_mut(|mem| { - mem.data.insert_temp(carousel_id.with("show_popup"), false); - }); - } - - // Handle dragging for pan - if response.dragged() { - let delta = response.drag_delta(); - - pan_offset.x -= delta.x; - pan_offset.y -= delta.y; - - if max_pan_x > 0.0 { - pan_offset.x = pan_offset.x.clamp(-max_pan_x, max_pan_x); - } else { - pan_offset.x = 0.0; - } - - if max_pan_y > 0.0 { - pan_offset.y = pan_offset.y.clamp(-max_pan_y, max_pan_y); - } else { - pan_offset.y = 0.0; - } - - ui.ctx().memory_mut(|mem| { - mem.data.insert_temp(pan_id, pan_offset); - }); - } - - // reset zoom on double-click - if response.double_clicked() { - pan_offset = egui::Vec2::ZERO; - zoom = 1.0; - ui.ctx().memory_mut(|mem| { - mem.data.insert_temp(pan_id, pan_offset); - mem.data.insert_temp(zoom_id, zoom); - }); - } - - copy_link(url, response); - }, - ); - }); - }); - } -} - -fn copy_link(url: &str, img_resp: Response) { - img_resp.context_menu(|ui| { - if ui.button("Copy Link").clicked() { - ui.ctx().copy_text(url.to_owned()); - ui.close_menu(); - } - }); -} diff --git a/crates/notedeck_ui/src/note/media.rs b/crates/notedeck_ui/src/note/media.rs @@ -0,0 +1,271 @@ +use egui::{Button, Color32, Image, Response, Sense, Window}; +use notedeck::{Images, MediaCacheType}; + +use crate::{ + gif::{handle_repaint, retrieve_latest_texture}, + images::{render_images, ImageType}, +}; + +pub(crate) fn image_carousel( + ui: &mut egui::Ui, + img_cache: &mut Images, + images: Vec<(String, MediaCacheType)>, + carousel_id: egui::Id, +) { + // let's make sure everything is within our area + + let height = 360.0; + let width = ui.available_size().x; + let spinsz = if height > width { width } else { height }; + + let show_popup = ui.ctx().memory(|mem| { + mem.data + .get_temp(carousel_id.with("show_popup")) + .unwrap_or(false) + }); + + let current_image = show_popup.then(|| { + ui.ctx().memory(|mem| { + mem.data + .get_temp::<(String, MediaCacheType)>(carousel_id.with("current_image")) + .unwrap_or_else(|| (images[0].0.clone(), images[0].1)) + }) + }); + + ui.add_sized([width, height], |ui: &mut egui::Ui| { + egui::ScrollArea::horizontal() + .id_salt(carousel_id) + .show(ui, |ui| { + ui.horizontal(|ui| { + for (image, cache_type) in images { + render_images( + ui, + img_cache, + &image, + ImageType::Content, + cache_type, + |ui| { + ui.allocate_space(egui::vec2(spinsz, spinsz)); + }, + |ui, _| { + ui.allocate_space(egui::vec2(spinsz, spinsz)); + }, + |ui, url, renderable_media, gifs| { + let texture = handle_repaint( + ui, + retrieve_latest_texture(&image, gifs, renderable_media), + ); + let img_resp = ui.add( + Button::image( + Image::new(texture) + .max_height(height) + .corner_radius(5.0) + .fit_to_original_size(1.0), + ) + .frame(false), + ); + + if img_resp.clicked() { + ui.ctx().memory_mut(|mem| { + mem.data.insert_temp(carousel_id.with("show_popup"), true); + mem.data.insert_temp( + carousel_id.with("current_image"), + (image.clone(), cache_type), + ); + }); + } + + copy_link(url, img_resp); + }, + ); + } + }) + .response + }) + .inner + }); + + if show_popup { + let current_image = current_image + .as_ref() + .expect("the image was actually clicked"); + let image = current_image.clone().0; + let cache_type = current_image.clone().1; + + Window::new("image_popup") + .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| { + let screen_rect = ui.ctx().screen_rect(); + + // escape + if ui.input(|i| i.key_pressed(egui::Key::Escape)) { + ui.ctx().memory_mut(|mem| { + mem.data.insert_temp(carousel_id.with("show_popup"), false); + }); + } + + // background + ui.painter() + .rect_filled(screen_rect, 0.0, Color32::from_black_alpha(230)); + + // zoom init + let zoom_id = carousel_id.with("zoom_level"); + let mut zoom = ui + .ctx() + .memory(|mem| mem.data.get_temp(zoom_id).unwrap_or(1.0_f32)); + + // pan init + let pan_id = carousel_id.with("pan_offset"); + let mut pan_offset = ui + .ctx() + .memory(|mem| mem.data.get_temp(pan_id).unwrap_or(egui::Vec2::ZERO)); + + // zoom & scroll + if ui.input(|i| i.pointer.hover_pos()).is_some() { + let scroll_delta = ui.input(|i| i.smooth_scroll_delta); + if scroll_delta.y != 0.0 { + let zoom_factor = if scroll_delta.y > 0.0 { 1.05 } else { 0.95 }; + zoom *= zoom_factor; + zoom = zoom.clamp(0.1, 5.0); + + if zoom <= 1.0 { + pan_offset = egui::Vec2::ZERO; + } + + ui.ctx().memory_mut(|mem| { + mem.data.insert_temp(zoom_id, zoom); + mem.data.insert_temp(pan_id, pan_offset); + }); + } + } + + ui.centered_and_justified(|ui| { + render_images( + ui, + img_cache, + &image, + ImageType::Content, + cache_type, + |ui| { + ui.allocate_space(egui::vec2(spinsz, spinsz)); + }, + |ui, _| { + ui.allocate_space(egui::vec2(spinsz, spinsz)); + }, + |ui, url, renderable_media, gifs| { + let texture = handle_repaint( + ui, + retrieve_latest_texture(&image, gifs, renderable_media), + ); + + let texture_size = texture.size_vec2(); + let screen_size = screen_rect.size(); + let scale = (screen_size.x / texture_size.x) + .min(screen_size.y / texture_size.y) + .min(1.0); + let scaled_size = texture_size * scale * zoom; + + let visible_width = scaled_size.x.min(screen_size.x); + let visible_height = scaled_size.y.min(screen_size.y); + + let max_pan_x = ((scaled_size.x - visible_width) / 2.0).max(0.0); + let max_pan_y = ((scaled_size.y - visible_height) / 2.0).max(0.0); + + if max_pan_x > 0.0 { + pan_offset.x = pan_offset.x.clamp(-max_pan_x, max_pan_x); + } else { + pan_offset.x = 0.0; + } + + if max_pan_y > 0.0 { + pan_offset.y = pan_offset.y.clamp(-max_pan_y, max_pan_y); + } else { + pan_offset.y = 0.0; + } + + let (rect, response) = ui.allocate_exact_size( + egui::vec2(visible_width, visible_height), + egui::Sense::click_and_drag(), + ); + + let uv_min = egui::pos2( + 0.5 - (visible_width / scaled_size.x) / 2.0 + + pan_offset.x / scaled_size.x, + 0.5 - (visible_height / scaled_size.y) / 2.0 + + pan_offset.y / scaled_size.y, + ); + + let uv_max = egui::pos2( + uv_min.x + visible_width / scaled_size.x, + uv_min.y + visible_height / scaled_size.y, + ); + + let uv = egui::Rect::from_min_max(uv_min, uv_max); + + ui.painter() + .image(texture.id(), rect, uv, egui::Color32::WHITE); + let img_rect = ui.allocate_rect(rect, Sense::click()); + + if img_rect.clicked() { + ui.ctx().memory_mut(|mem| { + mem.data.insert_temp(carousel_id.with("show_popup"), true); + }); + } else if img_rect.clicked_elsewhere() { + ui.ctx().memory_mut(|mem| { + mem.data.insert_temp(carousel_id.with("show_popup"), false); + }); + } + + // Handle dragging for pan + if response.dragged() { + let delta = response.drag_delta(); + + pan_offset.x -= delta.x; + pan_offset.y -= delta.y; + + if max_pan_x > 0.0 { + pan_offset.x = pan_offset.x.clamp(-max_pan_x, max_pan_x); + } else { + pan_offset.x = 0.0; + } + + if max_pan_y > 0.0 { + pan_offset.y = pan_offset.y.clamp(-max_pan_y, max_pan_y); + } else { + pan_offset.y = 0.0; + } + + ui.ctx().memory_mut(|mem| { + mem.data.insert_temp(pan_id, pan_offset); + }); + } + + // reset zoom on double-click + if response.double_clicked() { + pan_offset = egui::Vec2::ZERO; + zoom = 1.0; + ui.ctx().memory_mut(|mem| { + mem.data.insert_temp(pan_id, pan_offset); + mem.data.insert_temp(zoom_id, zoom); + }); + } + + copy_link(url, response); + }, + ); + }); + }); + } +} + +fn copy_link(url: &str, img_resp: Response) { + img_resp.context_menu(|ui| { + if ui.button("Copy Link").clicked() { + ui.ctx().copy_text(url.to_owned()); + ui.close_menu(); + } + }); +} diff --git a/crates/notedeck_ui/src/note/mod.rs b/crates/notedeck_ui/src/note/mod.rs @@ -1,5 +1,6 @@ pub mod contents; pub mod context; +pub mod media; pub mod options; pub mod reply_description;