diff --git a/src/app.rs b/src/app.rs index 2f8949ee..62299369 100644 --- a/src/app.rs +++ b/src/app.rs @@ -16,12 +16,12 @@ live_design! { use crate::home::home_screen::HomeScreen; use crate::profile::my_profile_screen::MyProfileScreen; use crate::verification_modal::VerificationModal; + use crate::image_viewer::ImageViewer; use crate::login::login_screen::LoginScreen; use crate::shared::popup_list::PopupList; use crate::home::new_message_context_menu::*; use crate::shared::callout_tooltip::CalloutTooltip; - APP_TAB_COLOR = #344054 APP_TAB_COLOR_HOVER = #636e82 APP_TAB_COLOR_ACTIVE = #091 @@ -131,6 +131,8 @@ live_design! { // but beneath the verification modal. new_message_context_menu = { } + image_viewer = {} + // message_source_modal = { // content: { // message_source_modal_inner = {} @@ -174,6 +176,7 @@ impl LiveRegister for App { crate::home::live_design(cx); crate::profile::live_design(cx); crate::login::live_design(cx); + crate::image_viewer::live_design(cx); } } diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 3e663532..85d677dc 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -12,7 +12,7 @@ use matrix_sdk::{room::RoomMember, ruma::{ AudioMessageEventContent, CustomEventContent, EmoteMessageEventContent, FileMessageEventContent, FormattedBody, ImageMessageEventContent, KeyVerificationRequestEventContent, LocationMessageEventContent, MessageFormat, MessageType, NoticeMessageEventContent, RoomMessageEventContent, ServerNoticeMessageEventContent, TextMessageEventContent, VideoMessageEventContent }, ImageInfo, MediaSource }, - sticker::StickerEventContent, Mentions}, matrix_uri::MatrixId, uint, EventId, MatrixToUri, MatrixUri, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedRoomId + sticker::StickerEventContent, Mentions}, matrix_uri::MatrixId, uint, EventId, MatrixToUri, MatrixUri, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId }, OwnedServerName}; use matrix_sdk_ui::timeline::{ self, EventTimelineItem, InReplyToDetails, MemberProfileChange, RepliedToInfo, RoomMembershipChange, TimelineDetails, TimelineEventItemId, TimelineItem, TimelineItemContent, TimelineItemKind, VirtualTimelineItem @@ -20,12 +20,12 @@ use matrix_sdk_ui::timeline::{ use robius_location::Coordinates; use crate::{ - avatar_cache, event_preview::{body_of_timeline_item, text_preview_of_member_profile_change, text_preview_of_other_state, text_preview_of_redacted_message, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, location::{get_latest_location, init_location_subscriber, request_location_update, LocationAction, LocationRequest, LocationUpdate}, media_cache::{MediaCache, MediaCacheEntry}, profile::{ + avatar_cache, event_preview::{body_of_timeline_item, text_preview_of_member_profile_change, text_preview_of_other_state, text_preview_of_redacted_message, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, image_viewer::ImageViewerAction, location::{get_latest_location, init_location_subscriber, request_location_update, LocationAction, LocationRequest, LocationUpdate}, media_cache::{image_viewer_insert_into_cache, insert_into_cache, MediaCache, MediaCacheEntry}, profile::{ user_profile::{AvatarState, ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt}, user_profile_cache, }, shared::{ - avatar::AvatarWidgetRefExt, callout_tooltip::TooltipAction, html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt, RobrixHtmlLinkAction}, jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, popup_list::enqueue_popup_notification, text_or_image::{TextOrImageRef, TextOrImageWidgetRefExt}, typing_animation::TypingAnimationWidgetExt - }, sliding_sync::{get_client, submit_async_request, take_timeline_endpoints, BackwardsPaginateUntilEventRequest, MatrixRequest, PaginationDirection, TimelineRequestSender, UserPowerLevels}, utils::{self, unix_time_millis_to_datetime, ImageFormat, MEDIA_THUMBNAIL_FORMAT} + avatar::AvatarWidgetRefExt, callout_tooltip::TooltipAction, html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt, RobrixHtmlLinkAction}, jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, popup_list::enqueue_popup_notification, text_or_image::{TextOrImageAction, TextOrImageRef, TextOrImageWidgetRefExt, TimelineImageInfo}, typing_animation::TypingAnimationWidgetExt + }, sliding_sync::{get_client, submit_async_request, take_timeline_endpoints, BackwardsPaginateUntilEventRequest, MatrixRequest, PaginationDirection, TimelineRequestSender, UserPowerLevels}, utils::{self, unix_time_millis_to_datetime, ImageFormat} }; use crate::home::event_reaction_list::ReactionListWidgetRefExt; use crate::home::room_read_receipt::AvatarRowWidgetRefExt; @@ -1021,6 +1021,14 @@ impl Widget for RoomScreen { for action in actions { // Handle the highlight animation. let Some(tl) = self.tl_state.as_mut() else { return }; + + if let Some(TextOrImageAction::Click(mxc_uri)) = action.downcast_ref() { + if let MediaCacheEntry::Loaded(image_viewer_image_data) = tl.media_cache.try_get_media_or_fetch(mxc_uri, image_viewer_insert_into_cache) { + cx.action(ImageViewerAction::Show); + cx.action(ImageViewerAction::SetImage(image_viewer_image_data)); + } + } + if let MessageHighlightAnimationState::Pending { item_id } = tl.message_highlight_animation_state { if portal_list.smooth_scroll_reached(actions) { cx.widget_action( @@ -3501,13 +3509,37 @@ fn populate_image_message_content( let mut fully_drawn = false; - // A closure that fetches and shows the image from the given `mxc_uri`, - // marking it as fully drawn if the image was available. - let mut fetch_and_show_image_uri = |cx: &mut Cx2d, mxc_uri: OwnedMxcUri, image_info: Option<&ImageInfo>| { - match media_cache.try_get_media_or_fetch(mxc_uri.clone(), MEDIA_THUMBNAIL_FORMAT.into()) { - (MediaCacheEntry::Loaded(data), _media_format) => { + let mut fetch_and_handle_image = |cx: &mut Cx2d, original_source: &MediaSource, image_info: Option<&ImageInfo>| { + let thumbnail_mxc_uri = image_info.and_then(|info|{info.thumbnail_source.as_ref()}).and_then(|source|{ + match source { + MediaSource::Encrypted(encrypted) => { + text_or_image_ref.show_text( + cx, + format!("{body}\n\n[TODO] fetch encrypted image at {:?}", encrypted.url) + ); + None + } + MediaSource::Plain(mxc_uri) => { + Some(mxc_uri) + } + } + }); + + let MediaSource::Plain(original_mxc_uri) = original_source else { return }; + + let timeline_mxc_uri = thumbnail_mxc_uri.unwrap_or(original_mxc_uri); + + media_cache.image_set_keys(original_mxc_uri, thumbnail_mxc_uri); + + match media_cache.try_get_media_or_fetch(timeline_mxc_uri, insert_into_cache) { + MediaCacheEntry::Loaded(timeline_image_data) => { + let timeline_image_info = TimelineImageInfo { + timeline_image_data: timeline_image_data.clone(), + original_mxc_uri: original_mxc_uri.clone() + }; + text_or_image_ref.set_status_to_image(timeline_image_info); let show_image_result = text_or_image_ref.show_image(cx, |cx, img| { - utils::load_png_or_jpg(&img, cx, &data) + utils::load_png_or_jpg(&img, cx, &timeline_image_data) .map(|()| img.size_in_pixels(cx).unwrap_or_default()) }); if let Err(e) = show_image_result { @@ -3515,11 +3547,10 @@ fn populate_image_message_content( error!("{err_str}"); text_or_image_ref.show_text(cx, &err_str); } - // We're done drawing the image, so mark it as fully drawn. fully_drawn = true; } - (MediaCacheEntry::Requested, _media_format) => { + MediaCacheEntry::Requested => { if let Some(image_info) = image_info { if let (Some(ref blurhash), Some(width), Some(height)) = (image_info.blurhash.clone(), image_info.width, image_info.height) { let show_image_result = text_or_image_ref.show_image(cx, |cx, img| { @@ -3543,38 +3574,20 @@ fn populate_image_message_content( } fully_drawn = false; } - (MediaCacheEntry::Failed, _media_format) => { + MediaCacheEntry::Failed => { text_or_image_ref - .show_text(cx, format!("{body}\n\nFailed to fetch image from {:?}", mxc_uri)); + .show_text(cx, format!("{body}\n\nFailed to fetch image from {:?}", timeline_mxc_uri)); // For now, we consider this as being "complete". In the future, we could support // retrying to fetch thumbnail of the image on a user click/tap. fully_drawn = true; } - } - }; - - let mut fetch_and_show_media_source = |cx: &mut Cx2d, media_source: MediaSource, image_info: Option<&ImageInfo>| { - match media_source { - MediaSource::Encrypted(encrypted) => { - // We consider this as "fully drawn" since we don't yet support encryption. - text_or_image_ref.show_text( - cx, - format!("{body}\n\n[TODO] fetch encrypted image at {:?}", encrypted.url) - ); - }, - MediaSource::Plain(mxc_uri) => { - fetch_and_show_image_uri(cx, mxc_uri, image_info) - } + _ => { } } }; match image_info_source { Some((image_info, original_source)) => { - // Use the provided thumbnail URI if it exists; otherwise use the original URI. - let media_source = image_info.clone() - .and_then(|image_info| image_info.thumbnail_source) - .unwrap_or(original_source); - fetch_and_show_media_source(cx, media_source, image_info.as_ref()); + fetch_and_handle_image(cx, &original_source, image_info.as_ref()); } None => { text_or_image_ref.show_text(cx, "{body}\n\nImage message had no source URL."); diff --git a/src/image_viewer.rs b/src/image_viewer.rs new file mode 100644 index 00000000..c73c366b --- /dev/null +++ b/src/image_viewer.rs @@ -0,0 +1,121 @@ +use std::sync::Arc; +use crate::utils; + +use makepad_widgets::*; + +live_design! { + use link::theme::*; + use link::shaders::*; + use link::widgets::*; + + use crate::shared::styles::*; + use crate::shared::icon_button::RobrixIconButton; + + pub ImageViewer = {{ImageViewer}} { + visible: false + width: Fill, height: Fill + align: {x: 0.5, y: 0.5} + spacing: 12 + flow: Overlay + show_bg: true + draw_bg: { + color: (COLOR_IMAGE_VIEWER_BG) + } + + { + align: {x: 1.0, y: 0.0} + width: Fill, height: Fill + close_button = { + padding: {left: 15, right: 15} + draw_icon: { + svg_file: (ICON_CLOSE) + color: (COLOR_CLOSE), + } + icon_walk: {width: 25, height: 25, margin: {left: -1, right: -1} } + + draw_bg: { + border_color: (COLOR_CLOSE_BG), + color: (COLOR_CLOSE_BG) // light red + } + } + } + + image_view = { + padding: {top: 40, bottom: 30, left: 20, right: 20} + flow: Overlay + align: {x: 0.5, y: 0.5} + width: Fill, height: Fill, + image = { + width: Fill, height: Fill, + fit: Smallest, + } + } + } +} + +#[derive(Live, LiveHook, Widget)] +pub struct ImageViewer { + #[deref] + view: View, +} + +/// Actions handled by the `ImageViewer` widget. +#[derive(Clone, Debug, DefaultNone)] +pub enum ImageViewerAction { + /// Make the ImageViewer widget visible. + Show, + /// Set the image being displayed by the ImageViewer. + SetImage(Arc<[u8]>), + None, +} + +impl Widget for ImageViewer { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + self.match_event(cx, event); + self.view.handle_event(cx, event, scope); + } + + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + self.view.draw_walk(cx, scope, walk) + } +} +impl MatchEvent for ImageViewer { + fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) { + if self.view.button(id!(close_button)).clicked(actions) { + self.close(cx); + } + + for action in actions { + match action.downcast_ref::() { + Some(ImageViewerAction::Show) => { + self.open(cx); + } + Some(ImageViewerAction::SetImage(data)) => { + self.load_with_data(cx, data); + } + _ => {} + } + } + } +} + +impl ImageViewer { + fn open(&mut self, cx: &mut Cx) { + self.visible = true; + self.redraw(cx); + } + fn close(&mut self, cx: &mut Cx) { + self.visible = false; + self.view.image(id!(image_view.image)).set_texture(cx, None); + self.redraw(cx); + } + fn load_with_data(&mut self, cx: &mut Cx, data: &[u8]) { + let image = self.view.image(id!(image_view.image)); + + if let Err(e) = utils::load_png_or_jpg(&image, cx, data) { + log!("Error to load image: {e}"); + } else { + self.view.redraw(cx); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 1c45c38a..33e77a19 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,8 @@ pub mod home; mod profile; /// A modal/dialog popup for interactive verification of users/devices. mod verification_modal; +// A image viewer for viewing images. +mod image_viewer; /// Shared UI components. pub mod shared; /// Generating text previews of timeline events/messages. diff --git a/src/media_cache.rs b/src/media_cache.rs index ae071011..d1225117 100644 --- a/src/media_cache.rs +++ b/src/media_cache.rs @@ -1,18 +1,13 @@ use std::{collections::{btree_map::Entry, BTreeMap}, ops::{Deref, DerefMut}, sync::{Arc, Mutex}, time::SystemTime}; -use makepad_widgets::{error, log, SignalToUI}; -use matrix_sdk::{media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, ruma::{events::room::MediaSource, OwnedMxcUri}}; -use crate::{home::room_screen::TimelineUpdate, sliding_sync::{self, MatrixRequest}}; +use makepad_widgets::{error, log, Cx, SignalToUI}; +use matrix_sdk::{media::{MediaFormat, MediaRequestParameters}, ruma::{events::room::MediaSource, OwnedMxcUri}}; +use crate::{home::room_screen::TimelineUpdate, image_viewer::ImageViewerAction, sliding_sync::{self, MatrixRequest, OnMediaFetchedFn}, utils::MEDIA_THUMBNAIL_FORMAT}; -/// The value type in the media cache, one per Matrix URI. -#[derive(Debug, Clone)] -pub struct MediaCacheValue { - full_file: Option, - thumbnail: Option<(MediaCacheEntryRef, MediaThumbnailSettings)>, -} /// An entry in the media cache. #[derive(Debug, Clone)] pub enum MediaCacheEntry { + NotInitialized, /// A request has been issued and we're waiting for it to complete. Requested, /// The media has been successfully loaded from the server. @@ -24,23 +19,24 @@ pub enum MediaCacheEntry { /// A reference to a media cache entry and its associated format. pub type MediaCacheEntryRef = Arc>; - /// A cache of fetched media, indexed by Matrix URI. /// /// A single Matrix URI may have multiple media formats associated with it, /// such as a thumbnail and a full-size image. pub struct MediaCache { /// The actual cached data. - cache: BTreeMap, + cache: BTreeMap, MediaCacheEntryRef)>, /// A channel to send updates to a particular timeline when a media request has completed. timeline_update_sender: Option>, } + impl Deref for MediaCache { - type Target = BTreeMap; + type Target = BTreeMap, MediaCacheEntryRef)>; fn deref(&self) -> &Self::Target { &self.cache } } + impl DerefMut for MediaCache { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.cache @@ -62,89 +58,56 @@ impl MediaCache { } } - /// Tries to get the media from the cache, or submits an async request to fetch it. - /// - /// This method *does not* block or wait for the media to be fetched, - /// and will return `MediaCache::Requested` while the async request is in flight. - /// If a request is already in flight, this will not issue a new redundant request. - /// - /// * If the `media_format` is requesting a thumbnail that is not yet in the cache, - /// this function will fetch the thumbnail, and return the full-size image (if it exists). - /// * If the `media_format` is requesting a full-size image that is not yet in the cache, - /// this function will fetch the full-size image, and return a thumbnail (if it exists). - /// - /// Returns a tuple of the media cache entry and the media format of that cached entry. + /// This function only for images. + /// Never call this method on populating audio, video, etc. + pub fn image_set_keys(&mut self, original_uri: &OwnedMxcUri, thumbnail_uri: Option<&OwnedMxcUri>) { + if let Entry::Vacant(v) = self.cache.entry(original_uri.clone()) { + v.insert((thumbnail_uri.cloned(), Arc::new(Mutex::new(MediaCacheEntry::NotInitialized)))); + } + + if let Some(thumbnail_uri) = thumbnail_uri { + if let Entry::Vacant(v) = self.cache.entry(thumbnail_uri.clone()) { + v.insert((None, Arc::new(Mutex::new(MediaCacheEntry::Requested)))); + } + } + } + + /// Returns the media cache entry. pub fn try_get_media_or_fetch( &mut self, - mxc_uri: OwnedMxcUri, - requested_format: MediaFormat, - ) -> (MediaCacheEntry, MediaFormat) { - let mut post_request_retval = (MediaCacheEntry::Requested, requested_format.clone()); - - let entry_ref = match self.entry(mxc_uri.clone()) { - Entry::Vacant(vacant) => match &requested_format { - MediaFormat::Thumbnail(requested_mts) => { - let entry_ref = Arc::new(Mutex::new(MediaCacheEntry::Requested)); - vacant.insert(MediaCacheValue { - full_file: None, - thumbnail: Some((Arc::clone(&entry_ref), requested_mts.clone())), - }); - entry_ref - }, - MediaFormat::File => { - let entry_ref = Arc::new(Mutex::new(MediaCacheEntry::Requested)); - vacant.insert(MediaCacheValue { - full_file: Some(Arc::clone(&entry_ref)), - thumbnail: None, - }); - entry_ref - }, + mxc_uri: &OwnedMxcUri, + on_fetched: OnMediaFetchedFn, + ) -> MediaCacheEntry { + let mut ret = MediaCacheEntry::Requested; + + let (destination, format_to_fetch ) = match self.cache.entry(mxc_uri.clone()) { + Entry::Vacant(v) => { + (v.insert((None, Arc::new(Mutex::new(MediaCacheEntry::Requested)))).1.clone(), MediaFormat::File) } - Entry::Occupied(mut occupied) => match requested_format { - MediaFormat::Thumbnail(ref requested_mts) => { - if let Some((entry_ref, existing_mts)) = occupied.get().thumbnail.as_ref() { - return ( - entry_ref.lock().unwrap().deref().clone(), - MediaFormat::Thumbnail(existing_mts.clone()), - ); + Entry::Occupied(o) => { + let (thumbnail_uri, media_cache_entry_ref) = o.get().clone(); + let mut media_cache_entry_mg = media_cache_entry_ref.lock().unwrap(); + let current_media_cache_entry = media_cache_entry_mg.clone(); + + match current_media_cache_entry { + MediaCacheEntry::Loaded(_) | MediaCacheEntry::Failed => { + return current_media_cache_entry; } - else { - // Here, a thumbnail was requested but not found, so fetch it. - let entry_ref = Arc::new(Mutex::new(MediaCacheEntry::Requested)); - occupied.get_mut().thumbnail = Some((Arc::clone(&entry_ref), requested_mts.clone())); - // If a full-size image is already loaded, return it. - if let Some(existing_file) = occupied.get().full_file.as_ref() { - if let MediaCacheEntry::Loaded(d) = existing_file.lock().unwrap().deref() { - post_request_retval = ( - MediaCacheEntry::Loaded(Arc::clone(d)), - MediaFormat::File, - ); - } + MediaCacheEntry::NotInitialized | MediaCacheEntry::Requested => { + if let MediaCacheEntry::NotInitialized = current_media_cache_entry { + *media_cache_entry_mg = MediaCacheEntry::Requested } - entry_ref - } - } - MediaFormat::File => { - if let Some(entry_ref) = occupied.get().full_file.as_ref() { - return ( - entry_ref.lock().unwrap().deref().clone(), - MediaFormat::File, - ); - } - else { - // Here, a full-size image was requested but not found, so fetch it. - let entry_ref = Arc::new(Mutex::new(MediaCacheEntry::Requested)); - occupied.get_mut().full_file = Some(entry_ref.clone()); - // If a thumbnail is already loaded, return it. - if let Some((existing_thumbnail, existing_mts)) = occupied.get().thumbnail.as_ref() { - if let MediaCacheEntry::Loaded(d) = existing_thumbnail.lock().unwrap().deref() { - post_request_retval = ( - MediaCacheEntry::Loaded(Arc::clone(d)), - MediaFormat::Thumbnail(existing_mts.clone()), - ); + match thumbnail_uri.as_ref() { + Some(uri) => { + if let Some(v) = self.cache.get(uri) { + ret = v.1.lock().unwrap().clone(); + } + (media_cache_entry_ref.clone(), MediaFormat::File) + } + None => { + (media_cache_entry_ref.clone(), MEDIA_THUMBNAIL_FORMAT.into()) } } - entry_ref } } } @@ -153,20 +116,21 @@ impl MediaCache { sliding_sync::submit_async_request( MatrixRequest::FetchMedia { media_request: MediaRequestParameters { - source: MediaSource::Plain(mxc_uri), - format: requested_format, + source: MediaSource::Plain(mxc_uri.clone()), + format: format_to_fetch }, - on_fetched: insert_into_cache, - destination: entry_ref, + on_fetched, + destination, update_sender: self.timeline_update_sender.clone(), } ); - post_request_retval + + ret } } /// Insert data into a previously-requested media cache entry. -fn insert_into_cache>>( +pub fn insert_into_cache>>( value_ref: &Mutex, _request: MediaRequestParameters, data: matrix_sdk::Result, @@ -207,3 +171,29 @@ fn insert_into_cache>>( } SignalToUI::set_ui_signal(); } + + +pub fn image_viewer_insert_into_cache>>( + value_ref: &Mutex, + _request: MediaRequestParameters, + data: matrix_sdk::Result, + _update_sender: Option>, +) { + let new_value = match data { + Ok(data) => { + let data = data.into(); + // This function just simply copy from `insert_from_cache`, + // only here is different, we just post an action on getting the image data. + Cx::post_action(ImageViewerAction::SetImage(data.clone())); + MediaCacheEntry::Loaded(data) + } + Err(e) => { + error!("Failed to fetch media for {:?}: {e:?}", _request.source); + MediaCacheEntry::Failed + } + }; + + *value_ref.lock().unwrap() = new_value; + + SignalToUI::set_ui_signal(); +} diff --git a/src/shared/styles.rs b/src/shared/styles.rs index cea5267b..c52c6dad 100644 --- a/src/shared/styles.rs +++ b/src/shared/styles.rs @@ -99,6 +99,11 @@ live_design! { pub COLOR_TEXT = #1C274C pub COLOR_TEXT_INPUT_IDLE = #d8d8d8 + pub COLOR_CLOSE = #000000 + pub COLOR_IMAGE_VIEWER_BG = #000000E0 + pub COLOR_CLOSE_BG = #FFFFFF90 + + // A text input widget styled for Robrix. pub RobrixTextInput = { width: Fill, height: Fit, diff --git a/src/shared/text_or_image.rs b/src/shared/text_or_image.rs index e1602b8b..b76f5805 100644 --- a/src/shared/text_or_image.rs +++ b/src/shared/text_or_image.rs @@ -3,7 +3,9 @@ //! This is useful to display a loading message while waiting for an image to be fetched, //! or to display an error message if the image fails to load, etc. +use std::sync::Arc; use makepad_widgets::*; +use matrix_sdk::ruma::OwnedMxcUri; live_design! { use link::theme::*; @@ -34,32 +36,60 @@ live_design! { } image_view = { visible: false, - cursor: Default, // Use `Hand` once we support clicking on the image + cursor: Hand, width: Fill, height: Fit, image = { width: Fill, height: Fit, - fit: Smallest, + fit: Size, // Only for a comfortable test, would set back to `Smallest` if this pr OK. } } } } +#[derive(Debug, Clone, DefaultNone)] +pub enum TextOrImageAction { + Click(OwnedMxcUri), + None, +} + +#[derive(Debug, Clone)] +pub struct TimelineImageInfo { + pub original_mxc_uri: OwnedMxcUri, + pub timeline_image_data: Arc<[u8]>, +} /// A view that holds an image or text content, and can switch between the two. /// -/// This is useful for displaying alternate text when an image is not (yet) available +/// This is useful for displaying alternate text when an image is not yet available /// or fails to load. It can also be used to display a loading message while an image /// is being fetched. #[derive(Live, Widget, LiveHook)] pub struct TextOrImage { #[deref] view: View, #[rust] status: TextOrImageStatus, - // #[rust(TextOrImageStatus::Text)] status: TextOrImageStatus, #[rust] size_in_pixels: (usize, usize), } impl Widget for TextOrImage { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + // We handle hit events if the status is `Image`. + if let TextOrImageStatus::Image(ref timeline_image_info) = self.status { + let image_area = self.view.image(id!(image_view.image)).area(); + match event.hits(cx, image_area) { + Hit::FingerDown(_) => { + cx.set_key_focus(image_area); + } + Hit::FingerUp(fe) if fe.is_over && fe.is_primary_hit() && fe.was_tap() => { + // We run the check to see if the original image was already fetched or not. + // + // If `image_value` is `None`, it can tell that the image has not been fetched, + // user actually clicks the blurhash, + // so we do nothing this condition. + cx.action(TextOrImageAction::Click(timeline_image_info.original_mxc_uri.clone())); + } + _ => { }, + } + } self.view.handle_event(cx, event, scope); } @@ -67,6 +97,7 @@ impl Widget for TextOrImage { self.view.draw_walk(cx, scope, walk) } } + impl TextOrImage { /// Sets the text content, which will be displayed on future draw operations. /// @@ -95,7 +126,6 @@ impl TextOrImage { let image_ref = self.view.image(id!(image_view.image)); match image_set_function(cx, image_ref) { Ok(size_in_pixels) => { - self.status = TextOrImageStatus::Image; self.size_in_pixels = size_in_pixels; self.view(id!(image_view)).set_visible(cx, true); self.view(id!(text_view)).set_visible(cx, false); @@ -109,8 +139,12 @@ impl TextOrImage { } /// Returns whether this `TextOrImage` is currently displaying an image or text. - pub fn status(&self) -> TextOrImageStatus { - self.status + pub fn get_status(&self) -> &TextOrImageStatus { + &self.status + } + + pub fn set_status_to_image(&mut self, timeline_image_info: TimelineImageInfo) { + self.status = TextOrImageStatus::Image(timeline_image_info) } } @@ -134,19 +168,23 @@ impl TextOrImageRef { } /// See [TextOrImage::status()]. - pub fn status(&self) -> TextOrImageStatus { + pub fn get_status(&self) -> TextOrImageStatus { if let Some(inner) = self.borrow() { - inner.status() + inner.get_status().clone() } else { TextOrImageStatus::Text } } + + pub fn set_status_to_image(&self, timeline_image_info: TimelineImageInfo) { + let Some(mut inner) = self.borrow_mut() else { return }; + inner.set_status_to_image(timeline_image_info); + } } /// Whether a `TextOrImage` instance is currently displaying text or an image. -#[derive(Debug, Default, Copy, Clone, PartialEq)] +#[derive(Debug, Default, Clone)] pub enum TextOrImageStatus { - #[default] - Text, - Image, + #[default] Text, + Image(TimelineImageInfo), }