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