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:
@@ -1,19 +1,15 @@
|
||||
use std::cell::OnceCell;
|
||||
|
||||
use crate::{
|
||||
blur::imeta_blurhashes,
|
||||
jobs::JobsCache,
|
||||
note::{NoteAction, NoteOptions, NoteResponse, NoteView},
|
||||
secondary_label,
|
||||
};
|
||||
use notedeck::{JobsCache, RenderableMedia};
|
||||
|
||||
use egui::{Color32, Hyperlink, RichText};
|
||||
use nostrdb::{BlockType, Mention, Note, NoteKey, Transaction};
|
||||
use tracing::warn;
|
||||
|
||||
use notedeck::{IsFollowing, NoteCache, NoteContext};
|
||||
|
||||
use super::media::{find_renderable_media, image_carousel, RenderableMedia};
|
||||
use super::media::image_carousel;
|
||||
use notedeck::{update_imeta_blurhashes, IsFollowing, NoteCache, NoteContext};
|
||||
|
||||
pub struct NoteContents<'a, 'd> {
|
||||
note_context: &'a mut NoteContext<'d>,
|
||||
@@ -127,11 +123,11 @@ pub fn render_note_preview(
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[profiling::function]
|
||||
pub fn render_note_contents(
|
||||
pub fn render_note_contents<'a>(
|
||||
ui: &mut egui::Ui,
|
||||
note_context: &mut NoteContext,
|
||||
txn: &Transaction,
|
||||
note: &Note,
|
||||
note: &'a Note,
|
||||
options: NoteOptions,
|
||||
jobs: &mut JobsCache,
|
||||
) -> NoteResponse {
|
||||
@@ -152,7 +148,6 @@ pub fn render_note_contents(
|
||||
}
|
||||
|
||||
let mut supported_medias: Vec<RenderableMedia> = vec![];
|
||||
let blurhashes = OnceCell::new();
|
||||
|
||||
let response = ui.horizontal_wrapped(|ui| {
|
||||
let blocks = if let Ok(blocks) = note_context.ndb.get_blocks_by_key(txn, note_key) {
|
||||
@@ -223,15 +218,15 @@ pub fn render_note_contents(
|
||||
let mut found_supported = || -> bool {
|
||||
let url = block.as_str();
|
||||
|
||||
let blurs = blurhashes.get_or_init(|| imeta_blurhashes(note));
|
||||
if !note_context.img_cache.metadata.contains_key(url) {
|
||||
update_imeta_blurhashes(note, &mut note_context.img_cache.metadata);
|
||||
}
|
||||
|
||||
let Some(media_type) =
|
||||
find_renderable_media(&mut note_context.img_cache.urls, blurs, url)
|
||||
else {
|
||||
let Some(media) = note_context.img_cache.get_renderable_media(url) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
supported_medias.push(media_type);
|
||||
supported_medias.push(media);
|
||||
true
|
||||
};
|
||||
|
||||
@@ -311,6 +306,7 @@ pub fn render_note_contents(
|
||||
.key
|
||||
.pubkey
|
||||
.bytes();
|
||||
|
||||
let trusted_media = is_self
|
||||
|| note_context
|
||||
.accounts
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
use std::{collections::HashMap, path::Path};
|
||||
use std::path::Path;
|
||||
|
||||
use egui::{
|
||||
Button, Color32, Context, CornerRadius, FontId, Image, Response, RichText, Sense,
|
||||
TextureHandle, UiBuilder, Window,
|
||||
};
|
||||
use egui::{Button, Color32, Context, CornerRadius, FontId, Image, Response, TextureHandle};
|
||||
use notedeck::{
|
||||
fonts::get_font_size, note::MediaAction, show_one_error_message, supported_mime_hosted_at_url,
|
||||
tr, GifState, GifStateMap, Images, JobPool, Localization, MediaCache, MediaCacheType,
|
||||
NotedeckTextStyle, TexturedImage, TexturesCache, UrlMimes,
|
||||
compute_blurhash, fonts::get_font_size, show_one_error_message, tr, BlurhashParams,
|
||||
GifStateMap, Images, Job, JobId, JobParams, JobPool, JobState, JobsCache, Localization,
|
||||
MediaAction, MediaCacheType, NotedeckTextStyle, ObfuscationType, PointDimensions,
|
||||
RenderableMedia, TexturedImage, TexturesCache,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app_images,
|
||||
blur::{compute_blurhash, Blur, ObfuscationType, PointDimensions},
|
||||
colors::PINK,
|
||||
gif::{handle_repaint, retrieve_latest_texture},
|
||||
images::{fetch_no_pfp_promise, get_render_state, ImageType},
|
||||
jobs::{BlurhashParams, Job, JobId, JobParams, JobState, JobsCache},
|
||||
AnimationHelper, PulseAlpha,
|
||||
};
|
||||
use notedeck::media::gif::ensure_latest_texture;
|
||||
use notedeck::media::images::{fetch_no_pfp_promise, ImageType};
|
||||
|
||||
use crate::{app_images, AnimationHelper, PulseAlpha};
|
||||
|
||||
pub enum MediaViewAction {
|
||||
/// Used to handle escape presses when the media viewer is open
|
||||
EscapePressed,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn image_carousel(
|
||||
@@ -36,7 +34,6 @@ pub(crate) fn image_carousel(
|
||||
let height = 360.0;
|
||||
let width = ui.available_width();
|
||||
|
||||
let show_popup = get_show_popup(ui, popup_id(carousel_id));
|
||||
let mut action = None;
|
||||
|
||||
//let has_touch_screen = ui.ctx().input(|i| i.has_touch_screen());
|
||||
@@ -46,6 +43,7 @@ pub(crate) fn image_carousel(
|
||||
.id_salt(carousel_id)
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
let mut media_action: Option<(usize, MediaUIAction)> = None;
|
||||
for (i, media) in medias.iter().enumerate() {
|
||||
let RenderableMedia {
|
||||
url,
|
||||
@@ -57,7 +55,6 @@ pub(crate) fn image_carousel(
|
||||
MediaCacheType::Image => &mut img_cache.static_imgs,
|
||||
MediaCacheType::Gif => &mut img_cache.gifs,
|
||||
};
|
||||
|
||||
let media_state = get_content_media_render_state(
|
||||
ui,
|
||||
job_pool,
|
||||
@@ -68,7 +65,7 @@ pub(crate) fn image_carousel(
|
||||
url,
|
||||
*media_type,
|
||||
&cache.cache_dir,
|
||||
blur_type.clone(),
|
||||
blur_type,
|
||||
);
|
||||
|
||||
if let Some(cur_action) = render_media(
|
||||
@@ -79,43 +76,25 @@ pub(crate) fn image_carousel(
|
||||
height,
|
||||
i18n,
|
||||
) {
|
||||
// clicked the media, lets set the active index
|
||||
if let MediaUIAction::Clicked = cur_action {
|
||||
set_show_popup(ui, popup_id(carousel_id), true);
|
||||
set_selected_index(ui, selection_id(carousel_id), i);
|
||||
}
|
||||
|
||||
action = cur_action.to_media_action(
|
||||
ui.ctx(),
|
||||
url,
|
||||
*media_type,
|
||||
cache,
|
||||
ImageType::Content(Some((width as u32, height as u32))),
|
||||
);
|
||||
media_action = Some((i, cur_action));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((i, media_action)) = &media_action {
|
||||
action = media_action.to_media_action(
|
||||
ui.ctx(),
|
||||
medias,
|
||||
*i,
|
||||
img_cache,
|
||||
ImageType::Content(Some((width as u32, height as u32))),
|
||||
);
|
||||
}
|
||||
})
|
||||
.response
|
||||
})
|
||||
.inner
|
||||
});
|
||||
|
||||
if show_popup {
|
||||
if medias.is_empty() {
|
||||
return None;
|
||||
};
|
||||
|
||||
let current_image_index = update_selected_image_index(ui, carousel_id, medias.len() as i32);
|
||||
|
||||
show_full_screen_media(
|
||||
ui,
|
||||
medias,
|
||||
current_image_index,
|
||||
img_cache,
|
||||
carousel_id,
|
||||
i18n,
|
||||
);
|
||||
}
|
||||
action
|
||||
}
|
||||
|
||||
@@ -130,146 +109,55 @@ impl MediaUIAction {
|
||||
pub fn to_media_action(
|
||||
&self,
|
||||
ctx: &egui::Context,
|
||||
url: &str,
|
||||
cache_type: MediaCacheType,
|
||||
cache: &mut MediaCache,
|
||||
medias: &[RenderableMedia],
|
||||
selected: usize,
|
||||
img_cache: &Images,
|
||||
img_type: ImageType,
|
||||
) -> Option<MediaAction> {
|
||||
match self {
|
||||
MediaUIAction::Clicked => {
|
||||
tracing::debug!("{} clicked", url);
|
||||
None
|
||||
}
|
||||
MediaUIAction::Clicked => Some(MediaAction::ViewMedias(
|
||||
medias.iter().map(|m| m.url.to_owned()).collect(),
|
||||
)),
|
||||
|
||||
MediaUIAction::Unblur => Some(MediaAction::FetchImage {
|
||||
url: url.to_owned(),
|
||||
cache_type,
|
||||
no_pfp_promise: crate::images::fetch_img(
|
||||
MediaUIAction::Unblur => {
|
||||
let url = &medias[selected].url;
|
||||
let cache = img_cache.get_cache(medias[selected].media_type);
|
||||
let cache_type = cache.cache_type;
|
||||
let no_pfp_promise = notedeck::media::images::fetch_img(
|
||||
&cache.cache_dir,
|
||||
ctx,
|
||||
url,
|
||||
img_type,
|
||||
cache_type,
|
||||
),
|
||||
}),
|
||||
);
|
||||
Some(MediaAction::FetchImage {
|
||||
url: url.to_owned(),
|
||||
cache_type,
|
||||
no_pfp_promise,
|
||||
})
|
||||
}
|
||||
|
||||
MediaUIAction::Error => {
|
||||
if !matches!(img_type, ImageType::Profile(_)) {
|
||||
return None;
|
||||
};
|
||||
|
||||
let cache = img_cache.get_cache(medias[selected].media_type);
|
||||
let cache_type = cache.cache_type;
|
||||
Some(MediaAction::FetchImage {
|
||||
url: url.to_owned(),
|
||||
url: medias[selected].url.to_owned(),
|
||||
cache_type,
|
||||
no_pfp_promise: fetch_no_pfp_promise(ctx, cache),
|
||||
})
|
||||
}
|
||||
MediaUIAction::DoneLoading => Some(MediaAction::DoneLoading {
|
||||
url: url.to_owned(),
|
||||
cache_type,
|
||||
url: medias[selected].url.to_owned(),
|
||||
cache_type: img_cache.get_cache(medias[selected].media_type).cache_type,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn show_full_screen_media(
|
||||
ui: &mut egui::Ui,
|
||||
medias: &[RenderableMedia],
|
||||
index: usize,
|
||||
img_cache: &mut Images,
|
||||
carousel_id: egui::Id,
|
||||
i18n: &mut Localization,
|
||||
) {
|
||||
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| {
|
||||
ui.centered_and_justified(|ui| 's: {
|
||||
let image_url = medias[index].url;
|
||||
|
||||
let media_type = medias[index].media_type;
|
||||
tracing::trace!(
|
||||
"show_full_screen_media using img {} @ {} for carousel_id {:?}",
|
||||
image_url,
|
||||
index,
|
||||
carousel_id
|
||||
);
|
||||
|
||||
let cur_state = get_render_state(
|
||||
ui.ctx(),
|
||||
img_cache,
|
||||
media_type,
|
||||
image_url,
|
||||
ImageType::Content(None),
|
||||
);
|
||||
|
||||
let notedeck::TextureState::Loaded(textured_image) = cur_state.texture_state else {
|
||||
break 's;
|
||||
};
|
||||
|
||||
render_full_screen_media(
|
||||
ui,
|
||||
medias.len(),
|
||||
index,
|
||||
textured_image,
|
||||
cur_state.gifs,
|
||||
image_url,
|
||||
carousel_id,
|
||||
i18n,
|
||||
);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn set_selected_index(ui: &mut egui::Ui, sel_id: egui::Id, index: usize) {
|
||||
ui.data_mut(|d| {
|
||||
d.insert_temp(sel_id, index);
|
||||
});
|
||||
}
|
||||
|
||||
fn get_selected_index(ui: &egui::Ui, selection_id: egui::Id) -> usize {
|
||||
ui.data(|d| d.get_temp(selection_id).unwrap_or(0))
|
||||
}
|
||||
|
||||
/// Checks to see if we have any left/right key presses and updates the carousel index
|
||||
fn update_selected_image_index(ui: &mut egui::Ui, carousel_id: egui::Id, num_urls: i32) -> usize {
|
||||
if num_urls > 1 {
|
||||
let (next_image, prev_image) = ui.data(|data| {
|
||||
(
|
||||
data.get_temp(carousel_id.with("next_image"))
|
||||
.unwrap_or_default(),
|
||||
data.get_temp(carousel_id.with("prev_image"))
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
});
|
||||
|
||||
if next_image
|
||||
|| ui.input(|i| i.key_pressed(egui::Key::ArrowRight) || i.key_pressed(egui::Key::L))
|
||||
{
|
||||
let ind = select_next_media(ui, carousel_id, num_urls, 1);
|
||||
tracing::debug!("carousel selecting right {}/{}", ind + 1, num_urls);
|
||||
if next_image {
|
||||
ui.data_mut(|data| data.remove_temp::<bool>(carousel_id.with("next_image")));
|
||||
}
|
||||
ind
|
||||
} else if prev_image
|
||||
|| ui.input(|i| i.key_pressed(egui::Key::ArrowLeft) || i.key_pressed(egui::Key::H))
|
||||
{
|
||||
let ind = select_next_media(ui, carousel_id, num_urls, -1);
|
||||
tracing::debug!("carousel selecting left {}/{}", ind + 1, num_urls);
|
||||
if prev_image {
|
||||
ui.data_mut(|data| data.remove_temp::<bool>(carousel_id.with("prev_image")));
|
||||
}
|
||||
ind
|
||||
} else {
|
||||
get_selected_index(ui, selection_id(carousel_id))
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn get_content_media_render_state<'a>(
|
||||
ui: &mut egui::Ui,
|
||||
@@ -281,11 +169,11 @@ pub fn get_content_media_render_state<'a>(
|
||||
url: &'a str,
|
||||
cache_type: MediaCacheType,
|
||||
cache_dir: &Path,
|
||||
obfuscation_type: ObfuscationType<'a>,
|
||||
obfuscation_type: &'a ObfuscationType,
|
||||
) -> MediaRenderState<'a> {
|
||||
let render_type = if media_trusted {
|
||||
cache.handle_and_get_or_insert_loadable(url, || {
|
||||
crate::images::fetch_img(
|
||||
notedeck::media::images::fetch_img(
|
||||
cache_dir,
|
||||
ui.ctx(),
|
||||
url,
|
||||
@@ -332,7 +220,7 @@ pub fn get_content_media_render_state<'a>(
|
||||
fn get_obfuscated<'a>(
|
||||
ui: &mut egui::Ui,
|
||||
url: &str,
|
||||
obfuscation_type: ObfuscationType<'a>,
|
||||
obfuscation_type: &'a ObfuscationType,
|
||||
job_pool: &'a mut JobPool,
|
||||
jobs: &'a mut JobsCache,
|
||||
height: f32,
|
||||
@@ -342,7 +230,7 @@ fn get_obfuscated<'a>(
|
||||
};
|
||||
|
||||
let params = BlurhashParams {
|
||||
blurhash: renderable_blur.blurhash,
|
||||
blurhash: &renderable_blur.blurhash,
|
||||
url,
|
||||
ctx: ui.ctx(),
|
||||
};
|
||||
@@ -379,336 +267,6 @@ fn get_obfuscated<'a>(
|
||||
ObfuscatedTexture::Blur(texture_handle)
|
||||
}
|
||||
|
||||
// simple selector memory
|
||||
fn select_next_media(
|
||||
ui: &mut egui::Ui,
|
||||
carousel_id: egui::Id,
|
||||
num_urls: i32,
|
||||
direction: i32,
|
||||
) -> usize {
|
||||
let sel_id = selection_id(carousel_id);
|
||||
let current = get_selected_index(ui, sel_id) as i32;
|
||||
let next = current + direction;
|
||||
let next = if next >= num_urls {
|
||||
0
|
||||
} else if next < 0 {
|
||||
num_urls - 1
|
||||
} else {
|
||||
next
|
||||
};
|
||||
|
||||
if next != current {
|
||||
set_selected_index(ui, sel_id, next as usize);
|
||||
}
|
||||
|
||||
next as usize
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_full_screen_media(
|
||||
ui: &mut egui::Ui,
|
||||
num_urls: usize,
|
||||
index: usize,
|
||||
renderable_media: &mut TexturedImage,
|
||||
gifs: &mut HashMap<String, GifState>,
|
||||
image_url: &str,
|
||||
carousel_id: egui::Id,
|
||||
i18n: &mut Localization,
|
||||
) {
|
||||
const TOP_BAR_HEIGHT: f32 = 30.0;
|
||||
const BOTTOM_BAR_HEIGHT: f32 = 60.0;
|
||||
|
||||
let screen_rect = ui.ctx().screen_rect();
|
||||
let screen_size = screen_rect.size();
|
||||
|
||||
// Escape key closes popup
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
// Draw background
|
||||
ui.painter()
|
||||
.rect_filled(screen_rect, 0.0, Color32::from_black_alpha(230));
|
||||
|
||||
let background_response = ui.interact(
|
||||
screen_rect,
|
||||
carousel_id.with("background"),
|
||||
egui::Sense::click(),
|
||||
);
|
||||
|
||||
// Zoom & pan state
|
||||
let zoom_id = carousel_id.with("zoom_level");
|
||||
let pan_id = carousel_id.with("pan_offset");
|
||||
|
||||
let mut zoom: f32 = ui
|
||||
.ctx()
|
||||
.memory(|mem| mem.data.get_temp(zoom_id).unwrap_or(1.0));
|
||||
let mut pan_offset = ui
|
||||
.ctx()
|
||||
.memory(|mem| mem.data.get_temp(pan_id).unwrap_or(egui::Vec2::ZERO));
|
||||
|
||||
// Handle scroll to zoom
|
||||
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 * zoom_factor).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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch image
|
||||
let texture = handle_repaint(
|
||||
ui,
|
||||
retrieve_latest_texture(image_url, gifs, renderable_media),
|
||||
);
|
||||
|
||||
let texture_size = texture.size_vec2();
|
||||
|
||||
let topbar_rect = egui::Rect::from_min_max(
|
||||
screen_rect.min + egui::vec2(0.0, 0.0),
|
||||
screen_rect.min + egui::vec2(screen_size.x, TOP_BAR_HEIGHT),
|
||||
);
|
||||
|
||||
let topbar_response = ui.interact(
|
||||
topbar_rect,
|
||||
carousel_id.with("topbar"),
|
||||
egui::Sense::click(),
|
||||
);
|
||||
|
||||
let mut keep_popup_open = false;
|
||||
if topbar_response.clicked() {
|
||||
keep_popup_open = true;
|
||||
}
|
||||
|
||||
ui.allocate_new_ui(
|
||||
UiBuilder::new()
|
||||
.max_rect(topbar_rect)
|
||||
.layout(egui::Layout::top_down(egui::Align::RIGHT)),
|
||||
|ui| {
|
||||
let color = ui.style().visuals.noninteractive().fg_stroke.color;
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let label_reponse = ui
|
||||
.label(RichText::new(image_url).color(color).small())
|
||||
.on_hover_text(image_url);
|
||||
if label_reponse.double_clicked()
|
||||
|| label_reponse.clicked()
|
||||
|| label_reponse.hovered()
|
||||
{
|
||||
keep_popup_open = true;
|
||||
|
||||
ui.ctx().copy_text(image_url.to_owned());
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Calculate available rect for image
|
||||
let image_rect = egui::Rect::from_min_max(
|
||||
screen_rect.min + egui::vec2(0.0, TOP_BAR_HEIGHT),
|
||||
screen_rect.max - egui::vec2(0.0, BOTTOM_BAR_HEIGHT),
|
||||
);
|
||||
|
||||
let image_area_size = image_rect.size();
|
||||
let scale = (image_area_size.x / texture_size.x)
|
||||
.min(image_area_size.y / texture_size.y)
|
||||
.min(1.0);
|
||||
let scaled_size = texture_size * scale * zoom;
|
||||
|
||||
let visible_width = scaled_size.x.min(image_area_size.x);
|
||||
let visible_height = scaled_size.y.min(image_area_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);
|
||||
|
||||
pan_offset.x = if max_pan_x > 0.0 {
|
||||
pan_offset.x.clamp(-max_pan_x, max_pan_x)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
pan_offset.y = if max_pan_y > 0.0 {
|
||||
pan_offset.y.clamp(-max_pan_y, max_pan_y)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let render_rect = egui::Rect::from_center_size(
|
||||
image_rect.center(),
|
||||
egui::vec2(visible_width, visible_height),
|
||||
);
|
||||
|
||||
// Compute UVs for zoom & pan
|
||||
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,
|
||||
);
|
||||
|
||||
// Paint image
|
||||
ui.painter().image(
|
||||
texture.id(),
|
||||
render_rect,
|
||||
egui::Rect::from_min_max(uv_min, uv_max),
|
||||
Color32::WHITE,
|
||||
);
|
||||
|
||||
// image actions
|
||||
let response = ui.interact(
|
||||
render_rect,
|
||||
carousel_id.with("img"),
|
||||
Sense::click_and_drag(),
|
||||
);
|
||||
|
||||
let swipe_accum_id = carousel_id.with("swipe_accum");
|
||||
let mut swipe_delta = ui.ctx().memory(|mem| {
|
||||
mem.data
|
||||
.get_temp::<egui::Vec2>(swipe_accum_id)
|
||||
.unwrap_or(egui::Vec2::ZERO)
|
||||
});
|
||||
|
||||
// Handle pan via drag
|
||||
if response.dragged() {
|
||||
let delta = response.drag_delta();
|
||||
swipe_delta += delta;
|
||||
ui.ctx().memory_mut(|mem| {
|
||||
mem.data.insert_temp(swipe_accum_id, swipe_delta);
|
||||
});
|
||||
pan_offset -= delta;
|
||||
pan_offset.x = pan_offset.x.clamp(-max_pan_x, max_pan_x);
|
||||
pan_offset.y = pan_offset.y.clamp(-max_pan_y, max_pan_y);
|
||||
ui.ctx()
|
||||
.memory_mut(|mem| mem.data.insert_temp(pan_id, pan_offset));
|
||||
}
|
||||
|
||||
// Double click to reset
|
||||
if response.double_clicked() {
|
||||
zoom = 1.0;
|
||||
pan_offset = egui::Vec2::ZERO;
|
||||
ui.ctx().memory_mut(|mem| {
|
||||
mem.data.insert_temp(pan_id, pan_offset);
|
||||
mem.data.insert_temp(zoom_id, zoom);
|
||||
});
|
||||
}
|
||||
|
||||
let swipe_threshold = 50.0;
|
||||
if response.drag_stopped() {
|
||||
if swipe_delta.x.abs() > swipe_threshold && swipe_delta.y.abs() < swipe_threshold {
|
||||
if swipe_delta.x < 0.0 {
|
||||
ui.ctx().data_mut(|data| {
|
||||
keep_popup_open = true;
|
||||
data.insert_temp(carousel_id.with("next_image"), true);
|
||||
});
|
||||
} else if swipe_delta.x > 0.0 {
|
||||
ui.ctx().data_mut(|data| {
|
||||
keep_popup_open = true;
|
||||
data.insert_temp(carousel_id.with("prev_image"), true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ui.ctx().memory_mut(|mem| {
|
||||
mem.data.remove::<egui::Vec2>(swipe_accum_id);
|
||||
});
|
||||
}
|
||||
|
||||
// bottom bar
|
||||
if num_urls > 1 {
|
||||
let bottom_rect = egui::Rect::from_min_max(
|
||||
screen_rect.max - egui::vec2(screen_size.x, BOTTOM_BAR_HEIGHT),
|
||||
screen_rect.max,
|
||||
);
|
||||
|
||||
let full_response = ui.interact(
|
||||
bottom_rect,
|
||||
carousel_id.with("bottom_bar"),
|
||||
egui::Sense::click(),
|
||||
);
|
||||
|
||||
if full_response.clicked() {
|
||||
keep_popup_open = true;
|
||||
}
|
||||
|
||||
let mut clicked_index: Option<usize> = None;
|
||||
|
||||
#[allow(deprecated)]
|
||||
ui.allocate_ui_at_rect(bottom_rect, |ui| {
|
||||
let dot_radius = 7.0;
|
||||
let dot_spacing = 20.0;
|
||||
let color_active = PINK;
|
||||
let color_inactive: Color32 = ui.style().visuals.widgets.inactive.bg_fill;
|
||||
|
||||
let center = bottom_rect.center();
|
||||
|
||||
for i in 0..num_urls {
|
||||
let distance = egui::vec2(
|
||||
(i as f32 - (num_urls as f32 - 1.0) / 2.0) * dot_spacing,
|
||||
0.0,
|
||||
);
|
||||
let pos = center + distance;
|
||||
|
||||
let circle_color = if i == index {
|
||||
color_active
|
||||
} else {
|
||||
color_inactive
|
||||
};
|
||||
|
||||
let circle_rect = egui::Rect::from_center_size(
|
||||
pos,
|
||||
egui::vec2(dot_radius * 2.0, dot_radius * 2.0),
|
||||
);
|
||||
|
||||
let resp = ui.interact(circle_rect, carousel_id.with(i), egui::Sense::click());
|
||||
|
||||
ui.painter().circle_filled(pos, dot_radius, circle_color);
|
||||
|
||||
if i != index && resp.hovered() {
|
||||
ui.painter()
|
||||
.circle_stroke(pos, dot_radius + 2.0, (1.0, PINK));
|
||||
}
|
||||
|
||||
if resp.clicked() {
|
||||
keep_popup_open = true;
|
||||
if i != index {
|
||||
clicked_index = Some(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(new_index) = clicked_index {
|
||||
ui.ctx().data_mut(|data| {
|
||||
data.insert_temp(selection_id(carousel_id), new_index);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if keep_popup_open || 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);
|
||||
});
|
||||
}
|
||||
|
||||
copy_link(i18n, image_url, &response);
|
||||
}
|
||||
|
||||
fn copy_link(i18n: &mut Localization, url: &str, img_resp: &Response) {
|
||||
img_resp.context_menu(|ui| {
|
||||
if ui
|
||||
@@ -905,12 +463,6 @@ fn render_default_blur_bg(ui: &mut egui::Ui, height: f32, url: &str, shimmer: bo
|
||||
rect
|
||||
}
|
||||
|
||||
pub(crate) struct RenderableMedia<'a> {
|
||||
url: &'a str,
|
||||
media_type: MediaCacheType,
|
||||
obfuscation_type: ObfuscationType<'a>,
|
||||
}
|
||||
|
||||
pub enum MediaRenderState<'a> {
|
||||
ActualImage(&'a mut TexturedImage),
|
||||
Transitioning {
|
||||
@@ -927,14 +479,15 @@ pub enum ObfuscatedTexture<'a> {
|
||||
Default,
|
||||
}
|
||||
|
||||
/*
|
||||
pub(crate) fn find_renderable_media<'a>(
|
||||
urls: &mut UrlMimes,
|
||||
blurhashes: &'a HashMap<&'a str, Blur<'a>>,
|
||||
imeta: &'a HashMap<String, ImageMetadata>,
|
||||
url: &'a str,
|
||||
) -> Option<RenderableMedia<'a>> {
|
||||
) -> Option<RenderableMedia> {
|
||||
let media_type = supported_mime_hosted_at_url(urls, url)?;
|
||||
|
||||
let obfuscation_type = match blurhashes.get(url) {
|
||||
let obfuscation_type = match imeta.get(url) {
|
||||
Some(blur) => ObfuscationType::Blurhash(blur.clone()),
|
||||
None => ObfuscationType::Default,
|
||||
};
|
||||
@@ -945,28 +498,7 @@ pub(crate) fn find_renderable_media<'a>(
|
||||
obfuscation_type,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn selection_id(carousel_id: egui::Id) -> egui::Id {
|
||||
carousel_id.with("sel")
|
||||
}
|
||||
|
||||
/// get the popup carousel window state
|
||||
#[inline]
|
||||
fn get_show_popup(ui: &egui::Ui, popup_id: egui::Id) -> bool {
|
||||
ui.data(|data| data.get_temp(popup_id).unwrap_or(false))
|
||||
}
|
||||
|
||||
/// set the popup carousel window state
|
||||
#[inline]
|
||||
fn set_show_popup(ui: &mut egui::Ui, popup_id: egui::Id, show_popup: bool) {
|
||||
ui.data_mut(|data| data.insert_temp(popup_id, show_popup));
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn popup_id(carousel_id: egui::Id) -> egui::Id {
|
||||
carousel_id.with("show_popup")
|
||||
}
|
||||
*/
|
||||
|
||||
fn render_success_media(
|
||||
ui: &mut egui::Ui,
|
||||
@@ -976,8 +508,8 @@ fn render_success_media(
|
||||
height: f32,
|
||||
i18n: &mut Localization,
|
||||
) -> Response {
|
||||
let texture = handle_repaint(ui, retrieve_latest_texture(url, gifs, tex));
|
||||
let img = texture_to_image(texture, height);
|
||||
let texture = ensure_latest_texture(ui, url, gifs, tex);
|
||||
let img = texture_to_image(&texture, height);
|
||||
let img_resp = ui.add(Button::image(img).frame(false));
|
||||
|
||||
copy_link(i18n, url, &img_resp);
|
||||
|
||||
@@ -4,7 +4,6 @@ pub mod media;
|
||||
pub mod options;
|
||||
pub mod reply_description;
|
||||
|
||||
use crate::jobs::JobsCache;
|
||||
use crate::{app_images, secondary_label};
|
||||
use crate::{
|
||||
profile::name::one_line_display_name_widget, widgets::x_button, ProfilePic, ProfilePreview,
|
||||
@@ -14,13 +13,14 @@ use crate::{
|
||||
pub use contents::{render_note_contents, render_note_preview, NoteContents};
|
||||
pub use context::NoteContextButton;
|
||||
use notedeck::get_current_wallet;
|
||||
use notedeck::note::MediaAction;
|
||||
use notedeck::note::ZapTargetAmount;
|
||||
use notedeck::ui::is_narrow;
|
||||
use notedeck::Accounts;
|
||||
use notedeck::GlobalWallet;
|
||||
use notedeck::Images;
|
||||
use notedeck::JobsCache;
|
||||
use notedeck::Localization;
|
||||
use notedeck::MediaAction;
|
||||
pub use options::NoteOptions;
|
||||
pub use reply_description::reply_desc;
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ use egui::{Label, RichText, Sense};
|
||||
use nostrdb::{NoteReply, Transaction};
|
||||
|
||||
use super::NoteOptions;
|
||||
use crate::{jobs::JobsCache, note::NoteView, Mention};
|
||||
use notedeck::{tr, NoteAction, NoteContext};
|
||||
use crate::{note::NoteView, Mention};
|
||||
use notedeck::{tr, JobsCache, NoteAction, NoteContext};
|
||||
|
||||
// Rich text segment types for internationalized rendering
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
Reference in New Issue
Block a user