590 lines
21 KiB
Rust
590 lines
21 KiB
Rust
//! The overlay itself: a layer surface for the chrome, one subsurface per tile,
|
|
//! and the keyboard.
|
|
//!
|
|
//! Scaling is the compositor's job. A tile attaches its capture buffer directly
|
|
//! and wp_viewporter names the rectangle to fit it into, so nothing here touches
|
|
//! a pixel of window content - only the background, selection and labels.
|
|
|
|
use std::error::Error;
|
|
use std::os::fd::AsFd;
|
|
|
|
use wayland_client::protocol::{
|
|
wl_keyboard::{self, WlKeyboard},
|
|
wl_pointer::{self, WlPointer},
|
|
wl_seat::{self, WlSeat},
|
|
wl_shm,
|
|
wl_surface::WlSurface,
|
|
};
|
|
use wayland_client::{Connection, Dispatch, QueueHandle, WEnum};
|
|
use wayland_protocols_wlr::layer_shell::v1::client::{
|
|
zwlr_layer_shell_v1::Layer,
|
|
zwlr_layer_surface_v1::{self, KeyboardInteractivity, ZwlrLayerSurfaceV1},
|
|
};
|
|
|
|
use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape;
|
|
|
|
use crate::app::{App, Ending};
|
|
use crate::shm;
|
|
use crate::theme::{Rect, fit_centred};
|
|
|
|
// evdev keycodes: physical positions, so navigation works on any keyboard
|
|
// layout without an xkb keymap. Reading typed characters would need one.
|
|
const KEY_ESC: u32 = 1;
|
|
const KEY_TAB: u32 = 15;
|
|
const KEY_Q: u32 = 16;
|
|
const KEY_ENTER: u32 = 28;
|
|
const KEY_LEFTCTRL: u32 = 29;
|
|
const KEY_LEFTSHIFT: u32 = 42;
|
|
const KEY_RIGHTSHIFT: u32 = 54;
|
|
const KEY_LEFTALT: u32 = 56;
|
|
const KEY_KPENTER: u32 = 96;
|
|
const KEY_RIGHTCTRL: u32 = 97;
|
|
const KEY_RIGHTALT: u32 = 100;
|
|
const KEY_HOME: u32 = 102;
|
|
const KEY_UP: u32 = 103;
|
|
const KEY_PGUP: u32 = 104;
|
|
const KEY_LEFT: u32 = 105;
|
|
const KEY_RIGHT: u32 = 106;
|
|
const KEY_END: u32 = 107;
|
|
const KEY_DOWN: u32 = 108;
|
|
const KEY_PGDN: u32 = 109;
|
|
const KEY_LEFTMETA: u32 = 125;
|
|
const KEY_RIGHTMETA: u32 = 126;
|
|
|
|
fn is_trigger_modifier(code: u32) -> bool {
|
|
matches!(
|
|
code,
|
|
KEY_LEFTALT | KEY_RIGHTALT | KEY_LEFTMETA | KEY_RIGHTMETA | KEY_LEFTCTRL | KEY_RIGHTCTRL
|
|
)
|
|
}
|
|
|
|
/// Keys that should fire repeatedly while held.
|
|
fn is_repeatable_key(code: u32) -> bool {
|
|
matches!(
|
|
code,
|
|
KEY_TAB
|
|
| KEY_RIGHT
|
|
| KEY_LEFT
|
|
| KEY_DOWN
|
|
| KEY_UP
|
|
| KEY_HOME
|
|
| KEY_END
|
|
| KEY_PGUP
|
|
| KEY_PGDN
|
|
)
|
|
}
|
|
|
|
/// evdev button code, as wl_pointer reports it.
|
|
const BTN_LEFT: u32 = 0x110;
|
|
|
|
/// Where the pointer is: the surface it entered, and the position within it.
|
|
/// Tiles are subsurfaces, so the surface alone usually names a tile; the
|
|
/// position is only needed over the chrome around them.
|
|
pub struct Hover {
|
|
pub surface: WlSurface,
|
|
pub x: f64,
|
|
pub y: f64,
|
|
}
|
|
|
|
impl App {
|
|
/// Map the overlay: a layer surface sized to hug the grid, plus the shm the
|
|
/// chrome is painted into.
|
|
pub fn show(&mut self, qh: &QueueHandle<Self>) -> Result<(), Box<dyn Error>> {
|
|
let (lw, lh) = (self.layout.width, self.layout.height);
|
|
let surface = self.compositor.create_surface(qh, ());
|
|
// Map on the display the layout was sized against, not wherever the
|
|
// compositor would otherwise put it.
|
|
let output = self
|
|
.outputs
|
|
.iter()
|
|
.find(|(_, name)| *name == self.output)
|
|
.map(|(output, _)| output);
|
|
let layer = self.layer_shell.get_layer_surface(
|
|
&surface,
|
|
output,
|
|
Layer::Overlay,
|
|
"wl-pick".to_string(),
|
|
qh,
|
|
(),
|
|
);
|
|
layer.set_size(lw as u32, lh as u32);
|
|
layer.set_keyboard_interactivity(KeyboardInteractivity::Exclusive);
|
|
surface.set_buffer_scale(self.scale);
|
|
surface.commit();
|
|
|
|
let (pw, ph) = (lw * self.scale, lh * self.scale);
|
|
let len = shm::Chrome::slot_len(pw, ph) * shm::Chrome::SLOTS;
|
|
let file = shm::memfd("wl-pick-chrome", len)?;
|
|
let pool = self.shm.create_pool(file.as_fd(), len as i32, qh, ());
|
|
for slot in 0..shm::Chrome::SLOTS {
|
|
self.chrome_buffers.push(pool.create_buffer(
|
|
(slot * shm::Chrome::slot_len(pw, ph)) as i32,
|
|
pw,
|
|
ph,
|
|
shm::Chrome::stride(pw),
|
|
wl_shm::Format::Argb8888,
|
|
qh,
|
|
(),
|
|
));
|
|
}
|
|
pool.destroy();
|
|
self.chrome = Some(shm::Chrome::new(&file, pw, ph)?);
|
|
if let (Some(mgr), Some(seat)) = (&self.inhibit_mgr, &self.seat) {
|
|
self.inhibitor = Some(mgr.inhibit_shortcuts(&surface, seat, qh, ()));
|
|
}
|
|
self.surface = Some(surface);
|
|
Ok(())
|
|
}
|
|
|
|
/// Put every visible tile where the viewport says, and unmap the rest.
|
|
///
|
|
/// Runs again after each scroll, so a tile scrolled off screen gets a null
|
|
/// buffer - the way to hide a subsurface - rather than being left behind.
|
|
/// Scaling stays the compositor's job: the capture buffer is attached as it
|
|
/// is, and wp_viewporter names the rectangle to fit it into.
|
|
pub fn sync_tiles(&mut self, qh: &QueueHandle<Self>) {
|
|
let parent = self.surface.clone().expect("show() runs first");
|
|
let scroll = self.scroll;
|
|
for i in 0..self.tiles.len() {
|
|
let Some(box_) = self.layout.tile(i as i32, scroll) else {
|
|
self.hide_tile(i);
|
|
continue;
|
|
};
|
|
if !self.tiles[i].ready {
|
|
continue;
|
|
}
|
|
let (bw, bh) = self.tiles[i].size;
|
|
let (fit_w, fit_h) = if self.tiles[i].rotated() {
|
|
(bh as i32, bw as i32)
|
|
} else {
|
|
(bw as i32, bh as i32)
|
|
};
|
|
let dst = fit_centred(fit_w, fit_h, box_);
|
|
if self.tiles[i].surface.is_none() {
|
|
let surface = self.compositor.create_surface(qh, ());
|
|
let subsurface = self.subcompositor.get_subsurface(&surface, &parent, qh, ());
|
|
let viewport = self.viewporter.get_viewport(&surface, qh, ());
|
|
// Tiles change independently of the chrome - a live frame
|
|
// arrives whenever its window does - so they must not wait on a
|
|
// parent commit.
|
|
subsurface.set_desync();
|
|
// The capture protocol reports the transform the compositor
|
|
// already applied to the buffer, which is exactly what this
|
|
// request means, so it passes straight through.
|
|
surface.set_buffer_transform(self.tiles[i].transform);
|
|
let t = &mut self.tiles[i];
|
|
t.surface = Some(surface);
|
|
t.subsurface = Some(subsurface);
|
|
t.viewport = Some(viewport);
|
|
}
|
|
let t = &self.tiles[i];
|
|
let (surface, subsurface, viewport) = (
|
|
t.surface.clone().expect("just created"),
|
|
t.subsurface.clone().expect("just created"),
|
|
t.viewport.clone().expect("just created"),
|
|
);
|
|
let slot = t.showing.expect("a ready tile has a slot");
|
|
subsurface.set_position(dst.x, dst.y);
|
|
viewport.set_destination(dst.w, dst.h);
|
|
surface.attach(Some(&t.slots[slot].buffer), 0, 0);
|
|
surface.damage_buffer(0, 0, bw as i32, bh as i32);
|
|
surface.commit();
|
|
}
|
|
// Subsurface placement is *parent* state: it only takes effect when the
|
|
// parent commits, desynced children included.
|
|
parent.commit();
|
|
}
|
|
|
|
/// Unmap a tile's subsurface by attaching nothing to it.
|
|
fn hide_tile(&mut self, i: usize) {
|
|
if let Some(surface) = self.tiles[i].surface.clone() {
|
|
surface.attach(None, 0, 0);
|
|
surface.commit();
|
|
}
|
|
}
|
|
|
|
/// Repaint background, selection highlight, labels and border.
|
|
pub fn paint(&mut self) {
|
|
let (scale, sel, scroll) = (self.scale, self.sel, self.scroll);
|
|
// Gather geometry before borrowing the chrome and the labels together.
|
|
let elem = self
|
|
.layout
|
|
.elem(sel as i32, scroll)
|
|
.map(|r| r.scaled(scale));
|
|
let label_boxes: Vec<(usize, Rect)> = (0..self.tiles.len())
|
|
.filter_map(|i| {
|
|
self.layout
|
|
.label(i as i32, scroll)
|
|
.map(|r| (i, r.scaled(scale)))
|
|
})
|
|
.collect();
|
|
let bar = self
|
|
.layout
|
|
.scrollbar(scroll, (self.theme.margin / 3).max(2))
|
|
.map(|(track, thumb)| (track.scaled(scale), thumb.scaled(scale)));
|
|
let t = &self.theme;
|
|
let (bg, sel_bg, fg, sel_fg, border, border_px) = (
|
|
t.bg,
|
|
t.sel_bg,
|
|
t.fg,
|
|
t.sel_fg,
|
|
t.border,
|
|
t.border_px * scale,
|
|
);
|
|
let labels = self.labels.as_mut();
|
|
let Some(chrome) = self.chrome.as_mut() else {
|
|
return;
|
|
};
|
|
let slot = chrome.next_slot();
|
|
let (cw, ch) = (chrome.w, chrome.h);
|
|
let mut p = chrome.painter();
|
|
p.fill(bg);
|
|
// The selection fills the whole element box, padding included - the same
|
|
// thing rofi's element background does. It can be scrolled out of sight.
|
|
if let Some(elem) = elem {
|
|
p.rect(elem, sel_bg);
|
|
}
|
|
if let Some(labels) = labels {
|
|
for (i, at) in label_boxes {
|
|
labels.draw(&mut p, i, at, if i == sel { sel_fg } else { fg });
|
|
}
|
|
}
|
|
// A scrollbar only appears when there is something to scroll, which
|
|
// makes it a hint rather than furniture.
|
|
if let Some((track, thumb)) = bar {
|
|
p.rect(track, bg);
|
|
p.rect(thumb, border);
|
|
}
|
|
p.frame(border_px, border);
|
|
|
|
let surface = self.surface.clone().expect("show() runs first");
|
|
surface.attach(self.chrome_buffers.get(slot), 0, 0);
|
|
surface.damage_buffer(0, 0, cw, ch);
|
|
surface.commit();
|
|
}
|
|
|
|
fn move_sel(&mut self, delta: i32, qh: &QueueHandle<Self>) {
|
|
let n = self.tiles.len() as i32;
|
|
if n == 0 {
|
|
return;
|
|
}
|
|
self.select((self.sel as i32 + delta).rem_euclid(n) as usize, qh);
|
|
}
|
|
|
|
fn move_row(&mut self, rows: i32, qh: &QueueHandle<Self>) {
|
|
let n = self.tiles.len() as i32;
|
|
let target = self.sel as i32 + rows * self.layout.cols;
|
|
if target >= 0 && target < n {
|
|
self.select(target as usize, qh);
|
|
}
|
|
}
|
|
|
|
/// Move the selection, scrolling the least that keeps it on screen. Every
|
|
/// move goes through here, so the selection is never off-view and the
|
|
/// subsurfaces always match the viewport.
|
|
fn select(&mut self, i: usize, qh: &QueueHandle<Self>) {
|
|
self.sel = i;
|
|
let scroll = self.layout.reveal(i, self.scroll);
|
|
if scroll != self.scroll {
|
|
self.scroll = scroll;
|
|
self.sync_tiles(qh);
|
|
}
|
|
self.paint();
|
|
}
|
|
|
|
/// The tile under the pointer, if it is over one. A tile's own subsurface
|
|
/// answers directly; over the parent surface - padding, labels, gaps - the
|
|
/// layout is asked instead.
|
|
fn tile_at_pointer(&self) -> Option<usize> {
|
|
let hover = self.hover.as_ref()?;
|
|
let on_tile = self
|
|
.tiles
|
|
.iter()
|
|
.position(|t| t.surface.as_ref() == Some(&hover.surface));
|
|
on_tile.or_else(|| {
|
|
(Some(&hover.surface) == self.surface.as_ref())
|
|
.then(|| self.layout.hit(hover.x as i32, hover.y as i32, self.scroll))
|
|
.flatten()
|
|
})
|
|
}
|
|
|
|
/// Press and release on the same tile picks it. Anywhere else - the margin,
|
|
/// a gap, an empty cell of the last row - does nothing at all.
|
|
fn click(&mut self, pressed: bool) {
|
|
if pressed {
|
|
self.pressed = self.tile_at_pointer();
|
|
return;
|
|
}
|
|
let released = self.tile_at_pointer();
|
|
if let Some(i) = self.pressed.take().filter(|i| Some(*i) == released) {
|
|
self.picked = self.tiles.get(i).map(|t| t.target.clone());
|
|
self.ending = Ending::Picked;
|
|
}
|
|
}
|
|
|
|
fn key(&mut self, code: u32, qh: &QueueHandle<Self>) {
|
|
if is_trigger_modifier(code) {
|
|
self.latched_modifiers.insert(code);
|
|
}
|
|
// Arm client-side repeat for navigation keys.
|
|
if is_repeatable_key(code) {
|
|
let delay = std::time::Duration::from_millis(self.repeat_delay_ms as u64);
|
|
self.repeat_key = Some(code);
|
|
self.repeat_next = Some(std::time::Instant::now() + delay);
|
|
} else {
|
|
// Non-repeating key clears any held repeat.
|
|
self.repeat_key = None;
|
|
self.repeat_next = None;
|
|
}
|
|
match code {
|
|
KEY_LEFTSHIFT | KEY_RIGHTSHIFT => self.shift = true,
|
|
KEY_ESC | KEY_Q => self.ending = Ending::Cancelled,
|
|
KEY_ENTER | KEY_KPENTER => {
|
|
self.picked = self.tiles.get(self.sel).map(|t| t.target.clone());
|
|
self.ending = Ending::Picked;
|
|
}
|
|
KEY_TAB if self.shift => self.move_sel(-1, qh),
|
|
KEY_TAB | KEY_RIGHT => self.move_sel(1, qh),
|
|
KEY_LEFT => self.move_sel(-1, qh),
|
|
KEY_DOWN => self.move_sel(1, qh),
|
|
KEY_UP => self.move_sel(-1, qh),
|
|
KEY_HOME => self.select(0, qh),
|
|
KEY_END => self.select(self.tiles.len().saturating_sub(1), qh),
|
|
KEY_PGUP => self.move_row(-self.layout.visible_rows, qh),
|
|
KEY_PGDN => self.move_row(self.layout.visible_rows, qh),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn key_up(&mut self, code: u32) {
|
|
if code == KEY_LEFTSHIFT || code == KEY_RIGHTSHIFT {
|
|
self.shift = false;
|
|
}
|
|
// Clear repeat if this is the key that was held.
|
|
if self.repeat_key == Some(code) {
|
|
self.repeat_key = None;
|
|
self.repeat_next = None;
|
|
}
|
|
if self.latched_modifiers.remove(&code) && self.latched_modifiers.is_empty() {
|
|
if self.ending == Ending::Running {
|
|
self.picked = self.tiles.get(self.sel).map(|t| t.target.clone());
|
|
self.ending = Ending::Picked;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Called by the main loop when the key-repeat timer fires. Fires the
|
|
/// currently held navigation action, then arms the next repeat tick.
|
|
pub fn fire_repeat(&mut self, qh: &QueueHandle<Self>) {
|
|
let Some(code) = self.repeat_key else { return };
|
|
let rate = std::time::Duration::from_millis(self.repeat_rate_ms as u64);
|
|
self.repeat_next = Some(std::time::Instant::now() + rate);
|
|
// Re-run the navigation action without re-arming the delay.
|
|
match code {
|
|
KEY_TAB if self.shift => self.move_sel(-1, qh),
|
|
KEY_TAB | KEY_RIGHT => self.move_sel(1, qh),
|
|
KEY_LEFT => self.move_sel(-1, qh),
|
|
KEY_DOWN => self.move_sel(1, qh),
|
|
KEY_UP => self.move_sel(-1, qh),
|
|
KEY_HOME => self.select(0, qh),
|
|
KEY_END => self.select(self.tiles.len().saturating_sub(1), qh),
|
|
KEY_PGUP => self.move_row(-self.layout.visible_rows, qh),
|
|
KEY_PGDN => self.move_row(self.layout.visible_rows, qh),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
fn keyboard_enter(&mut self, keys: Vec<u8>, _qh: &QueueHandle<Self>) {
|
|
self.focused = true;
|
|
let held_keys: Vec<u32> = keys
|
|
.chunks_exact(4)
|
|
.map(|chunk| u32::from_ne_bytes(chunk.try_into().unwrap()))
|
|
.collect();
|
|
|
|
if held_keys
|
|
.iter()
|
|
.any(|&k| k == KEY_LEFTSHIFT || k == KEY_RIGHTSHIFT)
|
|
{
|
|
self.shift = true;
|
|
}
|
|
|
|
let held_modifiers: Vec<u32> = held_keys
|
|
.iter()
|
|
.copied()
|
|
.filter(|&k| is_trigger_modifier(k))
|
|
.collect();
|
|
|
|
for &m in &held_modifiers {
|
|
self.latched_modifiers.insert(m);
|
|
}
|
|
|
|
// If no modifier is held on enter, the modifier (and/or Tab) was
|
|
// released before focus was acquired: commit selection immediately!
|
|
if self.latched_modifiers.is_empty() {
|
|
if self.ending == Ending::Running {
|
|
self.picked = self.tiles.get(self.sel).map(|t| t.target.clone());
|
|
self.ending = Ending::Picked;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// A modifier is held. If Tab is also held upon enter, arm key-repeat
|
|
// immediately so holding Tab cycles through windows.
|
|
if held_keys.iter().any(|&k| k == KEY_TAB) {
|
|
let delay = std::time::Duration::from_millis(self.repeat_delay_ms as u64);
|
|
self.repeat_key = Some(KEY_TAB);
|
|
self.repeat_next = Some(std::time::Instant::now() + delay);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- event plumbing -------------------------------------------------------
|
|
|
|
impl Dispatch<ZwlrLayerSurfaceV1, ()> for App {
|
|
fn event(
|
|
app: &mut Self,
|
|
layer: &ZwlrLayerSurfaceV1,
|
|
event: zwlr_layer_surface_v1::Event,
|
|
_: &(),
|
|
_: &Connection,
|
|
_: &QueueHandle<Self>,
|
|
) {
|
|
match event {
|
|
zwlr_layer_surface_v1::Event::Configure { serial, .. } => {
|
|
layer.ack_configure(serial);
|
|
app.configured = true;
|
|
}
|
|
zwlr_layer_surface_v1::Event::Closed => app.ending = Ending::Closed,
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Dispatch<WlSeat, ()> for App {
|
|
fn event(
|
|
app: &mut Self,
|
|
seat: &WlSeat,
|
|
event: wl_seat::Event,
|
|
_: &(),
|
|
_: &Connection,
|
|
qh: &QueueHandle<Self>,
|
|
) {
|
|
let wl_seat::Event::Capabilities {
|
|
capabilities: WEnum::Value(caps),
|
|
} = event
|
|
else {
|
|
return;
|
|
};
|
|
if caps.contains(wl_seat::Capability::Keyboard) {
|
|
seat.get_keyboard(qh, ());
|
|
}
|
|
if caps.contains(wl_seat::Capability::Pointer) {
|
|
let pointer = seat.get_pointer(qh, ());
|
|
app.cursor_device = app
|
|
.cursor_shape
|
|
.as_ref()
|
|
.map(|mgr| mgr.get_pointer(&pointer, qh, ()));
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Dispatch<WlKeyboard, ()> for App {
|
|
fn event(
|
|
app: &mut Self,
|
|
_: &WlKeyboard,
|
|
event: wl_keyboard::Event,
|
|
_: &(),
|
|
_: &Connection,
|
|
qh: &QueueHandle<Self>,
|
|
) {
|
|
match event {
|
|
wl_keyboard::Event::Key { key, state, .. } => match state {
|
|
WEnum::Value(wl_keyboard::KeyState::Pressed) => app.key(key, qh),
|
|
WEnum::Value(wl_keyboard::KeyState::Released) => app.key_up(key),
|
|
_ => {}
|
|
},
|
|
// Store compositor key-repeat settings for our client-side timer.
|
|
wl_keyboard::Event::RepeatInfo { rate, delay } => {
|
|
// rate == 0 means repeat is disabled.
|
|
if rate > 0 {
|
|
app.repeat_rate_ms = (1000 / rate as u32).max(1);
|
|
app.repeat_delay_ms = delay as u32;
|
|
} else {
|
|
app.repeat_key = None;
|
|
app.repeat_next = None;
|
|
app.repeat_delay_ms = 0;
|
|
app.repeat_rate_ms = 0;
|
|
}
|
|
}
|
|
// Focus is only tracked here. sway sends leave immediately
|
|
// followed by enter on the same surface when the pointer crosses
|
|
// it, so whether the grab is really gone is decided by the main
|
|
// loop, once the event batch has been dispatched.
|
|
wl_keyboard::Event::Enter { keys, .. } => app.keyboard_enter(keys, qh),
|
|
wl_keyboard::Event::Leave { .. } => {
|
|
app.focused = false;
|
|
// Clear any held repeat - we no longer have the keyboard.
|
|
app.repeat_key = None;
|
|
app.repeat_next = None;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Hovering does not move the selection - that belongs to the keyboard - so the
|
|
/// pointer only tracks where it is and what it clicked. Scrolling is a
|
|
/// deliberate gesture, so that does move the selection.
|
|
impl Dispatch<WlPointer, ()> for App {
|
|
fn event(
|
|
app: &mut Self,
|
|
_: &WlPointer,
|
|
event: wl_pointer::Event,
|
|
_: &(),
|
|
_: &Connection,
|
|
qh: &QueueHandle<Self>,
|
|
) {
|
|
match event {
|
|
wl_pointer::Event::Enter {
|
|
serial,
|
|
surface,
|
|
surface_x,
|
|
surface_y,
|
|
} => {
|
|
// A client owns the cursor over its own surfaces; without this
|
|
// the pointer keeps whatever shape the window below gave it.
|
|
if let Some(device) = &app.cursor_device {
|
|
device.set_shape(serial, Shape::Default);
|
|
}
|
|
app.hover = Some(Hover {
|
|
surface,
|
|
x: surface_x,
|
|
y: surface_y,
|
|
});
|
|
}
|
|
wl_pointer::Event::Motion {
|
|
surface_x,
|
|
surface_y,
|
|
..
|
|
} => {
|
|
if let Some(hover) = app.hover.as_mut() {
|
|
(hover.x, hover.y) = (surface_x, surface_y);
|
|
}
|
|
}
|
|
wl_pointer::Event::Leave { .. } => {
|
|
app.hover = None;
|
|
app.pressed = None;
|
|
}
|
|
wl_pointer::Event::Button {
|
|
button: BTN_LEFT,
|
|
state: WEnum::Value(state),
|
|
..
|
|
} => app.click(state == wl_pointer::ButtonState::Pressed),
|
|
wl_pointer::Event::Axis { value, .. } => {
|
|
app.move_sel(if value > 0.0 { 1 } else { -1 }, qh)
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|