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