diff --git a/resources/icons/zoom_in.svg b/resources/icons/zoom_in.svg new file mode 100644 index 00000000..12aa6d08 --- /dev/null +++ b/resources/icons/zoom_in.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/resources/icons/zoom_out.svg b/resources/icons/zoom_out.svg new file mode 100644 index 00000000..39419cae --- /dev/null +++ b/resources/icons/zoom_out.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 411feca3..714ee12a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -461,10 +461,6 @@ fn clear_all_app_state(cx: &mut Cx) { impl AppMain for App { fn handle_event(&mut self, cx: &mut Cx, event: &Event) { - // if let Event::WindowGeomChange(geom) = event { - // log!("App::handle_event(): Window geometry changed: {:?}", geom); - // } - if let Event::Shutdown = event { let window_ref = self.ui.window(ids!(main_window)); if let Err(e) = persistence::save_window_state(window_ref, cx) { diff --git a/src/home/room_read_receipt.rs b/src/home/room_read_receipt.rs index 14c38660..01da2ded 100644 --- a/src/home/room_read_receipt.rs +++ b/src/home/room_read_receipt.rs @@ -181,8 +181,14 @@ impl AvatarRow { self.buttons.iter_mut().zip(receipts_map.iter().rev()) { if !*drawn { - let (_, drawn_status) = - avatar_ref.set_avatar_and_get_username(cx, room_id, user_id, None, event_id); + let (_, drawn_status) = avatar_ref.set_avatar_and_get_username( + cx, + room_id, + user_id, + None, + event_id, + true, + ); *drawn = drawn_status; } } diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 31aa813a..897288c5 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -2907,6 +2907,7 @@ fn populate_message_view( event_tl_item.sender(), Some(event_tl_item.sender_profile()), event_tl_item.event_id(), + true, ); // Prepend a "* " to the emote body, as suggested by the Matrix spec. @@ -3195,6 +3196,7 @@ fn populate_message_view( event_tl_item.sender(), Some(event_tl_item.sender_profile()), event_tl_item.event_id(), + true, ) ); if is_notice { @@ -3664,6 +3666,7 @@ fn draw_replied_to_message( &replied_to_event.sender, Some(&replied_to_event.sender_profile), Some(in_reply_to_details.event_id.as_ref()), + true, ); fully_drawn = is_avatar_fully_drawn; @@ -3996,6 +3999,7 @@ fn populate_small_state_event( event_tl_item.sender(), Some(event_tl_item.sender_profile()), event_tl_item.event_id(), + true, ); // Draw the timestamp as part of the profile. if let Some(dt) = unix_time_millis_to_datetime(event_tl_item.timestamp()) { diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index abf6ab4a..982f430e 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -371,6 +371,7 @@ impl RoomInputBar { replying_to.0.sender(), Some(replying_to.0.sender_profile()), replying_to.0.event_id(), + true, ); replying_preview diff --git a/src/shared/avatar.rs b/src/shared/avatar.rs index b6abafa4..f4cf2172 100644 --- a/src/shared/avatar.rs +++ b/src/shared/avatar.rs @@ -37,7 +37,6 @@ live_design! { align: { x: 0.5, y: 0.5 } // the text_view and img_view are overlaid on top of each other. flow: Overlay, - cursor: Hand, text_view = { visible: true, @@ -94,6 +93,8 @@ live_design! { pub struct Avatar { #[deref] view: View, + /// Information about the user profile being shown in this Avatar. + /// If `Some`, this Avatar will respond to clicks/taps. #[rust] info: Option, } @@ -101,9 +102,7 @@ impl Widget for Avatar { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { self.view.handle_event(cx, event, scope); - let Some(info) = self.info.clone() else { - return; - }; + let Some(info) = self.info.clone() else { return }; let area = self.view.area(); let widget_uid = self.widget_uid(); match event.hits(cx, area) { @@ -152,16 +151,19 @@ impl Avatar { info: Option, username: T, ) { - self.info = info.map(|AvatarTextInfo { user_id, username, room_id }| - UserProfileAndRoomId { + if let Some(AvatarTextInfo { user_id, username, room_id }) = info { + self.info = Some(UserProfileAndRoomId { user_profile: UserProfile { user_id, username, avatar_state: AvatarState::Unknown, }, room_id, - } - ); + }); + self.view.apply_over(cx, live!{ cursor: Hand }); + } else { + self.view.apply_over(cx, live!{ cursor: Default }); + } self.set_text(cx, username.as_ref()); // Apply background color if provided @@ -200,16 +202,19 @@ impl Avatar { self.view(ids!(img_view)).set_visible(cx, true); self.view(ids!(text_view)).set_visible(cx, false); - self.info = info.map(|AvatarImageInfo { user_id, username, room_id, img_data }| - UserProfileAndRoomId { + if let Some(AvatarImageInfo { user_id, username, room_id, img_data }) = info { + self.info = Some(UserProfileAndRoomId { user_profile: UserProfile { user_id, username, avatar_state: AvatarState::Loaded(img_data), }, room_id, - } - ); + }); + self.view.apply_over(cx, live!{ cursor: Hand }); + } else { + self.view.apply_over(cx, live!{ cursor: Default }); + } } res } @@ -244,6 +249,8 @@ impl Avatar { /// our user profile cache , then the `username` and `avatar` will be the user ID /// and the first character of that user ID, respectively. /// + /// If `is_clickable` is `true`, this Avatar will respond to clicks. + /// /// ## Return /// Returns a tuple of: /// 1. The displayable username that should be used to populate the username field. @@ -256,6 +263,7 @@ impl Avatar { avatar_user_id: &UserId, avatar_profile_opt: Option<&TimelineDetails>, event_id: Option<&EventId>, + is_clickable: bool, ) -> (String, bool) { // Get the display name and avatar URL from the user's profile, if available, // or if the profile isn't ready, fall back to querying our user profile cache. @@ -330,12 +338,12 @@ impl Avatar { .and_then(|data| { self.show_image( cx, - Some(( + is_clickable.then(|| AvatarImageInfo::from(( avatar_user_id.to_owned(), username_opt.clone(), room_id.to_owned(), - data.clone()).into(), - ), + data.clone() + ))), |cx, img| utils::load_png_or_jpg(&img, cx, &data), ) .ok() @@ -344,7 +352,11 @@ impl Avatar { self.show_text( cx, None, - Some((avatar_user_id.to_owned(), username_opt, room_id.to_owned()).into()), + is_clickable.then(|| AvatarTextInfo::from(( + avatar_user_id.to_owned(), + username_opt, + room_id.to_owned(), + ))), &username, ) }); @@ -399,9 +411,17 @@ impl AvatarRef { avatar_user_id: &UserId, avatar_profile_opt: Option<&TimelineDetails>, event_id: Option<&EventId>, + is_clickable: bool, ) -> (String, bool) { if let Some(mut inner) = self.borrow_mut() { - inner.set_avatar_and_get_username(cx, room_id, avatar_user_id, avatar_profile_opt, event_id) + inner.set_avatar_and_get_username( + cx, + room_id, + avatar_user_id, + avatar_profile_opt, + event_id, + is_clickable, + ) } else { (avatar_user_id.to_string(), false) } diff --git a/src/shared/image_viewer.rs b/src/shared/image_viewer.rs index e9fc7f13..68f6747b 100644 --- a/src/shared/image_viewer.rs +++ b/src/shared/image_viewer.rs @@ -16,6 +16,9 @@ use matrix_sdk_ui::timeline::EventTimelineItem; use thiserror::Error; use crate::shared::{avatar::AvatarWidgetExt, timestamp::TimestampWidgetRefExt}; +/// The timeout for hiding the UI overlays after no user mouse/tap activity. +const SHOW_UI_DURATION: f64 = 3.0; + /// Loads the given image `data` into an `ImageBuffer` as either a PNG or JPEG, using the `imghdr` library to determine which format it is. /// /// Returns an error if either load fails or if the image format is unknown. @@ -36,15 +39,13 @@ pub fn get_png_or_jpg_image_buffer(data: Vec) -> Result Self { Self { - min_zoom: 0.5, - max_zoom: 4.0, + min_zoom: 0.1, zoom_scale_factor: 1.2, pan_sensitivity: 2.0, } @@ -87,7 +87,7 @@ struct DragState { drag_start: DVec2, /// The zoom level of the image. /// The larger the value, the more zoomed in the image is. - zoom_level: f32, + zoom_level: f64, /// The pan offset of the image. pan_offset: Option, } @@ -111,129 +111,43 @@ live_design! { use crate::shared::avatar::Avatar; use crate::shared::timestamp::Timestamp; - pub MagnifyingGlass = { - width: Fit, height: Fit - flow: Overlay - visible: true - - magnifying_glass_button = { - width: Fit, height: Fit, - spacing: 0, - margin: 8, - padding: 3 - draw_bg: { - color: (COLOR_PRIMARY) - } - draw_icon: { - svg_file: (ICON_ZOOM), - fn get_color(self) -> vec4 { - return #x0; - } - } - icon_walk: {width: 30, height: 30} - } - - sign_label = { - width: Fill, height: Fill, - align: { x: 0.4, y: 0.35 } + UI_ANIMATION_DURATION_SECS = 0.5 + ROTATION_ANIMATION_DURATION_SECS = 0.2 - magnifying_glass_sign =