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.directories {
268                    "Open Folder"
269                } else {
270                    "Open File"
271                };
272
273                let request = match OpenFileRequest::default()
274                    .modal(true)
275                    .title(title)
276                    .multiple(options.multiple)
277                    .directory(options.directories)
278                    .send()
279                    .await
280                {
281                    Ok(request) => request,
282                    Err(err) => {
283                        let result = match err {
284                            ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
285                            err => err.into(),
286                        };
287                        done_tx.send(Err(result));
288                        return;
289                    }
290                };
291
292                let result = match request.response() {
293                    Ok(response) => Ok(Some(
294                        response
295                            .uris()
296                            .iter()
297                            .filter_map(|uri| uri.to_file_path().ok())
298                            .collect::<Vec<_>>(),
299                    )),
300                    Err(ashpd::Error::Response(ResponseError::Cancelled)) => Ok(None),
301                    Err(e) => Err(e.into()),
302                };
303                done_tx.send(result);
304            })
305            .detach();
306        done_rx
307    }
308
309    fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>> {
310        let (done_tx, done_rx) = oneshot::channel();
311        let directory = directory.to_owned();
312        self.foreground_executor()
313            .spawn(async move {
314                let request = match SaveFileRequest::default()
315                    .modal(true)
316                    .title("Save File")
317                    .current_folder(directory)
318                    .expect("pathbuf should not be nul terminated")
319                    .send()
320                    .await
321                {
322                    Ok(request) => request,
323                    Err(err) => {
324                        let result = match err {
325                            ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
326                            err => err.into(),
327                        };
328                        done_tx.send(Err(result));
329                        return;
330                    }
331                };
332
333                let result = match request.response() {
334                    Ok(response) => Ok(response
335                        .uris()
336                        .first()
337                        .and_then(|uri| uri.to_file_path().ok())),
338                    Err(ashpd::Error::Response(ResponseError::Cancelled)) => Ok(None),
339                    Err(e) => Err(e.into()),
340                };
341                done_tx.send(result);
342            })
343            .detach();
344
345        done_rx
346    }
347
348    fn reveal_path(&self, path: &Path) {
349        self.reveal_path(path.to_owned());
350    }
351
352    fn on_quit(&self, callback: Box<dyn FnMut()>) {
353        self.with_common(|common| {
354            common.callbacks.quit = Some(callback);
355        });
356    }
357
358    fn on_reopen(&self, callback: Box<dyn FnMut()>) {
359        self.with_common(|common| {
360            common.callbacks.reopen = Some(callback);
361        });
362    }
363
364    fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
365        self.with_common(|common| {
366            common.callbacks.app_menu_action = Some(callback);
367        });
368    }
369
370    fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
371        self.with_common(|common| {
372            common.callbacks.will_open_app_menu = Some(callback);
373        });
374    }
375
376    fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
377        self.with_common(|common| {
378            common.callbacks.validate_app_menu_command = Some(callback);
379        });
380    }
381
382    fn app_path(&self) -> Result<PathBuf> {
383        // get the path of the executable of the current process
384        let exe_path = std::env::current_exe()?;
385        Ok(exe_path)
386    }
387
388    fn set_menus(&self, menus: Vec<Menu>, _keymap: &Keymap) {
389        self.with_common(|common| {
390            common.menus = menus.into_iter().map(|menu| menu.owned()).collect();
391        })
392    }
393
394    fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
395        self.with_common(|common| Some(common.menus.clone()))
396    }
397
398    fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap) {}
399
400    fn local_timezone(&self) -> UtcOffset {
401        UtcOffset::UTC
402    }
403
404    fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
405        Err(anyhow::Error::msg(
406            "Platform<LinuxPlatform>::path_for_auxiliary_executable is not implemented yet",
407        ))
408    }
409
410    fn set_cursor_style(&self, style: CursorStyle) {
411        self.set_cursor_style(style)
412    }
413
414    fn should_auto_hide_scrollbars(&self) -> bool {
415        self.with_common(|common| common.auto_hide_scrollbars)
416    }
417
418    fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
419        let url = url.to_string();
420        let username = username.to_string();
421        let password = password.to_vec();
422        self.background_executor().spawn(async move {
423            let keyring = oo7::Keyring::new().await?;
424            keyring.unlock().await?;
425            keyring
426                .create_item(
427                    KEYRING_LABEL,
428                    &vec![("url", &url), ("username", &username)],
429                    password,
430                    true,
431                )
432                .await?;
433            Ok(())
434        })
435    }
436
437    fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
438        let url = url.to_string();
439        self.background_executor().spawn(async move {
440            let keyring = oo7::Keyring::new().await?;
441            keyring.unlock().await?;
442
443            let items = keyring.search_items(&vec![("url", &url)]).await?;
444
445            for item in items.into_iter() {
446                if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
447                    let attributes = item.attributes().await?;
448                    let username = attributes
449                        .get("username")
450                        .ok_or_else(|| anyhow!("Cannot find username in stored credentials"))?;
451                    let secret = item.secret().await?;
452
453                    // we lose the zeroizing capabilities at this boundary,
454                    // a current limitation GPUI's credentials api
455                    return Ok(Some((username.to_string(), secret.to_vec())));
456                } else {
457                    continue;
458                }
459            }
460            Ok(None)
461        })
462    }
463
464    fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
465        let url = url.to_string();
466        self.background_executor().spawn(async move {
467            let keyring = oo7::Keyring::new().await?;
468            keyring.unlock().await?;
469
470            let items = keyring.search_items(&vec![("url", &url)]).await?;
471
472            for item in items.into_iter() {
473                if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
474                    item.delete().await?;
475                    return Ok(());
476                }
477            }
478
479            Ok(())
480        })
481    }
482
483    fn window_appearance(&self) -> WindowAppearance {
484        self.with_common(|common| common.appearance)
485    }
486
487    fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
488        Task::ready(Err(anyhow!("register_url_scheme unimplemented")))
489    }
490
491    fn write_to_primary(&self, item: ClipboardItem) {
492        self.write_to_primary(item)
493    }
494
495    fn write_to_clipboard(&self, item: ClipboardItem) {
496        self.write_to_clipboard(item)
497    }
498
499    fn read_from_primary(&self) -> Option<ClipboardItem> {
500        self.read_from_primary()
501    }
502
503    fn read_from_clipboard(&self) -> Option<ClipboardItem> {
504        self.read_from_clipboard()
505    }
506
507    fn add_recent_document(&self, _path: &Path) {}
508}
509
510pub(super) fn open_uri_internal(
511    executor: BackgroundExecutor,
512    uri: &str,
513    activation_token: Option<String>,
514) {
515    if let Some(uri) = url::Url::parse(uri).log_err() {
516        executor
517            .spawn(async move {
518                match OpenUriRequest::default()
519                    .activation_token(activation_token.clone().map(ActivationToken::from))
520                    .send_uri(&uri)
521                    .await
522                {
523                    Ok(_) => return,
524                    Err(e) => log::error!("Failed to open with dbus: {}", e),
525                }
526
527                for mut command in open::commands(uri.to_string()) {
528                    if let Some(token) = activation_token.as_ref() {
529                        command.env("XDG_ACTIVATION_TOKEN", token);
530                    }
531                    match command.spawn() {
532                        Ok(_) => return,
533                        Err(e) => {
534                            log::error!("Failed to open with {:?}: {}", command.get_program(), e)
535                        }
536                    }
537                }
538            })
539            .detach();
540    }
541}
542
543pub(super) fn reveal_path_internal(
544    executor: BackgroundExecutor,
545    path: PathBuf,
546    activation_token: Option<String>,
547) {
548    executor
549        .spawn(async move {
550            if let Some(dir) = File::open(path.clone()).log_err() {
551                match OpenDirectoryRequest::default()
552                    .activation_token(activation_token.map(ActivationToken::from))
553                    .send(&dir.as_fd())
554                    .await
555                {
556                    Ok(_) => return,
557                    Err(e) => log::error!("Failed to open with dbus: {}", e),
558                }
559                if path.is_dir() {
560                    open::that_detached(path).log_err();
561                } else {
562                    open::that_detached(path.parent().unwrap_or(Path::new(""))).log_err();
563                }
564            }
565        })
566        .detach();
567}
568
569pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> bool {
570    let diff = a - b;
571    diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE
572}
573
574pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option<xkb::compose::State> {
575    let mut locales = Vec::default();
576    if let Some(locale) = std::env::var_os("LC_CTYPE") {
577        locales.push(locale);
578    }
579    locales.push(OsString::from("C"));
580    let mut state: Option<xkb::compose::State> = None;
581    for locale in locales {
582        if let Ok(table) =
583            xkb::compose::Table::new_from_locale(&cx, &locale, xkb::compose::COMPILE_NO_FLAGS)
584        {
585            state = Some(xkb::compose::State::new(
586                &table,
587                xkb::compose::STATE_NO_FLAGS,
588            ));
589            break;
590        }
591    }
592    state
593}
594
595pub(super) unsafe fn read_fd(mut fd: FileDescriptor) -> Result<String> {
596    let mut file = File::from_raw_fd(fd.as_raw_fd());
597
598    let mut buffer = String::new();
599    file.read_to_string(&mut buffer)?;
600
601    // Normalize the text to unix line endings, otherwise
602    // copying from eg: firefox inserts a lot of blank
603    // lines, and that is super annoying.
604    let result = buffer.replace("\r\n", "\n");
605    Ok(result)
606}
607
608impl CursorStyle {
609    pub(super) fn to_shape(&self) -> Shape {
610        match self {
611            CursorStyle::Arrow => Shape::Default,
612            CursorStyle::IBeam => Shape::Text,
613            CursorStyle::Crosshair => Shape::Crosshair,
614            CursorStyle::ClosedHand => Shape::Grabbing,
615            CursorStyle::OpenHand => Shape::Grab,
616            CursorStyle::PointingHand => Shape::Pointer,
617            CursorStyle::ResizeLeft => Shape::WResize,
618            CursorStyle::ResizeRight => Shape::EResize,
619            CursorStyle::ResizeLeftRight => Shape::EwResize,
620            CursorStyle::ResizeUp => Shape::NResize,
621            CursorStyle::ResizeDown => Shape::SResize,
622            CursorStyle::ResizeUpDown => Shape::NsResize,
623            CursorStyle::ResizeUpLeftDownRight => Shape::NwseResize,
624            CursorStyle::ResizeUpRightDownLeft => Shape::NeswResize,
625            CursorStyle::ResizeColumn => Shape::ColResize,
626            CursorStyle::ResizeRow => Shape::RowResize,
627            CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText,
628            CursorStyle::OperationNotAllowed => Shape::NotAllowed,
629            CursorStyle::DragLink => Shape::Alias,
630            CursorStyle::DragCopy => Shape::Copy,
631            CursorStyle::ContextualMenu => Shape::ContextMenu,
632        }
633    }
634
635    pub(super) fn to_icon_name(&self) -> String {
636        // Based on cursor names from https://gitlab.gnome.org/GNOME/adwaita-icon-theme (GNOME)
637        // and https://github.com/KDE/breeze (KDE). Both of them seem to be also derived from
638        // Web CSS cursor names: https://developer.mozilla.org/en-US/docs/Web/CSS/cursor#values
639        match self {
640            CursorStyle::Arrow => "arrow",
641            CursorStyle::IBeam => "text",
642            CursorStyle::Crosshair => "crosshair",
643            CursorStyle::ClosedHand => "grabbing",
644            CursorStyle::OpenHand => "grab",
645            CursorStyle::PointingHand => "pointer",
646            CursorStyle::ResizeLeft => "w-resize",
647            CursorStyle::ResizeRight => "e-resize",
648            CursorStyle::ResizeLeftRight => "ew-resize",
649            CursorStyle::ResizeUp => "n-resize",
650            CursorStyle::ResizeDown => "s-resize",
651            CursorStyle::ResizeUpDown => "ns-resize",
652            CursorStyle::ResizeUpLeftDownRight => "nwse-resize",
653            CursorStyle::ResizeUpRightDownLeft => "nesw-resize",
654            CursorStyle::ResizeColumn => "col-resize",
655            CursorStyle::ResizeRow => "row-resize",
656            CursorStyle::IBeamCursorForVerticalLayout => "vertical-text",
657            CursorStyle::OperationNotAllowed => "not-allowed",
658            CursorStyle::DragLink => "alias",
659            CursorStyle::DragCopy => "copy",
660            CursorStyle::ContextualMenu => "context-menu",
661        }
662        .to_string()
663    }
664}
665
666impl Keystroke {
667    pub(super) fn from_xkb(state: &State, modifiers: Modifiers, keycode: Keycode) -> Self {
668        let mut modifiers = modifiers;
669
670        let key_utf32 = state.key_get_utf32(keycode);
671        let key_utf8 = state.key_get_utf8(keycode);
672        let key_sym = state.key_get_one_sym(keycode);
673
674        let key = match key_sym {
675            Keysym::Return => "enter".to_owned(),
676            Keysym::Prior => "pageup".to_owned(),
677            Keysym::Next => "pagedown".to_owned(),
678            Keysym::ISO_Left_Tab => "tab".to_owned(),
679            Keysym::KP_Prior => "pageup".to_owned(),
680            Keysym::KP_Next => "pagedown".to_owned(),
681
682            Keysym::comma => ",".to_owned(),
683            Keysym::period => ".".to_owned(),
684            Keysym::less => "<".to_owned(),
685            Keysym::greater => ">".to_owned(),
686            Keysym::slash => "/".to_owned(),
687            Keysym::question => "?".to_owned(),
688
689            Keysym::semicolon => ";".to_owned(),
690            Keysym::colon => ":".to_owned(),
691            Keysym::apostrophe => "'".to_owned(),
692            Keysym::quotedbl => "\"".to_owned(),
693
694            Keysym::bracketleft => "[".to_owned(),
695            Keysym::braceleft => "{".to_owned(),
696            Keysym::bracketright => "]".to_owned(),
697            Keysym::braceright => "}".to_owned(),
698            Keysym::backslash => "\\".to_owned(),
699            Keysym::bar => "|".to_owned(),
700
701            Keysym::grave => "`".to_owned(),
702            Keysym::asciitilde => "~".to_owned(),
703            Keysym::exclam => "!".to_owned(),
704            Keysym::at => "@".to_owned(),
705            Keysym::numbersign => "#".to_owned(),
706            Keysym::dollar => "$".to_owned(),
707            Keysym::percent => "%".to_owned(),
708            Keysym::asciicircum => "^".to_owned(),
709            Keysym::ampersand => "&".to_owned(),
710            Keysym::asterisk => "*".to_owned(),
711            Keysym::parenleft => "(".to_owned(),
712            Keysym::parenright => ")".to_owned(),
713            Keysym::minus => "-".to_owned(),
714            Keysym::underscore => "_".to_owned(),
715            Keysym::equal => "=".to_owned(),
716            Keysym::plus => "+".to_owned(),
717
718            _ => {
719                let name = xkb::keysym_get_name(key_sym).to_lowercase();
720                if key_sym.is_keypad_key() {
721                    name.replace("kp_", "")
722                } else {
723                    name
724                }
725            }
726        };
727
728        if modifiers.shift {
729            // we only include the shift for upper-case letters by convention,
730            // so don't include for numbers and symbols, but do include for
731            // tab/enter, etc.
732            if key.chars().count() == 1 && key_utf8 == key {
733                modifiers.shift = false;
734            }
735        }
736
737        // Ignore control characters (and DEL) for the purposes of ime_key
738        let ime_key =
739            (key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8);
740
741        Keystroke {
742            modifiers,
743            key,
744            ime_key,
745        }
746    }
747
748    /**
749     * Returns which symbol the dead key represents
750     * https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#dead_keycodes_for_linux
751     */
752    pub fn underlying_dead_key(keysym: Keysym) -> Option<String> {
753        match keysym {
754            Keysym::dead_grave => Some("`".to_owned()),
755            Keysym::dead_acute => Some("´".to_owned()),
756            Keysym::dead_circumflex => Some("^".to_owned()),
757            Keysym::dead_tilde => Some("~".to_owned()),
758            Keysym::dead_perispomeni => Some("͂".to_owned()),
759            Keysym::dead_macron => Some("¯".to_owned()),
760            Keysym::dead_breve => Some("˘".to_owned()),
761            Keysym::dead_abovedot => Some("˙".to_owned()),
762            Keysym::dead_diaeresis => Some("¨".to_owned()),
763            Keysym::dead_abovering => Some("˚".to_owned()),
764            Keysym::dead_doubleacute => Some("˝".to_owned()),
765            Keysym::dead_caron => Some("ˇ".to_owned()),
766            Keysym::dead_cedilla => Some("¸".to_owned()),
767            Keysym::dead_ogonek => Some("˛".to_owned()),
768            Keysym::dead_iota => Some("ͅ".to_owned()),
769            Keysym::dead_voiced_sound => Some("".to_owned()),
770            Keysym::dead_semivoiced_sound => Some("".to_owned()),
771            Keysym::dead_belowdot => Some("̣̣".to_owned()),
772            Keysym::dead_hook => Some("̡".to_owned()),
773            Keysym::dead_horn => Some("̛".to_owned()),
774            Keysym::dead_stroke => Some("̶̶".to_owned()),
775            Keysym::dead_abovecomma => Some("̓̓".to_owned()),
776            Keysym::dead_psili => Some("᾿".to_owned()),
777            Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()),
778            Keysym::dead_dasia => Some("".to_owned()),
779            Keysym::dead_doublegrave => Some("̏".to_owned()),
780            Keysym::dead_belowring => Some("˳".to_owned()),
781            Keysym::dead_belowmacron => Some("̱".to_owned()),
782            Keysym::dead_belowcircumflex => Some("".to_owned()),
783            Keysym::dead_belowtilde => Some("̰".to_owned()),
784            Keysym::dead_belowbreve => Some("̮".to_owned()),
785            Keysym::dead_belowdiaeresis => Some("̤".to_owned()),
786            Keysym::dead_invertedbreve => Some("̯".to_owned()),
787            Keysym::dead_belowcomma => Some("̦".to_owned()),
788            Keysym::dead_currency => None,
789            Keysym::dead_lowline => None,
790            Keysym::dead_aboveverticalline => None,
791            Keysym::dead_belowverticalline => None,
792            Keysym::dead_longsolidusoverlay => None,
793            Keysym::dead_a => None,
794            Keysym::dead_A => None,
795            Keysym::dead_e => None,
796            Keysym::dead_E => None,
797            Keysym::dead_i => None,
798            Keysym::dead_I => None,
799            Keysym::dead_o => None,
800            Keysym::dead_O => None,
801            Keysym::dead_u => None,
802            Keysym::dead_U => None,
803            Keysym::dead_small_schwa => Some("ə".to_owned()),
804            Keysym::dead_capital_schwa => Some("Ə".to_owned()),
805            Keysym::dead_greek => None,
806            _ => None,
807        }
808    }
809}
810
811impl Modifiers {
812    pub(super) fn from_xkb(keymap_state: &State) -> Self {
813        let shift = keymap_state.mod_name_is_active(xkb::MOD_NAME_SHIFT, xkb::STATE_MODS_EFFECTIVE);
814        let alt = keymap_state.mod_name_is_active(xkb::MOD_NAME_ALT, xkb::STATE_MODS_EFFECTIVE);
815        let control =
816            keymap_state.mod_name_is_active(xkb::MOD_NAME_CTRL, xkb::STATE_MODS_EFFECTIVE);
817        let platform =
818            keymap_state.mod_name_is_active(xkb::MOD_NAME_LOGO, xkb::STATE_MODS_EFFECTIVE);
819        Modifiers {
820            shift,
821            alt,
822            control,
823            platform,
824            function: false,
825        }
826    }
827}
828
829#[cfg(test)]
830mod tests {
831    use super::*;
832    use crate::{px, Point};
833
834    #[test]
835    fn test_is_within_click_distance() {
836        let zero = Point::new(px(0.0), px(0.0));
837        assert_eq!(
838            is_within_click_distance(zero, Point::new(px(5.0), px(5.0))),
839            true
840        );
841        assert_eq!(
842            is_within_click_distance(zero, Point::new(px(-4.9), px(5.0))),
843            true
844        );
845        assert_eq!(
846            is_within_click_distance(Point::new(px(3.0), px(2.0)), Point::new(px(-2.0), px(-2.0))),
847            true
848        );
849        assert_eq!(
850            is_within_click_distance(zero, Point::new(px(5.0), px(5.1))),
851            false
852        );
853    }
854}