hyprshell_windows_lib/overview/
open.rs1use crate::data::{SortConfig, collect_data};
2use crate::global::WindowsOverviewData;
3use crate::icon::set_icon;
4use adw::gtk::gdk::Cursor;
5use adw::gtk::prelude::*;
6use adw::gtk::{Button, Fixed, Frame, Image, Label, Overflow, Overlay, pango};
7use anyhow::Context;
8use async_channel::Sender;
9use core_lib::transfer::{CloseOverviewConfig, TransferType, WindowsOverride};
10use core_lib::{ClientId, WarnWithDetails};
11use exec_lib::set_no_follow_mouse;
12use std::borrow::Cow;
13use tracing::{debug, debug_span, trace};
14
15fn scale<T: Into<f64>>(value: T, scale: f64) -> i32 {
16 (value.into() / (15f64 - scale)) as i32
17}
18
19#[must_use]
20pub fn overview_already_open(data: &WindowsOverviewData) -> bool {
21 data.window_list.iter().any(|w| w.0.get_visible())
22}
23
24#[allow(clippy::too_many_lines)]
25pub fn open_overview(
26 data: &mut WindowsOverviewData,
27 event_sender: &Sender<TransferType>,
28) -> anyhow::Result<()> {
29 let _span = debug_span!("open_overview").entered();
30 set_no_follow_mouse().warn_details("Failed to set set_remain_focused");
31
32 let (hypr_data, active) = collect_data(&SortConfig {
33 filter_current_monitor: data.config.filter_current_monitor,
34 filter_current_workspace: data.config.filter_current_workspace,
35 filter_same_class: data.config.filter_same_class,
36 sort_recent: false,
37 })
38 .context("Failed to collect data")?;
39 let remove_html = regex::Regex::new(r"<[^>]*>").context("Invalid regex")?;
40
41 for (window, monitor_data) in &mut data.window_list {
42 trace!("Showing window {:?}", window.id());
43 window.set_visible(true);
44
45 'workspaces: for (wid, workspace) in &hypr_data.workspaces {
46 if workspace.monitor != monitor_data.id {
47 continue 'workspaces;
48 }
49 trace!(
50 "Creating workspace {wid} with ({}x{})",
51 scale(workspace.width, data.config.scale),
52 scale(workspace.height, data.config.scale)
53 );
54 let workspace_fixed = Fixed::builder()
55 .width_request(scale(workspace.width, data.config.scale))
56 .height_request(scale(workspace.height, data.config.scale))
57 .build();
58 let id_string = wid.to_string();
59 let title = if workspace.name.trim().is_empty() {
60 Cow::from(&id_string)
61 } else {
62 remove_html.replace_all(&workspace.name, "")
63 };
64
65 let workspace_frame = Frame::builder()
66 .label(title)
67 .label_xalign(0.5)
68 .child(&workspace_fixed)
69 .build();
70
71 let workspace_button = {
72 let workspace_overlay = Overlay::builder().child(&workspace_frame).build();
73 let button = adw::gtk::Box::builder().css_classes(["workspace"]).build();
74 button.append(&workspace_overlay);
75 if active.client.is_none() && active.workspace == *wid {
76 button.add_css_class("active");
77 }
78 button
79 };
80 if workspace.name.starts_with("special:") {
81 workspace_button.add_css_class("special");
82 }
83 monitor_data.workspaces_flow.insert(&workspace_button, -1);
84 monitor_data.workspaces.insert(*wid, workspace_button);
85
86 'clients: for (address, client) in &hypr_data.clients {
87 if client.workspace != *wid {
88 continue 'clients;
89 }
90 let client_button = {
91 let title = if client.title.trim().is_empty() {
92 &client.class
93 } else {
94 &client.title
95 };
96 let client_label = Label::builder()
97 .label(title)
98 .overflow(Overflow::Visible)
99 .margin_start(6)
100 .ellipsize(pango::EllipsizeMode::End)
101 .build();
102 let client_frame = Frame::builder()
103 .label_xalign(0.5)
104 .label_widget(&client_label)
105 .build();
106
107 let client_h_w = scale(client.height, data.config.scale)
109 .min(scale(client.width, data.config.scale));
110 if client_h_w > 70 {
111 let image = Image::builder()
112 .css_classes(["client-image"])
113 .pixel_size((f64::from(client_h_w.clamp(50, 600)) / 1.6) as i32 - 20)
114 .build();
115 if !client.enabled {
116 image.add_css_class("monochrome");
117 }
118 set_icon(&client.class, client.pid, &image);
119 client_frame.set_child(Some(&image));
120 }
121
122 let client_overlay = Overlay::builder()
123 .overflow(Overflow::Hidden)
124 .child(&client_frame)
125 .build();
126 let button = Button::builder()
127 .child(&client_overlay)
128 .css_classes(["client"])
129 .width_request(scale(client.width, data.config.scale))
130 .height_request(scale(client.height, data.config.scale))
131 .build();
132 button.set_cursor(Cursor::from_name("pointer", None).as_ref());
133
134 if active.client == Some(*address) {
136 button.add_css_class("active");
137 }
138
139 click_client(&button, *address, event_sender.clone());
140 button
141 };
142 trace!(
143 "Creating Client {address} with ({}x{}) at ({}x{})",
144 scale(client.width, data.config.scale),
145 scale(client.height, data.config.scale),
146 f64::from(scale(client.x, data.config.scale)),
147 f64::from(scale(client.y, data.config.scale))
148 );
149 workspace_fixed.put(
150 &client_button,
151 f64::from(scale(client.x, data.config.scale)),
152 f64::from(scale(client.y, data.config.scale)),
153 );
154 monitor_data.clients.insert(*address, client_button);
155 }
156 }
157 }
158
159 data.active = active;
160 data.initial_active = active;
161 data.hypr_data = hypr_data;
162 Ok(())
163}
164
165fn click_client(button: &Button, client_id: ClientId, event_sender: Sender<TransferType>) {
166 button.connect_clicked(move |_| {
167 debug!("Exiting on click of client button");
168 event_sender
169 .send_blocking(TransferType::CloseOverview(CloseOverviewConfig::Windows(
170 WindowsOverride::ClientId(client_id),
171 )))
172 .warn_details("unable to send");
173 });
174}