Add initial support for toplevel titles and app ids

Should work with most app titles, but for some reason some app ids have
the first letter capitalized (Remmina) and some windows don't get the
class/title set at all (xterm)
Part of #9
This commit is contained in:
Supreeeme 2024-05-11 00:37:11 -04:00
parent e70cb81751
commit 7976e3ad37
5 changed files with 337 additions and 99 deletions

View file

@ -7,7 +7,7 @@ mod tests;
use self::event::*; use self::event::*;
use super::FromServerState; use super::FromServerState;
use crate::clientside::*; use crate::clientside::*;
use crate::xstate::{Atoms, WindowDims, WmNormalHints}; use crate::xstate::{Atoms, WindowDims, WmHints, WmName, WmNormalHints};
use crate::XConnection; use crate::XConnection;
use log::{debug, warn}; use log::{debug, warn};
use rustix::event::{poll, PollFd, PollFlags}; use rustix::event::{poll, PollFd, PollFlags};
@ -76,8 +76,11 @@ struct WindowData {
surface_id: u32, surface_id: u32,
popup_for: Option<x::Window>, popup_for: Option<x::Window>,
dims: WindowDims, dims: WindowDims,
hints: Option<WmNormalHints>, size_hints: Option<WmNormalHints>,
override_redirect: bool, override_redirect: bool,
title: Option<WmName>,
class: Option<String>,
group: Option<x::Window>,
} }
impl WindowData { impl WindowData {
@ -94,8 +97,11 @@ impl WindowData {
popup_for: parent, popup_for: parent,
surface_id: 0, surface_id: 0,
dims, dims,
hints: None, size_hints: None,
override_redirect, override_redirect,
title: None,
class: None,
group: None
} }
} }
} }
@ -482,10 +488,54 @@ impl<C: XConnection> ServerState<C> {
); );
} }
pub fn set_win_hints(&mut self, window: x::Window, hints: WmNormalHints) { pub fn set_win_title(&mut self, window: x::Window, name: WmName) {
let win = self.windows.get_mut(&window).unwrap(); let win = self.windows.get_mut(&window).unwrap();
if win.hints.is_none() || *win.hints.as_ref().unwrap() != hints { let new_title = match &mut win.title {
Some(w) => {
if matches!(w, WmName::NetWmName(_)) && matches!(name, WmName::WmName(_)) {
debug!("skipping setting window name to {name:?} because a _NET_WM_NAME title is already set");
None
} else {
*w = name;
Some(w)
}
}
None => Some(win.title.insert(name)),
};
let Some(title) = new_title else {
return;
};
if let Some(key) = win.surface_key {
let surface: &SurfaceData = self.objects[key].as_ref();
if let Some(SurfaceRole::Toplevel(Some(data))) = &surface.role {
data.toplevel.set_title(title.name().to_string());
}
}
}
pub fn set_win_class(&mut self, window: x::Window, class: String) {
let win = self.windows.get_mut(&window).unwrap();
let class = win.class.insert(class);
if let Some(key) = win.surface_key {
let surface: &SurfaceData = self.objects[key].as_ref();
if let Some(SurfaceRole::Toplevel(Some(data))) = &surface.role {
data.toplevel.set_app_id(class.to_string());
}
}
}
pub fn set_win_hints(&mut self, window: x::Window, hints: WmHints) {
let win = self.windows.get_mut(&window).unwrap();
win.group = hints.window_group;
}
pub fn set_size_hints(&mut self, window: x::Window, hints: WmNormalHints) {
let win = self.windows.get_mut(&window).unwrap();
if win.size_hints.is_none() || *win.size_hints.as_ref().unwrap() != hints {
debug!("setting {window:?} hints {hints:?}"); debug!("setting {window:?} hints {hints:?}");
if let Some(surface) = win.surface_key { if let Some(surface) = win.surface_key {
let surface: &SurfaceData = self.objects[surface].as_ref(); let surface: &SurfaceData = self.objects[surface].as_ref();
@ -498,7 +548,7 @@ impl<C: XConnection> ServerState<C> {
} }
} }
} }
win.hints = Some(hints); win.size_hints = Some(hints);
} }
} }
@ -720,8 +770,9 @@ impl<C: XConnection> ServerState<C> {
xdg: XdgSurface, xdg: XdgSurface,
) -> ToplevelData { ) -> ToplevelData {
debug!("creating toplevel for {:?}", window.window); debug!("creating toplevel for {:?}", window.window);
let toplevel = xdg.get_toplevel(&self.qh, surface_key); let toplevel = xdg.get_toplevel(&self.qh, surface_key);
if let Some(hints) = &window.hints { if let Some(hints) = &window.size_hints {
if let Some(min) = &hints.min_size { if let Some(min) = &hints.min_size {
toplevel.set_min_size(min.width, min.height); toplevel.set_min_size(min.width, min.height);
} }
@ -730,6 +781,22 @@ impl<C: XConnection> ServerState<C> {
} }
} }
let group = window.group.and_then(|win| self.windows.get(&win));
if let Some(class) = window
.class
.as_ref()
.or(group.and_then(|g| g.class.as_ref()))
{
toplevel.set_app_id(class.to_string());
}
if let Some(title) = window
.title
.as_ref()
.or(group.and_then(|g| g.title.as_ref()))
{
toplevel.set_title(title.name().to_string());
}
ToplevelData { ToplevelData {
xdg: XdgSurfaceData { xdg: XdgSurfaceData {
surface: xdg, surface: xdg,

View file

@ -1,5 +1,5 @@
use super::{ServerState, WindowDims}; use super::{ServerState, WindowDims};
use crate::xstate::SetState; use crate::xstate::{SetState, WmName};
use paste::paste; use paste::paste;
use rustix::event::{poll, PollFd, PollFlags}; use rustix::event::{poll, PollFd, PollFlags};
use std::collections::HashMap; use std::collections::HashMap;
@ -354,7 +354,7 @@ impl TestFixture {
width: 50, width: 50,
height: 50, height: 50,
}, },
fullscreen: false fullscreen: false,
}; };
let dims = data.dims; let dims = data.dims;
@ -393,7 +393,7 @@ impl TestFixture {
width: 50, width: 50,
height: 50, height: 50,
}, },
fullscreen: false fullscreen: false,
}; };
let dims = data.dims; let dims = data.dims;
@ -468,7 +468,7 @@ impl TestFixture {
width: 50, width: 50,
height: 50, height: 50,
}, },
fullscreen: false fullscreen: false,
}; };
let dims = data.dims; let dims = data.dims;
self.register_window(window, data); self.register_window(window, data);
@ -861,6 +861,87 @@ fn fullscreen() {
.contains(&xdg_toplevel::State::Fullscreen)); .contains(&xdg_toplevel::State::Fullscreen));
} }
#[test]
fn window_title_and_class() {
let (mut f, comp) = TestFixture::new_with_compositor();
let win = unsafe { Window::new(1) };
let (_, id) = f.create_toplevel(&comp, win);
f.exwayland
.set_win_title(win, WmName::WmName("window".into()));
f.exwayland.set_win_class(win, "class".into());
f.run();
let data = f.testwl.get_surface_data(id).unwrap();
assert_eq!(data.toplevel().title, Some("window".into()));
assert_eq!(data.toplevel().app_id, Some("class".into()));
f.exwayland
.set_win_title(win, WmName::NetWmName("superwindow".into()));
f.run();
let data = f.testwl.get_surface_data(id).unwrap();
assert_eq!(data.toplevel().title, Some("superwindow".into()));
f.exwayland
.set_win_title(win, WmName::WmName("shwindow".into()));
f.run();
let data = f.testwl.get_surface_data(id).unwrap();
assert_eq!(data.toplevel().title, Some("superwindow".into()));
}
#[test]
fn window_group_properties() {
let (mut f, comp) = TestFixture::new_with_compositor();
let prop_win = unsafe { Window::new(1) };
f.exwayland.new_window(
prop_win,
false,
super::WindowDims {
width: 1,
height: 1,
..Default::default()
},
None,
);
f.exwayland
.set_win_title(prop_win, WmName::WmName("window".into()));
f.exwayland.set_win_class(prop_win, "class".into());
let win = unsafe { Window::new(2) };
let data = WindowData {
mapped: true,
dims: WindowDims {
width: 50,
height: 50,
..Default::default()
},
fullscreen: false,
};
let (_, surface) = comp.create_surface();
let dims = data.dims;
f.register_window(win, data);
f.exwayland.new_window(win, false, dims, None);
f.exwayland.set_win_hints(
win,
super::WmHints {
window_group: Some(prop_win),
..Default::default()
},
);
f.exwayland.map_window(win);
f.exwayland
.associate_window(win, surface.id().protocol_id());
f.run();
let id = f.testwl.last_created_surface_id().unwrap();
let data = f.testwl.get_surface_data(id).unwrap();
assert_eq!(data.toplevel().title, Some("window".into()));
assert_eq!(data.toplevel().app_id, Some("class".into()));
}
/// See Pointer::handle_event for an explanation. /// See Pointer::handle_event for an explanation.
#[test] #[test]
fn popup_pointer_motion_workaround() {} fn popup_pointer_motion_workaround() {}

