hyprshell_windows_lib/overview/
open.rs

1use 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                    // hide picture if client so small
108                    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                    // add initial border around initial active client
135                    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}