client.rs

  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}