hyprshell_windows_lib/switch/
open.rs

1use crate::data::{SortConfig, collect_data};
2use crate::global::WindowsSwitchData;
3use crate::icon::set_icon;
4use crate::next::{find_next_client, find_next_workspace};
5use adw::gtk::gdk::Cursor;
6use adw::gtk::prelude::*;
7use adw::gtk::{Button, Fixed, Frame, Image, Label, Overflow, Overlay, pango};
8use anyhow::Context;
9use core_lib::transfer::{Direction, OpenSwitch};
10use core_lib::{ClientData, ClientId, WarnWithDetails};
11use exec_lib::{get_current_monitor, set_no_follow_mouse};
12use std::borrow::Cow;
13use tracing::{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 switch_already_open(data: &WindowsSwitchData) -> bool {
21    data.window.get_visible()
22}
23
24#[allow(clippy::too_many_lines)]
25pub fn open_switch(data: &mut WindowsSwitchData, config: &OpenSwitch) -> anyhow::Result<()> {
26    let _span = debug_span!("open_switch").entered();
27    set_no_follow_mouse().warn_details("Failed to set set_remain_focused");
28
29    let (clients_data, active_prev) = collect_data(&SortConfig {
30        filter_current_monitor: data.config.filter_current_monitor,
31        filter_current_workspace: data.config.filter_current_workspace,
32        filter_same_class: data.config.filter_same_class,
33        sort_recent: true,
34    })
35    .context("Failed to collect data")?;
36    let dir = if config.reverse {
37        Direction::Left
38    } else {
39        Direction::Right
40    };
41    let active = if data.config.switch_workspaces {
42        find_next_workspace(
43            &dir,
44            true,
45            &clients_data,
46            active_prev,
47            data.config.items_per_row,
48        )
49    } else {
50        find_next_client(
51            &dir,
52            true,
53            &clients_data,
54            active_prev,
55            data.config.items_per_row,
56        )
57    };
58
59    let remove_html = regex::Regex::new(r"<[^>]*>").context("Invalid regex")?;
60
61    trace!("Showing window {:?}", data.window.id());
62    data.window.set_visible(true);
63
64    let current_monitor = get_current_monitor().context("Failed to get current monitor")?;
65
66    if data.config.switch_workspaces {
67        for (wid, workspace) in &clients_data.workspaces {
68            let clients: Vec<&(ClientId, ClientData)> = {
69                let mut clients = clients_data
70                    .clients
71                    .iter()
72                    .filter(|(_, client)| client.workspace == *wid && client.enabled)
73                    .collect::<Vec<_>>();
74                clients.sort_by(|(_, a), (_, b)| {
75                    // prefer smaller windows
76                    if a.floating && b.floating {
77                        (b.width * b.height).cmp(&(a.width * a.height))
78                    } else {
79                        a.floating.cmp(&b.floating)
80                    }
81                });
82                clients
83            };
84            if clients.is_empty() {
85                continue;
86            }
87            let workspace_fixed = Fixed::builder()
88                .width_request(scale(workspace.width, data.config.scale))
89                .height_request(scale(workspace.height, data.config.scale))
90                .build();
91            let id_string = wid.to_string();
92            let title = if workspace.name.trim().is_empty() {
93                Cow::from(&id_string)
94            } else {
95                remove_html.replace_all(&workspace.name, "")
96            };
97            let workspace_frame = Frame::builder()
98                .label(title)
99                .label_xalign(0.5)
100                .child(&workspace_fixed)
101                .build();
102
103            let workspace_button = {
104                let workspace_overlay = Overlay::builder().child(&workspace_frame).build();
105                let button = Button::builder()
106                    .child(&workspace_overlay)
107                    .css_classes(["workspace", "no-hover"])
108                    .build();
109                button.set_cursor(Cursor::from_name("pointer", None).as_ref());
110                if active.workspace == *wid {
111                    button.add_css_class("active");
112                }
113                button
114            };
115            if workspace.name.starts_with("special:") {
116                workspace_button.add_css_class("special");
117            }
118            data.main_flow.insert(&workspace_button, -1);
119            data.workspaces.insert(*wid, workspace_button);
120
121            for (address, client) in clients {
122                if !client.enabled {
123                    continue;
124                }
125
126                let client_button = {
127                    let title = if client.title.trim().is_empty() {
128                        &client.class
129                    } else {
130                        &client.title
131                    };
132                    let client_label = Label::builder()
133                        .label(title)
134                        .overflow(Overflow::Visible)
135                        .margin_start(6)
136                        .ellipsize(pango::EllipsizeMode::End)
137                        .build();
138                    let client_frame = Frame::builder()
139                        .label_xalign(0.5)
140                        .label_widget(&client_label)
141                        .build();
142
143                    // hide picture if client so small
144                    let client_h_w = scale(client.height, data.config.scale)
145                        .min(scale(client.width, data.config.scale));
146                    if client_h_w > 70 {
147                        let image = Image::builder()
148                            .css_classes(["client-image"])
149                            .pixel_size((f64::from(client_h_w.clamp(50, 600)) / 1.6) as i32 - 20)
150                            .build();
151                        if !client.enabled {
152                            image.add_css_class("monochrome");
153                        }
154                        set_icon(&client.class, client.pid, &image);
155                        client_frame.set_child(Some(&image));
156                    }
157
158                    let client_overlay = Overlay::builder()
159                        .overflow(Overflow::Hidden)
160                        .child(&client_frame)
161                        .build();
162                    let button = Button::builder()
163                        .child(&client_overlay)
164                        .css_classes(["client", "no-hover"])
165                        .width_request(scale(client.width, data.config.scale))
166                        .height_request(scale(client.height, data.config.scale))
167                        .build();
168                    button.set_cursor(Cursor::from_name("pointer", None).as_ref());
169
170                    // add initial border around initial active client
171                    if active.client == Some(*address) {
172                        button.add_css_class("active");
173                    }
174                    button
175                };
176                workspace_fixed.put(
177                    &client_button,
178                    f64::from(scale(client.x, data.config.scale)),
179                    f64::from(scale(client.y, data.config.scale)),
180                );
181                data.clients.insert(*address, client_button);
182            }
183        }
184    } else {
185        for (address, client) in &clients_data.clients {
186            if !client.enabled {
187                continue;
188            }
189            let client_button = {
190                let title = if client.title.trim().is_empty() {
191                    &client.class
192                } else {
193                    &client.title
194                };
195                let client_label = Label::builder()
196                    .label(title)
197                    .overflow(Overflow::Visible)
198                    .margin_start(6)
199                    .ellipsize(pango::EllipsizeMode::End)
200                    .build();
201                let client_frame = Frame::builder()
202                    .label_xalign(0.5)
203                    .label_widget(&client_label)
204                    .build();
205
206                // hide picture if client so small
207                let client_h_w = scale(i16::try_from(current_monitor.height)?, data.config.scale)
208                    .min(scale(
209                        (f32::from(current_monitor.height) / current_monitor.scale) as i16,
210                        data.config.scale,
211                    ));
212                if client_h_w > 70 {
213                    let image = Image::builder()
214                        .css_classes(["client-image"])
215                        .pixel_size((f64::from(client_h_w.clamp(50, 600)) / 1.5) as i32 - 20)
216                        .build();
217                    if !client.enabled {
218                        image.add_css_class("monochrome");
219                    }
220                    set_icon(&client.class, client.pid, &image);
221                    client_frame.set_child(Some(&image));
222                }
223
224                let client_overlay = Overlay::builder()
225                    .overflow(Overflow::Hidden)
226                    .child(&client_frame)
227                    .build();
228                let button = Button::builder()
229                    .child(&client_overlay)
230                    .css_classes(["client", "no-hover"])
231                    .width_request(scale(
232                        (f32::from(current_monitor.width) / current_monitor.scale) as i16,
233                        data.config.scale,
234                    ))
235                    .height_request(scale(
236                        (f32::from(current_monitor.height) / current_monitor.scale) as i16,
237                        data.config.scale,
238                    ))
239                    .build();
240                button.set_cursor(Cursor::from_name("pointer", None).as_ref());
241
242                // add border around initial active client
243                if active.client == Some(*address) {
244                    button.add_css_class("active");
245                }
246                button
247            };
248            data.main_flow.insert(&client_button, -1);
249            data.clients.insert(*address, client_button);
250        }
251    }
252
253    data.active = active;
254    data.hypr_data = clients_data;
255    Ok(())
256}