Skip to content

Commit

Permalink
Sliding pane basics are fully working.
Browse files Browse the repository at this point in the history
Redesigned how the Avatar works to be a bit more flexible, e.g.,
it can now accept any username string.

Avatars also store data about the user they're displaying,
which enables them to be self-sufficient when handling actions
like being clicked on.
  • Loading branch information
kevinaboos committed Jul 13, 2024
1 parent 0aba8a6 commit b6995e2
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 126 deletions.
8 changes: 3 additions & 5 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,14 +378,12 @@ impl MatchEvent for App {
// Handle the user selecting a room to view (a RoomPreview in the RoomsList).
if let RoomListAction::Selected { room_index: _, room_id, room_name } = action.as_widget_action().cast() {
// Set the title of the RoomScreen's header to the room name.
stack_navigation.set_title(
live_id!(rooms_stack_view),
&room_name.unwrap_or_else(|| format!("Room {}", &room_id)),
);
let displayed_room_name = room_name.unwrap_or_else(|| format!("Room ID {}", &room_id));
stack_navigation.set_title(live_id!(rooms_stack_view), &displayed_room_name);
// Get a reference to the `RoomScreen` widget and tell it which room's data to show.
stack_navigation
.room_screen(id!(rooms_stack_view.room_screen))
.set_displayed_room(room_id);
.set_displayed_room(displayed_room_name, room_id);
}
}
}
Expand Down
49 changes: 22 additions & 27 deletions src/home/room_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@ use matrix_sdk_ui::timeline::{
};

