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