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::{super::SCROLL_LINES, 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 scroll_direction = if event.detail == 4 { 1.0 } else { -1.0 };
225 let scroll_y = SCROLL_LINES * scroll_direction;
226 window.handle_input(PlatformInput::ScrollWheel(crate::ScrollWheelEvent {
227 position,
228 delta: ScrollDelta::Lines(Point::new(0.0, scroll_y as f32)),
229 modifiers,
230 touch_phase: TouchPhase::Moved,
231 }));
232 } else {
233 log::warn!("Unknown button press: {event:?}");
234 }
235 }
236 Event::ButtonRelease(event) => {
237 let window = self.get_window(event.event)?;
238 let modifiers = super::modifiers_from_state(event.state);
239 let position =
240 Point::new((event.event_x as f32).into(), (event.event_y as f32).into());
241 if let Some(button) = super::button_of_key(event.detail) {
242 window.handle_input(PlatformInput::MouseUp(crate::MouseUpEvent {
243 button,
244 position,
245 modifiers,
246 click_count: 1,
247 }));
248 }
249 }
250 Event::MotionNotify(event) => {
251 let window = self.get_window(event.event)?;
252 let pressed_button = super::button_from_state(event.state);
253 let position =
254 Point::new((event.event_x as f32).into(), (event.event_y as f32).into());
255 let modifiers = super::modifiers_from_state(event.state);
256 window.handle_input(PlatformInput::MouseMove(crate::MouseMoveEvent {
257 pressed_button,
258 position,
259 modifiers,
260 }));
261 }
262 Event::LeaveNotify(event) => {
263 let window = self.get_window(event.event)?;
264 let pressed_button = super::button_from_state(event.state);
265 let position =
266 Point::new((event.event_x as f32).into(), (event.event_y as f32).into());
267 let modifiers = super::modifiers_from_state(event.state);
268 window.handle_input(PlatformInput::MouseExited(crate::MouseExitEvent {
269 pressed_button,
270 position,
271 modifiers,
272 }));
273 }
274 _ => {}
275 };
276
277 Some(())
278 }
279}
280
281impl Client for X11Client {
282 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
283 let setup = self.xcb_connection.setup();
284 setup
285 .roots
286 .iter()
287 .enumerate()
288 .filter_map(|(root_id, _)| {
289 Some(Rc::new(X11Display::new(&self.xcb_connection, root_id)?)
290 as Rc<dyn PlatformDisplay>)
291 })
292 .collect()
293 }
294
295 fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
296 Some(Rc::new(X11Display::new(
297 &self.xcb_connection,
298 id.0 as usize,
299 )?))
300 }
301
302 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
303 Some(Rc::new(
304 X11Display::new(&self.xcb_connection, self.x_root_index)
305 .expect("There should always be a root index"),
306 ))
307 }
308
309 fn open_window(
310 &self,
311 _handle: AnyWindowHandle,
312 options: WindowParams,
313 ) -> Box<dyn PlatformWindow> {
314 let x_window = self.xcb_connection.generate_id().unwrap();
315
316 let window_ptr = Rc::new(X11WindowState::new(
317 options,
318 &self.xcb_connection,
319 self.x_root_index,
320 x_window,
321 &self.atoms,
322 ));
323
324 let screen_resources = self
325 .xcb_connection
326 .randr_get_screen_resources(x_window)
327 .unwrap()
328 .reply()
329 .expect("TODO");
330
331 let mode = screen_resources
332 .crtcs
333 .iter()
334 .find_map(|crtc| {
335 let crtc_info = self
336 .xcb_connection
337 .randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME)
338 .ok()?
339 .reply()
340 .ok()?;
341
342 screen_resources
343 .modes
344 .iter()
345 .find(|m| m.id == crtc_info.mode)
346 })
347 .expect("Unable to find screen refresh rate");
348
349 // .expect("Missing screen mode for crtc specified mode id");
350
351 let refresh_event_token = self
352 .platform_inner
353 .loop_handle
354 .insert_source(calloop::timer::Timer::immediate(), {
355 let refresh_duration = mode_refresh_rate(mode);
356 let xcb_connection = Rc::clone(&self.xcb_connection);
357 move |mut instant, (), _| {
358 xcb_connection
359 .send_event(
360 false,
361 x_window,
362 xproto::EventMask::EXPOSURE,
363 xproto::ExposeEvent {
364 response_type: xproto::EXPOSE_EVENT,
365 sequence: 0,
366 window: x_window,
367 x: 0,
368 y: 0,
369 width: 0,
370 height: 0,
371 count: 1,
372 },
373 )
374 .unwrap();
375 let _ = xcb_connection.flush().unwrap();
376 // Take into account that some frames have been skipped
377 let now = time::Instant::now();
378 while instant < now {
379 instant += refresh_duration;
380 }
381 calloop::timer::TimeoutAction::ToInstant(instant)
382 }
383 })
384 .expect("Failed to initialize refresh timer");
385
386 let window_ref = WindowRef {
387 state: Rc::clone(&window_ptr),
388 refresh_event_token,
389 };
390 self.state.borrow_mut().windows.insert(x_window, window_ref);
391 Box::new(X11Window(window_ptr))
392 }
393
394 //todo(linux)
395 fn set_cursor_style(&self, _style: CursorStyle) {}
396
397 fn get_clipboard(&self) -> Rc<RefCell<dyn ClipboardProvider>> {
398 self.state.borrow().clipboard.clone()
399 }
400
401 fn get_primary(&self) -> Rc<RefCell<dyn ClipboardProvider>> {
402 self.state.borrow().primary.clone()
403 }
404}
405
406// Adatpted from:
407// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111
408pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
409 let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64);
410 let micros = 1_000_000_000 / millihertz;
411 log::info!("Refreshing at {} micros", micros);
412 Duration::from_micros(micros)
413}