Skip to main content

hyprshell_windows_lib/overview/
root.rs

1use crate::data::{SortConfig, collect_data};
2use crate::next::{find_next_client, find_next_workspace};
3use crate::overview::window::{
4    OverviewWindow, OverviewWindowData, OverviewWindowInit, OverviewWindowInput,
5    OverviewWindowOutput,
6};
7use core_lib::{Active, ByFirst, ClientId, Direction, HyprlandData, MonitorId, WorkspaceId};
8use exec_lib::switch::{switch_client, switch_workspace};
9use launcher_lib::{LauncherRoot, LauncherRootInit, LauncherRootInput, LauncherRootOutput};
10use relm4::adw::gdk::{Display, Monitor};
11use relm4::adw::glib::ControlFlow;
12use relm4::adw::prelude::*;
13use relm4::adw::{glib, gtk};
14use relm4::prelude::*;
15use std::collections::BTreeMap;
16use std::path::PathBuf;
17use std::rc::Rc;
18use std::time::Duration;
19use tracing::{debug, error, trace};
20
21const KILL_TIMEOUT: Duration = Duration::from_millis(200);
22
23#[derive(Debug)]
24pub struct OverviewRoot {
25    general: config_lib::WindowsGeneral,
26    overview: config_lib::Overview,
27    open: bool,
28    data: OverviewData,
29
30    launcher_root: Controller<LauncherRoot>,
31    windows: BTreeMap<MonitorId, Controller<OverviewWindow>>,
32}
33
34#[derive(Debug)]
35pub enum OverviewRootInput {
36    SetOverview(config_lib::Overview),
37    SetGeneral(config_lib::WindowsGeneral),
38    OpenOverview,
39    Switch(Direction, bool),
40    CloseOverview(bool),
41    CloseOverviewClick(WorkspaceId),
42    CloseOverviewClickC(ClientId),
43    CloseItem(ClientId),
44    ReloadOverview,
45}
46
47#[derive(Debug)]
48pub struct OverviewRootInit {
49    pub general: config_lib::WindowsGeneral,
50    pub overview: config_lib::Overview,
51    pub data_dir: Rc<PathBuf>,
52}
53
54#[derive(Debug)]
55pub enum OverviewRootOutput {}
56
57#[relm4::component(pub)]
58impl SimpleComponent for OverviewRoot {
59    type Init = OverviewRootInit;
60    type Input = OverviewRootInput;
61    type Output = OverviewRootOutput;
62
63    view! {
64        gtk::Window {
65        }
66    }
67    fn init(
68        init: Self::Init,
69        root: Self::Root,
70        sender: ComponentSender<Self>,
71    ) -> ComponentParts<Self> {
72        trace!("Initializing OverviewRoot");
73        let app = relm4::main_application();
74        let mut windows = BTreeMap::new();
75
76        let monitors = exec_lib::collect::get_monitors();
77        let gmonitors = Display::default()
78            .expect("Could not connect to a display")
79            .monitors()
80            .iter()
81            .filter_map(Result::ok)
82            .collect::<Vec<Monitor>>();
83        for gtk_monitor in gmonitors {
84            let monitor_conn = gtk_monitor.connector().unwrap_or_default();
85            if let Some(monitor) = monitors.iter().find(|m| m.connector == monitor_conn) {
86                let overview_window = OverviewWindow::builder();
87                let window = &overview_window.root;
88                app.add_window(window);
89                let overview_window = overview_window
90                    .launch(OverviewWindowInit {
91                        general: init.general.clone(),
92                        monitor: monitor.clone(),
93                        gtk_monitor,
94                    })
95                    .forward(sender.input_sender(), |m| match m {
96                        OverviewWindowOutput::Clicked(ws) => {
97                            OverviewRootInput::CloseOverviewClick(ws)
98                        }
99                        OverviewWindowOutput::ClickedC(cl) => {
100                            OverviewRootInput::CloseOverviewClickC(cl)
101                        }
102                    });
103                windows.entry(monitor.id).insert_entry(overview_window);
104            }
105        }
106        let launcher_root = LauncherRoot::builder();
107        let window = &launcher_root.root;
108        app.add_window(window);
109        let launcher_root = launcher_root
110            .launch(LauncherRootInit {
111                launcher: init.overview.launcher.clone(),
112                data_dir: init.data_dir,
113            })
114            .forward(sender.input_sender(), |msg| match msg {
115                LauncherRootOutput::Switch(dir, ws) => OverviewRootInput::Switch(dir, ws),
116                LauncherRootOutput::Close(do_switch) => OverviewRootInput::CloseOverview(do_switch),
117            });
118
119        let model = Self {
120            general: init.general,
121            overview: init.overview,
122            open: false,
123            windows,
124            launcher_root,
125            data: OverviewData::default(),
126        };
127
128        let widgets = view_output!();
129        sender
130            .input_sender()
131            .emit(OverviewRootInput::SetOverview(model.overview.clone()));
132        ComponentParts { model, widgets }
133    }
134
135    fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
136        trace!("overview::root::update: {message:?}");
137        match message {
138            OverviewRootInput::SetOverview(overview) => {
139                self.overview = overview;
140                self.launcher_root.emit(LauncherRootInput::SetLauncher(
141                    self.overview.launcher.clone(),
142                ));
143            }
144            OverviewRootInput::SetGeneral(general) => {
145                for window in self.windows.values_mut() {
146                    window.emit(OverviewWindowInput::SetGeneral(general.clone()));
147                }
148                self.general = general;
149            }
150            OverviewRootInput::OpenOverview => {
151                if !self.open {
152                    self.open = true;
153                    self.launcher_root.emit(LauncherRootInput::OpenLauncher);
154                    self.open_overview();
155                } else {
156                    sender
157                        .input_sender()
158                        .emit(OverviewRootInput::CloseOverview(false));
159                }
160            }
161            OverviewRootInput::Switch(direction, workspace) => {
162                if self.open {
163                    self.navigate(direction, workspace);
164                } else {
165                    trace!("not open");
166                }
167            }
168            OverviewRootInput::CloseOverview(do_switch) => {
169                if self.open {
170                    self.open = false;
171                    self.launcher_root.emit(LauncherRootInput::CloseLauncher);
172                    self.close_overview(do_switch);
173                } else {
174                    trace!("not open");
175                }
176            }
177            OverviewRootInput::CloseItem(id) => {
178                if self.open {
179                    self.close_item(id);
180                } else {
181                    trace!("not open");
182                }
183                sender
184                    .input_sender()
185                    .emit(OverviewRootInput::ReloadOverview);
186            }
187            OverviewRootInput::ReloadOverview => {
188                if self.open {
189                    self.reload_overview();
190                } else {
191                    trace!("not open");
192                }
193            }
194            OverviewRootInput::CloseOverviewClick(ws) => {
195                self.data.active.client = None;
196                self.data.active.workspace = ws;
197                sender
198                    .input_sender()
199                    .emit(OverviewRootInput::CloseOverview(true));
200            }
201            OverviewRootInput::CloseOverviewClickC(cl) => {
202                self.data.active.client = Some(cl);
203                sender
204                    .input_sender()
205                    .emit(OverviewRootInput::CloseOverview(true));
206            }
207        }
208    }
209}
210
211impl OverviewRoot {
212    fn open_overview(&mut self) {
213        let (hypr_data, active) = match collect_data(&SortConfig {
214            filter_current_monitor: self.overview.filter_by_current_monitor,
215            filter_current_workspace: self.overview.filter_by_current_workspace,
216            filter_same_class: self.overview.filter_by_same_class,
217            sort_recent: false,
218            exclude_workspaces: if self.overview.exclude_workspaces.is_empty() {
219                None
220            } else {
221                Some(self.overview.exclude_workspaces.clone())
222            },
223        }) {
224            Ok(data) => data,
225            Err(e) => {
226                error!("Failed to collect data: {}", e);
227                return;
228            }
229        };
230        self.data = OverviewData {
231            active,
232            hypr_data: hypr_data.clone(),
233        };
234        self.render(hypr_data, self.data.active, true);
235    }
236
237    fn navigate(&mut self, direction: Direction, workspace: bool) {
238        let new_active = if workspace {
239            find_next_workspace(
240                &direction,
241                false,
242                &self.data.hypr_data,
243                self.data.active,
244                self.general.items_per_row,
245            )
246        } else {
247            if direction == Direction::Up || direction == Direction::Down {
248                error!(
249                    "Clients in overview can only be switched left and right (forwards and backwards)"
250                );
251                return;
252            }
253            find_next_client(
254                &direction,
255                false,
256                &self.data.hypr_data,
257                self.data.active,
258                self.general.items_per_row,
259            )
260        };
261
262        let old_active = self.data.active;
263        self.data.active = new_active;
264
265        if new_active != old_active {
266            for (_, window) in &self.windows {
267                window.emit(OverviewWindowInput::SetActive(old_active, new_active))
268            }
269        }
270    }
271
272    fn close_item(&mut self, id: ClientId) {
273        if let Err(e) = exec_lib::kill::kill_client_blocking(id, KILL_TIMEOUT) {
274            // TODO: close on killed to let user close window themself
275            tracing::warn!("Failed to kill client {id}: {e}");
276        }
277    }
278
279    fn close_overview(&mut self, do_switch: bool) {
280        for (_, window) in &self.windows {
281            window.emit(OverviewWindowInput::CloseOverview)
282        }
283
284        if do_switch {
285            if let Some(id) = self.data.active.client {
286                debug!(
287                    "Switching to client {}",
288                    self.data
289                        .hypr_data
290                        .clients
291                        .iter()
292                        .find(|(cid, _)| *cid == id)
293                        .map_or_else(|| "<Unknown>".to_string(), |(_, c)| c.title.clone())
294                );
295                // Defer execution to ensure window is hidden first
296                glib::idle_add_local(move || {
297                    if let Err(e) = switch_client(id) {
298                        tracing::warn!("Failed to switch to client {id:?}: {e}");
299                    }
300                    ControlFlow::Break
301                });
302            } else {
303                let id = self.data.active.workspace;
304                debug!(
305                    "Switching to workspace {}",
306                    self.data
307                        .hypr_data
308                        .workspaces
309                        .iter()
310                        .find(|(wid, _)| *wid == id)
311                        .map_or_else(|| "<Unknown>".to_string(), |(_, w)| w.name.clone())
312                );
313                glib::idle_add_local(move || {
314                    if let Err(e) = switch_workspace(id) {
315                        tracing::warn!("Failed to switch to workspace {id:?}: {e}");
316                    }
317                    ControlFlow::Break
318                });
319            }
320        }
321    }
322
323    fn reload_overview(&mut self) {
324        let (hypr_data, _active) = match collect_data(&SortConfig {
325            filter_current_monitor: self.overview.filter_by_current_monitor,
326            filter_current_workspace: self.overview.filter_by_current_workspace,
327            filter_same_class: self.overview.filter_by_same_class,
328            sort_recent: false,
329            exclude_workspaces: if self.overview.exclude_workspaces.is_empty() {
330                None
331            } else {
332                Some(self.overview.exclude_workspaces.clone())
333            },
334        }) {
335            Ok(data) => data,
336            Err(e) => {
337                error!("Failed to collect data: {}", e);
338                return;
339            }
340        };
341
342        while match self.data.active {
343            Active {
344                client: Some(id), ..
345            } => hypr_data.clients.find_by_first(&id).is_none(),
346            Active { workspace: id, .. } => hypr_data.workspaces.find_by_first(&id).is_none(),
347        } {
348            self.data.active = find_next_workspace(
349                &Direction::Right,
350                true,
351                &hypr_data,
352                self.data.active,
353                self.general.items_per_row,
354            )
355        }
356
357        self.data = OverviewData {
358            active: self.data.active,
359            hypr_data: hypr_data.clone(),
360        };
361        self.render(hypr_data, self.data.active, false);
362    }
363
364    fn render(&mut self, hypr_data: HyprlandData, active: Active, open: bool) {
365        let mut mapped_ws = BTreeMap::new();
366        for (i, workspace_data) in hypr_data.workspaces.into_iter() {
367            mapped_ws
368                .entry(workspace_data.monitor)
369                .or_insert_with(Vec::new)
370                .push((i, workspace_data));
371        }
372        let mut mapped_cl = BTreeMap::new();
373        for (i, client_data) in hypr_data.clients.into_iter() {
374            mapped_cl
375                .entry(client_data.monitor)
376                .or_insert_with(Vec::new)
377                .push((i, client_data));
378        }
379
380        for (monitor_id, window) in &self.windows {
381            if let Some(data) = hypr_data.monitors.find_by_first(monitor_id) {
382                // always update, maybe last client got removed
383                let data = OverviewWindowData {
384                    active,
385                    clients: mapped_cl.remove(monitor_id).unwrap_or_default(),
386                    workspaces: mapped_ws.remove(monitor_id).unwrap_or_default(),
387                    monitor: data.clone(),
388                };
389                if open {
390                    window.emit(OverviewWindowInput::OpenOverview((
391                        data,
392                        self.overview.top_offset,
393                    )))
394                } else {
395                    window.emit(OverviewWindowInput::ReloadOverview(data))
396                }
397            }
398        }
399    }
400}
401
402#[derive(Debug)]
403pub struct OverviewData {
404    pub active: Active,
405    pub hypr_data: HyprlandData,
406}
407
408impl Default for OverviewData {
409    fn default() -> Self {
410        Self {
411            active: Active {
412                client: None,
413                workspace: -1,
414                monitor: -1,
415            },
416            hypr_data: HyprlandData::default(),
417        }
418    }
419}