fractal/session/view/content/explore/
mod.rs1use adw::{prelude::*, subclass::prelude::*};
2use gtk::{gio, glib, glib::clone, CompositeTemplate};
3use tracing::error;
4
5mod public_room_row;
6mod search;
7mod server;
8mod server_list;
9mod server_row;
10mod servers_popover;
11
12use self::{
13 public_room_row::PublicRoomRow, search::ExploreSearch, server::ExploreServer,
14 server_list::ExploreServerList, server_row::ExploreServerRow,
15 servers_popover::ExploreServersPopover,
16};
17use crate::{
18 components::LoadingRow,
19 prelude::*,
20 session::model::{RemoteRoom, Session},
21 utils::{LoadingState, SingleItemListModel},
22};
23
24mod imp {
25 use std::cell::OnceCell;
26
27 use glib::subclass::InitializingObject;
28
29 use super::*;
30
31 #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
32 #[template(resource = "/org/gnome/Fractal/ui/session/view/content/explore/mod.ui")]
33 #[properties(wrapper_type = super::Explore)]
34 pub struct Explore {
35 #[template_child]
36 pub(super) header_bar: TemplateChild<adw::HeaderBar>,
37 #[template_child]
38 second_top_bar: TemplateChild<adw::Bin>,
39 #[template_child]
40 search_clamp: TemplateChild<adw::Clamp>,
41 #[template_child]
42 search_entry: TemplateChild<gtk::SearchEntry>,
43 #[template_child]
44 servers_button: TemplateChild<gtk::MenuButton>,
45 #[template_child]
46 servers_popover: TemplateChild<ExploreServersPopover>,
47 #[template_child]
48 stack: TemplateChild<gtk::Stack>,
49 #[template_child]
50 scrolled_window: TemplateChild<gtk::ScrolledWindow>,
51 #[template_child]
52 listview: TemplateChild<gtk::ListView>,
53 #[property(get, set = Self::set_session, explicit_notify)]
55 session: glib::WeakRef<Session>,
56 search: ExploreSearch,
58 end_items: OnceCell<SingleItemListModel>,
60 full_model: OnceCell<gio::ListStore>,
62 }
63
64 #[glib::object_subclass]
65 impl ObjectSubclass for Explore {
66 const NAME: &'static str = "ContentExplore";
67 type Type = super::Explore;
68 type ParentType = adw::BreakpointBin;
69
70 fn class_init(klass: &mut Self::Class) {
71 Self::bind_template(klass);
72 Self::bind_template_callbacks(klass);
73
74 klass.set_accessible_role(gtk::AccessibleRole::Group);
75 }
76
77 fn instance_init(obj: &InitializingObject<Self>) {
78 obj.init_template();
79 }
80 }
81
82 #[glib::derived_properties]
83 impl ObjectImpl for Explore {
84 fn constructed(&self) {
85 self.parent_constructed();
86
87 self.servers_popover.connect_selected_server_notify(clone!(
89 #[weak(rename_to = imp)]
90 self,
91 move |_| {
92 imp.server_changed();
93 }
94 ));
95
96 let adj = self.scrolled_window.vadjustment();
98 adj.connect_value_changed(clone!(
99 #[weak(rename_to = imp)]
100 self,
101 move |adj| {
102 if adj.upper() - adj.value() < adj.page_size() * 2.0 {
103 imp.search.load_more();
104 }
105 }
106 ));
107
108 let factory = gtk::SignalListItemFactory::new();
110 factory.connect_bind(move |_, list_item| {
111 let Some(list_item) = list_item.downcast_ref::<gtk::ListItem>() else {
112 error!("List item factory did not receive a list item: {list_item:?}");
113 return;
114 };
115 list_item.set_activatable(false);
116 list_item.set_selectable(false);
117
118 let Some(item) = list_item.item() else {
119 return;
120 };
121
122 if let Some(room) = item.downcast_ref::<RemoteRoom>() {
123 let public_room_row = list_item.child_or_default::<PublicRoomRow>();
124 public_room_row.set_room(room);
125 } else if let Some(loading_row) = item.downcast_ref::<LoadingRow>() {
126 list_item.set_child(Some(loading_row));
127 }
128 });
129 self.listview.set_factory(Some(&factory));
130
131 let flattened_model = gtk::FlattenListModel::new(Some(self.full_model().clone()));
132 self.listview
133 .set_model(Some(>k::NoSelection::new(Some(flattened_model))));
134
135 self.search.connect_loading_state_notify(clone!(
137 #[weak(rename_to = imp)]
138 self,
139 move |_| {
140 imp.update_visible_child();
141 }
142 ));
143
144 self.search.list().connect_items_changed(clone!(
146 #[weak(rename_to = imp)]
147 self,
148 move |_, _, _, _| {
149 imp.update_visible_child();
150 }
151 ));
152 }
153 }
154
155 impl WidgetImpl for Explore {
156 fn grab_focus(&self) -> bool {
157 self.search_entry.grab_focus()
158 }
159
160 fn map(&self) {
161 self.parent_map();
162 self.trigger_search();
163 }
164 }
165
166 impl BreakpointBinImpl for Explore {}
167
168 #[gtk::template_callbacks]
169 impl Explore {
170 fn set_session(&self, session: Option<&Session>) {
172 if self.session.upgrade().as_ref() == session {
173 return;
174 }
175
176 self.session.set(session);
177
178 self.trigger_search();
179 self.obj().notify_session();
180 }
181
182 fn end_items(&self) -> &SingleItemListModel {
184 self.end_items.get_or_init(|| {
185 let model = SingleItemListModel::new(&LoadingRow::new());
186 model.set_is_hidden(true);
187 model
188 })
189 }
190
191 fn full_model(&self) -> &gio::ListStore {
193 self.full_model.get_or_init(|| {
194 let model = gio::ListStore::new::<gio::ListModel>();
195 model.append(&self.search.list());
196 model.append(self.end_items());
197 model
198 })
199 }
200
201 #[template_callback]
203 fn switch_to_narrow_mode(&self) {
204 if self
205 .header_bar
206 .title_widget()
207 .is_some_and(|widget| widget == *self.servers_button)
208 {
209 return;
211 }
212
213 self.header_bar.remove(&*self.search_clamp);
215 self.header_bar.remove(&*self.servers_button);
216
217 self.header_bar
220 .set_title_widget(Some(&*self.servers_button));
221 self.second_top_bar.set_child(Some(&*self.search_clamp));
222 self.second_top_bar.set_visible(true);
223 }
224
225 #[template_callback]
227 fn switch_to_wide_mode(&self) {
228 if self
229 .header_bar
230 .title_widget()
231 .is_some_and(|widget| widget == *self.search_clamp)
232 {
233 return;
235 }
236
237 self.header_bar.remove(&*self.servers_button);
239 self.second_top_bar.set_child(None::<>k::Widget>);
240 self.second_top_bar.set_visible(false);
241
242 self.header_bar.set_title_widget(Some(&*self.search_clamp));
244 self.header_bar.pack_end(&*self.servers_button);
245 }
246
247 fn update_visible_child(&self) {
249 let loading_state = self.search.loading_state();
250 let is_empty = self.search.is_empty();
251
252 let show_loading_row = matches!(loading_state, LoadingState::Loading) && !is_empty;
254 self.end_items().set_is_hidden(!show_loading_row);
255
256 let page_name = match loading_state {
258 LoadingState::Initial | LoadingState::Loading => {
259 if is_empty {
260 "loading"
261 } else {
262 "results"
263 }
264 }
265 LoadingState::Ready => {
266 if is_empty {
267 "empty"
268 } else {
269 "results"
270 }
271 }
272 LoadingState::Error => "error",
273 };
274 self.stack.set_visible_child_name(page_name);
275 }
276
277 #[template_callback]
279 pub(super) fn trigger_search(&self) {
280 if !self.obj().is_mapped() {
281 return;
283 }
284
285 let Some(session) = self.session.upgrade() else {
286 return;
287 };
288
289 self.servers_popover.set_session(&session);
290
291 let text = self.search_entry.text().into();
292 let server = self
293 .servers_popover
294 .selected_server()
295 .expect("a server should be selected");
296 self.search.search(&session, Some(text), &server);
297 }
298
299 fn server_changed(&self) {
301 if let Some(server) = self.servers_popover.selected_server() {
302 self.servers_button.set_label(&server.name());
303 self.trigger_search();
304 }
305 }
306 }
307}
308
309glib::wrapper! {
310 pub struct Explore(ObjectSubclass<imp::Explore>)
312 @extends gtk::Widget, adw::BreakpointBin, @implements gtk::Accessible;
313}
314
315impl Explore {
316 pub fn new(session: &Session) -> Self {
317 glib::Object::builder().property("session", session).build()
318 }
319
320 pub(crate) fn header_bar(&self) -> &adw::HeaderBar {
322 &self.imp().header_bar
323 }
324}