use rangemap::RangeSet;
use unicode_segmentation::UnicodeSegmentation;
use crate::{
media_cache::{MediaCache, MediaCacheEntry, AVATAR_CACHE},
profile::user_profile::{ShowUserProfileAction, UserProfileSlidingPaneWidgetExt},
profile::user_profile::{ShowUserProfileAction, UserProfileInfo, UserProfileSlidingPaneWidgetExt},
shared::{avatar::{AvatarRef, AvatarWidgetRefExt}, html_or_plaintext::HtmlOrPlaintextWidgetRefExt, text_or_image::TextOrImageWidgetRefExt},
sliding_sync::{submit_async_request, take_timeline_update_receiver, MatrixRequest},
utils::{self, unix_time_millis_to_datetime, MediaFormatConst},
Expand Down Expand Up @@ -549,6 +548,7 @@ live_design! {
struct RoomScreen {
#[deref] view: View,
#[rust] room_id: Option<OwnedRoomId>,
#[rust] room_name: String,
}
impl Widget for RoomScreen {
fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
Expand Down Expand Up @@ -583,9 +583,12 @@ impl Widget for RoomScreen {

for action in actions {
// Handle the action that requests to show the user profile sliding pane.
if let ShowUserProfileAction::ShowUserProfile(room_id, user_id) = action.as_widget_action().cast() {
if let ShowUserProfileAction::ShowUserProfile(avatar_info) = action.as_widget_action().cast() {
let pane = self.user_profile_sliding_pane(id!(user_profile_sliding_pane));
pane.set_info(room_id, user_id);
pane.set_info(UserProfileInfo {
avatar_info,
room_name: self.room_name.clone(),
});
pane.show(cx);
// TODO: Hack for error that when you first open the modal, doesnt draw until an event
// this forces the entire ui to rerender, still weird that only happens the first time.
Expand All @@ -599,8 +602,9 @@ impl Widget for RoomScreen {
}
impl RoomScreenRef {
/// Sets this `RoomScreen` widget to display the timeline for the given room.
pub fn set_displayed_room(&self, room_id: OwnedRoomId) {
pub fn set_displayed_room(&self, room_name: String, room_id: OwnedRoomId) {
let Some(mut room_screen) = self.borrow_mut() else { return };
room_screen.room_name = room_name;
room_screen.room_id = Some(room_id.clone());
room_screen.timeline(id!(timeline)).set_room(room_id);
}
Expand Down Expand Up @@ -1689,19 +1693,6 @@ fn set_avatar_and_get_username(

let user_id = event_tl_item.sender();

// A closure to set the item's avatar to text data,
// skipping the first `skip` characters of the given `name`.
let set_avatar_text = |name: &str, skip: usize| {
avatar.show_text(
Some((room_id.to_owned(), user_id.to_owned())),
name.graphemes(true)
.skip(skip)
.next()
.map(ToString::to_string)
.unwrap_or_default()
);
};

// Set sender to the display name if available, otherwise the user id.
match event_tl_item.sender_profile() {
TimelineDetails::Ready(profile) => {
Expand All @@ -1725,29 +1716,33 @@ fn set_avatar_and_get_username(
};

// Set the username to the display name if available, otherwise the user ID after the '@'.
let (skip, un) = if let Some(dn) = profile.display_name.as_ref() {
(0, dn.to_owned())
} else {
(1, user_id.to_string())
};
username = un;
username = profile.display_name
.as_ref()
.cloned()
.unwrap_or_else(|| user_id.to_string());

// Draw the avatar image if available, otherwise set the avatar to text.
let drew_avatar_img = avatar_img.map(|data|
avatar.show_image(
Some((room_id.to_owned(), user_id.to_owned())),
Some((username.clone(), user_id.to_owned(), room_id.to_owned(), data.clone())),
|img| utils::load_png_or_jpg(&img, cx, &data)
).is_ok()
).unwrap_or(false);

if !drew_avatar_img {
set_avatar_text(&username, skip);
avatar.show_text(
Some((user_id.to_owned(), room_id.to_owned())),
username.clone(),
);
}
}
other => {
// log!("populate_message_view(): sender profile not ready yet for event {_other:?}");
username = user_id.to_string();
set_avatar_text(&username, 1);
avatar.show_text(
Some((user_id.to_owned(), room_id.to_owned())),
username.clone(),
);
// If there was an error fetching the profile, treat that condition as fully drawn,
// since we don't yet have a good way to re-request profile information.
profile_drawn = matches!(other, TimelineDetails::Error(_));
Expand Down
2 changes: 1 addition & 1 deletion src/home/rooms_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ impl Widget for RoomsList {
}
RoomPreviewAvatar::Image(ref img_bytes) => {
let _ = item.avatar(id!(avatar)).show_image(
None,
None, // don't make room preview avatars clickable.
|img| utils::load_png_or_jpg(&img, cx, img_bytes)
);
}
Expand Down
162 changes: 95 additions & 67 deletions src/profile/user_profile.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
use std::ops::Deref;
use makepad_widgets::*;
use matrix_sdk::ruma::{OwnedRoomId, OwnedUserId};
use crate::{
shared::avatar::{AvatarInfo, AvatarWidgetExt},
utils,
};

live_design! {
import makepad_draw::shader::std::*;
Expand Down Expand Up @@ -27,7 +31,7 @@ live_design! {
// Customized button widget, based on the RoundedView shaders with some modifications
// which is a better fit with our application UI design
MoxinButton = <Button> {
width: Fit,
width: Fit,
height: Fit,
spacing: 10,
padding: {top: 10, bottom: 10, left: 15, right: 15}
Expand Down Expand Up @@ -172,7 +176,7 @@ live_design! {
spacing: 15,
align: {x: 0.0, y: 0.0}

role_info_section_label = <Label> {
role_info_title_label = <Label> {
width: Fill, height: Fit
padding: {left: 15}
draw_text: {
Expand Down Expand Up @@ -261,56 +265,50 @@ live_design! {

UserProfileSlidingPane = {{UserProfileSlidingPane}} {
flow: Overlay,
width: Fit,
width: Fill,
height: Fill,
align: {x: 1.0, y: 0}

bg_view = <View> {
width: Fill
height: Fill
visible: false,
show_bg: true
draw_bg: {
fn pixel(self) -> vec4 {
return vec4(0., 0., 0., 0.7)
}
}
}

// TODO FIXME: I would like to wrap the entire content in a RoundedView,
// but it breaks everything when I try.

// wrapper = <RoundedView> {
// width: Fit
// height: Fit
// flow: Down
// padding: 10
// spacing: 10

// show_bg: true
// draw_bg: {
// color: #fff
// radius: 3
// }


main_content = <FadeView> {
width: Fill,
height: Fill
flow: Overlay,

user_profile_view = <UserProfileView> { }

// The "X" close button on the top left
close_button = <MoxinButton> {
width: Fit,
height: Fit,
align: {x: 0.0, y: 0.0},
margin: 5
padding: 10,

draw_icon: {
svg_file: (ICON_CLOSE),
fn get_color(self) -> vec4 {
return #000;
}
}
draw_bg: {
color: #ccc,
color_hover: #fff,
main_content = <FadeView> {
width: 300,
height: Fill
flow: Overlay,

user_profile_view = <UserProfileView> { }

// The "X" close button on the top left
close_button = <MoxinButton> {
width: Fit,
height: Fit,
align: {x: 0.0, y: 0.0},
margin: 7,
padding: 10,

draw_icon: {
svg_file: (ICON_CLOSE),
fn get_color(self) -> vec4 {
return #fff;
}
icon_walk: {width: 16, height: 16}
}
draw_bg: {
color: #777,
color_hover: #fff,
}
icon_walk: {width: 16, height: 16}
}

// }
}

animator: {
panel = {
Expand All @@ -325,7 +323,7 @@ live_design! {
redraw: true,
from: {all: Forward {duration: 0.5}}
ease: ExpDecay {d1: 0.80, d2: 0.97}
apply: {main_content = { width: 110, draw_bg: {opacity: 0.0} }}
apply: {main_content = { width: 0, draw_bg: {opacity: 0.0} }}
}
}
}
Expand All @@ -335,20 +333,42 @@ live_design! {

#[derive(Clone, DefaultNone, Debug)]
pub enum ShowUserProfileAction {
ShowUserProfile(OwnedRoomId, OwnedUserId),
ShowUserProfile(AvatarInfo),
None,
}

#[derive(Clone, Debug)]
pub struct UserProfileInfo {
pub avatar_info: AvatarInfo,
pub room_name: String,
}
impl Deref for UserProfileInfo {
type Target = AvatarInfo;
fn deref(&self) -> &Self::Target {
&self.avatar_info
}
}
impl UserProfileInfo {
fn role_in_room_title(&self) -> String {
if self.room_name.is_empty() {
format!("Role in Room ID: {}", self.room_id.as_str())
} else {
format!("Role in {}", self.room_name)
}
}

fn role_in_room(&self) -> &str {
// TODO: acquire a user's role in the room to set their `role_info_label``
"<TODO>"
}
}

#[derive(Live, LiveHook, Widget)]
pub struct UserProfileSlidingPane {
#[deref]
view: View,
#[deref] view: View,
#[animator] animator: Animator,

#[animator]
animator: Animator,

#[rust] info: Option<(OwnedRoomId, OwnedUserId)>,
#[rust] info: Option<UserProfileInfo>,
}

impl Widget for UserProfileSlidingPane {
Expand All @@ -359,7 +379,8 @@ impl Widget for UserProfileSlidingPane {
}
// if !self.visible { return; }

// Determine whether we should close the pane.
// Close the pane if the close button is clicked, the back mouse button is clicked,
// the escape key is pressed, or the back button is pressed.
let close_pane = match event {
Event::Actions(actions) => self.button(id!(close_button)).clicked(actions),
Event::MouseUp(mouse) => mouse.button == 3, // the "back" button on the mouse
Expand All @@ -369,6 +390,7 @@ impl Widget for UserProfileSlidingPane {
};
if close_pane {
self.animator_play(cx, id!(panel.hide));
self.view(id!(bg_view)).set_visible(false);
}

// TODO: handle button clicks for things like:
Expand All @@ -381,35 +403,41 @@ impl Widget for UserProfileSlidingPane {


fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
let Some((room_id, user_id)) = self.info.clone() else {
let Some(info) = self.info.clone() else {
self.visible = false;
return self.view.draw_walk(cx, scope, walk);
};
self.visible = true;
self.label(id!(user_id)).set_text(user_id.as_str());
self.label(id!(role_info)).set_text(&format!("Role in {}", room_id.as_str()));

// TODO: use room_id & user_id to retrieve and set other fields:
// * avatar
// * user_name
// * role_info_section_label
// * role_info_label
// Set the user name, using the user ID as a fallback.
self.label(id!(user_name)).set_text(info.user_name());
self.label(id!(user_id)).set_text(info.user_id.as_str());
self.label(id!(role_info_title_label)).set_text(&info.role_in_room_title());

// Set the avatar image, using the user name as a fallback.
let avatar_ref = self.avatar(id!(avatar));
info.avatar_img_data.as_ref()
.and_then(|data| avatar_ref.show_image(None, |img| utils::load_png_or_jpg(&img, cx, &data)).ok())
.unwrap_or_else(|| avatar_ref.show_text(None, info.user_name()));

self.label(id!(role_info_label)).set_text(info.role_in_room());

self.view.draw_walk(cx, scope, walk)
}
}

impl UserProfileSlidingPaneRef {
pub fn set_info(&self, room_id: OwnedRoomId, user_id: OwnedUserId) {
pub fn set_info(&self, info: UserProfileInfo) {
if let Some(mut inner) = self.borrow_mut() {
inner.info = Some((room_id, user_id));
inner.info = Some(info);
}
}

pub fn show(&self, cx: &mut Cx) {
if let Some(mut inner) = self.borrow_mut() {
inner.visible = true;
inner.animator_play(cx, id!(panel.show));
inner.view(id!(bg_view)).set_visible(true);
inner.redraw(cx);
}
}
Expand Down
Loading

0 comments on commit b6995e2

Please sign in to comment.