platform.rs

  1#![allow(unused)]
  2
  3use std::any::{type_name, Any};
  4use std::cell::{self, RefCell};
  5use std::env;
  6use std::ffi::OsString;
  7use std::fs::File;
  8use std::io::Read;
  9use std::ops::{Deref, DerefMut};
 10use std::os::fd::{AsFd, AsRawFd, FromRawFd};
 11use std::panic::Location;
 12use std::rc::Weak;
 13use std::{
 14    path::{Path, PathBuf},
 15    process::Command,
 16    rc::Rc,
 17    sync::Arc,
 18    time::Duration,
 19};
 20
 21use anyhow::anyhow;
 22use ashpd::desktop::file_chooser::{OpenFileRequest, SaveFileRequest};
 23use ashpd::desktop::open_uri::{OpenDirectoryRequest, OpenFileRequest as OpenUriRequest};
 24use ashpd::{url, ActivationToken};
 25use async_task::Runnable;
 26use calloop::channel::Channel;
 27use calloop::{EventLoop, LoopHandle, LoopSignal};
 28use filedescriptor::FileDescriptor;
 29use flume::{Receiver, Sender};
 30use futures::channel::oneshot;
 31use parking_lot::Mutex;
 32use util::ResultExt;
 33use wayland_client::Connection;
 34use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape;
 35use xkbcommon::xkb::{self, Keycode, Keysym, State};
 36
 37use crate::platform::linux::wayland::WaylandClient;
 38use crate::{
 39    px, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CosmicTextSystem, CursorStyle,
 40    DisplayId, ForegroundExecutor, Keymap, Keystroke, LinuxDispatcher, Menu, MenuItem, Modifiers,
 41    OwnedMenu, PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformInputHandler,
 42    PlatformTextSystem, PlatformWindow, Point, PromptLevel, Result, SemanticVersion, SharedString,
 43    Size, Task, WindowAppearance, WindowOptions, WindowParams,
 44};
 45
 46use super::x11::X11Client;
 47
 48pub(crate) const SCROLL_LINES: f64 = 3.0;
 49
 50// Values match the defaults on GTK.
 51// Taken from https://github.com/GNOME/gtk/blob/main/gtk/gtksettings.c#L320
 52pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400);
 53pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0);
 54pub(crate) const KEYRING_LABEL: &str = "zed-github-account";
 55
 56const FILE_PICKER_PORTAL_MISSING: &str =
 57    "Couldn't open file picker due to missing xdg-desktop-portal implementation.";
 58
 59pub trait LinuxClient {
 60    fn compositor_name(&self) -> &'static str;
 61    fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R;
 62    fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
 63    fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
 64    fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
 65
 66    fn open_window(
 67        &self,
 68        handle: AnyWindowHandle,
 69        options: WindowParams,
 70    ) -> anyhow::Result<Box<dyn PlatformWindow>>;
 71    fn set_cursor_style(&self, style: CursorStyle);
 72    fn open_uri(&self, uri: &str);
 73    fn reveal_path(&self, path: PathBuf);
 74    fn write_to_primary(&self, item: ClipboardItem);
 75    fn write_to_clipboard(&self, item: ClipboardItem);
 76    fn read_from_primary(&self) -> Option<ClipboardItem>;
 77    fn read_from_clipboard(&self) -> Option<ClipboardItem>;
 78    fn active_window(&self) -> Option<AnyWindowHandle>;
 79    fn window_stack(&self) -> Option<Vec<AnyWindowHandle>>;
 80    fn run(&self);
 81}
 82
 83#[derive(Default)]
 84pub(crate) struct PlatformHandlers {
 85    pub(crate) open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
 86    pub(crate) quit: Option<Box<dyn FnMut()>>,
 87    pub(crate) reopen: Option<Box<dyn FnMut()>>,
 88    pub(crate) app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>,
 89    pub(crate) will_open_app_menu: Option<Box<dyn FnMut()>>,
 90    pub(crate) validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
 91}
 92
 93pub(crate) struct LinuxCommon {
 94    pub(crate) background_executor: BackgroundExecutor,
 95    pub(crate) foreground_executor: ForegroundExecutor,
 96    pub(crate) text_system: Arc<CosmicTextSystem>,
 97    pub(crate) appearance: WindowAppearance,
 98    pub(crate) auto_hide_scrollbars: bool,
 99    pub(crate) callbacks: PlatformHandlers,
100    pub(crate) signal: LoopSignal,
101    pub(crate) menus: Vec<OwnedMenu>,
102}
103
104impl LinuxCommon {
105    pub fn new(signal: LoopSignal) -> (Self, Channel<Runnable>) {
106        let (main_sender, main_receiver) = calloop::channel::channel::<Runnable>();
107        let text_system = Arc::new(CosmicTextSystem::new());
108        let callbacks = PlatformHandlers::default();
109
110        let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone()));
111
112        let background_executor = BackgroundExecutor::new(dispatcher.clone());
113
114        let common = LinuxCommon {
115            background_executor,
116            foreground_executor: ForegroundExecutor::new(dispatcher.clone()),
117            text_system,
118            appearance: WindowAppearance::Light,
119            auto_hide_scrollbars: false,
120            callbacks,
121            signal,
122            menus: Vec::new(),
123        };
124
125        (common, main_receiver)
126    }
127}
128
129impl<P: LinuxClient + 'static> Platform for P {
130    fn background_executor(&self) -> BackgroundExecutor {
131        self.with_common(|common| common.background_executor.clone())
132    }
133
134    fn foreground_executor(&self) -> ForegroundExecutor {
135        self.with_common(|common| common.foreground_executor.clone())
136    }
137
138    fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
139        self.with_common(|common| common.text_system.clone())
140    }
141
142    fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
143        on_finish_launching();
144
145        LinuxClient::run(self);
146
147        let quit = self.with_common(|common| common.callbacks.quit.take());
148        if let Some(mut fun) = quit {
149            fun();
150        }
151    }
152
153    fn quit(&self) {
154        self.with_common(|common| common.signal.stop());
155    }
156
157    fn compositor_name(&self) -> &'static str {
158        self.compositor_name()
159    }
160
161    fn restart(&self, binary_path: Option<PathBuf>) {
162        use std::os::unix::process::CommandExt as _;
163
164        // get the process id of the current process
165        let app_pid = std::process::id().to_string();
166        // get the path to the executable
167        let app_path = if let Some(path) = binary_path {
168            path
169        } else {
170            match self.app_path() {
171                Ok(path) => path,
172                Err(err) => {
173                    log::error!("Failed to get app path: {:?}", err);
174                    return;
175                }
176            }
177        };
178
179        log::info!("Restarting process, using app path: {:?}", app_path);
180
181        // Script to wait for the current process to exit and then restart the app.
182        // We also wait for possibly open TCP sockets by the process to be closed,
183        // since on Linux it's not guaranteed that a process' resources have been
184        // cleaned up when `kill -0` returns.
185        let script = format!(
186            r#"
187            while kill -0 {pid} 2>/dev/null; do
188                sleep 0.1
189            done
190
191            while lsof -nP -iTCP -a -p {pid} 2>/dev/null; do
192                sleep 0.1
193            done
194
195            {app_path}
196            "#,
197            pid = app_pid,
198            app_path = app_path.display()
199        );
200
201        // execute the script using /bin/bash
202        let restart_process = Command::new("/bin/bash")
203            .arg("-c")
204            .arg(script)
205            .process_group(0)
206            .spawn();
207
208        match restart_process {
209            Ok(_) => self.quit(),
210            Err(e) => log::error!("failed to spawn restart script: {:?}", e),
211        }
212    }
213
214    fn activate(&self, ignoring_other_apps: bool) {
215        log::info!("activate is not implemented on Linux, ignoring the call")
216    }
217
218    fn hide(&self) {
219        log::info!("hide is not implemented on Linux, ignoring the call")
220    }
221
222    fn hide_other_apps(&self) {
223        log::info!("hide_other_apps is not implemented on Linux, ignoring the call")
224    }
225
226    fn unhide_other_apps(&self) {
227        log::info!("unhide_other_apps is not implemented on Linux, ignoring the call")
228    }
229
230    fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
231        self.primary_display()
232    }
233
234    fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
235        self.displays()
236    }
237
238    fn active_window(&self) -> Option<AnyWindowHandle> {
239        self.active_window()
240    }
241
242    fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
243        self.window_stack()
244    }
245
246    fn open_window(
247        &self,
248        handle: AnyWindowHandle,
249        options: WindowParams,
250    ) -> anyhow::Result<Box<dyn PlatformWindow>> {
251        self.open_window(handle, options)
252    }
253
254    fn open_url(&self, url: &str) {
255        self.open_uri(url);
256    }
257
258    fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
259        self.with_common(|common| common.callbacks.open_urls = Some(callback));
260    }
261
262    fn prompt_for_paths(
263        &self,
264        options: PathPromptOptions,
265    ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
266        let (done_tx, done_rx) = oneshot::channel();
267        self.foreground_executor()
268            .spawn(async move {
269                let title = if options.directories {
270                    "Open Folder"
271                } else {
272                    "Open File"
273                };
274
275                let request = match OpenFileRequest::default()
276                    .modal(true)
277                    .title(title)
278                    .multiple(options.multiple)
279                    .directory(options.directories)
280                    .send()
281                    .await
282                {
283                    Ok(request) => request,
284                    Err(err) => {
285                        let result = match err {
286                            ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
287                            err => err.into(),
288                        };
289                        done_tx.send(Err(result));
290                        return;
291                    }
292                };
293
294                let result = match request.response() {
295                    Ok(response) => Ok(Some(
296                        response
297                            .uris()
298                            .iter()
299                            .filter_map(|uri| uri.to_file_path().ok())
300                            .collect::<Vec<_>>(),
301                    )),
302                    Err(ashpd::Error::Response(_)) => Ok(None),
303                    Err(e) => Err(e.into()),
304                };
305                done_tx.send(result);
306            })
307            .detach();
308        done_rx
309    }
310
311    fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>> {
312        let (done_tx, done_rx) = oneshot::channel();
313        let directory = directory.to_owned();
314        self.foreground_executor()
315            .spawn(async move {
316                let request = match SaveFileRequest::default()
317                    .modal(true)
318                    .title("Save File")
319                    .current_folder(directory)
320                    .expect("pathbuf should not be nul terminated")
321                    .send()
322                    .await
323                {
324                    Ok(request) => request,
325                    Err(err) => {
326                        let result = match err {
327                            ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
328                            err => err.into(),
329                        };
330                        done_tx.send(Err(result));
331                        return;
332                    }
333                };
334
335                let result = match request.response() {
336                    Ok(response) => Ok(response
337                        .uris()
338                        .first()
339                        .and_then(|uri| uri.to_file_path().ok())),
340                    Err(ashpd::Error::Response(_)) => Ok(None),
341                    Err(e) => Err(e.into()),
342                };
343                done_tx.send(result);
344            })
345            .detach();
346
347        done_rx
348    }
349
350    fn reveal_path(&self, path: &Path) {
351        self.reveal_path(path.to_owned());
352    }
353
354    fn open_with_system(&self, path: &Path) {
355        let executor = self.background_executor().clone();
356        let path = path.to_owned();
357        executor
358            .spawn(async move {
359                let _ = std::process::Command::new("xdg-open")
360                    .arg(path)
361                    .spawn()
362                    .expect("Failed to open file with xdg-open");
363            })
364            .detach();
365    }
366
367    fn on_quit(&self, callback: Box<dyn FnMut()>) {
368        self.with_common(|common| {
369            common.callbacks.quit = Some(callback);
370        });
371    }
372
373    fn on_reopen(&self, callback: Box<dyn FnMut()>) {
374        self.with_common(|common| {
375            common.callbacks.reopen = Some(callback);
376        });
377    }
378
379    fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
380        self.with_common(|common| {
381            common.callbacks.app_menu_action = Some(callback);
382        });
383    }
384
385    fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
386        self.with_common(|common| {
387            common.callbacks.will_open_app_menu = Some(callback);
388        });
389    }
390
391    fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
392        self.with_common(|common| {
393            common.callbacks.validate_app_menu_command = Some(callback);
394        });
395    }
396
397    fn app_path(&self) -> Result<PathBuf> {
398        // get the path of the executable of the current process
399        let exe_path = std::env::current_exe()?;
400        Ok(exe_path)
401    }
402
403    fn set_menus(&self, menus: Vec<Menu>, _keymap: &Keymap) {
404        self.with_common(|common| {
405            common.menus = menus.into_iter().map(|menu| menu.owned()).collect();
406        })
407    }
408
409    fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
410        self.with_common(|common| Some(common.menus.clone()))
411    }
412
413    fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap) {}
414
415    fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
416        Err(anyhow::Error::msg(
417            "Platform<LinuxPlatform>::path_for_auxiliary_executable is not implemented yet",
418        ))
419    }
420
421    fn set_cursor_style(&self, style: CursorStyle) {
422        self.set_cursor_style(style)
423    }
424
425    fn should_auto_hide_scrollbars(&self) -> bool {
426        self.with_common(|common| common.auto_hide_scrollbars)
427    }
428
429    fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
430        let url = url.to_string();
431        let username = username.to_string();
432        let password = password.to_vec();
433        self.background_executor().spawn(async move {
434            let keyring = oo7::Keyring::new().await?;
435            keyring.unlock().await?;
436            keyring
437                .create_item(
438                    KEYRING_LABEL,
439                    &vec![("url", &url), ("username", &username)],
440                    password,
441                    true,
442                )
443                .await?;
444            Ok(())
445        })
446    }
447
448    fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
449        let url = url.to_string();
450        self.background_executor().spawn(async move {
451            let keyring = oo7::Keyring::new().await?;
452            keyring.unlock().await?;
453
454            let items = keyring.search_items(&vec![("url", &url)]).await?;
455
456            for item in items.into_iter() {
457                if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
458                    let attributes = item.attributes().await?;
459                    let username = attributes
460                        .get("username")
461                        .ok_or_else(|| anyhow!("Cannot find username in stored credentials"))?;
462                    let secret = item.secret().await?;
463
464                    // we lose the zeroizing capabilities at this boundary,
465                    // a current limitation GPUI's credentials api
466                    return Ok(Some((username.to_string(), secret.to_vec())));
467                } else {
468                    continue;
469                }
470            }
471            Ok(None)
472        })
473    }
474
475    fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
476        let url = url.to_string();
477        self.background_executor().spawn(async move {
478            let keyring = oo7::Keyring::new().await?;
479            keyring.unlock().await?;
480
481            let items = keyring.search_items(&vec![("url", &url)]).await?;
482
483            for item in items.into_iter() {
484                if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
485                    item.delete().await?;
486                    return Ok(());
487                }
488            }
489
490            Ok(())
491        })
492    }
493
494    fn window_appearance(&self) -> WindowAppearance {
495        self.with_common(|common| common.appearance)
496    }
497
498    fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
499        Task::ready(Err(anyhow!("register_url_scheme unimplemented")))
500    }
501
502    fn write_to_primary(&self, item: ClipboardItem) {
503        self.write_to_primary(item)
504    }
505
506    fn write_to_clipboard(&self, item: ClipboardItem) {
507        self.write_to_clipboard(item)
508    }
509
510    fn read_from_primary(&self) -> Option<ClipboardItem> {
511        self.read_from_primary()
512    }
513
514    fn read_from_clipboard(&self) -> Option<ClipboardItem> {
515        self.read_from_clipboard()
516    }
517
518    fn add_recent_document(&self, _path: &Path) {}
519}
520
521pub(super) fn open_uri_internal(
522    executor: BackgroundExecutor,
523    uri: &str,
524    activation_token: Option<String>,
525) {
526    if let Some(uri) = url::Url::parse(uri).log_err() {
527        executor
528            .spawn(async move {
529                match OpenUriRequest::default()
530                    .activation_token(activation_token.clone().map(ActivationToken::from))
531                    .send_uri(&uri)
532                    .await
533                {
534                    Ok(_) => return,
535                    Err(e) => log::error!("Failed to open with dbus: {}", e),
536                }
537
538                for mut command in open::commands(uri.to_string()) {
539                    if let Some(token) = activation_token.as_ref() {
540                        command.env("XDG_ACTIVATION_TOKEN", token);
541                    }
542                    match command.spawn() {
543                        Ok(_) => return,
544                        Err(e) => {
545                            log::error!("Failed to open with {:?}: {}", command.get_program(), e)
546                        }
547                    }
548                }
549            })
550            .detach();
551    }
552}
553
554pub(super) fn reveal_path_internal(
555    executor: BackgroundExecutor,
556    path: PathBuf,
557    activation_token: Option<String>,
558) {
559    executor
560        .spawn(async move {
561            if let Some(dir) = File::open(path.clone()).log_err() {
562                match OpenDirectoryRequest::default()
563                    .activation_token(activation_token.map(ActivationToken::from))
564                    .send(&dir.as_fd())
565                    .await
566                {
567                    Ok(_) => return,
568                    Err(e) => log::error!("Failed to open with dbus: {}", e),
569                }
570                if path.is_dir() {
571                    open::that_detached(path).log_err();
572                } else {
573                    open::that_detached(path.parent().unwrap_or(Path::new(""))).log_err();
574                }
575            }
576        })
577        .detach();
578}
579
580pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> bool {
581    let diff = a - b;
582    diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE
583}
584
585pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option<xkb::compose::State> {
586    let mut locales = Vec::default();
587    if let Some(locale) = std::env::var_os("LC_CTYPE") {
588        locales.push(locale);
589    }
590    locales.push(OsString::from("C"));
591    let mut state: Option<xkb::compose::State> = None;
592    for locale in locales {
593        if let Ok(table) =
594            xkb::compose::Table::new_from_locale(&cx, &locale, xkb::compose::COMPILE_NO_FLAGS)
595        {
596            state = Some(xkb::compose::State::new(
597                &table,
598                xkb::compose::STATE_NO_FLAGS,
599            ));
600            break;
601        }
602    }
603    state
604}
605
606pub(super) unsafe fn read_fd(mut fd: FileDescriptor) -> Result<String> {
607    let mut file = File::from_raw_fd(fd.as_raw_fd());
608
609    let mut buffer = String::new();
610    file.read_to_string(&mut buffer)?;
611
612    // Normalize the text to unix line endings, otherwise
613    // copying from eg: firefox inserts a lot of blank
614    // lines, and that is super annoying.
615    let result = buffer.replace("\r\n", "\n");
616    Ok(result)
617}
618
619impl CursorStyle {
620    pub(super) fn to_shape(&self) -> Shape {
621        match self {
622            CursorStyle::Arrow => Shape::Default,
623            CursorStyle::IBeam => Shape::Text,
624            CursorStyle::Crosshair => Shape::Crosshair,
625            CursorStyle::ClosedHand => Shape::Grabbing,
626            CursorStyle::OpenHand => Shape::Grab,
627            CursorStyle::PointingHand => Shape::Pointer,
628            CursorStyle::ResizeLeft => Shape::WResize,
629            CursorStyle::ResizeRight => Shape::EResize,
630            CursorStyle::ResizeLeftRight => Shape::EwResize,
631            CursorStyle::ResizeUp => Shape::NResize,
632            CursorStyle::ResizeDown => Shape::SResize,
633            CursorStyle::ResizeUpDown => Shape::NsResize,
634            CursorStyle::ResizeUpLeftDownRight => Shape::NwseResize,
635            CursorStyle::ResizeUpRightDownLeft => Shape::NeswResize,
636            CursorStyle::ResizeColumn => Shape::ColResize,
637            CursorStyle::ResizeRow => Shape::RowResize,
638            CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText,
639            CursorStyle::OperationNotAllowed => Shape::NotAllowed,
640            CursorStyle::DragLink => Shape::Alias,
641            CursorStyle::DragCopy => Shape::Copy,
642            CursorStyle::ContextualMenu => Shape::ContextMenu,
643        }
644    }
645
646    pub(super) fn to_icon_name(&self) -> String {
647        // Based on cursor names from https://gitlab.gnome.org/GNOME/adwaita-icon-theme (GNOME)
648        // and https://github.com/KDE/breeze (KDE). Both of them seem to be also derived from
649        // Web CSS cursor names: https://developer.mozilla.org/en-US/docs/Web/CSS/cursor#values
650        match self {
651            CursorStyle::Arrow => "arrow",
652            CursorStyle::IBeam => "text",
653            CursorStyle::Crosshair => "crosshair",
654            CursorStyle::ClosedHand => "grabbing",
655            CursorStyle::OpenHand => "grab",
656            CursorStyle::PointingHand => "pointer",
657            CursorStyle::ResizeLeft => "w-resize",
658            CursorStyle::ResizeRight => "e-resize",
659            CursorStyle::ResizeLeftRight => "ew-resize",
660            CursorStyle::ResizeUp => "n-resize",
661            CursorStyle::ResizeDown => "s-resize",
662            CursorStyle::ResizeUpDown => "ns-resize",
663            CursorStyle::ResizeUpLeftDownRight => "nwse-resize",
664            CursorStyle::ResizeUpRightDownLeft => "nesw-resize",
665            CursorStyle::ResizeColumn => "col-resize",
666            CursorStyle::ResizeRow => "row-resize",
667            CursorStyle::IBeamCursorForVerticalLayout => "vertical-text",
668            CursorStyle::OperationNotAllowed => "not-allowed",
669            CursorStyle::DragLink => "alias",
670            CursorStyle::DragCopy => "copy",
671            CursorStyle::ContextualMenu => "context-menu",
672        }
673        .to_string()
674    }
675}
676
677impl Keystroke {
678    pub(super) fn from_xkb(state: &State, modifiers: Modifiers, keycode: Keycode) -> Self {
679        let mut modifiers = modifiers;
680
681        let key_utf32 = state.key_get_utf32(keycode);
682        let key_utf8 = state.key_get_utf8(keycode);
683        let key_sym = state.key_get_one_sym(keycode);
684
685        let key = match key_sym {
686            Keysym::Return => "enter".to_owned(),
687            Keysym::Prior => "pageup".to_owned(),
688            Keysym::Next => "pagedown".to_owned(),
689            Keysym::ISO_Left_Tab => "tab".to_owned(),
690            Keysym::KP_Prior => "pageup".to_owned(),
691            Keysym::KP_Next => "pagedown".to_owned(),
692
693            Keysym::comma => ",".to_owned(),
694            Keysym::period => ".".to_owned(),
695            Keysym::less => "<".to_owned(),
696            Keysym::greater => ">".to_owned(),
697            Keysym::slash => "/".to_owned(),
698            Keysym::question => "?".to_owned(),
699
700            Keysym::semicolon => ";".to_owned(),
701            Keysym::colon => ":".to_owned(),
702            Keysym::apostrophe => "'".to_owned(),
703            Keysym::quotedbl => "\"".to_owned(),
704
705            Keysym::bracketleft => "[".to_owned(),
706            Keysym::braceleft => "{".to_owned(),
707            Keysym::bracketright => "]".to_owned(),
708            Keysym::braceright => "}".to_owned(),
709            Keysym::backslash => "\\".to_owned(),
710            Keysym::bar => "|".to_owned(),
711
712            Keysym::grave => "`".to_owned(),
713            Keysym::asciitilde => "~".to_owned(),
714            Keysym::exclam => "!".to_owned(),
715            Keysym::at => "@".to_owned(),
716            Keysym::numbersign => "#".to_owned(),
717            Keysym::dollar => "$".to_owned(),
718            Keysym::percent => "%".to_owned(),
719            Keysym::asciicircum => "^".to_owned(),
720            Keysym::ampersand => "&".to_owned(),
721            Keysym::asterisk => "*".to_owned(),
722            Keysym::parenleft => "(".to_owned(),
723            Keysym::parenright => ")".to_owned(),
724            Keysym::minus => "-".to_owned(),
725            Keysym::underscore => "_".to_owned(),
726            Keysym::equal => "=".to_owned(),
727            Keysym::plus => "+".to_owned(),
728
729            _ => {
730                let name = xkb::keysym_get_name(key_sym).to_lowercase();
731                if key_sym.is_keypad_key() {
732                    name.replace("kp_", "")
733                } else {
734                    name
735                }
736            }
737        };
738
739        if modifiers.shift {
740            // we only include the shift for upper-case letters by convention,
741            // so don't include for numbers and symbols, but do include for
742            // tab/enter, etc.
743            if key.chars().count() == 1 && key.to_lowercase() == key.to_uppercase() {
744                modifiers.shift = false;
745            }
746        }
747
748        // Ignore control characters (and DEL) for the purposes of ime_key
749        let ime_key =
750            (key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8);
751
752        Keystroke {
753            modifiers,
754            key,
755            ime_key,
756        }
757    }
758
759    /**
760     * Returns which symbol the dead key represents
761     * https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#dead_keycodes_for_linux
762     */
763    pub fn underlying_dead_key(keysym: Keysym) -> Option<String> {
764        match keysym {
765            Keysym::dead_grave => Some("`".to_owned()),
766            Keysym::dead_acute => Some("´".to_owned()),
767            Keysym::dead_circumflex => Some("^".to_owned()),
768            Keysym::dead_tilde => Some("~".to_owned()),
769            Keysym::dead_perispomeni => Some("͂".to_owned()),
770            Keysym::dead_macron => Some("¯".to_owned()),
771            Keysym::dead_breve => Some("˘".to_owned()),
772            Keysym::dead_abovedot => Some("˙".to_owned()),
773            Keysym::dead_diaeresis => Some("¨".to_owned()),
774            Keysym::dead_abovering => Some("˚".to_owned()),
775            Keysym::dead_doubleacute => Some("˝".to_owned()),
776            Keysym::dead_caron => Some("ˇ".to_owned()),
777            Keysym::dead_cedilla => Some("¸".to_owned()),
778            Keysym::dead_ogonek => Some("˛".to_owned()),
779            Keysym::dead_iota => Some("ͅ".to_owned()),
780            Keysym::dead_voiced_sound => Some("".to_owned()),
781            Keysym::dead_semivoiced_sound => Some("".to_owned()),
782            Keysym::dead_belowdot => Some("̣̣".to_owned()),
783            Keysym::dead_hook => Some("̡".to_owned()),
784            Keysym::dead_horn => Some("̛".to_owned()),
785            Keysym::dead_stroke => Some("̶̶".to_owned()),
786            Keysym::dead_abovecomma => Some("̓̓".to_owned()),
787            Keysym::dead_psili => Some("᾿".to_owned()),
788            Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()),
789            Keysym::dead_dasia => Some("".to_owned()),
790            Keysym::dead_doublegrave => Some("̏".to_owned()),
791            Keysym::dead_belowring => Some("˳".to_owned()),
792            Keysym::dead_belowmacron => Some("̱".to_owned()),
793            Keysym::dead_belowcircumflex => Some("".to_owned()),
794            Keysym::dead_belowtilde => Some("̰".to_owned()),
795            Keysym::dead_belowbreve => Some("̮".to_owned()),
796            Keysym::dead_belowdiaeresis => Some("̤".to_owned()),
797            Keysym::dead_invertedbreve => Some("̯".to_owned()),
798            Keysym::dead_belowcomma => Some("̦".to_owned()),
799            Keysym::dead_currency => None,
800            Keysym::dead_lowline => None,
801            Keysym::dead_aboveverticalline => None,
802            Keysym::dead_belowverticalline => None,
803            Keysym::dead_longsolidusoverlay => None,
804            Keysym::dead_a => None,
805            Keysym::dead_A => None,
806            Keysym::dead_e => None,
807            Keysym::dead_E => None,
808            Keysym::dead_i => None,
809            Keysym::dead_I => None,
810            Keysym::dead_o => None,
811            Keysym::dead_O => None,
812            Keysym::dead_u => None,
813            Keysym::dead_U => None,
814            Keysym::dead_small_schwa => Some("ə".to_owned()),
815            Keysym::dead_capital_schwa => Some("Ə".to_owned()),
816            Keysym::dead_greek => None,
817            _ => None,
818        }
819    }
820}
821
822impl Modifiers {
823    pub(super) fn from_xkb(keymap_state: &State) -> Self {
824        let shift = keymap_state.mod_name_is_active(xkb::MOD_NAME_SHIFT, xkb::STATE_MODS_EFFECTIVE);
825        let alt = keymap_state.mod_name_is_active(xkb::MOD_NAME_ALT, xkb::STATE_MODS_EFFECTIVE);
826        let control =
827            keymap_state.mod_name_is_active(xkb::MOD_NAME_CTRL, xkb::STATE_MODS_EFFECTIVE);
828        let platform =
829            keymap_state.mod_name_is_active(xkb::MOD_NAME_LOGO, xkb::STATE_MODS_EFFECTIVE);
830        Modifiers {
831            shift,
832            alt,
833            control,
834            platform,
835            function: false,
836        }
837    }
838}
839
840#[cfg(test)]
841mod tests {
842    use super::*;
843    use crate::{px, Point};
844
845    #[test]
846    fn test_is_within_click_distance() {
847        let zero = Point::new(px(0.0), px(0.0));
848        assert_eq!(
849            is_within_click_distance(zero, Point::new(px(5.0), px(5.0))),
850            true
851        );
852        assert_eq!(
853            is_within_click_distance(zero, Point::new(px(-4.9), px(5.0))),
854            true
855        );
856        assert_eq!(
857            is_within_click_distance(Point::new(px(3.0), px(2.0)), Point::new(px(-2.0), px(-2.0))),
858            true
859        );
860        assert_eq!(
861            is_within_click_distance(zero, Point::new(px(5.0), px(5.1))),
862            false
863        );
864    }
865}