Support xdg decorations

This commit is contained in:
bbb651 2025-03-20 15:49:35 +02:00 committed by Shawn Wallace
parent b2613aec05
commit 0559ace758
5 changed files with 328 additions and 4 deletions

View file

@ -19,6 +19,8 @@ use wayland_protocols::wp::relative_pointer::zv1::client::{
zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1,
zwp_relative_pointer_v1::ZwpRelativePointerV1, zwp_relative_pointer_v1::ZwpRelativePointerV1,
}; };
use wayland_protocols::xdg::decoration::zv1::client::zxdg_decoration_manager_v1::ZxdgDecorationManagerV1;
use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1;
use wayland_protocols::{ use wayland_protocols::{
wp::{ wp::{
linux_dmabuf::zv1::client::{ linux_dmabuf::zv1::client::{
@ -140,6 +142,8 @@ delegate_noop!(Globals: ZxdgOutputManagerV1);
delegate_noop!(Globals: ZwpPointerConstraintsV1); delegate_noop!(Globals: ZwpPointerConstraintsV1);
delegate_noop!(Globals: ZwpTabletManagerV2); delegate_noop!(Globals: ZwpTabletManagerV2);
delegate_noop!(Globals: XdgActivationV1); delegate_noop!(Globals: XdgActivationV1);
delegate_noop!(Globals: ZxdgDecorationManagerV1);
delegate_noop!(Globals: ignore ZxdgToplevelDecorationV1);
impl Dispatch<WlRegistry, GlobalListContents> for Globals { impl Dispatch<WlRegistry, GlobalListContents> for Globals {
fn event( fn event(

View file

@ -6,7 +6,7 @@ mod tests;
use self::event::*; use self::event::*;
use crate::clientside::*; use crate::clientside::*;
use crate::xstate::{WindowDims, WmHints, WmName, WmNormalHints}; use crate::xstate::{Decorations, WindowDims, WmHints, WmName, WmNormalHints};
use crate::{X11Selection, XConnection}; use crate::{X11Selection, XConnection};
use log::{debug, warn}; use log::{debug, warn};
use rustix::event::{poll, PollFd, PollFlags}; use rustix::event::{poll, PollFd, PollFlags};
@ -22,6 +22,10 @@ use std::os::fd::{AsFd, BorrowedFd};
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
use std::rc::{Rc, Weak}; use std::rc::{Rc, Weak};
use wayland_client::{globals::Global, protocol as client, Proxy}; use wayland_client::{globals::Global, protocol as client, Proxy};
use wayland_protocols::xdg::decoration::zv1::client::zxdg_decoration_manager_v1::ZxdgDecorationManagerV1;
use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1::{
self, ZxdgToplevelDecorationV1,
};
use wayland_protocols::{ use wayland_protocols::{
wp::{ wp::{
linux_dmabuf::zv1::{client as c_dmabuf, server as s_dmabuf}, linux_dmabuf::zv1::{client as c_dmabuf, server as s_dmabuf},
@ -87,6 +91,7 @@ pub struct WindowAttributes {
pub title: Option<WmName>, pub title: Option<WmName>,
pub class: Option<String>, pub class: Option<String>,
pub group: Option<x::Window>, pub group: Option<x::Window>,
pub decorations: Option<Decorations>,
} }
#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] #[derive(Debug, Default, PartialEq, Eq, Copy, Clone)]
@ -244,6 +249,7 @@ struct ToplevelData {
toplevel: XdgToplevel, toplevel: XdgToplevel,
xdg: XdgSurfaceData, xdg: XdgSurfaceData,
fullscreen: bool, fullscreen: bool,
decoration: Option<ZxdgToplevelDecorationV1>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -509,6 +515,7 @@ pub struct ServerState<C: XConnection> {
activation_state: Option<ActivationState>, activation_state: Option<ActivationState>,
global_output_offset: GlobalOutputOffset, global_output_offset: GlobalOutputOffset,
global_offset_updated: bool, global_offset_updated: bool,
decoration_manager: Option<ZxdgDecorationManagerV1>,
} }
impl<C: XConnection> ServerState<C> { impl<C: XConnection> ServerState<C> {
@ -542,6 +549,14 @@ impl<C: XConnection> ServerState<C> {
}) })
.ok(); .ok();
let decoration_manager = clientside
.global_list
.bind::<ZxdgDecorationManagerV1, _, _>(&qh, 1..=1, ())
.inspect_err(|e| {
warn!("Could not bind xdg decoration ({e:?}). Windows might not have decorations.")
})
.ok();
dh.create_global::<Self, XwaylandShellV1, _>(1, ()); dh.create_global::<Self, XwaylandShellV1, _>(1, ());
clientside clientside
.global_list .global_list
@ -578,6 +593,7 @@ impl<C: XConnection> ServerState<C> {
}, },
}, },
global_offset_updated: false, global_offset_updated: false,
decoration_manager,
} }
} }
@ -717,6 +733,35 @@ impl<C: XConnection> ServerState<C> {
} }
} }
pub fn set_win_decorations(&mut self, window: x::Window, decorations: Decorations) {
if self.decoration_manager.is_none() {
return;
};
let Some(win) = self.windows.get_mut(&window) else {
debug!("not setting decorations for unknown window {window:?}");
return;
};
if win.attrs.decorations != Some(decorations) {
debug!("setting {window:?} decorations {decorations:?}");
if let Some(key) = win.surface_key {
if let Some(object) = self.objects.get(key) {
let surface: &SurfaceData = object.as_ref();
if let Some(SurfaceRole::Toplevel(Some(data))) = &surface.role {
data.decoration
.as_ref()
.unwrap()
.set_mode(decorations.into());
}
} else {
warn!("could not set decorations on {window:?}: stale surface")
}
}
win.attrs.decorations = Some(decorations);
}
}
pub fn set_window_serial(&mut self, window: x::Window, serial: [u32; 2]) { pub fn set_window_serial(&mut self, window: x::Window, serial: [u32; 2]) {
let Some(win) = self.windows.get_mut(&window) else { let Some(win) = self.windows.get_mut(&window) else {
warn!("Tried to set serial for unknown window {window:?}"); warn!("Tried to set serial for unknown window {window:?}");
@ -1224,6 +1269,17 @@ impl<C: XConnection> ServerState<C> {
toplevel.set_fullscreen(None); toplevel.set_fullscreen(None);
} }
let decoration = self.decoration_manager.as_ref().map(|decoration_manager| {
let decoration = decoration_manager.get_toplevel_decoration(&toplevel, &self.qh, ());
decoration.set_mode(
window
.attrs
.decorations
.map_or(zxdg_toplevel_decoration_v1::Mode::ServerSide, From::from),
);
decoration
});
let surface: &SurfaceData = self.objects[surface_key].as_ref(); let surface: &SurfaceData = self.objects[surface_key].as_ref();
if let (Some(activation_state), Some(token)) = ( if let (Some(activation_state), Some(token)) = (
self.activation_state.as_ref(), self.activation_state.as_ref(),
@ -1240,6 +1296,7 @@ impl<C: XConnection> ServerState<C> {
}, },
toplevel, toplevel,
fullscreen: false, fullscreen: false,
decoration,
} }
} }

View file

@ -1,5 +1,6 @@
mod selection; mod selection;
use selection::{Selection, SelectionData}; use selection::{Selection, SelectionData};
use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1;
use crate::{server::WindowAttributes, XConnection}; use crate::{server::WindowAttributes, XConnection};
use bitflags::bitflags; use bitflags::bitflags;
@ -246,7 +247,11 @@ impl XState {
self.set_root_property(self.atoms.wm_check, x::ATOM_WINDOW, &[self.wm_window]); self.set_root_property(self.atoms.wm_check, x::ATOM_WINDOW, &[self.wm_window]);
self.set_root_property(self.atoms.active_win, x::ATOM_WINDOW, &[x::Window::none()]); self.set_root_property(self.atoms.active_win, x::ATOM_WINDOW, &[x::Window::none()]);
self.set_root_property(self.atoms.supported, x::ATOM_ATOM, &[self.atoms.active_win]); self.set_root_property(
self.atoms.supported,
x::ATOM_ATOM,
&[self.atoms.active_win, self.atoms.motif_wm_hints],
);
self.connection self.connection
.send_and_check_request(&x::ChangeProperty { .send_and_check_request(&x::ChangeProperty {
@ -491,6 +496,7 @@ impl XState {
let class = self.get_wm_class(window); let class = self.get_wm_class(window);
let wm_hints = self.get_wm_hints(window); let wm_hints = self.get_wm_hints(window);
let size_hints = self.get_wm_size_hints(window); let size_hints = self.get_wm_size_hints(window);
let motif_wm_hints = self.get_motif_wm_hints(window);
let geometry = self.connection.wait_for_reply(geometry)?; let geometry = self.connection.wait_for_reply(geometry)?;
debug!("{window:?} geometry: {geometry:?}"); debug!("{window:?} geometry: {geometry:?}");
@ -503,6 +509,7 @@ impl XState {
let class = class.resolve()?; let class = class.resolve()?;
let wm_hints = wm_hints.resolve()?; let wm_hints = wm_hints.resolve()?;
let size_hints = size_hints.resolve()?; let size_hints = size_hints.resolve()?;
let motif_wm_hints = motif_wm_hints.resolve()?;
Ok(WindowAttributes { Ok(WindowAttributes {
override_redirect: attrs.override_redirect(), override_redirect: attrs.override_redirect(),
@ -517,6 +524,7 @@ impl XState {
class, class,
group: wm_hints.and_then(|h| h.window_group), group: wm_hints.and_then(|h| h.window_group),
size_hints, size_hints,
decorations: motif_wm_hints.and_then(|h| h.decorations),
}) })
} }
@ -535,6 +543,9 @@ impl XState {
if let Some(hints) = attrs.size_hints { if let Some(hints) = attrs.size_hints {
server_state.set_size_hints(window, hints); server_state.set_size_hints(window, hints);
} }
if let Some(decorations) = attrs.decorations {
server_state.set_win_decorations(window, decorations);
}
} }
fn get_property_cookie( fn get_property_cookie(
@ -660,6 +671,28 @@ impl XState {
} }
} }
fn get_motif_wm_hints(
&self,
window: x::Window,
) -> PropertyCookieWrapper<impl PropertyResolver<Output = MotifWmHints>> {
let cookie = self.get_property_cookie(
window,
self.atoms.motif_wm_hints,
self.atoms.motif_wm_hints,
5,
);
let resolver = |reply: x::GetPropertyReply| {
let data: &[u32] = reply.value();
MotifWmHints::from(data)
};
PropertyCookieWrapper {
connection: &self.connection,
cookie,
resolver,
}
}
fn get_pid(&self, window: x::Window) -> Option<u32> { fn get_pid(&self, window: x::Window) -> Option<u32> {
let Some(pid) = self let Some(pid) = self
.connection .connection
@ -718,6 +751,13 @@ impl XState {
unwrap_or_skip_bad_window!(self.get_wm_class(window).resolve()).unwrap(); unwrap_or_skip_bad_window!(self.get_wm_class(window).resolve()).unwrap();
server_state.set_win_class(window, class); server_state.set_win_class(window, class);
} }
x if x == self.atoms.motif_wm_hints => {
let motif_hints =
unwrap_or_skip_bad_window!(self.get_motif_wm_hints(window).resolve()).unwrap();
if let Some(decorations) = motif_hints.decorations {
server_state.set_win_decorations(window, decorations);
}
}
_ => { _ => {
if !self.handle_selection_property_change(&event) if !self.handle_selection_property_change(&event)
&& log::log_enabled!(log::Level::Debug) && log::log_enabled!(log::Level::Debug)
@ -750,6 +790,7 @@ xcb::atoms_struct! {
active_win => b"_NET_ACTIVE_WINDOW" only_if_exists = false, active_win => b"_NET_ACTIVE_WINDOW" only_if_exists = false,
client_list => b"_NET_CLIENT_LIST" only_if_exists = false, client_list => b"_NET_CLIENT_LIST" only_if_exists = false,
supported => b"_NET_SUPPORTED" only_if_exists = false, supported => b"_NET_SUPPORTED" only_if_exists = false,
motif_wm_hints => b"_MOTIF_WM_HINTS" only_if_exists = false,
utf8_string => b"UTF8_STRING" only_if_exists = false, utf8_string => b"UTF8_STRING" only_if_exists = false,
clipboard => b"CLIPBOARD" only_if_exists = false, clipboard => b"CLIPBOARD" only_if_exists = false,
targets => b"TARGETS" only_if_exists = false, targets => b"TARGETS" only_if_exists = false,
@ -795,6 +836,12 @@ bitflags! {
} }
} }
bitflags! {
pub struct MotifWmHintsFlags: u32 {
const Decorations = 2;
}
}
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct WinSize { pub struct WinSize {
pub width: i32, pub width: i32,
@ -849,6 +896,52 @@ impl From<&[u32]> for WmHints {
} }
} }
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Decorations {
Client = 0,
Server = 1,
}
impl TryFrom<u32> for Decorations {
type Error = ();
fn try_from(value: u32) -> Result<Self, ()> {
match value {
0 => Ok(Self::Client),
1 => Ok(Self::Server),
_ => Err(()),
}
}
}
impl From<Decorations> for zxdg_toplevel_decoration_v1::Mode {
fn from(value: Decorations) -> Self {
match value {
Decorations::Client => zxdg_toplevel_decoration_v1::Mode::ClientSide,
Decorations::Server => zxdg_toplevel_decoration_v1::Mode::ServerSide,
}
}
}
#[derive(Default, Debug, PartialEq, Eq)]
pub struct MotifWmHints {
pub decorations: Option<Decorations>,
}
impl From<&[u32]> for MotifWmHints {
fn from(value: &[u32]) -> Self {
let mut ret = Self::default();
let flags = MotifWmHintsFlags::from_bits_truncate(value[0]);
if flags.contains(MotifWmHintsFlags::Decorations) {
ret.decorations = value[2].try_into().ok();
}
ret
}
}
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum SetState { pub enum SetState {
Remove, Remove,

View file

@ -10,7 +10,9 @@ use std::sync::{
}; };
use std::thread::JoinHandle; use std::thread::JoinHandle;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use wayland_protocols::xdg::shell::server::xdg_toplevel; use wayland_protocols::xdg::{
decoration::zv1::server::zxdg_toplevel_decoration_v1, shell::server::xdg_toplevel,
};
use wayland_server::Resource; use wayland_server::Resource;
use xcb::{x, Xid}; use xcb::{x, Xid};
use xwayland_satellite as xwls; use xwayland_satellite as xwls;
@ -317,6 +319,7 @@ xcb::atoms_struct! {
multiple => b"MULTIPLE", multiple => b"MULTIPLE",
wm_state => b"WM_STATE", wm_state => b"WM_STATE",
wm_check => b"_NET_SUPPORTING_WM_CHECK", wm_check => b"_NET_SUPPORTING_WM_CHECK",
motif_wm_hints => b"_MOTIF_WM_HINTS" only_if_exists = false,
mime1 => b"text/plain" only_if_exists = false, mime1 => b"text/plain" only_if_exists = false,
mime2 => b"blah/blah" only_if_exists = false, mime2 => b"blah/blah" only_if_exists = false,
incr => b"INCR", incr => b"INCR",
@ -1519,3 +1522,57 @@ fn negative_output_coordinates() {
assert_eq!(ptr_reply.win_x(), 30); assert_eq!(ptr_reply.win_x(), 30);
assert_eq!(ptr_reply.win_y(), 40); assert_eq!(ptr_reply.win_y(), 40);
} }
#[test]
fn xdg_decorations() {
let mut f = Fixture::new();
let mut connection = Connection::new(&f.display);
let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
let surface = f.map_as_toplevel(&mut connection, window);
let data = f.testwl.get_surface_data(surface).unwrap();
// The default decoration mode in x11 is SDD
assert_eq!(
data.toplevel()
.decoration
.as_ref()
.and_then(|(_, decoration)| *decoration),
Some(zxdg_toplevel_decoration_v1::Mode::ServerSide)
);
// CSD
connection.set_property(
window,
connection.atoms.motif_wm_hints,
connection.atoms.motif_wm_hints,
&[2u32, 0, 0, 0, 0],
);
std::thread::sleep(std::time::Duration::from_millis(1));
f.testwl.dispatch();
let data = f.testwl.get_surface_data(surface).unwrap();
assert_eq!(
data.toplevel()
.decoration
.as_ref()
.and_then(|(_, decoration)| *decoration),
Some(zxdg_toplevel_decoration_v1::Mode::ClientSide)
);
// SSD
connection.set_property(
window,
connection.atoms.motif_wm_hints,
connection.atoms.motif_wm_hints,
&[2u32, 0, 1, 0, 0],
);
std::thread::sleep(std::time::Duration::from_millis(1));
f.testwl.dispatch();
let data = f.testwl.get_surface_data(surface).unwrap();
assert_eq!(
data.toplevel()
.decoration
.as_ref()
.and_then(|(_, decoration)| *decoration),
Some(zxdg_toplevel_decoration_v1::Mode::ServerSide)
);
}

View file

@ -27,6 +27,10 @@ use wayland_protocols::{
xdg_activation_token_v1::{self, XdgActivationTokenV1}, xdg_activation_token_v1::{self, XdgActivationTokenV1},
xdg_activation_v1::{self, XdgActivationV1}, xdg_activation_v1::{self, XdgActivationV1},
}, },
decoration::zv1::server::{
zxdg_decoration_manager_v1::{self, ZxdgDecorationManagerV1},
zxdg_toplevel_decoration_v1::{self, ZxdgToplevelDecorationV1},
},
shell::server::{ shell::server::{
xdg_popup::{self, XdgPopup}, xdg_popup::{self, XdgPopup},
xdg_positioner::{self, XdgPositioner}, xdg_positioner::{self, XdgPositioner},
@ -62,7 +66,7 @@ use wayland_server::{
wl_shm_pool::WlShmPool, wl_shm_pool::WlShmPool,
wl_surface::WlSurface, wl_surface::WlSurface,
}, },
Client, Dispatch, Display, DisplayHandle, GlobalDispatch, Resource, Client, Dispatch, Display, DisplayHandle, GlobalDispatch, Resource, WEnum,
}; };
use wl_drm::server::wl_drm::WlDrm; use wl_drm::server::wl_drm::WlDrm;
@ -123,6 +127,10 @@ pub struct Toplevel {
pub closed: bool, pub closed: bool,
pub title: Option<String>, pub title: Option<String>,
pub app_id: Option<String>, pub app_id: Option<String>,
pub decoration: Option<(
ZxdgToplevelDecorationV1,
Option<zxdg_toplevel_decoration_v1::Mode>,
)>,
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
@ -383,6 +391,7 @@ impl Server {
dh.create_global::<State, WlDataDeviceManager, _>(3, ()); dh.create_global::<State, WlDataDeviceManager, _>(3, ());
dh.create_global::<State, ZwpTabletManagerV2, _>(1, ()); dh.create_global::<State, ZwpTabletManagerV2, _>(1, ());
dh.create_global::<State, XdgActivationV1, _>(1, ()); dh.create_global::<State, XdgActivationV1, _>(1, ());
dh.create_global::<State, ZxdgDecorationManagerV1, _>(1, ());
global_noop!(ZwpLinuxDmabufV1); global_noop!(ZwpLinuxDmabufV1);
global_noop!(ZwpRelativePointerManagerV1); global_noop!(ZwpRelativePointerManagerV1);
global_noop!(WpViewporter); global_noop!(WpViewporter);
@ -733,6 +742,7 @@ simple_global_dispatch!(WlCompositor);
simple_global_dispatch!(XdgWmBase); simple_global_dispatch!(XdgWmBase);
simple_global_dispatch!(ZxdgOutputManagerV1); simple_global_dispatch!(ZxdgOutputManagerV1);
simple_global_dispatch!(ZwpTabletManagerV2); simple_global_dispatch!(ZwpTabletManagerV2);
simple_global_dispatch!(ZxdgDecorationManagerV1);
impl Dispatch<ZwpTabletManagerV2, ()> for State { impl Dispatch<ZwpTabletManagerV2, ()> for State {
fn request( fn request(
@ -1219,6 +1229,7 @@ impl Dispatch<XdgSurface, SurfaceId> for State {
closed: false, closed: false,
title: None, title: None,
app_id: None, app_id: None,
decoration: 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));
@ -1676,3 +1687,105 @@ impl Dispatch<XdgActivationTokenV1, Mutex<ActivationTokenData>> for State {
} }
} }
} }
impl Dispatch<ZxdgDecorationManagerV1, ()> for State {
fn request(
state: &mut Self,
_: &Client,
resource: &ZxdgDecorationManagerV1,
request: <ZxdgDecorationManagerV1 as Resource>::Request,
_: &(),
_: &DisplayHandle,
data_init: &mut wayland_server::DataInit<'_, Self>,
) {
match request {
zxdg_decoration_manager_v1::Request::GetToplevelDecoration { id, toplevel } => {
let surface_id = *toplevel.data::<SurfaceId>().unwrap();
let data = state.surfaces.get_mut(&surface_id).unwrap();
let Some(SurfaceRole::Toplevel(toplevel)) = &mut data.role else {
unreachable!();
};
if toplevel.decoration.is_some() {
resource.post_error(
zxdg_toplevel_decoration_v1::Error::AlreadyConstructed,
"Toplevel already has an decoration object",
);
return;
}
toplevel.decoration = Some((data_init.init(id, surface_id), None));
}
zxdg_decoration_manager_v1::Request::Destroy => {}
_ => todo!(),
}
}
}
impl Dispatch<ZxdgToplevelDecorationV1, SurfaceId> for State {
fn request(
state: &mut Self,
_: &Client,
resource: &ZxdgToplevelDecorationV1,
request: <ZxdgToplevelDecorationV1 as Resource>::Request,
surface_id: &SurfaceId,
_: &DisplayHandle,
_: &mut wayland_server::DataInit<'_, Self>,
) {
match request {
zxdg_toplevel_decoration_v1::Request::SetMode { mode } => {
let WEnum::Value(mode) = mode else {
resource.post_error(
zxdg_toplevel_decoration_v1::Error::InvalidMode,
"Invalid decoration mode",
);
return;
};
if let Some(data) = state.surfaces.get_mut(surface_id) {
let Some(SurfaceRole::Toplevel(toplevel)) = &mut data.role else {
unreachable!();
};
*toplevel
.decoration
.as_mut()
.map(|(_, decoration)| decoration)
.unwrap() = Some(mode);
} else {
resource.post_error(
zxdg_toplevel_decoration_v1::Error::Orphaned,
"Toplevel was destroyed",
);
}
}
zxdg_toplevel_decoration_v1::Request::UnsetMode => {
if let Some(data) = state.surfaces.get_mut(surface_id) {
let Some(SurfaceRole::Toplevel(toplevel)) = &mut data.role else {
unreachable!();
};
*toplevel
.decoration
.as_mut()
.map(|(_, decoration)| decoration)
.unwrap() = None;
} else {
resource.post_error(
zxdg_toplevel_decoration_v1::Error::Orphaned,
"Toplevel was destroyed",
);
}
}
zxdg_toplevel_decoration_v1::Request::Destroy => {
if let Some(data) = state.surfaces.get_mut(surface_id) {
let Some(SurfaceRole::Toplevel(toplevel)) = &mut data.role else {
unreachable!();
};
toplevel.decoration = None;
} else {
resource.post_error(
zxdg_toplevel_decoration_v1::Error::Orphaned,
"Toplevel was destroyed",
);
}
}
_ => unreachable!(),
}
}
}