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