1use std::cell::RefCell;
2use std::ops::Deref;
3use std::rc::Rc;
4use std::time::{Duration, Instant};
5
6use calloop::{EventLoop, LoopHandle};
7use collections::HashMap;
8use copypasta::x11_clipboard::{Clipboard, Primary, X11ClipboardContext};
9use copypasta::ClipboardProvider;
10
11use util::ResultExt;
12use x11rb::connection::{Connection, RequestConnection};
13use x11rb::errors::ConnectionError;
14use x11rb::protocol::randr::ConnectionExt as _;
15use x11rb::protocol::xkb::ConnectionExt as _;
16use x11rb::protocol::xproto::ConnectionExt as _;
17use x11rb::protocol::{randr, xkb, xproto, Event};
18use x11rb::xcb_ffi::XCBConnection;
19use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION};
20use xkbcommon::xkb as xkbc;
21
22use crate::platform::linux::LinuxClient;
23use crate::platform::{LinuxCommon, PlatformWindow};
24use crate::{
25 px, AnyWindowHandle, Bounds, CursorStyle, DisplayId, Modifiers, ModifiersChangedEvent, Pixels,
26 PlatformDisplay, PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowParams,
27};
28
29use super::{super::SCROLL_LINES, X11Display, X11Window, XcbAtoms};
30use super::{button_of_key, modifiers_from_state};
31use crate::platform::linux::is_within_click_distance;
32use crate::platform::linux::platform::DOUBLE_CLICK_INTERVAL;
33use calloop::{
34 generic::{FdWrapper, Generic},
35 RegistrationToken,
36};
37
38pub(crate) struct WindowRef {
39 window: X11Window,
40 refresh_event_token: RegistrationToken,
41}
42
43impl Deref for WindowRef {
44 type Target = X11Window;
45
46 fn deref(&self) -> &Self::Target {
47 &self.window
48 }
49}
50
51pub struct X11ClientState {
52 pub(crate) loop_handle: LoopHandle<'static, X11Client>,
53 pub(crate) event_loop: Option<calloop::EventLoop<'static, X11Client>>,
54
55 pub(crate) last_click: Instant,
56 pub(crate) last_location: Point<Pixels>,
57 pub(crate) current_count: usize,
58
59 pub(crate) xcb_connection: Rc<XCBConnection>,
60 pub(crate) x_root_index: usize,
61 pub(crate) atoms: XcbAtoms,
62 pub(crate) windows: HashMap<xproto::Window, WindowRef>,
63 pub(crate) focused_window: Option<xproto::Window>,
64 pub(crate) xkb: xkbc::State,
65
66 pub(crate) common: LinuxCommon,
67 pub(crate) clipboard: X11ClipboardContext<Clipboard>,
68 pub(crate) primary: X11ClipboardContext<Primary>,
69}
70
71#[derive(Clone)]
72pub(crate) struct X11Client(Rc<RefCell<X11ClientState>>);
73
74impl X11Client {
75 pub(crate) fn new() -> Self {
76 let event_loop = EventLoop::try_new().unwrap();
77
78 let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
79
80 let handle = event_loop.handle();
81
82 handle.insert_source(main_receiver, |event, _, _: &mut X11Client| {
83 if let calloop::channel::Event::Msg(runnable) = event {
84 runnable.run();
85 }
86 });
87
88 let (xcb_connection, x_root_index) = XCBConnection::connect(None).unwrap();
89 xcb_connection
90 .prefetch_extension_information(xkb::X11_EXTENSION_NAME)
91 .unwrap();
92 xcb_connection
93 .prefetch_extension_information(randr::X11_EXTENSION_NAME)
94 .unwrap();
95
96 let atoms = XcbAtoms::new(&xcb_connection).unwrap();
97 let xkb = xcb_connection
98 .xkb_use_extension(XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION)
99 .unwrap();
100
101 let atoms = atoms.reply().unwrap();
102 let xkb = xkb.reply().unwrap();
103 let events = xkb::EventType::STATE_NOTIFY;
104 xcb_connection
105 .xkb_select_events(
106 xkb::ID::USE_CORE_KBD.into(),
107 0u8.into(),
108 events,
109 0u8.into(),
110 0u8.into(),
111 &xkb::SelectEventsAux::new(),
112 )
113 .unwrap();
114 assert!(xkb.supported);
115
116 let xkb_state = {
117 let xkb_context = xkbc::Context::new(xkbc::CONTEXT_NO_FLAGS);
118 let xkb_device_id = xkbc::x11::get_core_keyboard_device_id(&xcb_connection);
119 let xkb_keymap = xkbc::x11::keymap_new_from_device(
120 &xkb_context,
121 &xcb_connection,
122 xkb_device_id,
123 xkbc::KEYMAP_COMPILE_NO_FLAGS,
124 );
125 xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id)
126 };
127
128 let clipboard = X11ClipboardContext::<Clipboard>::new().unwrap();
129 let primary = X11ClipboardContext::<Primary>::new().unwrap();
130
131 let xcb_connection = Rc::new(xcb_connection);
132
133 // Safety: Safe if xcb::Connection always returns a valid fd
134 let fd = unsafe { FdWrapper::new(Rc::clone(&xcb_connection)) };
135
136 handle
137 .insert_source(
138 Generic::new_with_error::<ConnectionError>(
139 fd,
140 calloop::Interest::READ,
141 calloop::Mode::Level,
142 ),
143 {
144 let xcb_connection = xcb_connection.clone();
145 move |_readiness, _, client| {
146 while let Some(event) = xcb_connection.poll_for_event()? {
147 client.handle_event(event);
148 }
149 Ok(calloop::PostAction::Continue)
150 }
151 },
152 )
153 .expect("Failed to initialize x11 event source");
154
155 X11Client(Rc::new(RefCell::new(X11ClientState {
156 event_loop: Some(event_loop),
157 loop_handle: handle,
158 common,
159 last_click: Instant::now(),
160 last_location: Point::new(px(0.0), px(0.0)),
161 current_count: 0,
162
163 xcb_connection,
164 x_root_index,
165 atoms,
166 windows: HashMap::default(),
167 focused_window: None,
168 xkb: xkb_state,
169 clipboard,
170 primary,
171 })))
172 }
173
174 fn get_window(&self, win: xproto::Window) -> Option<X11Window> {
175 let state = self.0.borrow();
176 state
177 .windows
178 .get(&win)
179 .map(|window_reference| window_reference.window.clone())
180 }
181
182 fn handle_event(&self, event: Event) -> Option<()> {
183 match event {
184 Event::ClientMessage(event) => {
185 let [atom, ..] = event.data.as_data32();
186 let mut state = self.0.borrow_mut();
187
188 if atom == state.atoms.WM_DELETE_WINDOW {
189 // window "x" button clicked by user, we gracefully exit
190 let window_ref = state.windows.remove(&event.window)?;
191
192 state.loop_handle.remove(window_ref.refresh_event_token);
193 window_ref.window.destroy();
194
195 if state.windows.is_empty() {
196 state.common.signal.stop();
197 }
198 }
199 }
200 Event::ConfigureNotify(event) => {
201 let bounds = Bounds {
202 origin: Point {
203 x: event.x.into(),
204 y: event.y.into(),
205 },
206 size: Size {
207 width: event.width.into(),
208 height: event.height.into(),
209 },
210 };
211 let window = self.get_window(event.window)?;
212 window.configure(bounds);
213 }
214 Event::Expose(event) => {
215 let window = self.get_window(event.window)?;
216 window.refresh();
217 }
218 Event::FocusIn(event) => {
219 let window = self.get_window(event.event)?;
220 window.set_focused(true);
221 self.0.borrow_mut().focused_window = Some(event.event);
222 }
223 Event::FocusOut(event) => {
224 let window = self.get_window(event.event)?;
225 window.set_focused(false);
226 self.0.borrow_mut().focused_window = None;
227 }
228 Event::XkbStateNotify(event) => {
229 let mut state = self.0.borrow_mut();
230 state.xkb.update_mask(
231 event.base_mods.into(),
232 event.latched_mods.into(),
233 event.locked_mods.into(),
234 0,
235 0,
236 event.locked_group.into(),
237 );
238 let modifiers = Modifiers::from_xkb(&state.xkb);
239 let focused_window_id = state.focused_window?;
240 drop(state);
241
242 let focused_window = self.get_window(focused_window_id)?;
243 focused_window.handle_input(PlatformInput::ModifiersChanged(
244 ModifiersChangedEvent { modifiers },
245 ));
246 }
247 Event::KeyPress(event) => {
248 let window = self.get_window(event.event)?;
249 let mut state = self.0.borrow_mut();
250
251 let modifiers = modifiers_from_state(event.state);
252 let keystroke = {
253 let code = event.detail.into();
254 let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
255 state.xkb.update_key(code, xkbc::KeyDirection::Down);
256 let keysym = state.xkb.key_get_one_sym(code);
257 if keysym.is_modifier_key() {
258 return Some(());
259 }
260 keystroke
261 };
262
263 drop(state);
264 window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent {
265 keystroke,
266 is_held: false,
267 }));
268 }
269 Event::KeyRelease(event) => {
270 let window = self.get_window(event.event)?;
271 let mut state = self.0.borrow_mut();
272
273 let modifiers = modifiers_from_state(event.state);
274 let keystroke = {
275 let code = event.detail.into();
276 let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
277 state.xkb.update_key(code, xkbc::KeyDirection::Up);
278 let keysym = state.xkb.key_get_one_sym(code);
279 if keysym.is_modifier_key() {
280 return Some(());
281 }
282 keystroke
283 };
284 drop(state);
285 window.handle_input(PlatformInput::KeyUp(crate::KeyUpEvent { keystroke }));
286 }
287 Event::ButtonPress(event) => {
288 let window = self.get_window(event.event)?;
289 let mut state = self.0.borrow_mut();
290
291 let modifiers = modifiers_from_state(event.state);
292 let position =
293 Point::new((event.event_x as f32).into(), (event.event_y as f32).into());
294 if let Some(button) = button_of_key(event.detail) {
295 let click_elapsed = state.last_click.elapsed();
296
297 if click_elapsed < DOUBLE_CLICK_INTERVAL
298 && is_within_click_distance(state.last_location, position)
299 {
300 state.current_count += 1;
301 } else {
302 state.current_count = 1;
303 }
304
305 state.last_click = Instant::now();
306 state.last_location = position;
307 let current_count = state.current_count;
308
309 drop(state);
310 window.handle_input(PlatformInput::MouseDown(crate::MouseDownEvent {
311 button,
312 position,
313 modifiers,
314 click_count: current_count,
315 first_mouse: false,
316 }));
317 } else if event.detail >= 4 && event.detail <= 5 {
318 // https://stackoverflow.com/questions/15510472/scrollwheel-event-in-x11
319 let scroll_direction = if event.detail == 4 { 1.0 } else { -1.0 };
320 let scroll_y = SCROLL_LINES * scroll_direction;
321
322 drop(state);
323 window.handle_input(PlatformInput::ScrollWheel(crate::ScrollWheelEvent {
324 position,
325 delta: ScrollDelta::Lines(Point::new(0.0, scroll_y as f32)),
326 modifiers,
327 touch_phase: TouchPhase::Moved,
328 }));
329 } else {
330 log::warn!("Unknown button press: {event:?}");
331 }
332 }
333 Event::ButtonRelease(event) => {
334 let window = self.get_window(event.event)?;
335 let state = self.0.borrow();
336 let modifiers = modifiers_from_state(event.state);
337 let position =
338 Point::new((event.event_x as f32).into(), (event.event_y as f32).into());
339 if let Some(button) = button_of_key(event.detail) {
340 let click_count = state.current_count;
341 drop(state);
342 window.handle_input(PlatformInput::MouseUp(crate::MouseUpEvent {
343 button,
344 position,
345 modifiers,
346 click_count,
347 }));
348 }
349 }
350 Event::MotionNotify(event) => {
351 let window = self.get_window(event.event)?;
352 let pressed_button = super::button_from_state(event.state);
353 let position =
354 Point::new((event.event_x as f32).into(), (event.event_y as f32).into());
355 let modifiers = modifiers_from_state(event.state);
356 window.handle_input(PlatformInput::MouseMove(crate::MouseMoveEvent {
357 pressed_button,
358 position,
359 modifiers,
360 }));
361 }
362 Event::LeaveNotify(event) => {
363 let window = self.get_window(event.event)?;
364 let pressed_button = super::button_from_state(event.state);
365 let position =
366 Point::new((event.event_x as f32).into(), (event.event_y as f32).into());
367 let modifiers = modifiers_from_state(event.state);
368 window.handle_input(PlatformInput::MouseExited(crate::MouseExitEvent {
369 pressed_button,
370 position,
371 modifiers,
372 }));
373 }
374 _ => {}
375 };
376
377 Some(())
378 }
379}
380
381impl LinuxClient for X11Client {
382 fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R {
383 f(&mut self.0.borrow_mut().common)
384 }
385
386 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
387 let state = self.0.borrow();
388 let setup = state.xcb_connection.setup();
389 setup
390 .roots
391 .iter()
392 .enumerate()
393 .filter_map(|(root_id, _)| {
394 Some(Rc::new(X11Display::new(&state.xcb_connection, root_id)?)
395 as Rc<dyn PlatformDisplay>)
396 })
397 .collect()
398 }
399
400 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
401 let state = self.0.borrow();
402
403 Some(Rc::new(
404 X11Display::new(&state.xcb_connection, state.x_root_index)
405 .expect("There should always be a root index"),
406 ))
407 }
408
409 fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
410 let state = self.0.borrow();
411
412 Some(Rc::new(X11Display::new(
413 &state.xcb_connection,
414 id.0 as usize,
415 )?))
416 }
417
418 fn open_window(
419 &self,
420 _handle: AnyWindowHandle,
421 params: WindowParams,
422 ) -> Box<dyn PlatformWindow> {
423 let mut state = self.0.borrow_mut();
424 let x_window = state.xcb_connection.generate_id().unwrap();
425
426 let window = X11Window::new(
427 params,
428 &state.xcb_connection,
429 state.x_root_index,
430 x_window,
431 &state.atoms,
432 );
433
434 let screen_resources = state
435 .xcb_connection
436 .randr_get_screen_resources(x_window)
437 .unwrap()
438 .reply()
439 .expect("Could not find available screens");
440
441 let mode = screen_resources
442 .crtcs
443 .iter()
444 .find_map(|crtc| {
445 let crtc_info = state
446 .xcb_connection
447 .randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME)
448 .ok()?
449 .reply()
450 .ok()?;
451
452 screen_resources
453 .modes
454 .iter()
455 .find(|m| m.id == crtc_info.mode)
456 })
457 .expect("Unable to find screen refresh rate");
458
459 let refresh_event_token = state
460 .loop_handle
461 .insert_source(calloop::timer::Timer::immediate(), {
462 let refresh_duration = mode_refresh_rate(mode);
463 move |mut instant, (), client| {
464 let state = client.0.borrow_mut();
465 state
466 .xcb_connection
467 .send_event(
468 false,
469 x_window,
470 xproto::EventMask::EXPOSURE,
471 xproto::ExposeEvent {
472 response_type: xproto::EXPOSE_EVENT,
473 sequence: 0,
474 window: x_window,
475 x: 0,
476 y: 0,
477 width: 0,
478 height: 0,
479 count: 1,
480 },
481 )
482 .unwrap();
483 let _ = state.xcb_connection.flush().unwrap();
484 // Take into account that some frames have been skipped
485 let now = time::Instant::now();
486 while instant < now {
487 instant += refresh_duration;
488 }
489 calloop::timer::TimeoutAction::ToInstant(instant)
490 }
491 })
492 .expect("Failed to initialize refresh timer");
493
494 let window_ref = WindowRef {
495 window: window.clone(),
496 refresh_event_token,
497 };
498
499 state.windows.insert(x_window, window_ref);
500 Box::new(window)
501 }
502
503 //todo(linux)
504 fn set_cursor_style(&self, _style: CursorStyle) {}
505
506 fn write_to_primary(&self, item: crate::ClipboardItem) {
507 self.0.borrow_mut().primary.set_contents(item.text);
508 }
509
510 fn write_to_clipboard(&self, item: crate::ClipboardItem) {
511 self.0.borrow_mut().clipboard.set_contents(item.text);
512 }
513
514 fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
515 self.0
516 .borrow_mut()
517 .primary
518 .get_contents()
519 .ok()
520 .map(|text| crate::ClipboardItem {
521 text,
522 metadata: None,
523 })
524 }
525
526 fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
527 self.0
528 .borrow_mut()
529 .clipboard
530 .get_contents()
531 .ok()
532 .map(|text| crate::ClipboardItem {
533 text,
534 metadata: None,
535 })
536 }
537
538 fn run(&self) {
539 let mut event_loop = self
540 .0
541 .borrow_mut()
542 .event_loop
543 .take()
544 .expect("App is already running");
545
546 event_loop.run(None, &mut self.clone(), |_| {}).log_err();
547 }
548}
549
550// Adatpted from:
551// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111
552pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
553 let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64);
554 let micros = 1_000_000_000 / millihertz;
555 log::info!("Refreshing at {} micros", micros);
556 Duration::from_micros(micros)
557}