max / shop
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
1 file changed,
+74 insertions,
-8 deletions
| @@ -45,7 +45,10 @@ | |||
| 45 | 45 | }, | |
| 46 | 46 | registry_handlers, | |
| 47 | 47 | seat::keyboard::{KeyEvent, KeyboardHandler, Keysym, Modifiers, RawModifiers, RepeatInfo}, | |
| 48 | - | seat::pointer::{PointerEvent, PointerEventKind, PointerHandler}, | |
| 48 | + | seat::pointer::{ | |
| 49 | + | CursorIcon, PointerEvent, PointerEventKind, PointerHandler, ThemeSpec, ThemedPointer, | |
| 50 | + | }, | |
| 51 | + | shm::{Shm, ShmHandler}, | |
| 49 | 52 | }; | |
| 50 | 53 | use std::collections::VecDeque; | |
| 51 | 54 | use std::io::{Read, Write}; | |
| @@ -243,6 +246,14 @@ | |||
| 243 | 246 | .inspect_err(|e| warn!("no primary selection, middle-click paste disabled: {e}")) | |
| 244 | 247 | .ok(); | |
| 245 | 248 | ||
| 249 | + | // Only the XCursor half of the themed pointer reads from it, and only on | |
| 250 | + | // compositors without `wp_cursor_shape_v1`. shop paints with the GPU, so | |
| 251 | + | // this is the one shm buffer in the process and losing it costs the | |
| 252 | + | // I-beam, not the terminal. | |
| 253 | + | let shm = Shm::bind(&globals, &qh) | |
| 254 | + | .inspect_err(|e| warn!("no wl_shm, pointer falls back to the compositor default: {e}")) | |
| 255 | + | .ok(); | |
| 256 | + | ||
| 246 | 257 | // The loop is built before the app so the app can hold a handle to it. | |
| 247 | 258 | let mut event_loop: EventLoop<'static, App> = EventLoop::try_new()?; | |
| 248 | 259 | let loop_handle = event_loop.handle(); | |
| @@ -267,6 +278,7 @@ | |||
| 267 | 278 | scale: 1, | |
| 268 | 279 | keyboard: None, | |
| 269 | 280 | pointer: None, | |
| 281 | + | shm, | |
| 270 | 282 | modifiers: Modifiers::default(), | |
| 271 | 283 | pointer_at: (0.0, 0.0), | |
| 272 | 284 | selection: None, | |
| @@ -700,7 +712,13 @@ | |||
| 700 | 712 | /// HiDPI integer scale from `wl_surface.enter` outputs. | |
| 701 | 713 | scale: u32, | |
| 702 | 714 | keyboard: Option<wl_keyboard::WlKeyboard>, | |
| 703 | - | pointer: Option<wl_pointer::WlPointer>, | |
| 715 | + | /// Themed rather than plain so the pointer can be an I-beam over the grid. | |
| 716 | + | /// SCTK sends `wp_cursor_shape_v1` where the compositor has it and loads | |
| 717 | + | /// the XCursor theme where it does not, so shop picks neither route. | |
| 718 | + | pointer: Option<ThemedPointer>, | |
| 719 | + | /// Only the XCursor fallback needs it, so a compositor without `wl_shm` | |
| 720 | + | /// costs the fallback and nothing else. | |
| 721 | + | shm: Option<Shm>, | |
| 704 | 722 | modifiers: Modifiers, | |
| 705 | 723 | /// Pointer position in surface-local logical pixels, from the last event | |
| 706 | 724 | /// that carried one. Every event does, so this is always current. | |
| @@ -1032,6 +1050,15 @@ | |||
| 1032 | 1050 | fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {} | |
| 1033 | 1051 | } | |
| 1034 | 1052 | ||
| 1053 | + | impl ShmHandler for App { | |
| 1054 | + | fn shm_state(&mut self) -> &mut Shm { | |
| 1055 | + | // Unreachable without a binding: SCTK routes wl_shm events to the | |
| 1056 | + | // wl_shm `Shm::bind` created, so if the bind failed there is no object | |
| 1057 | + | // for an event to arrive on. | |
| 1058 | + | self.shm.as_mut().expect("wl_shm event without a wl_shm") | |
| 1059 | + | } | |
| 1060 | + | } | |
| 1061 | + | ||
| 1035 | 1062 | impl SeatHandler for App { | |
| 1036 | 1063 | fn seat_state(&mut self) -> &mut SeatState { | |
| 1037 | 1064 | &mut self.chrome.seat | |
| @@ -1077,10 +1104,30 @@ | |||
| 1077 | 1104 | Err(e) => warn!("get_keyboard_with_repeat: {e}"), | |
| 1078 | 1105 | } | |
| 1079 | 1106 | } | |
| 1080 | - | if capability == Capability::Pointer && self.pointer.is_none() { | |
| 1081 | - | match self.chrome.seat.get_pointer(qh, &seat) { | |
| 1107 | + | if capability == Capability::Pointer | |
| 1108 | + | && self.pointer.is_none() | |
| 1109 | + | && let Some(shm) = &self.shm | |
| 1110 | + | { | |
| 1111 | + | // The surface is the cursor's, not the window's: the XCursor | |
| 1112 | + | // fallback attaches its image to it. `wp_cursor_shape_v1` leaves | |
| 1113 | + | // it unused, which is the common case and costs one wl_surface. | |
| 1114 | + | // | |
| 1115 | + | // `ThemeSpec::System` reads XCURSOR_THEME and XCURSOR_SIZE, so a | |
| 1116 | + | // user who themes their pointer keeps their theme rather than | |
| 1117 | + | // getting shop's idea of one. | |
| 1118 | + | let cursor_surface = self.chrome.compositor.create_surface(qh); | |
| 1119 | + | // `S` is the surface's user data, `()` because `create_surface` | |
| 1120 | + | // above makes a plain one; inference has nothing else to go on. | |
| 1121 | + | let themed = self.chrome.seat.get_pointer_with_theme::<Self, ()>( | |
| 1122 | + | qh, | |
| 1123 | + | &seat, | |
| 1124 | + | shm.wl_shm(), | |
| 1125 | + | cursor_surface, | |
| 1126 | + | ThemeSpec::System, | |
| 1127 | + | ); | |
| 1128 | + | match themed { | |
| 1082 | 1129 | Ok(ptr) => self.pointer = Some(ptr), | |
| 1083 | - | Err(e) => warn!("get_pointer: {e}"), | |
| 1130 | + | Err(e) => warn!("get_pointer_with_theme: {e}"), | |
| 1084 | 1131 | } | |
| 1085 | 1132 | } | |
| 1086 | 1133 | } | |
| @@ -1099,7 +1146,10 @@ | |||
| 1099 | 1146 | if capability == Capability::Pointer | |
| 1100 | 1147 | && let Some(ptr) = self.pointer.take() | |
| 1101 | 1148 | { | |
| 1102 | - | ptr.release(); | |
| 1149 | + | // ThemedPointer's Drop releases the wl_pointer and destroys the | |
| 1150 | + | // shape device and the cursor surface, so taking it is the whole | |
| 1151 | + | // teardown; an explicit release here would be a double release. | |
| 1152 | + | drop(ptr); | |
| 1103 | 1153 | // A pointer that goes away mid-drag leaves no way to finish the | |
| 1104 | 1154 | // gesture, so end it where it stands rather than latching. | |
| 1105 | 1155 | self.dragging = false; | |
| @@ -1387,7 +1437,7 @@ | |||
| 1387 | 1437 | impl PointerHandler for App { | |
| 1388 | 1438 | fn pointer_frame( | |
| 1389 | 1439 | &mut self, | |
| 1390 | - | _: &Connection, | |
| 1440 | + | conn: &Connection, | |
| 1391 | 1441 | _: &QueueHandle<Self>, | |
| 1392 | 1442 | _: &wl_pointer::WlPointer, | |
| 1393 | 1443 | events: &[PointerEvent], | |
| @@ -1401,7 +1451,23 @@ | |||
| 1401 | 1451 | } | |
| 1402 | 1452 | self.pointer_at = event.position; | |
| 1403 | 1453 | match event.kind { | |
| 1404 | - | PointerEventKind::Motion { .. } | PointerEventKind::Enter { .. } => { | |
| 1454 | + | PointerEventKind::Enter { .. } => { | |
| 1455 | + | // Per entry, not once at startup: the shape is attached to | |
| 1456 | + | // an enter serial, and the compositor resets the pointer to | |
| 1457 | + | // its own default every time it crosses back in. | |
| 1458 | + | // | |
| 1459 | + | // The whole window gets the I-beam, not only the cells. The | |
| 1460 | + | // padding is dead space that `cell_at` already treats as | |
| 1461 | + | // the nearest cell, so a shape change at its edge would | |
| 1462 | + | // advertise a boundary that selection does not have. | |
| 1463 | + | if let Some(ptr) = &self.pointer | |
| 1464 | + | && let Err(e) = ptr.set_cursor(conn, CursorIcon::Text) | |
| 1465 | + | { | |
| 1466 | + | warn!("set_cursor: {e}"); | |
| 1467 | + | } | |
| 1468 | + | self.drag_selection(); | |
| 1469 | + | } | |
| 1470 | + | PointerEventKind::Motion { .. } => { | |
| 1405 | 1471 | self.drag_selection(); | |
| 1406 | 1472 | } | |
| 1407 | 1473 | PointerEventKind::Press { |