View file

@ -1,5 +1,6 @@
use bitflags::bitflags; use bitflags::bitflags;
use log::{debug, trace, warn}; use log::{debug, trace, warn};
use std::ffi::CString;
use std::os::fd::{AsRawFd, BorrowedFd}; use std::os::fd::{AsRawFd, BorrowedFd};
use std::sync::Arc; use std::sync::Arc;
use xcb::{x, Xid, XidNew}; use xcb::{x, Xid, XidNew};
@ -9,7 +10,21 @@ pub struct XState {
pub connection: Arc<xcb::Connection>, pub connection: Arc<xcb::Connection>,
root: x::Window, root: x::Window,
pub atoms: Atoms, pub atoms: Atoms,
window_types: WindowTypes, }
#[derive(Debug)]
pub enum WmName {
WmName(String),
NetWmName(String),
}
impl WmName {
pub fn name(&self) -> &str {
match self {
Self::WmName(n) => n,
Self::NetWmName(n) => n,
}
}
} }
impl XState { impl XState {
@ -32,7 +47,6 @@ impl XState {
let atoms = Atoms::intern_all(&connection).unwrap(); let atoms = Atoms::intern_all(&connection).unwrap();
trace!("atoms: {atoms:#?}"); trace!("atoms: {atoms:#?}");
let window_types = WindowTypes::new(&connection);
// This makes Xwayland spit out damage tracking // This makes Xwayland spit out damage tracking
connection connection
@ -56,7 +70,6 @@ impl XState {
connection, connection,
root, root,
atoms, atoms,
window_types,
}; };
r.create_ewmh_window(); r.create_ewmh_window();
r r
@ -97,7 +110,7 @@ impl XState {
self.set_root_property( self.set_root_property(
self.atoms.supported, self.atoms.supported,
x::ATOM_ATOM, x::ATOM_ATOM,
&[self.atoms.active_win, self.atoms.client_list], &[self.atoms.active_win],
); );
self.connection self.connection
@ -114,7 +127,7 @@ impl XState {
.send_and_check_request(&x::ChangeProperty { .send_and_check_request(&x::ChangeProperty {
mode: x::PropMode::Replace, mode: x::PropMode::Replace,
window, window,
property: self.atoms.wm_name, property: self.atoms.net_wm_name,
r#type: x::ATOM_STRING, r#type: x::ATOM_STRING,
data: b"exwayland wm", data: b"exwayland wm",
}) })
@ -244,10 +257,16 @@ impl XState {
event: x::PropertyNotifyEvent, event: x::PropertyNotifyEvent,
server_state: &mut super::RealServerState, server_state: &mut super::RealServerState,
) { ) {
if event.state() != x::Property::NewValue {
println!("ignoring non newvalue for property {:?}", event.atom());
return;
}
let window = event.window();
let get_prop = |r#type, long_length| { let get_prop = |r#type, long_length| {
self.connection self.connection
.wait_for_reply(self.connection.send_request(&x::GetProperty { .wait_for_reply(self.connection.send_request(&x::GetProperty {
window: event.window(), window,
property: event.atom(), property: event.atom(),
r#type, r#type,
long_offset: 0, long_offset: 0,
@ -255,23 +274,14 @@ impl XState {
delete: false, delete: false,
})) }))
}; };
if event.state() != x::Property::NewValue {
return;
}
match event.atom() { match event.atom() {
x if x == self.atoms.wm_window_type => { x if x == self.atoms.wm_hints => {
let Ok(prop) = get_prop(x::ATOM_ATOM, 8) else { let prop = get_prop(self.atoms.wm_hints, 9).unwrap();
return; let data: &[u32] = prop.value();
}; let hints = WmHints::from(data);
let types: &[x::Atom] = prop.value(); debug!("wm hints: {hints:?}");
let win_type = types.iter().find_map(|a| self.window_types.get_type(*a)); server_state.set_win_hints(event.window(), hints);
debug!(
"set {:?} type to {} ({})",
event.window(),
win_type.unwrap_or("[Unknown/Unrecognized]".to_string()),
types.len()
);
} }
x if x == x::ATOM_WM_NORMAL_HINTS => { x if x == x::ATOM_WM_NORMAL_HINTS => {
let Ok(prop) = get_prop(x::ATOM_WM_SIZE_HINTS, 9) else { let Ok(prop) = get_prop(x::ATOM_WM_SIZE_HINTS, 9) else {
@ -279,9 +289,37 @@ impl XState {
}; };
let data: &[u32] = prop.value(); let data: &[u32] = prop.value();
let hints = WmNormalHints::from(data); let hints = WmNormalHints::from(data);
server_state.set_win_hints(event.window(), hints); server_state.set_size_hints(window, hints);
}
x if x == x::ATOM_WM_NAME || x == self.atoms.net_wm_name => {
let ty = if x == x::ATOM_WM_NAME {
x::ATOM_STRING
} else {
self.atoms.utf8_string
};
let prop = get_prop(ty, 256).unwrap();
let data: &[u8] = prop.value();
let name = String::from_utf8(data.to_vec()).unwrap();
debug!("{:?} named: {name}", window);
let name = if x == x::ATOM_WM_NAME {
WmName::WmName(name)
} else {
WmName::NetWmName(name)
};
server_state.set_win_title(window, name);
}
x if x == x::ATOM_WM_CLASS => {
let prop = get_prop(x::ATOM_STRING, 256).unwrap();
let data: &[u8] = prop.value();
// wm class is instance + class - ignore instance
let class_start = data.iter().copied().position(|b| b == 0u8).unwrap() + 1;
let data = data[class_start..].to_vec();
let class = CString::from_vec_with_nul(data).unwrap();
debug!("{:?} class: {class:?}", window);
server_state.set_win_class(window, class.to_string_lossy().to_string());
} }
_ => { _ => {
if log::log_enabled!(log::Level::Debug) {
let prop = self let prop = self
.connection .connection
.wait_for_reply( .wait_for_reply(
@ -290,11 +328,8 @@ impl XState {
) )
.unwrap(); .unwrap();
debug!( debug!("changed property {:?} for {:?}", prop.name(), window);
"changed property {:?} for {:?}", }
prop.name(),
event.window()
);
} }
} }
} }
@ -309,14 +344,14 @@ xcb::atoms_struct! {
pub wm_transient_for => b"WM_TRANSIENT_FOR" only_if_exists = false, pub wm_transient_for => b"WM_TRANSIENT_FOR" only_if_exists = false,
pub wm_hints => b"WM_HINTS" only_if_exists = false, pub wm_hints => b"WM_HINTS" only_if_exists = false,
pub wm_check => b"_NET_SUPPORTING_WM_CHECK" only_if_exists = false, pub wm_check => b"_NET_SUPPORTING_WM_CHECK" only_if_exists = false,
pub wm_name => b"_NET_WM_NAME" only_if_exists = false, pub net_wm_name => b"_NET_WM_NAME" only_if_exists = false,
pub wm_window_type => b"_NET_WM_WINDOW_TYPE" only_if_exists = false,
pub wm_pid => b"_NET_WM_PID" only_if_exists = false, pub wm_pid => b"_NET_WM_PID" only_if_exists = false,
pub net_wm_state => b"_NET_WM_STATE" only_if_exists = false, pub net_wm_state => b"_NET_WM_STATE" only_if_exists = false,
pub wm_fullscreen => b"_NET_WM_STATE_FULLSCREEN" only_if_exists = false, pub wm_fullscreen => b"_NET_WM_STATE_FULLSCREEN" only_if_exists = false,
pub active_win => b"_NET_ACTIVE_WINDOW" only_if_exists = false, pub active_win => b"_NET_ACTIVE_WINDOW" only_if_exists = false,
pub client_list => b"_NET_CLIENT_LIST" only_if_exists = false, pub client_list => b"_NET_CLIENT_LIST" only_if_exists = false,
pub supported => b"_NET_SUPPORTED" only_if_exists = false, pub supported => b"_NET_SUPPORTED" only_if_exists = false,
pub utf8_string => b"UTF8_STRING" only_if_exists = false,
} }
} }
@ -330,25 +365,6 @@ xcb::atoms_struct! {
} }
} }
impl WindowTypes {
pub fn new(connection: &xcb::Connection) -> Self {
let r = Self::intern_all(connection).unwrap();
assert_ne!(r.normal, x::ATOM_NONE);
assert_ne!(r.dialog, x::ATOM_NONE);
assert_ne!(r.utility, x::ATOM_NONE);
r
}
pub fn get_type(&self, atom: x::Atom) -> Option<String> {
match atom {
x if x == self.normal => Some("Normal".to_string()),
x if x == self.dialog => Some("Dialog".to_string()),
x if x == self.utility => Some("Utility".to_string()),
x if x == self.menu => Some("Menu".to_string()),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, Default)] #[derive(Clone, Copy, Debug, Default)]
pub struct WindowDims { pub struct WindowDims {
pub x: i16, pub x: i16,
@ -361,16 +377,16 @@ bitflags! {
/// From ICCCM spec. /// From ICCCM spec.
/// https://tronche.com/gui/x/icccm/sec-4.html#s-4.1.2.3 /// https://tronche.com/gui/x/icccm/sec-4.html#s-4.1.2.3
pub struct WmSizeHintsFlags: u32 { pub struct WmSizeHintsFlags: u32 {
const UserPosition = 1;
const UserSize = 2;
const ProgramPosition = 4;
const ProgramSize = 8;
const ProgramMinSize = 16; const ProgramMinSize = 16;
const ProgramMaxSize = 32; const ProgramMaxSize = 32;
const ProgramResizeIncrement = 64; }
const ProgramAspect = 128; }
const ProgramBaseSize = 256;
const ProgramWinGravity = 512; bitflags! {
/// https://tronche.com/gui/x/icccm/sec-4.html#s-4.1.2.4
pub struct WmHintsFlags: u32 {
const Input = 1;
const WindowGroup = 64;
} }
} }
@ -386,6 +402,49 @@ pub struct WmNormalHints {
pub max_size: Option<WinSize>, pub max_size: Option<WinSize>,
} }
impl From<&[u32]> for WmNormalHints {
fn from(value: &[u32]) -> Self {
let mut ret = Self::default();
let flags = WmSizeHintsFlags::from_bits_truncate(value[0]);
if flags.contains(WmSizeHintsFlags::ProgramMinSize) {
ret.min_size = Some(WinSize {
width: value[5] as _,
height: value[6] as _,
});
}
if flags.contains(WmSizeHintsFlags::ProgramMaxSize) {
ret.max_size = Some(WinSize {
width: value[7] as _,
height: value[8] as _,
});
}
ret
}
}
#[derive(Default, Debug, PartialEq, Eq)]
pub struct WmHints {
pub input: Option<bool>,
pub window_group: Option<x::Window>,
}
impl From<&[u32]> for WmHints {
fn from(value: &[u32]) -> Self {
let mut ret = Self::default();
let flags = WmHintsFlags::from_bits_truncate(value[0]);
if flags.contains(WmHintsFlags::WindowGroup) {
let window = unsafe { x::Window::new(value[8]) };
ret.window_group = Some(window);
}
ret
}
}
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum SetState { pub enum SetState {
Remove, Remove,
@ -405,29 +464,6 @@ impl TryFrom<u32> for SetState {
} }
} }
impl From<&[u32]> for WmNormalHints {
fn from(value: &[u32]) -> Self {
let mut ret = Self::default();
let flags = WmSizeHintsFlags::from_bits(value[0]).unwrap();
if flags.contains(WmSizeHintsFlags::ProgramMinSize) {
ret.min_size = Some(WinSize {
width: value[5] as _,
height: value[6] as _,
});
}
if flags.contains(WmSizeHintsFlags::ProgramMaxSize) {
ret.max_size = Some(WinSize {
width: value[7] as _,
height: value[8] as _,
});
}
ret
}
}
impl super::XConnection for Arc<xcb::Connection> { impl super::XConnection for Arc<xcb::Connection> {
type ExtraData = Atoms; type ExtraData = Atoms;

View file

@ -30,6 +30,8 @@ xcb::atoms_struct! {
struct Atoms { struct Atoms {
wm_protocols => b"WM_PROTOCOLS", wm_protocols => b"WM_PROTOCOLS",
wm_delete_window => b"WM_DELETE_WINDOW", wm_delete_window => b"WM_DELETE_WINDOW",
wm_class => b"WM_CLASS",
wm_name => b"WM_NAME",
} }
} }
@ -108,7 +110,7 @@ impl Fixture {
} }
} }
fn create_window( fn create_and_map_window(
&mut self, &mut self,
connection: &xcb::Connection, connection: &xcb::Connection,
override_redirect: bool, override_redirect: bool,
@ -155,7 +157,7 @@ impl Fixture {
width: u16, width: u16,
height: u16, height: u16,
) -> (x::Window, testwl::SurfaceId) { ) -> (x::Window, testwl::SurfaceId) {
let (window, surface) = self.create_window(connection, false, 0, 0, width, height); let (window, surface) = self.create_and_map_window(connection, false, 0, 0, width, height);
let data = self let data = self
.testwl .testwl
.get_surface_data(surface) .get_surface_data(surface)
@ -258,6 +260,39 @@ fn toplevel_flow() {
let mut f = Fixture::new(); let mut f = Fixture::new();
let mut connection = Connection::new(); let mut connection = Connection::new();
let (window, surface) = f.create_toplevel(&connection.inner, 200, 200); let (window, surface) = f.create_toplevel(&connection.inner, 200, 200);
connection
.inner
.send_and_check_request(&x::ChangeProperty {
mode: x::PropMode::Replace,
window,
r#type: x::ATOM_STRING,
property: connection.atoms.wm_name,
data: c"window".to_bytes(),
})
.unwrap();
connection
.inner
.send_and_check_request(&x::ChangeProperty {
mode: x::PropMode::Replace,
window,
r#type: x::ATOM_STRING,
property: connection.atoms.wm_class,
data: &[
c"instance".to_bytes_with_nul(),
c"class".to_bytes_with_nul(),
]
.concat(),
})
.unwrap();
f.wait_and_dispatch();
let data = f.testwl.get_surface_data(surface).unwrap();
assert_eq!(data.toplevel().title, Some("window".into()));
assert_eq!(data.toplevel().app_id, Some("class".into()));
f.close_toplevel(&mut connection, window, surface); f.close_toplevel(&mut connection, window, surface);
// Simulate killing client // Simulate killing client

View file

@ -91,6 +91,8 @@ pub struct Toplevel {
pub max_size: Option<Vec2>, pub max_size: Option<Vec2>,
pub states: Vec<xdg_toplevel::State>, pub states: Vec<xdg_toplevel::State>,
pub closed: bool, pub closed: bool,
pub title: Option<String>,
pub app_id: Option<String>
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
@ -486,6 +488,21 @@ impl Dispatch<XdgToplevel, SurfaceId> for State {
state.configure_toplevel(*surface_id, 100, 100, states); state.configure_toplevel(*surface_id, 100, 100, states);
} }
xdg_toplevel::Request::Destroy => {} xdg_toplevel::Request::Destroy => {}
xdg_toplevel::Request::SetTitle { title } => {
let data = state.surfaces.get_mut(surface_id).unwrap();
let Some(SurfaceRole::Toplevel(toplevel)) = &mut data.role else {
unreachable!();
};
toplevel.title = title.into();
}
xdg_toplevel::Request::SetAppId { app_id } => {
let data = state.surfaces.get_mut(surface_id).unwrap();
let Some(SurfaceRole::Toplevel(toplevel)) = &mut data.role else {
unreachable!();
};
toplevel.app_id = app_id.into();
}
other => todo!("unhandled request {other:?}"), other => todo!("unhandled request {other:?}"),
} }
} }
@ -513,6 +530,8 @@ impl Dispatch<XdgSurface, SurfaceId> for State {
max_size: None, max_size: None,
states: Vec::new(), states: Vec::new(),
closed: false, closed: false,
title: None,
app_id: None
}; };
let data = state.surfaces.get_mut(surface_id).unwrap(); let data = state.surfaces.get_mut(surface_id).unwrap();
data.role = Some(SurfaceRole::Toplevel(t)); data.role = Some(SurfaceRole::Toplevel(t));