Skip to content

Commit 9ada9f0

Browse files
committed
Add reusable room loading tabs with optional details in dock
1 parent d3ee1c6 commit 9ada9f0

4 files changed

Lines changed: 276 additions & 5 deletions

File tree

src/home/main_desktop_ui.rs

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use matrix_sdk::ruma::OwnedRoomId;
33
use tokio::sync::Notify;
44
use std::{collections::HashMap, sync::Arc};
55

6-
use crate::{app::{AppState, AppStateAction, SelectedRoom}, utils::room_name_or_id};
6+
use crate::{app::{AppState, AppStateAction, SelectedRoom}, room::loading_screen::{RoomLoadingScreenAction, RoomLoadingScreenWidgetRefExt, drain_room_loading_screen_actions, loading_tab_live_id}, utils::room_name_or_id};
77
use super::{invite_screen::InviteScreenWidgetRefExt, room_screen::RoomScreenWidgetRefExt, rooms_list::RoomsListAction};
88

99
live_design! {
@@ -17,6 +17,7 @@ live_design! {
1717
use crate::home::welcome_screen::WelcomeScreen;
1818
use crate::home::room_screen::RoomScreen;
1919
use crate::home::invite_screen::InviteScreen;
20+
use crate::room::loading_screen::RoomLoadingScreen;
2021

2122
pub MainDesktopUI = {{MainDesktopUI}} {
2223
dock = <Dock> {
@@ -54,6 +55,7 @@ live_design! {
5455
welcome_screen = <WelcomeScreen> {}
5556
room_screen = <RoomScreen> {}
5657
invite_screen = <InviteScreen> {}
58+
loading_screen = <RoomLoadingScreen> { visible: true }
5759
}
5860
}
5961
}
@@ -67,6 +69,10 @@ pub struct MainDesktopUI {
6769
#[rust]
6870
open_rooms: HashMap<LiveId, SelectedRoom>,
6971

72+
/// Tabs that are currently showing a loading screen (tab_id -> last message).
73+
#[rust]
74+
loading_tabs: HashMap<LiveId, (Option<String>, Option<String>)>,
75+
7076
/// The tab that should be closed in the next draw event
7177
#[rust]
7278
tab_to_close: Option<LiveId>,
@@ -92,6 +98,20 @@ pub struct MainDesktopUI {
9298

9399
impl Widget for MainDesktopUI {
94100
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
101+
// Apply queued loading tab actions when the UI thread is signalled.
102+
if let Event::Signal = event {
103+
for action in drain_room_loading_screen_actions() {
104+
match action {
105+
RoomLoadingScreenAction::ShowTab { tab_id, tab_name, message, details } => {
106+
self.show_loading_tab(cx, tab_id, &tab_name, message.as_deref(), details.as_deref());
107+
}
108+
RoomLoadingScreenAction::HideTab { tab_id } => {
109+
self.close_loading_tab(cx, tab_id);
110+
}
111+
}
112+
}
113+
}
114+
95115
self.widget_match_event(cx, event, scope); // invokes `WidgetMatchEvent` impl
96116
self.view.handle_event(cx, event, scope);
97117
}
@@ -121,6 +141,10 @@ impl MainDesktopUI {
121141

122142
// If the room is already open, select (jump to) its existing tab
123143
let room_id_as_live_id = LiveId::from_str(room.room_id().as_str());
144+
let loading_id = loading_tab_live_id(room.room_id().as_str());
145+
if self.loading_tabs.remove(&loading_id).is_some() {
146+
dock.close_tab(cx, loading_id);
147+
}
124148
if self.open_rooms.contains_key(&room_id_as_live_id) {
125149
dock.select_tab(cx, room_id_as_live_id);
126150
self.most_recently_selected_room = Some(room);
@@ -204,6 +228,8 @@ impl MainDesktopUI {
204228
dock.select_tab(cx, id!(home_tab));
205229
self.most_recently_selected_room = None;
206230
}
231+
} else if self.loading_tabs.remove(&tab_id).is_some() {
232+
// Nothing else to do; just close the loading tab.
207233
}
208234

209235
dock.close_tab(cx, tab_id);
@@ -226,6 +252,57 @@ impl MainDesktopUI {
226252
self.tab_to_close = None;
227253
self.room_order.clear();
228254
self.most_recently_selected_room = None;
255+
256+
for tab_id in self.loading_tabs.keys().copied().collect::<Vec<_>>() {
257+
dock.close_tab(cx, tab_id);
258+
}
259+
self.loading_tabs.clear();
260+
}
261+
262+
/// Show or create a loading-only tab.
263+
fn show_loading_tab(&mut self, cx: &mut Cx, tab_id: LiveId, tab_name: &str, message: Option<&str>, details: Option<&str>) {
264+
let dock_ref = self.view.dock(ids!(dock));
265+
266+
// If the tab already exists and is a loading tab, just update it and select it.
267+
let mut should_select_existing = false;
268+
if let Some(mut dock) = dock_ref.borrow_mut() {
269+
if let Some((_, widget)) = dock.items().get(&tab_id) {
270+
widget.as_room_loading_screen().show(cx, message, details);
271+
self.loading_tabs.insert(tab_id, (message.map(str::to_owned), details.map(str::to_owned)));
272+
should_select_existing = true;
273+
}
274+
}
275+
if should_select_existing {
276+
dock_ref.select_tab(cx, tab_id);
277+
return;
278+
}
279+
280+
// Otherwise, create a new loading tab at the end.
281+
let (tab_bar, _pos) = dock_ref.find_tab_bar_of_tab(id!(home_tab)).unwrap();
282+
let new_tab_widget = dock_ref.create_and_select_tab(
283+
cx,
284+
tab_bar,
285+
tab_id,
286+
id!(loading_screen),
287+
tab_name.to_string(),
288+
id!(CloseableTab),
289+
None,
290+
);
291+
292+
if let Some(widget) = new_tab_widget {
293+
widget.as_room_loading_screen().show(cx, message, details);
294+
self.loading_tabs.insert(tab_id, (message.map(str::to_owned), details.map(str::to_owned)));
295+
dock_ref.select_tab(cx, tab_id);
296+
} else {
297+
error!("BUG: failed to create loading tab for {tab_name}");
298+
}
299+
}
300+
301+
/// Close a loading-only tab if it exists.
302+
fn close_loading_tab(&mut self, cx: &mut Cx, tab_id: LiveId) {
303+
if self.loading_tabs.remove(&tab_id).is_some() {
304+
self.view.dock(ids!(dock)).close_tab(cx, tab_id);
305+
}
229306
}
230307

231308
/// Replaces an invite with a joined room in the dock.

src/home/room_screen.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ use crate::{
3030
user_profile::{AvatarState, ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt},
3131
user_profile_cache,
3232
},
33-
room::{room_input_bar::RoomInputBarState, typing_notice::TypingNoticeWidgetExt},
33+
room::{loading_screen::{loading_tab_live_id, show_room_loading_tab}, room_input_bar::RoomInputBarState, typing_notice::TypingNoticeWidgetExt},
3434
shared::{
3535
avatar::AvatarWidgetRefExt, callout_tooltip::{CalloutTooltipOptions, TooltipAction, TooltipPosition}, html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt, RobrixHtmlLinkAction}, jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, popup_list::{PopupItem, PopupKind, enqueue_popup_notification}, restore_status_view::RestoreStatusViewWidgetExt, styles::*, text_or_image::{TextOrImageRef, TextOrImageWidgetRefExt}, timestamp::TimestampWidgetRefExt
3636
},
@@ -1483,16 +1483,28 @@ impl RoomScreen {
14831483
log!("TODO: jump to known room {}", room_id);
14841484
} else {
14851485
log!("TODO: fetch and display room preview for room {}", room_id);
1486+
show_room_loading_tab(
1487+
loading_tab_live_id(room_id.as_str()),
1488+
"Loading...",
1489+
Some("Loading...".to_string()),
1490+
Some("Loading Room ID...".to_string()),
1491+
);
14861492
}
1487-
false
1493+
true
14881494
}
14891495
MatrixId::RoomAlias(room_alias) => {
14901496
log!("TODO: open room alias {}", room_alias);
14911497
// TODO: open a room loading screen that shows a spinner
14921498
// while our background async task calls Client::resolve_room_alias()
14931499
// and then either jumps to the room if known, or fetches and displays
14941500
// a room preview for that room.
1495-
false
1501+
show_room_loading_tab(
1502+
loading_tab_live_id(room_alias.alias()),
1503+
"Loading...",
1504+
Some("Loading...".to_string()),
1505+
Some("Loading Room Alias...".to_string()),
1506+
);
1507+
true
14961508
}
14971509
MatrixId::Event(room_id, event_id) => {
14981510
log!("TODO: open event {} in room {}", event_id, room_id);
@@ -4174,4 +4186,4 @@ pub fn clear_timeline_states(_cx: &mut Cx) {
41744186
TIMELINE_STATES.with_borrow_mut(|states| {
41754187
states.clear();
41764188
});
4177-
}
4189+
}

src/room/loading_screen.rs

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
//! A dock tab loading view for room operations.
2+
//! Use `show_room_loading_tab()` / `hide_room_loading_tab()` to toggle.
3+
4+
use crossbeam_queue::SegQueue;
5+
use makepad_widgets::*;
6+
7+
/// Actions for loading tabs inside the dock.
8+
#[derive(Debug, Clone)]
9+
pub enum RoomLoadingScreenAction {
10+
/// Show or create a loading tab with the given id/name/message.
11+
ShowTab {
12+
tab_id: LiveId,
13+
tab_name: String,
14+
message: Option<String>,
15+
details: Option<String>,
16+
},
17+
/// Close the loading tab with the given id.
18+
HideTab {
19+
tab_id: LiveId,
20+
},
21+
}
22+
23+
/// Pending actions that should be applied on the UI thread.
24+
static PENDING_LOADING_ACTIONS: SegQueue<RoomLoadingScreenAction> = SegQueue::new();
25+
26+
/// Show (or create) a dock tab that only contains a room loading screen.
27+
///
28+
/// `tab_id` should be unique within the dock; a common pattern is
29+
/// `LiveId::from_str(room_id.as_str())` or `LiveId::from_str(&format!(\"loading_{room_id}\"))`.
30+
pub fn show_room_loading_tab(
31+
tab_id: LiveId,
32+
tab_name: impl Into<String>,
33+
message: impl Into<Option<String>>,
34+
details: impl Into<Option<String>>,
35+
) {
36+
PENDING_LOADING_ACTIONS.push(RoomLoadingScreenAction::ShowTab {
37+
tab_id,
38+
tab_name: tab_name.into(),
39+
message: message.into(),
40+
details: details.into(),
41+
});
42+
SignalToUI::set_ui_signal();
43+
}
44+
45+
/// Hide and close the loading tab with the given id, if it exists.
46+
pub fn hide_room_loading_tab(tab_id: LiveId) {
47+
PENDING_LOADING_ACTIONS.push(RoomLoadingScreenAction::HideTab { tab_id });
48+
SignalToUI::set_ui_signal();
49+
}
50+
51+
/// Drain all pending actions for loading tabs.
52+
pub fn drain_room_loading_screen_actions(
53+
) -> impl Iterator<Item = RoomLoadingScreenAction> {
54+
std::iter::from_fn(|| PENDING_LOADING_ACTIONS.pop())
55+
}
56+
57+
/// Deterministic helper to derive a unique LiveId for a loading tab
58+
/// from any stable string (e.g., room id or alias). This keeps the same tab
59+
/// reusable across multiple jumps/clicks.
60+
pub fn loading_tab_live_id(key: &str) -> LiveId {
61+
LiveId::from_str(&format!("loading_{key}"))
62+
}
63+
64+
live_design! {
65+
use link::theme::*;
66+
use link::shaders::*;
67+
use link::widgets::*;
68+
69+
use crate::shared::helpers::*;
70+
use crate::shared::styles::*;
71+
72+
73+
pub RoomLoadingScreen = {{RoomLoadingScreen}}<ScrollXYView> {
74+
width: Fill, height: Fill,
75+
flow: Down,
76+
align: {x: 0.5, y: 0.5},
77+
spacing: 10.0,
78+
79+
show_bg: true,
80+
draw_bg: {
81+
color: (COLOR_PRIMARY_DARKER),
82+
}
83+
84+
loading_spinner = <LoadingSpinner> {
85+
width: 60,
86+
height: 60,
87+
visible: true,
88+
draw_bg: {
89+
color: (COLOR_ACTIVE_PRIMARY)
90+
border_size: 4.0,
91+
}
92+
}
93+
94+
title = <Label> {
95+
width: Fill, height: Fit,
96+
align: {x: 0.5, y: 0.0},
97+
padding: {left: 5.0, right: 0.0}
98+
margin: {top: 10.0},
99+
flow: RightWrap,
100+
draw_text: {
101+
color: (TYPING_NOTICE_TEXT_COLOR),
102+
}
103+
text: "Loading..."
104+
}
105+
106+
details = <Label> {
107+
width: Fill, height: Fit,
108+
align: {x: 0.5, y: 0.0},
109+
padding: {left: 5.0, right: 0.0}
110+
margin: {top: 5.0},
111+
flow: RightWrap,
112+
draw_text: {
113+
color: (TYPING_NOTICE_TEXT_COLOR),
114+
}
115+
text: ""
116+
}
117+
}
118+
}
119+
120+
/// A centered overlay with a spinner and status text.
121+
#[derive(Live, LiveHook, Widget)]
122+
pub struct RoomLoadingScreen {
123+
#[deref]
124+
view: View,
125+
126+
#[live(false)]
127+
visible: bool,
128+
}
129+
130+
impl Widget for RoomLoadingScreen {
131+
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
132+
if self.visible {
133+
self.view.handle_event(cx, event, scope);
134+
}
135+
}
136+
137+
fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
138+
if self.visible {
139+
self.view.draw_walk(cx, scope, walk)
140+
} else {
141+
DrawStep::done()
142+
}
143+
}
144+
}
145+
146+
impl RoomLoadingScreenRef {
147+
/// Show the overlay and update the displayed message and optional details.
148+
pub fn show(&self, cx: &mut Cx, message: Option<&str>, details: Option<&str>) {
149+
if let Some(mut inner) = self.borrow_mut() {
150+
inner.visible = true;
151+
inner.view.set_visible(cx, true);
152+
let text = message.unwrap_or("Loading...");
153+
inner.view.label(ids!(title)).set_text(cx, text);
154+
let details_label = inner.view.label(ids!(details));
155+
if let Some(detail_text) = details {
156+
details_label.set_visible(cx, true);
157+
details_label.set_text(cx, detail_text);
158+
} else {
159+
details_label.set_visible(cx, false);
160+
details_label.set_text(cx, "");
161+
}
162+
}
163+
}
164+
165+
/// Update the message without toggling visibility.
166+
pub fn set_message(&self, cx: &mut Cx, message: Option<&str>) {
167+
if let Some(inner) = self.borrow() {
168+
let text = message.unwrap_or("Loading...");
169+
inner.view.label(ids!(title)).set_text(cx, text);
170+
}
171+
}
172+
173+
/// Hide the overlay.
174+
pub fn hide(&self, cx: &mut Cx) {
175+
if let Some(mut inner) = self.borrow_mut() {
176+
inner.visible = false;
177+
inner.view.set_visible(cx, false);
178+
}
179+
}
180+
}

src/room/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ use crate::utils::avatar_from_room_name;
77
pub mod reply_preview;
88
pub mod room_input_bar;
99
pub mod room_display_filter;
10+
pub mod loading_screen;
1011
pub mod typing_notice;
1112

1213
pub fn live_design(cx: &mut Cx) {
1314
reply_preview::live_design(cx);
1415
room_input_bar::live_design(cx);
16+
loading_screen::live_design(cx);
1517
typing_notice::live_design(cx);
1618
}
1719

0 commit comments

Comments
 (0)