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