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