use smithay_client_toolkit::{
compositor::{CompositorHandler, CompositorState},
delegate_compositor, delegate_layer, delegate_output, delegate_pointer, delegate_registry,
delegate_seat, delegate_shm,
output::{OutputHandler, OutputState},
registry::{ProvidesRegistryState, RegistryState},
registry_handlers,
seat::{
pointer::{PointerEvent, PointerEventKind, PointerHandler},
Capability, SeatHandler, SeatState,
},
shell::{
wlr_layer::{
Anchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler, LayerSurface,
LayerSurfaceConfigure,
},
WaylandSurface,
},
shm::{slot::SlotPool, Shm, ShmHandler},
};
use wayland_client::{
globals::registry_queue_init,
protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_shm, wl_surface},
Connection, Dispatch, QueueHandle, WEnum,
};
use crate::error::{Error, Result};
use crate::flyout::{next_flyout, place_flyout, HoverTarget};
use crate::geometry::{Edge, LogicalPoint, LogicalRect, LogicalSize};
use crate::keynav::{handle_key, FlyoutFocus, MenuFocus, NavAction, NavKey};
use crate::menu::{Item, Menu, MenuId};
use crate::render::paint::{render_menu, LaidMenu};
use crate::render::RasterDrawer;
use crate::style::Color;
use crate::theme::{MenuOptions, OsFamily, Theme};
const SCALE: f32 = 1.0;
const POPUP_GAP: f32 = 2.0;
const BTN_LEFT: u32 = 0x110;
const BTN_RIGHT: u32 = 0x111;
pub(super) fn layer_shell_available() -> bool {
let Ok(conn) = Connection::connect_to_env() else {
return false;
};
let Ok((globals, _queue)) = registry_queue_init::<PopupState>(&conn) else {
return false;
};
globals
.contents()
.with_list(|list| list.iter().any(|g| g.interface == "zwlr_layer_shell_v1"))
}
pub(super) fn cursor_position() -> Option<LogicalPoint> {
None
}
pub(super) fn open_popup_session(
menu: Menu,
options: MenuOptions,
on_click: &(dyn Fn(&MenuId) + '_),
anchor: LogicalRect,
edge: Edge,
dark: bool,
) -> Result<()> {
let activated = run_popup(menu, options, anchor, edge, dark)?;
if let Some(id) = activated {
if !id.is_none() {
on_click(&id);
}
}
Ok(())
}
fn run_popup(
menu: Menu,
options: MenuOptions,
anchor: LogicalRect,
edge: Edge,
dark: bool,
) -> Result<Option<MenuId>> {
let conn = Connection::connect_to_env()
.map_err(|e| Error::Platform(format!("Wayland connect failed: {e}")))?;
let (globals, mut event_queue) = registry_queue_init::<PopupState>(&conn)
.map_err(|e| Error::Platform(format!("Wayland registry init failed: {e}")))?;
let qh = event_queue.handle();
let compositor = CompositorState::bind(&globals, &qh)
.map_err(|e| Error::Platform(format!("wl_compositor unavailable: {e}")))?;
let layer_shell = LayerShell::bind(&globals, &qh)
.map_err(|e| Error::Platform(format!("zwlr_layer_shell_v1 unavailable: {e}")))?;
let shm = Shm::bind(&globals, &qh)
.map_err(|e| Error::Platform(format!("wl_shm unavailable: {e}")))?;
let surface = compositor.create_surface(&qh);
let layer =
layer_shell.create_layer_surface(&qh, surface, Layer::Overlay, Some("muri-menu"), None);
layer.set_anchor(Anchor::TOP | Anchor::LEFT | Anchor::BOTTOM | Anchor::RIGHT);
layer.set_exclusive_zone(-1);
layer.set_keyboard_interactivity(KeyboardInteractivity::OnDemand);
layer.set_size(0, 0);
layer.commit();
let theme = resolve_theme(&options, dark);
let a11y = super::a11y::PopupA11y::attach(&menu);
let mut state = PopupState {
registry_state: RegistryState::new(&globals),
seat_state: SeatState::new(&globals, &qh),
output_state: OutputState::new(&globals, &qh),
shm,
pool: None,
layer,
keyboard: None,
pointer: None,
width: 0,
height: 0,
configured: false,
menu,
options,
theme,
edge,
anchor,
popup: None,
flyouts: Vec::new(),
keyboard_focus: false,
had_focus: false,
activated: None,
done: false,
needs_redraw: false,
a11y,
};
while !state.done {
event_queue
.blocking_dispatch(&mut state)
.map_err(|e| Error::Platform(format!("Wayland dispatch failed: {e}")))?;
if state.needs_redraw {
state.needs_redraw = false;
state.draw(&qh)?;
}
}
Ok(state.activated)
}
struct Rendered {
ox: i32,
oy: i32,
w: u32,
h: u32,
rgba: Vec<u8>,
}
struct Panel {
drawer: RasterDrawer,
laid: LaidMenu,
origin: LogicalPoint,
hovered: Option<usize>,
}
struct Flyout {
parent: usize,
panel: Panel,
}
struct PopupState {
registry_state: RegistryState,
seat_state: SeatState,
output_state: OutputState,
shm: Shm,
pool: Option<SlotPool>,
layer: LayerSurface,
keyboard: Option<wl_keyboard::WlKeyboard>,
pointer: Option<wl_pointer::WlPointer>,
width: u32,
height: u32,
configured: bool,
menu: Menu,
options: MenuOptions,
theme: Theme,
edge: Edge,
anchor: LogicalRect,
popup: Option<Panel>,
flyouts: Vec<Flyout>,
keyboard_focus: bool,
had_focus: bool,
activated: Option<MenuId>,
done: bool,
needs_redraw: bool,
a11y: Option<super::a11y::PopupA11y>,
}
impl PopupState {
fn work_area(&self) -> LogicalRect {
LogicalRect::new(
LogicalPoint::new(0.0, 0.0),
LogicalSize::new(self.width as f32 / SCALE, self.height as f32 / SCALE),
)
}
fn open_popup(&mut self) {
let mut drawer = RasterDrawer::for_menu_options(SCALE, &self.options);
let laid = render_menu(&mut drawer, &self.menu, &self.theme, &self.options, None);
let origin = crate::anchor::place_popup(
self.anchor,
laid.size,
self.work_area(),
self.edge,
POPUP_GAP,
);
self.popup = Some(Panel {
drawer,
laid,
origin,
hovered: None,
});
self.needs_redraw = true;
}
fn draw(&mut self, _qh: &QueueHandle<Self>) -> Result<()> {
if !self.configured {
return Ok(());
}
let (ow, oh) = (self.width, self.height);
if ow == 0 || oh == 0 {
return Ok(());
}
let mut rendered: Vec<Rendered> = Vec::with_capacity(1 + self.flyouts.len());
for level in 0..=self.flyouts.len() {
if let Some(r) = self.render_level(level) {
rendered.push(r);
}
}
let stride = ow as i32 * 4;
let pool = self
.pool
.as_mut()
.ok_or_else(|| Error::Platform("wl_shm pool missing".into()))?;
let (buffer, canvas) = pool
.create_buffer(ow as i32, oh as i32, stride, wl_shm::Format::Argb8888)
.map_err(|e| Error::Platform(format!("wl_shm create_buffer failed: {e}")))?;
canvas.fill(0);
for r in &rendered {
composite_panel(canvas, ow, oh, r);
}
let surface = self.layer.wl_surface();
surface.damage_buffer(0, 0, ow as i32, oh as i32);
buffer
.attach_to(surface)
.map_err(|e| Error::Platform(format!("wl_shm attach failed: {e}")))?;
self.layer.commit();
Ok(())
}
fn render_level(&mut self, level: usize) -> Option<Rendered> {
let menu = crate::menu::descend(
&self.menu,
self.flyouts.iter().take(level).map(|f| f.parent),
)?;
let theme = self.theme.clone();
let options = self.options.clone();
let panel = if level == 0 {
self.popup.as_mut()?
} else {
&mut self.flyouts.get_mut(level - 1)?.panel
};
let laid = render_menu(&mut panel.drawer, menu, &theme, &options, panel.hovered);
panel.laid = laid;
let origin = panel.origin;
let fb = panel.drawer.framebuffer();
let (w, h) = (fb.width(), fb.height());
let rgba = fb.pixels().to_vec();
let (ox, oy) = to_device(origin);
Some(Rendered { ox, oy, w, h, rgba })
}
fn on_motion(&mut self, pt: LogicalPoint) {
let target = self.hover_target(pt);
let mut dirty = false;
if let Some((level, local)) = self.panel_at(pt) {
if let Some(panel) = self.panel_mut(level) {
let h = panel.laid.hit(local);
if h != panel.hovered {
panel.hovered = h;
dirty = true;
}
}
}
let next = next_flyout(&self.flyout_stack(), target);
let changed = self.apply_flyout_stack(&next);
if dirty || changed {
self.needs_redraw = true;
self.sync_a11y_focus();
}
}
fn on_click(&mut self, pt: LogicalPoint) {
let Some((level, local)) = self.panel_at(pt) else {
self.done = true;
return;
};
let Some(menu) = self.menu_at_level(level) else {
return;
};
let (hit, id) = {
let Some(panel) = self.panel_ref(level) else {
return;
};
(panel.laid.hit(local), panel.laid.id_at(local))
};
if let Some(i) = hit {
if matches!(menu.items.get(i), Some(Item::Submenu { .. })) {
self.truncate_flyouts(level);
self.push_flyout(i);
return;
}
}
if let Some(id) = id {
if !id.is_none() {
self.activated = Some(id);
}
self.done = true;
}
}
fn on_key(&mut self, key: NavKey) {
let mut focus = self.current_focus();
let action = handle_key(&self.menu, &mut focus, key);
if let Some(popup) = self.popup.as_mut() {
popup.hovered = focus.top;
}
match action {
NavAction::None => return,
NavAction::Redraw => {}
NavAction::OpenFlyout(i) => self.push_flyout(i),
NavAction::CloseFlyout => {
let keep = self.flyouts.len().saturating_sub(1);
self.truncate_flyouts(keep);
}
NavAction::Activate(id) => {
if !id.is_none() {
self.activated = Some(id);
}
self.done = true;
return;
}
NavAction::CloseAll => {
self.done = true;
return;
}
}
for (k, f) in self.flyouts.iter_mut().enumerate() {
if let Some(ff) = focus.flyout.get(k) {
f.panel.hovered = ff.child;
}
}
self.needs_redraw = true;
self.sync_a11y_focus();
}
fn apply_flyout_stack(&mut self, target: &[usize]) -> bool {
let mut common = 0;
while common < target.len()
&& common < self.flyouts.len()
&& self.flyouts[common].parent == target[common]
{
common += 1;
}
let mut changed = self.flyouts.len() != common;
self.truncate_flyouts(common);
for &parent in &target[common..] {
self.push_flyout(parent);
changed = true;
}
changed
}
fn push_flyout(&mut self, parent_index: usize) {
let depth = self.flyouts.len();
let Some(parent_menu) = self.menu_at_level(depth) else {
return;
};
let Some(child) = (match parent_menu.items.get(parent_index) {
Some(Item::Submenu { menu, .. }) => Some(menu.clone()),
_ => None,
}) else {
return;
};
let (parent_origin, parent_size, row_rect) = {
let Some(pp) = self.panel_ref(depth) else {
return;
};
let Some(rect) = pp
.laid
.rows
.iter()
.find(|r| r.index == parent_index)
.map(|r| r.rect)
else {
return;
};
(pp.origin, pp.laid.size, rect)
};
let mut drawer = RasterDrawer::for_menu_options(SCALE, &self.options);
let child_laid = render_menu(&mut drawer, &child, &self.theme, &self.options, None);
let parent_rect = LogicalRect::new(parent_origin, parent_size);
let placement = place_flyout(parent_rect, row_rect, child_laid.size, self.work_area());
self.flyouts.push(Flyout {
parent: parent_index,
panel: Panel {
drawer,
laid: child_laid,
origin: placement.origin,
hovered: None,
},
});
self.needs_redraw = true;
}
fn truncate_flyouts(&mut self, len: usize) {
while self.flyouts.len() > len {
self.flyouts.pop();
self.needs_redraw = true;
}
}
fn flyout_stack(&self) -> Vec<usize> {
self.flyouts.iter().map(|f| f.parent).collect()
}
fn current_focus(&self) -> MenuFocus {
MenuFocus {
top: self.popup.as_ref().and_then(|p| p.hovered),
flyout: self
.flyouts
.iter()
.map(|f| FlyoutFocus {
parent: f.parent,
child: f.panel.hovered,
})
.collect(),
}
}
fn menu_at_level(&self, level: usize) -> Option<&Menu> {
crate::menu::descend(
&self.menu,
self.flyouts.iter().take(level).map(|f| f.parent),
)
}
fn panel_ref(&self, level: usize) -> Option<&Panel> {
if level == 0 {
self.popup.as_ref()
} else {
self.flyouts.get(level - 1).map(|f| &f.panel)
}
}
fn panel_mut(&mut self, level: usize) -> Option<&mut Panel> {
if level == 0 {
self.popup.as_mut()
} else {
self.flyouts.get_mut(level - 1).map(|f| &mut f.panel)
}
}
fn panel_at(&self, pt: LogicalPoint) -> Option<(usize, LogicalPoint)> {
for level in (0..=self.flyouts.len()).rev() {
if let Some(panel) = self.panel_ref(level) {
let rect = LogicalRect::new(panel.origin, panel.laid.size);
if rect.contains(pt) {
let local = LogicalPoint::new(pt.x - panel.origin.x, pt.y - panel.origin.y);
return Some((level, local));
}
}
}
None
}
fn hover_target(&self, pt: LogicalPoint) -> HoverTarget {
let Some((level, local)) = self.panel_at(pt) else {
return HoverTarget::Outside;
};
let Some(panel) = self.panel_ref(level) else {
return HoverTarget::Outside;
};
match panel.laid.hit(local) {
Some(i) => {
let is_submenu = self
.menu_at_level(level)
.is_some_and(|m| matches!(m.items.get(i), Some(Item::Submenu { .. })));
if is_submenu {
HoverTarget::ParentRow {
panel: level,
index: i,
}
} else {
HoverTarget::OtherRow { panel: level }
}
}
None if level > 0 => HoverTarget::Flyout,
None => HoverTarget::OtherRow { panel: level },
}
}
fn sync_a11y_focus(&mut self) {
let top = self.popup.as_ref().and_then(|p| p.hovered);
if let Some(a) = self.a11y.as_mut() {
a.focus_row(top);
}
}
}
fn to_device(p: LogicalPoint) -> (i32, i32) {
((p.x * SCALE).round() as i32, (p.y * SCALE).round() as i32)
}
fn composite_panel(canvas: &mut [u8], out_w: u32, out_h: u32, r: &Rendered) {
let row_bytes = r.w as usize * 4;
for row in 0..r.h {
let dy = r.oy + row as i32;
if dy < 0 || dy as u32 >= out_h {
continue;
}
let src_start = (row as usize) * row_bytes;
let src_row = &r.rgba[src_start..src_start + row_bytes];
if r.ox >= 0 && (r.ox as u32 + r.w) <= out_w {
let dst_start = ((dy as u32 * out_w + r.ox as u32) * 4) as usize;
let dst_row = &mut canvas[dst_start..dst_start + row_bytes];
let _ = blit_argb8888(src_row, dst_row);
continue;
}
for col in 0..r.w {
let dx = r.ox + col as i32;
if dx < 0 || dx as u32 >= out_w {
continue;
}
let si = (col as usize) * 4;
let di = ((dy as u32 * out_w + dx as u32) * 4) as usize;
canvas[di] = src_row[si + 2]; canvas[di + 1] = src_row[si + 1]; canvas[di + 2] = src_row[si]; canvas[di + 3] = src_row[si + 3]; }
}
}
fn blit_argb8888(src: &[u8], dst: &mut [u8]) -> Result<()> {
if src.len() != dst.len() {
return Err(Error::Platform(format!(
"wl_shm blit size mismatch: src {} bytes, dst {} bytes",
src.len(),
dst.len()
)));
}
for (d, s) in dst.chunks_exact_mut(4).zip(src.chunks_exact(4)) {
d[0] = s[2]; d[1] = s[1]; d[2] = s[0]; d[3] = s[3]; }
Ok(())
}
fn evdev_to_navkey(code: u32) -> Option<NavKey> {
match code {
1 => Some(NavKey::Escape), 28 | 96 => Some(NavKey::Activate), 57 => Some(NavKey::Activate), 103 => Some(NavKey::Up), 108 => Some(NavKey::Down), 105 => Some(NavKey::Left), 106 => Some(NavKey::Right), 102 => Some(NavKey::Home), 107 => Some(NavKey::End), other => evdev_us_char(other).map(NavKey::Char),
}
}
fn evdev_us_char(code: u32) -> Option<char> {
let c = match code {
16 => 'q',
17 => 'w',
18 => 'e',
19 => 'r',
20 => 't',
21 => 'y',
22 => 'u',
23 => 'i',
24 => 'o',
25 => 'p',
30 => 'a',
31 => 's',
32 => 'd',
33 => 'f',
34 => 'g',
35 => 'h',
36 => 'j',
37 => 'k',
38 => 'l',
44 => 'z',
45 => 'x',
46 => 'c',
47 => 'v',
48 => 'b',
49 => 'n',
50 => 'm',
2 => '1',
3 => '2',
4 => '3',
5 => '4',
6 => '5',
7 => '6',
8 => '7',
9 => '8',
10 => '9',
11 => '0',
_ => return None,
};
Some(c)
}
fn resolve_theme(options: &MenuOptions, dark: bool) -> Theme {
let mut theme = options.theme.resolve(OsFamily::Gnome, dark);
if options.theme.injects_system() {
if let Some((r, g, b, a)) = super::system_accent() {
theme.accent = Color::Rgba(r, g, b, a);
}
}
theme
}
impl CompositorHandler for PopupState {
fn scale_factor_changed(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
_new_factor: i32,
) {
}
fn transform_changed(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
_new_transform: wl_output::Transform,
) {
}
fn frame(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
_time: u32,
) {
}
fn surface_enter(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
_output: &wl_output::WlOutput,
) {
}
fn surface_leave(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_surface: &wl_surface::WlSurface,
_output: &wl_output::WlOutput,
) {
}
}
impl OutputHandler for PopupState {
fn output_state(&mut self) -> &mut OutputState {
&mut self.output_state
}
fn new_output(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_output: wl_output::WlOutput,
) {
}
fn update_output(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_output: wl_output::WlOutput,
) {
}
fn output_destroyed(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_output: wl_output::WlOutput,
) {
}
}
impl LayerShellHandler for PopupState {
fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _layer: &LayerSurface) {
self.done = true;
}
fn configure(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_layer: &LayerSurface,
configure: LayerSurfaceConfigure,
_serial: u32,
) {
let (w, h) = configure.new_size;
if w == 0 || h == 0 {
return;
}
self.width = w;
self.height = h;
if self.pool.is_none() {
let len = (w as usize * h as usize * 4).max(4);
self.pool = SlotPool::new(len, &self.shm).ok();
}
let first = !self.configured;
self.configured = true;
if first {
self.open_popup();
}
}
}
impl SeatHandler for PopupState {
fn seat_state(&mut self) -> &mut SeatState {
&mut self.seat_state
}
fn new_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
fn new_capability(
&mut self,
_conn: &Connection,
qh: &QueueHandle<Self>,
seat: wl_seat::WlSeat,
capability: Capability,
) {
if capability == Capability::Keyboard && self.keyboard.is_none() {
self.keyboard = Some(seat.get_keyboard(qh, ()));
}
if capability == Capability::Pointer && self.pointer.is_none() {
if let Ok(pointer) = self.seat_state.get_pointer(qh, &seat) {
self.pointer = Some(pointer);
}
}
}
fn remove_capability(
&mut self,
_conn: &Connection,
_: &QueueHandle<Self>,
_: wl_seat::WlSeat,
capability: Capability,
) {
if capability == Capability::Keyboard {
if let Some(kbd) = self.keyboard.take() {
kbd.release();
}
}
if capability == Capability::Pointer {
if let Some(ptr) = self.pointer.take() {
ptr.release();
}
}
}
fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
}
impl PointerHandler for PopupState {
fn pointer_frame(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_pointer: &wl_pointer::WlPointer,
events: &[PointerEvent],
) {
for event in events {
if &event.surface != self.layer.wl_surface() {
continue;
}
let pt = LogicalPoint::new(
event.position.0 as f32 / SCALE,
event.position.1 as f32 / SCALE,
);
match event.kind {
PointerEventKind::Enter { .. } | PointerEventKind::Motion { .. } => {
self.on_motion(pt);
}
PointerEventKind::Press { button, .. } => {
if button == BTN_LEFT || button == BTN_RIGHT {
self.on_click(pt);
}
}
PointerEventKind::Leave { .. }
| PointerEventKind::Release { .. }
| PointerEventKind::Axis { .. } => {}
}
}
}
}
impl ShmHandler for PopupState {
fn shm_state(&mut self) -> &mut Shm {
&mut self.shm
}
}
impl Dispatch<wl_keyboard::WlKeyboard, ()> for PopupState {
fn event(
this: &mut Self,
_kbd: &wl_keyboard::WlKeyboard,
event: wl_keyboard::Event,
_data: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
match event {
wl_keyboard::Event::Enter { surface, .. } => {
if &surface == this.layer.wl_surface() {
this.keyboard_focus = true;
this.had_focus = true;
if let Some(a) = this.a11y.as_mut() {
a.set_focused(true);
}
}
}
wl_keyboard::Event::Leave { surface, .. } => {
if &surface == this.layer.wl_surface() {
this.keyboard_focus = false;
if let Some(a) = this.a11y.as_mut() {
a.set_focused(false);
}
if this.had_focus {
this.done = true;
}
}
}
wl_keyboard::Event::Key {
key,
state: WEnum::Value(wl_keyboard::KeyState::Pressed),
..
} => {
if let Some(nav) = evdev_to_navkey(key) {
this.on_key(nav);
}
}
_ => {}
}
}
}
delegate_compositor!(PopupState);
delegate_output!(PopupState);
delegate_shm!(PopupState);
delegate_seat!(PopupState);
delegate_pointer!(PopupState);
delegate_layer!(PopupState);
delegate_registry!(PopupState);
impl ProvidesRegistryState for PopupState {
fn registry(&mut self) -> &mut RegistryState {
&mut self.registry_state
}
registry_handlers![OutputState, SeatState];
}
#[cfg(test)]
mod tests {
use super::{blit_argb8888, composite_panel, evdev_to_navkey, Rendered};
use crate::keynav::NavKey;
#[test]
fn blit_swaps_r_and_b_and_keeps_alpha() {
let src = [10u8, 20, 30, 40];
let mut dst = [0u8; 4];
blit_argb8888(&src, &mut dst).unwrap();
assert_eq!(dst, [30, 20, 10, 40]);
}
#[test]
fn blit_rejects_size_mismatch() {
let src = [0u8; 8];
let mut dst = [0u8; 4];
assert!(blit_argb8888(&src, &mut dst).is_err());
}
#[test]
fn composite_places_panel_at_device_origin_in_bgra() {
let mut canvas = vec![0u8; 2 * 2 * 4];
let panel = Rendered {
ox: 1,
oy: 1,
w: 1,
h: 1,
rgba: vec![10, 20, 30, 255], };
composite_panel(&mut canvas, 2, 2, &panel);
assert_eq!(&canvas[0..12], &[0u8; 12]);
assert_eq!(&canvas[12..16], &[30, 20, 10, 255]);
}
#[test]
fn composite_clips_panels_that_overflow_the_output() {
let mut canvas = vec![0u8; 2 * 2 * 4];
let panel = Rendered {
ox: 1,
oy: 1,
w: 2,
h: 2,
rgba: vec![
1, 2, 3, 255, 4, 5, 6, 255, 7, 8, 9, 255, 10, 11, 12, 255, ],
};
composite_panel(&mut canvas, 2, 2, &panel);
assert_eq!(&canvas[12..16], &[3, 2, 1, 255]);
assert_eq!(&canvas[0..12], &[0u8; 12]);
}
#[test]
fn evdev_navigation_keys_map_layout_independently() {
assert_eq!(evdev_to_navkey(1), Some(NavKey::Escape));
assert_eq!(evdev_to_navkey(103), Some(NavKey::Up));
assert_eq!(evdev_to_navkey(108), Some(NavKey::Down));
assert_eq!(evdev_to_navkey(28), Some(NavKey::Activate));
assert_eq!(evdev_to_navkey(96), Some(NavKey::Activate));
assert_eq!(evdev_to_navkey(30), Some(NavKey::Char('a')));
assert_eq!(evdev_to_navkey(0), None);
}
}