hyprshell_windows_lib/overview/
root.rs1use 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 gtk_monitor,
93 })
94 .forward(sender.input_sender(), |m| match m {
95 OverviewWindowOutput::Clicked(ws) => {
96 OverviewRootInput::CloseOverviewClick(ws)
97 }
98 OverviewWindowOutput::ClickedC(cl) => {
99 OverviewRootInput::CloseOverviewClickC(cl)
100 }
101 });
102 windows.entry(monitor.id).insert_entry(overview_window);
103 }
104 }
105 let launcher_root = LauncherRoot::builder();
106 let window = &launcher_root.root;
107 app.add_window(window);
108 let launcher_root = launcher_root
109 .launch(LauncherRootInit {
110 launcher: init.overview.launcher.clone(),
111 data_dir: init.data_dir,
112 })
113 .forward(sender.input_sender(), |msg| match msg {
114 LauncherRootOutput::Switch(dir, ws) => OverviewRootInput::Switch(dir, ws),
115 LauncherRootOutput::Close(do_switch) => OverviewRootInput::CloseOverview(do_switch),
116 });
117
118 let model = Self {
119 general: init.general,
120 overview: init.overview,
121 open: false,
122 windows,
123 launcher_root,
124 data: OverviewData::default(),
125 };
126
127 let widgets = view_output!();
128 sender
129 .input_sender()
130 .emit(OverviewRootInput::SetOverview(model.overview.clone()));
131 ComponentParts { model, widgets }
132 }
133
134 fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
135 trace!("overview::root::update: {message:?}");
136 match message {
137 OverviewRootInput::SetOverview(overview) => {
138 self.overview = overview;
139 self.launcher_root.emit(LauncherRootInput::SetLauncher(
140 self.overview.launcher.clone(),
141 ));
142 }
143 OverviewRootInput::SetGeneral(general) => {
144 for window in self.windows.values_mut() {
145 window.emit(OverviewWindowInput::SetGeneral(general.clone()));
146 }
147 self.general = general;
148 }
149 OverviewRootInput::OpenOverview => {
150 if self.open {
151 sender
152 .input_sender()
153 .emit(OverviewRootInput::CloseOverview(false));
154 } else {
155 self.open = true;
156 self.launcher_root.emit(LauncherRootInput::OpenLauncher);
157 self.open_overview();
158 }
159 }
160 OverviewRootInput::Switch(direction, workspace) => {
161 if self.open {
162 self.navigate(direction, workspace);
163 } else {
164 trace!("not open");
165 }
166 }
167 OverviewRootInput::CloseOverview(do_switch) => {
168 if self.open {
169 self.open = false;
170 self.launcher_root.emit(LauncherRootInput::CloseLauncher);
171 self.close_overview(do_switch);
172 } else {
173 trace!("not open");
174 }
175 }
176 OverviewRootInput::CloseItem(id) => {
177 if self.open {
178 self.close_item(id);
179 } else {
180 trace!("not open");
181 }
182 sender
183 .input_sender()
184 .emit(OverviewRootInput::ReloadOverview);
185 }
186 OverviewRootInput::ReloadOverview => {
187 if self.open {
188 self.reload_overview();
189 } else {
190 trace!("not open");
191 }
192 }
193 OverviewRootInput::CloseOverviewClick(ws) => {
194 self.data.active.client = None;
195 self.data.active.workspace = ws;
196 sender
197 .input_sender()
198 .emit(OverviewRootInput::CloseOverview(true));
199 }
200 OverviewRootInput::CloseOverviewClickC(cl) => {
201 self.data.active.client = Some(cl);
202 sender
203 .input_sender()
204 .emit(OverviewRootInput::CloseOverview(true));
205 }
206 }
207 }
208}
209
210impl OverviewRoot {
211 fn open_overview(&mut self) {
212 let (hypr_data, active) = match collect_data(&SortConfig {
213 filter_current_monitor: self.overview.filter_by_current_monitor,
214 filter_current_workspace: self.overview.filter_by_current_workspace,
215 filter_same_class: self.overview.filter_by_same_class,
216 sort_recent: false,
217 exclude_workspaces: if self.overview.exclude_workspaces.is_empty() {
218 None
219 } else {
220 Some(self.overview.exclude_workspaces.clone())
221 },
222 }) {
223 Ok(data) => data,
224 Err(e) => {
225 error!("Failed to collect data: {}", e);
226 return;
227 }
228 };
229 self.data = OverviewData {
230 active,
231 hypr_data: hypr_data.clone(),
232 };
233 self.render(hypr_data, self.data.active, true);
234 }
235
236 fn navigate(&mut self, direction: Direction, workspace: bool) {
237 let new_active = if workspace {
238 find_next_workspace(
239 direction,
240 false,
241 &self.data.hypr_data,
242 self.data.active,
243 self.general.items_per_row,
244 )
245 } else {
246 if direction == Direction::Up || direction == Direction::Down {
247 error!(
248 "Clients in overview can only be switched left and right (forwards and backwards)"
249 );
250 return;
251 }
252 find_next_client(
253 direction,
254 false,
255 &self.data.hypr_data,
256 self.data.active,
257 self.general.items_per_row,
258 )
259 };
260
261 let old_active = self.data.active;
262 self.data.active = new_active;
263
264 if new_active != old_active {
265 for window in self.windows.values() {
266 window.emit(OverviewWindowInput::SetActive(old_active, new_active));
267 }
268 }
269 }
270
271 #[allow(clippy::unused_self)]
272 fn close_item(&self, id: ClientId) {
273 if let Err(e) = exec_lib::kill::kill_client_blocking(id, KILL_TIMEOUT) {
274 tracing::warn!("Failed to kill client {id}: {e}");
276 }
277 }
278
279 fn close_overview(&self, do_switch: bool) {
280 for window in self.windows.values() {
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 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(&self, hypr_data: HyprlandData, active: Active, open: bool) {
365 let mut mapped_ws = BTreeMap::new();
366 for (i, workspace_data) in hypr_data.workspaces {
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 {
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 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}