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