Skip to main content

fractal/session_view/
content.rs

1use adw::{prelude::*, subclass::prelude::*};
2use gtk::{glib, glib::clone};
3
4use super::{Explore, Invite, InviteRequest, RoomHistory};
5use crate::{
6    identity_verification_view::IdentityVerificationView,
7    session::{
8        IdentityVerification, Room, RoomCategory, Session, SidebarIconItem, SidebarIconItemType,
9    },
10    utils::BoundObject,
11};
12
13/// A page of the content stack.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15enum ContentPage {
16    /// The placeholder page when no content is presented.
17    Empty,
18    /// The history of the selected room.
19    RoomHistory,
20    /// The selected invite request.
21    InviteRequest,
22    /// The selected room invite.
23    Invite,
24    /// The explore page.
25    Explore,
26    /// The selected identity verification.
27    Verification,
28}
29
30impl ContentPage {
31    /// The name of this page.
32    const fn name(self) -> &'static str {
33        match self {
34            Self::Empty => "empty",
35            Self::RoomHistory => "room-history",
36            Self::InviteRequest => "invite-request",
37            Self::Invite => "invite",
38            Self::Explore => "explore",
39            Self::Verification => "verification",
40        }
41    }
42
43    /// Get the page matching the given name.
44    ///
45    /// Panics if the name does not match any of the variants.
46    fn from_name(name: &str) -> Self {
47        match name {
48            "empty" => Self::Empty,
49            "room-history" => Self::RoomHistory,
50            "invite-request" => Self::InviteRequest,
51            "invite" => Self::Invite,
52            "explore" => Self::Explore,
53            "verification" => Self::Verification,
54            _ => panic!("Unknown ContentPage: {name}"),
55        }
56    }
57}
58
59mod imp {
60    use std::cell::{Cell, RefCell};
61
62    use glib::subclass::InitializingObject;
63
64    use super::*;
65
66    #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
67    #[template(resource = "/org/gnome/Fractal/ui/session_view/content.ui")]
68    #[properties(wrapper_type = super::Content)]
69    pub struct Content {
70        #[template_child]
71        stack: TemplateChild<gtk::Stack>,
72        #[template_child]
73        room_history: TemplateChild<RoomHistory>,
74        #[template_child]
75        invite_request: TemplateChild<InviteRequest>,
76        #[template_child]
77        invite: TemplateChild<Invite>,
78        #[template_child]
79        explore: TemplateChild<Explore>,
80        #[template_child]
81        empty_page: TemplateChild<adw::ToolbarView>,
82        #[template_child]
83        empty_page_header_bar: TemplateChild<adw::HeaderBar>,
84        #[template_child]
85        verification_page: TemplateChild<adw::ToolbarView>,
86        #[template_child]
87        verification_page_header_bar: TemplateChild<adw::HeaderBar>,
88        #[template_child]
89        identity_verification_widget: TemplateChild<IdentityVerificationView>,
90        /// The current session.
91        #[property(get, set = Self::set_session, explicit_notify, nullable)]
92        session: glib::WeakRef<Session>,
93        /// Whether this is the only visible view, i.e. there is no sidebar.
94        #[property(get, set)]
95        only_view: Cell<bool>,
96        item_binding: RefCell<Option<glib::Binding>>,
97        /// The item currently displayed.
98        #[property(get, set = Self::set_item, explicit_notify, nullable)]
99        item: BoundObject<glib::Object>,
100    }
101
102    #[glib::object_subclass]
103    impl ObjectSubclass for Content {
104        const NAME: &'static str = "Content";
105        type Type = super::Content;
106        type ParentType = adw::NavigationPage;
107
108        fn class_init(klass: &mut Self::Class) {
109            Self::bind_template(klass);
110
111            klass.set_accessible_role(gtk::AccessibleRole::Group);
112        }
113
114        fn instance_init(obj: &InitializingObject<Self>) {
115            obj.init_template();
116        }
117    }
118
119    #[glib::derived_properties]
120    impl ObjectImpl for Content {
121        fn constructed(&self) {
122            self.parent_constructed();
123
124            self.stack.connect_visible_child_notify(clone!(
125                #[weak(rename_to = imp)]
126                self,
127                move |_| {
128                    if imp.visible_page() != ContentPage::Verification {
129                        imp.identity_verification_widget
130                            .set_verification(None::<IdentityVerification>);
131                    }
132                }
133            ));
134        }
135
136        fn dispose(&self) {
137            if let Some(binding) = self.item_binding.take() {
138                binding.unbind();
139            }
140        }
141    }
142
143    impl WidgetImpl for Content {}
144
145    impl NavigationPageImpl for Content {
146        fn hidden(&self) {
147            self.obj().set_item(None::<glib::Object>);
148        }
149    }
150
151    impl Content {
152        /// The visible page of the content.
153        pub(super) fn visible_page(&self) -> ContentPage {
154            ContentPage::from_name(
155                &self
156                    .stack
157                    .visible_child_name()
158                    .expect("Content stack should always have a visible child name"),
159            )
160        }
161
162        /// Set the visible page of the content.
163        fn set_visible_page(&self, page: ContentPage) {
164            if self.visible_page() == page {
165                return;
166            }
167
168            self.stack.set_visible_child_name(page.name());
169        }
170
171        /// Set the current session.
172        fn set_session(&self, session: Option<&Session>) {
173            if session == self.session.upgrade().as_ref() {
174                return;
175            }
176            let obj = self.obj();
177
178            if let Some(binding) = self.item_binding.take() {
179                binding.unbind();
180            }
181
182            if let Some(session) = session {
183                let item_binding = session
184                    .sidebar_list_model()
185                    .selection_model()
186                    .bind_property("selected-item", &*obj, "item")
187                    .sync_create()
188                    .bidirectional()
189                    .build();
190
191                self.item_binding.replace(Some(item_binding));
192            }
193
194            self.session.set(session);
195            obj.notify_session();
196        }
197
198        /// Set the item currently displayed.
199        fn set_item(&self, item: Option<glib::Object>) {
200            if self.item.obj() == item {
201                return;
202            }
203
204            self.item.disconnect_signals();
205
206            if let Some(item) = item {
207                let handler = if let Some(room) = item.downcast_ref::<Room>() {
208                    let category_handler = room.connect_category_notify(clone!(
209                        #[weak(rename_to = imp)]
210                        self,
211                        move |_| {
212                            imp.update_visible_child();
213                        }
214                    ));
215
216                    Some(category_handler)
217                } else if let Some(verification) = item.downcast_ref::<IdentityVerification>() {
218                    let dismiss_handler = verification.connect_dismiss(clone!(
219                        #[weak(rename_to = imp)]
220                        self,
221                        move |_| {
222                            imp.set_item(None);
223                        }
224                    ));
225
226                    Some(dismiss_handler)
227                } else {
228                    None
229                };
230
231                self.item.set(item, handler.into_iter().collect());
232            }
233
234            self.update_visible_child();
235            self.obj().notify_item();
236
237            if let Some(page) = self.stack.visible_child() {
238                page.grab_focus();
239            }
240        }
241
242        /// Update the visible child according to the current item.
243        fn update_visible_child(&self) {
244            let Some(item) = self.item.obj() else {
245                self.set_visible_page(ContentPage::Empty);
246                return;
247            };
248
249            if let Some(room) = item.downcast_ref::<Room>() {
250                match room.category() {
251                    RoomCategory::Knocked => {
252                        self.invite_request.set_room(Some(room.clone()));
253                        self.set_visible_page(ContentPage::InviteRequest);
254                    }
255                    RoomCategory::Invited => {
256                        self.invite.set_room(Some(room.clone()));
257                        self.set_visible_page(ContentPage::Invite);
258                    }
259                    _ => {
260                        self.room_history.set_timeline(Some(room.live_timeline()));
261                        self.set_visible_page(ContentPage::RoomHistory);
262                    }
263                }
264            } else if item
265                .downcast_ref::<SidebarIconItem>()
266                .is_some_and(|i| i.item_type() == SidebarIconItemType::Explore)
267            {
268                self.set_visible_page(ContentPage::Explore);
269            } else if let Some(verification) = item.downcast_ref::<IdentityVerification>() {
270                self.identity_verification_widget
271                    .set_verification(Some(verification.clone()));
272                self.set_visible_page(ContentPage::Verification);
273            }
274        }
275
276        /// Handle a paste action.
277        pub(super) fn handle_paste_action(&self) {
278            if self.visible_page() == ContentPage::RoomHistory {
279                self.room_history.handle_paste_action();
280            }
281        }
282
283        /// All the header bars of the children of the content.
284        pub(super) fn header_bars(&self) -> [&adw::HeaderBar; 6] {
285            [
286                &self.empty_page_header_bar,
287                self.room_history.header_bar(),
288                self.invite_request.header_bar(),
289                self.invite.header_bar(),
290                self.explore.header_bar(),
291                &self.verification_page_header_bar,
292            ]
293        }
294    }
295}
296
297glib::wrapper! {
298    /// A view displaying the selected content in the sidebar.
299    pub struct Content(ObjectSubclass<imp::Content>)
300        @extends gtk::Widget, adw::NavigationPage,
301        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
302}
303
304impl Content {
305    pub fn new(session: &Session) -> Self {
306        glib::Object::builder().property("session", session).build()
307    }
308
309    /// Handle a paste action.
310    pub(crate) fn handle_paste_action(&self) {
311        self.imp().handle_paste_action();
312    }
313
314    /// All the header bars of the children of the content.
315    pub(crate) fn header_bars(&self) -> [&adw::HeaderBar; 6] {
316        self.imp().header_bars()
317    }
318}