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 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 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 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 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}