Skip to main content

pixelflux/wayland/
foreign_toplevel.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5 */
6
7//! `zwlr_foreign_toplevel_manager_v1`: the window list a taskbar reads and acts on. Every
8//! mapped toplevel is a handle on each bound manager, carrying its title, app id, states, and
9//! output; a taskbar's requests (activate, minimize, maximize, fullscreen, close) go to the
10//! window policy where one is installed, and without one the activate raises and focuses, the
11//! close asks the client, and the rest are ignored since every window is fullscreen then. A
12//! taskbar's `set_rectangle` tells the policy where it shows the window, the target of a
13//! minimize effect, in layout coordinates: the rectangle arrives relative to the taskbar's own
14//! surface, a layer surface whose place on its output the layer map knows. A window the policy
15//! put on another virtual desktop stays listed but on no output, neither minimized nor shown,
16//! so a taskbar following the outputs drops it from the current desktop's list and takes it
17//! back when the window enters its output again.
18
19use std::collections::HashMap;
20
21use smithay::desktop::{Window, layer_map_for_output};
22use smithay::input::pointer::Focus;
23use smithay::output::Output;
24use smithay::reexports::wayland_protocols_wlr::foreign_toplevel::v1::server::{
25    zwlr_foreign_toplevel_handle_v1::{self, ZwlrForeignToplevelHandleV1},
26    zwlr_foreign_toplevel_manager_v1::{self, ZwlrForeignToplevelManagerV1},
27};
28use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
29use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
30use smithay::reexports::wayland_server::{
31    Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
32    backend::{ClientId, GlobalId},
33};
34use smithay::utils::Rectangle;
35
36use crate::wayland::frontend::{AppState, FocusTarget, next_serial, window_meta, window_output_id};
37use crate::wayland::wm;
38use crate::wm::{Rect, Request, WindowId};
39
40/// The user data of a handle: the window it stands for.
41#[derive(Clone, Copy, Debug)]
42pub struct HandleData {
43    pub window: WindowId,
44}
45
46/// What every handle of a window was last told.
47#[derive(Clone, Debug, Default, PartialEq)]
48struct Published {
49    title: String,
50    app_id: String,
51    maximized: bool,
52    minimized: bool,
53    activated: bool,
54    fullscreen: bool,
55    output: Option<u32>,
56}
57
58/// The manager instances bound and the handles each window has on them.
59#[derive(Default)]
60pub struct ForeignToplevelState {
61    managers: Vec<ZwlrForeignToplevelManagerV1>,
62    handles: HashMap<WindowId, Vec<ZwlrForeignToplevelHandleV1>>,
63    published: HashMap<WindowId, Published>,
64    global: Option<GlobalId>,
65}
66
67impl ForeignToplevelState {
68    /// Publish the global.
69    pub fn new(dh: &DisplayHandle) -> ForeignToplevelState {
70        let global = dh.create_global::<AppState, ZwlrForeignToplevelManagerV1, _>(3, ());
71        ForeignToplevelState {
72            global: Some(global),
73            ..ForeignToplevelState::default()
74        }
75    }
76
77    /// Whether any taskbar is bound.
78    pub fn bound(&self) -> bool {
79        !self.managers.is_empty()
80    }
81}
82
83/// Whether a window is listed: a toplevel on screen or hidden, never a parked one.
84/// Whether a window is mapped: in the space, or hidden by the policy (minimized, or on another
85/// virtual desktop). One a client withdrew, an unmapped X11 window or an xdg toplevel waiting
86/// for its next map, is not, so a change to it does not list it again.
87fn mapped(state: &AppState, window: &Window, id: WindowId) -> bool {
88    state.space.elements().any(|w| w == window)
89        || state
90            .wm
91            .as_ref()
92            .is_some_and(|wm| wm.hidden.contains_key(&id))
93}
94
95fn listed(window: &Window) -> bool {
96    window_meta(window).is_some_and(|m| !m.parked.load(std::sync::atomic::Ordering::Relaxed))
97}
98
99/// What a window's handles say about it now.
100fn published_of(state: &AppState, window: &Window, id: WindowId) -> Published {
101    let info = wm::window_info(state, window);
102    let states = wm::states_of(state, id);
103    let focused = match state.wm.as_ref() {
104        Some(w) if wm::active(state) => w.focus == Some(id),
105        _ => state.keyboard_focus == Some(id),
106    };
107    let (maximized, minimized, fullscreen, hidden) = match states {
108        Some(s) => (
109            s.maximized,
110            s.minimized && !s.hidden,
111            s.fullscreen,
112            s.hidden,
113        ),
114        None => (false, false, true, false),
115    };
116    Published {
117        title: info.title,
118        app_id: info.app_id,
119        maximized,
120        minimized,
121        activated: focused,
122        fullscreen,
123        output: (!hidden).then(|| window_output_id(window)),
124    }
125}
126
127fn send_all(
128    state: &AppState,
129    handle: &ZwlrForeignToplevelHandleV1,
130    published: &Published,
131    previous: Option<&Published>,
132) {
133    if previous.is_none_or(|p| p.title != published.title) {
134        handle.title(published.title.clone());
135    }
136    if previous.is_none_or(|p| p.app_id != published.app_id) {
137        handle.app_id(published.app_id.clone());
138    }
139    let states_changed = previous.is_none_or(|p| {
140        (p.maximized, p.minimized, p.activated, p.fullscreen)
141            != (
142                published.maximized,
143                published.minimized,
144                published.activated,
145                published.fullscreen,
146            )
147    });
148    if states_changed {
149        let mut states: Vec<u8> = Vec::new();
150        let mut push = |s: zwlr_foreign_toplevel_handle_v1::State| {
151            states.extend_from_slice(&(s as u32).to_ne_bytes());
152        };
153        if published.maximized {
154            push(zwlr_foreign_toplevel_handle_v1::State::Maximized);
155        }
156        if published.minimized {
157            push(zwlr_foreign_toplevel_handle_v1::State::Minimized);
158        }
159        if published.activated {
160            push(zwlr_foreign_toplevel_handle_v1::State::Activated);
161        }
162        if published.fullscreen && handle.version() >= 2 {
163            push(zwlr_foreign_toplevel_handle_v1::State::Fullscreen);
164        }
165        handle.state(states);
166    }
167    if previous.is_none_or(|p| p.output != published.output) {
168        if let Some(old) = previous.and_then(|p| p.output)
169            && let Some(idx) = state.node_idx_for_id(old)
170            && let Some(client) = handle.client()
171        {
172            for wl_output in state.output_nodes[idx].output.client_outputs(&client) {
173                handle.output_leave(&wl_output);
174            }
175        }
176        if let Some(new) = published.output
177            && let Some(idx) = state.node_idx_for_id(new)
178            && let Some(client) = handle.client()
179        {
180            for wl_output in state.output_nodes[idx].output.client_outputs(&client) {
181                handle.output_enter(&wl_output);
182            }
183        }
184    }
185    handle.done();
186}
187
188/// A window mapped: give every bound taskbar a handle for it.
189pub fn add(state: &mut AppState, window: &Window) {
190    let Some(id) = window_meta(window).map(|m| m.id) else {
191        return;
192    };
193    if !listed(window) || state.foreign_toplevel.handles.contains_key(&id) {
194        return;
195    }
196    let published = published_of(state, window, id);
197    let managers = state.foreign_toplevel.managers.clone();
198    let mut handles = Vec::new();
199    for manager in &managers {
200        let Some(client) = manager.client() else {
201            continue;
202        };
203        let Ok(handle) = client
204            .create_resource::<ZwlrForeignToplevelHandleV1, HandleData, AppState>(
205                &state.dh,
206                manager.version(),
207                HandleData { window: id },
208            )
209        else {
210            continue;
211        };
212        manager.toplevel(&handle);
213        send_all(state, &handle, &published, None);
214        handles.push(handle);
215    }
216    state.foreign_toplevel.handles.insert(id, handles);
217    state.foreign_toplevel.published.insert(id, published);
218}
219
220/// A window's title, app id, states, or output may have changed: tell its handles.
221pub fn update(state: &mut AppState, id: WindowId) {
222    let Some(window) = wm::find_window(state, id) else {
223        return;
224    };
225    if !state.foreign_toplevel.handles.contains_key(&id) {
226        if mapped(state, &window, id) {
227            add(state, &window);
228        }
229        return;
230    }
231    let published = published_of(state, &window, id);
232    let previous = state.foreign_toplevel.published.get(&id).cloned();
233    if previous.as_ref() == Some(&published) {
234        return;
235    }
236    let handles = state
237        .foreign_toplevel
238        .handles
239        .get(&id)
240        .cloned()
241        .unwrap_or_default();
242    for handle in &handles {
243        send_all(state, handle, &published, previous.as_ref());
244    }
245    state.foreign_toplevel.published.insert(id, published);
246}
247
248/// Every listed window's handles brought up to date, after a focus change.
249pub fn update_all(state: &mut AppState) {
250    let ids: Vec<WindowId> = state.foreign_toplevel.handles.keys().copied().collect();
251    for id in ids {
252        update(state, id);
253    }
254}
255
256/// A client bound a `wl_output`: the handles it holds of the windows on that output enter it,
257/// since an enter reaches only the objects the client held when it was sent.
258pub fn output_bound(state: &mut AppState, output: &Output, wl_output: &WlOutput) {
259    let Some(node_id) = state
260        .output_nodes
261        .iter()
262        .find(|n| n.output == *output)
263        .map(|n| n.id)
264    else {
265        return;
266    };
267    let client = wl_output.client().map(|c| c.id());
268    for (id, handles) in &state.foreign_toplevel.handles {
269        if state
270            .foreign_toplevel
271            .published
272            .get(id)
273            .and_then(|p| p.output)
274            != Some(node_id)
275        {
276            continue;
277        }
278        for handle in handles
279            .iter()
280            .filter(|h| h.client().map(|c| c.id()) == client)
281        {
282            handle.output_enter(wl_output);
283            handle.done();
284        }
285    }
286}
287
288/// A window is gone, or withdrawn: close its handles.
289pub fn remove(state: &mut AppState, id: WindowId) {
290    if let Some(handles) = state.foreign_toplevel.handles.remove(&id) {
291        for handle in handles {
292            handle.closed();
293        }
294    }
295    state.foreign_toplevel.published.remove(&id);
296}
297
298impl GlobalDispatch<ZwlrForeignToplevelManagerV1, ()> for AppState {
299    fn bind(
300        state: &mut Self,
301        _dh: &DisplayHandle,
302        client: &Client,
303        resource: New<ZwlrForeignToplevelManagerV1>,
304        _global_data: &(),
305        data_init: &mut DataInit<'_, Self>,
306    ) {
307        let manager = data_init.init(resource, ());
308        let ids: Vec<WindowId> = state.foreign_toplevel.handles.keys().copied().collect();
309        for id in ids {
310            let Some(published) = state.foreign_toplevel.published.get(&id).cloned() else {
311                continue;
312            };
313            let Ok(handle) = client
314                .create_resource::<ZwlrForeignToplevelHandleV1, HandleData, AppState>(
315                    &state.dh,
316                    manager.version(),
317                    HandleData { window: id },
318                )
319            else {
320                continue;
321            };
322            manager.toplevel(&handle);
323            send_all(state, &handle, &published, None);
324            if let Some(handles) = state.foreign_toplevel.handles.get_mut(&id) {
325                handles.push(handle);
326            }
327        }
328        state.foreign_toplevel.managers.push(manager);
329    }
330}
331
332impl Dispatch<ZwlrForeignToplevelManagerV1, ()> for AppState {
333    fn request(
334        state: &mut Self,
335        _client: &Client,
336        manager: &ZwlrForeignToplevelManagerV1,
337        request: zwlr_foreign_toplevel_manager_v1::Request,
338        _data: &(),
339        _dh: &DisplayHandle,
340        _data_init: &mut DataInit<'_, Self>,
341    ) {
342        if let zwlr_foreign_toplevel_manager_v1::Request::Stop = request {
343            manager.finished();
344            state.foreign_toplevel.managers.retain(|m| m != manager);
345        }
346    }
347
348    fn destroyed(
349        state: &mut Self,
350        _client: ClientId,
351        manager: &ZwlrForeignToplevelManagerV1,
352        _data: &(),
353    ) {
354        state.foreign_toplevel.managers.retain(|m| m != manager);
355        for handles in state.foreign_toplevel.handles.values_mut() {
356            handles.retain(|h| {
357                h.client().map(|c| c.id()) != manager.client().map(|c| c.id()) || h.is_alive()
358            });
359        }
360    }
361}
362
363impl Dispatch<ZwlrForeignToplevelHandleV1, HandleData> for AppState {
364    fn request(
365        state: &mut Self,
366        _client: &Client,
367        handle: &ZwlrForeignToplevelHandleV1,
368        request: zwlr_foreign_toplevel_handle_v1::Request,
369        data: &HandleData,
370        _dh: &DisplayHandle,
371        _data_init: &mut DataInit<'_, Self>,
372    ) {
373        use zwlr_foreign_toplevel_handle_v1::Request as R;
374        let id = data.window;
375        let policy =
376            |state: &mut AppState, request: Request| -> bool { wm::request(state, id, request) };
377        match request {
378            R::SetMaximized => {
379                policy(state, Request::Maximize);
380            }
381            R::UnsetMaximized => {
382                policy(state, Request::Unmaximize);
383            }
384            R::SetMinimized => {
385                policy(state, Request::Minimize);
386            }
387            R::UnsetMinimized => {
388                policy(state, Request::Unminimize);
389            }
390            R::SetFullscreen { output } => {
391                let id_of = output.and_then(|o| {
392                    state
393                        .output_nodes
394                        .iter()
395                        .find(|n| n.output.owns(&o))
396                        .map(|n| n.id)
397                });
398                policy(state, Request::Fullscreen(id_of));
399            }
400            R::UnsetFullscreen => {
401                policy(state, Request::Unfullscreen);
402            }
403            R::Activate { seat: _ } => {
404                if !policy(state, Request::Activate) {
405                    activate_plain(state, id);
406                }
407            }
408            R::Close => {
409                if !policy(state, Request::Close)
410                    && let Some(window) = wm::find_window(state, id)
411                {
412                    if let Some(toplevel) = window.toplevel() {
413                        toplevel.send_close();
414                    }
415                    if let Some(surface) = window.x11_surface() {
416                        let _ = surface.close();
417                    }
418                }
419            }
420            R::SetRectangle {
421                surface,
422                x,
423                y,
424                width,
425                height,
426            } => {
427                let rect = (width > 0 && height > 0)
428                    .then(|| layer_surface_origin(state, &surface))
429                    .flatten()
430                    .map(|(ox, oy)| Rect::new(ox + x, oy + y, width, height));
431                wm::taskbar_rect(state, id, rect);
432            }
433            R::Destroy => {
434                if let Some(handles) = state.foreign_toplevel.handles.get_mut(&id) {
435                    handles.retain(|h| h != handle);
436                }
437            }
438            _ => {}
439        }
440    }
441}
442
443/// Where a taskbar's surface sits on the layout: a layer surface's geometry on its output.
444fn layer_surface_origin(state: &AppState, surface: &WlSurface) -> Option<(i32, i32)> {
445    for node in &state.output_nodes {
446        let map = layer_map_for_output(&node.output);
447        for layer in map.layers() {
448            if layer.wl_surface() == surface {
449                let geo: Rectangle<i32, smithay::utils::Logical> = map.layer_geometry(layer)?;
450                return Some((node.pos.0 + geo.loc.x, node.pos.1 + geo.loc.y));
451            }
452        }
453    }
454    None
455}
456
457/// Raise and focus a window where no policy decides.
458fn activate_plain(state: &mut AppState, id: WindowId) {
459    let Some(window) = wm::find_window(state, id) else {
460        return;
461    };
462    state.space.raise_element(&window, true);
463    if let Some(keyboard) = state.seat.get_keyboard() {
464        keyboard.set_focus(state, Some(FocusTarget::Window(window)), next_serial());
465    }
466    let _ = Focus::Keep;
467    update_all(state);
468}