Add Caps Lock support (#30470)

Maxim Zaks , Mikayla Maki , Mikayla Maki , and 张小白 created

Closes #21700

Release Notes:

- Added caps lock support and show a warning if the user is entering an
SSH password with Caps Lock enabled

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: 张小白 <364772080@qq.com>

Change summary

crates/gpui/src/app/test_context.rs              | 17 ++++++++-
crates/gpui/src/interactive.rs                   |  6 ++-
crates/gpui/src/platform.rs                      |  1 
crates/gpui/src/platform/keystroke.rs            |  8 ++++
crates/gpui/src/platform/linux/platform.rs       |  8 ++++
crates/gpui/src/platform/linux/wayland/client.rs |  7 +++
crates/gpui/src/platform/linux/wayland/window.rs | 17 +++++++---
crates/gpui/src/platform/linux/x11/client.rs     | 18 +++++++++-
crates/gpui/src/platform/linux/x11/window.rs     | 11 ++++++
crates/gpui/src/platform/mac/events.rs           |  7 +++
crates/gpui/src/platform/mac/window.rs           | 30 +++++++++++++----
crates/gpui/src/platform/test/window.rs          |  4 ++
crates/gpui/src/platform/windows/events.rs       |  8 ++++
crates/gpui/src/platform/windows/window.rs       |  4 ++
crates/gpui/src/window.rs                        | 28 +++++++++++-----
crates/recent_projects/src/ssh_connections.rs    |  3 +
16 files changed, 146 insertions(+), 31 deletions(-)

Detailed changes

crates/gpui/src/app/test_context.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
     Action, AnyView, AnyWindowHandle, App, AppCell, AppContext, AsyncApp, AvailableSpace,
-    BackgroundExecutor, BorrowAppContext, Bounds, ClipboardItem, DrawPhase, Drawable, Element,
-    Empty, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Modifiers,
+    BackgroundExecutor, BorrowAppContext, Bounds, Capslock, ClipboardItem, DrawPhase, Drawable,
+    Element, Empty, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Modifiers,
     ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
     Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform,
     TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window, WindowBounds,
@@ -771,7 +771,18 @@ impl VisualTestContext {
 
     /// Simulate a modifiers changed event
     pub fn simulate_modifiers_change(&mut self, modifiers: Modifiers) {
-        self.simulate_event(ModifiersChangedEvent { modifiers })
+        self.simulate_event(ModifiersChangedEvent {
+            modifiers,
+            capslock: Capslock { on: false },
+        })
+    }
+
+    /// Simulate a capslock changed event
+    pub fn simulate_capslock_change(&mut self, on: bool) {
+        self.simulate_event(ModifiersChangedEvent {
+            modifiers: Modifiers::none(),
+            capslock: Capslock { on },
+        })
     }
 
     /// Simulates the user resizing the window to the new size.

crates/gpui/src/interactive.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
-    Context, Empty, IntoElement, Keystroke, Modifiers, Pixels, Point, Render, Window, point,
-    seal::Sealed,
+    Capslock, Context, Empty, IntoElement, Keystroke, Modifiers, Pixels, Point, Render, Window,
+    point, seal::Sealed,
 };
 use smallvec::SmallVec;
 use std::{any::Any, fmt::Debug, ops::Deref, path::PathBuf};
@@ -55,6 +55,8 @@ impl KeyEvent for KeyUpEvent {}
 pub struct ModifiersChangedEvent {
     /// The new state of the modifier keys
     pub modifiers: Modifiers,
+    /// The new state of the capslock key
+    pub capslock: Capslock,
 }
 
 impl Sealed for ModifiersChangedEvent {}

crates/gpui/src/platform.rs 🔗

@@ -415,6 +415,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
     fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
     fn mouse_position(&self) -> Point<Pixels>;
     fn modifiers(&self) -> Modifiers;
+    fn capslock(&self) -> Capslock;
     fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
     fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
     fn prompt(

crates/gpui/src/platform/keystroke.rs 🔗

@@ -538,3 +538,11 @@ impl Modifiers {
             && (other.function || !self.function)
     }
 }
+
+/// The state of the capslock key at some point in time
+#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize, Hash, JsonSchema)]
+pub struct Capslock {
+    /// The capslock key is on
+    #[serde(default)]
+    pub on: bool,
+}

crates/gpui/src/platform/linux/platform.rs 🔗

@@ -873,6 +873,14 @@ impl crate::Modifiers {
     }
 }
 
+#[cfg(any(feature = "wayland", feature = "x11"))]
+impl crate::Capslock {
+    pub(super) fn from_xkb(keymap_state: &State) -> Self {
+        let on = keymap_state.mod_name_is_active(xkb::MOD_NAME_CAPS, xkb::STATE_MODS_EFFECTIVE);
+        Self { on }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

crates/gpui/src/platform/linux/wayland/client.rs 🔗

@@ -73,7 +73,7 @@ use super::{
 
 use crate::platform::{PlatformWindow, blade::BladeContext};
 use crate::{
-    AnyWindowHandle, Bounds, CursorStyle, DOUBLE_CLICK_INTERVAL, DevicePixels, DisplayId,
+    AnyWindowHandle, Bounds, Capslock, CursorStyle, DOUBLE_CLICK_INTERVAL, DevicePixels, DisplayId,
     FileDropEvent, ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, LinuxCommon,
     LinuxKeyboardLayout, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent,
     MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay,
@@ -217,6 +217,7 @@ pub(crate) struct WaylandClientState {
     click: ClickState,
     repeat: KeyRepeat,
     pub modifiers: Modifiers,
+    pub capslock: Capslock,
     axis_source: AxisSource,
     pub mouse_location: Option<Point<Pixels>>,
     continuous_scroll_delta: Option<Point<Pixels>>,
@@ -595,6 +596,7 @@ impl WaylandClient {
                 function: false,
                 platform: false,
             },
+            capslock: Capslock { on: false },
             scroll_event_received: false,
             axis_source: AxisSource::Wheel,
             mouse_location: None,
@@ -1251,9 +1253,12 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
                     keymap_state.serialize_layout(xkbcommon::xkb::STATE_LAYOUT_EFFECTIVE);
                 keymap_state.update_mask(mods_depressed, mods_latched, mods_locked, 0, 0, group);
                 state.modifiers = Modifiers::from_xkb(keymap_state);
+                let keymap_state = state.keymap_state.as_mut().unwrap();
+                state.capslock = Capslock::from_xkb(keymap_state);
 
                 let input = PlatformInput::ModifiersChanged(ModifiersChangedEvent {
                     modifiers: state.modifiers,
+                    capslock: state.capslock,
                 });
                 drop(state);
 

crates/gpui/src/platform/linux/wayland/window.rs 🔗

@@ -21,11 +21,6 @@ use wayland_protocols::xdg::shell::client::xdg_surface;
 use wayland_protocols::xdg::shell::client::xdg_toplevel::{self};
 use wayland_protocols_plasma::blur::client::org_kde_kwin_blur;
 
-use crate::platform::{
-    PlatformAtlas, PlatformInputHandler, PlatformWindow,
-    blade::{BladeContext, BladeRenderer, BladeSurfaceConfig},
-    linux::wayland::{display::WaylandDisplay, serial::SerialKind},
-};
 use crate::scene::Scene;
 use crate::{
     AnyWindowHandle, Bounds, Decorations, Globals, GpuSpecs, Modifiers, Output, Pixels,
@@ -34,6 +29,14 @@ use crate::{
     WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowControls, WindowDecorations,
     WindowParams, px, size,
 };
+use crate::{
+    Capslock,
+    platform::{
+        PlatformAtlas, PlatformInputHandler, PlatformWindow,
+        blade::{BladeContext, BladeRenderer, BladeSurfaceConfig},
+        linux::wayland::{display::WaylandDisplay, serial::SerialKind},
+    },
+};
 
 #[derive(Default)]
 pub(crate) struct Callbacks {
@@ -861,6 +864,10 @@ impl PlatformWindow for WaylandWindow {
         self.borrow().client.get_client().borrow().modifiers
     }
 
+    fn capslock(&self) -> Capslock {
+        self.borrow().client.get_client().borrow().capslock
+    }
+
     fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
         self.borrow_mut().input_handler = Some(input_handler);
     }

crates/gpui/src/platform/linux/x11/client.rs 🔗

@@ -1,3 +1,4 @@
+use crate::Capslock;
 use core::str;
 use std::{
     cell::RefCell,
@@ -203,8 +204,11 @@ pub struct X11ClientState {
     pub(crate) ximc: Option<X11rbClient<Rc<XCBConnection>>>,
     pub(crate) xim_handler: Option<XimHandler>,
     pub modifiers: Modifiers,
+    pub capslock: Capslock,
     // TODO: Can the other updates to `modifiers` be removed so that this is unnecessary?
+    // capslock logic was done analog to modifiers
     pub last_modifiers_changed_event: Modifiers,
+    pub last_capslock_changed_event: Capslock,
 
     pub(crate) compose_state: Option<xkbc::compose::State>,
     pub(crate) pre_edit_text: Option<String>,
@@ -473,7 +477,9 @@ impl X11Client {
 
         X11Client(Rc::new(RefCell::new(X11ClientState {
             modifiers: Modifiers::default(),
+            capslock: Capslock::default(),
             last_modifiers_changed_event: Modifiers::default(),
+            last_capslock_changed_event: Capslock::default(),
             event_loop: Some(event_loop),
             loop_handle: handle,
             common,
@@ -961,17 +967,25 @@ impl X11Client {
                 };
 
                 let modifiers = Modifiers::from_xkb(&state.xkb);
-                if state.last_modifiers_changed_event == modifiers {
+                let capslock = Capslock::from_xkb(&state.xkb);
+                if state.last_modifiers_changed_event == modifiers
+                    && state.last_capslock_changed_event == capslock
+                {
                     drop(state);
                 } else {
                     let focused_window_id = state.keyboard_focused_window?;
                     state.modifiers = modifiers;
                     state.last_modifiers_changed_event = modifiers;
+                    state.capslock = capslock;
+                    state.last_capslock_changed_event = capslock;
                     drop(state);
 
                     let focused_window = self.get_window(focused_window_id)?;
                     focused_window.handle_input(PlatformInput::ModifiersChanged(
-                        ModifiersChangedEvent { modifiers },
+                        ModifiersChangedEvent {
+                            modifiers,
+                            capslock,
+                        },
                     ));
                 }
 

crates/gpui/src/platform/linux/x11/window.rs 🔗

@@ -1215,6 +1215,17 @@ impl PlatformWindow for X11Window {
             .unwrap_or_default()
     }
 
+    fn capslock(&self) -> crate::Capslock {
+        self.0
+            .state
+            .borrow()
+            .client
+            .0
+            .upgrade()
+            .map(|ref_cell| ref_cell.borrow().capslock)
+            .unwrap_or_default()
+    }
+
     fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
         self.0.state.borrow_mut().input_handler = Some(input_handler);
     }

crates/gpui/src/platform/mac/events.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{
-    KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
+    Capslock, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
     MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels,
     PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase,
     platform::mac::{
@@ -121,6 +121,11 @@ impl PlatformInput {
                 NSEventType::NSFlagsChanged => {
                     Some(Self::ModifiersChanged(ModifiersChangedEvent {
                         modifiers: read_modifiers(native_event),
+                        capslock: Capslock {
+                            on: native_event
+                                .modifierFlags()
+                                .contains(NSEventModifierFlags::NSAlphaShiftKeyMask),
+                        },
                     }))
                 }
                 NSEventType::NSKeyDown => Some(Self::KeyDown(KeyDownEvent {

crates/gpui/src/platform/mac/window.rs 🔗

@@ -1,11 +1,11 @@
 use super::{BoolExt, MacDisplay, NSRange, NSStringExt, ns_string, renderer};
 use crate::{
-    AnyWindowHandle, Bounds, DisplayLink, ExternalPaths, FileDropEvent, ForegroundExecutor,
-    KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent,
-    MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
-    PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions, ScaledPixels, Size,
-    Timer, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea,
-    WindowKind, WindowParams, platform::PlatformInputHandler, point, px, size,
+    AnyWindowHandle, Bounds, Capslock, DisplayLink, ExternalPaths, FileDropEvent,
+    ForegroundExecutor, KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
+    MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay,
+    PlatformInput, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions,
+    ScaledPixels, Size, Timer, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
+    WindowControlArea, WindowKind, WindowParams, platform::PlatformInputHandler, point, px, size,
 };
 use block::ConcreteBlock;
 use cocoa::{
@@ -890,6 +890,16 @@ impl PlatformWindow for MacWindow {
         }
     }
 
+    fn capslock(&self) -> Capslock {
+        unsafe {
+            let modifiers: NSEventModifierFlags = msg_send![class!(NSEvent), modifierFlags];
+
+            Capslock {
+                on: modifiers.contains(NSEventModifierFlags::NSAlphaShiftKeyMask),
+            }
+        }
+    }
+
     fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
         self.0.as_ref().lock().input_handler = Some(input_handler);
     }
@@ -1556,13 +1566,17 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
                 lock.synthetic_drag_counter += 1;
             }
 
-            PlatformInput::ModifiersChanged(ModifiersChangedEvent { modifiers }) => {
+            PlatformInput::ModifiersChanged(ModifiersChangedEvent {
+                modifiers,
+                capslock,
+            }) => {
                 // Only raise modifiers changed event when they have actually changed
                 if let Some(PlatformInput::ModifiersChanged(ModifiersChangedEvent {
                     modifiers: prev_modifiers,
+                    capslock: prev_capslock,
                 })) = &lock.previous_modifiers_changed_event
                 {
-                    if prev_modifiers == modifiers {
+                    if prev_modifiers == modifiers && prev_capslock == capslock {
                         return;
                     }
                 }

crates/gpui/src/platform/test/window.rs 🔗

@@ -153,6 +153,10 @@ impl PlatformWindow for TestWindow {
         crate::Modifiers::default()
     }
 
+    fn capslock(&self) -> crate::Capslock {
+        crate::Capslock::default()
+    }
+
     fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
         self.0.lock().input_handler = Some(input_handler);
     }

crates/gpui/src/platform/windows/events.rs 🔗

@@ -1227,6 +1227,7 @@ where
 {
     let virtual_key = VIRTUAL_KEY(wparam.loword());
     let mut modifiers = current_modifiers();
+    let capslock = current_capslock();
 
     match virtual_key {
         VK_SHIFT | VK_CONTROL | VK_MENU | VK_LWIN | VK_RWIN => {
@@ -1239,6 +1240,7 @@ where
             state.last_reported_modifiers = Some(modifiers);
             Some(PlatformInput::ModifiersChanged(ModifiersChangedEvent {
                 modifiers,
+                capslock,
             }))
         }
         vkey => {
@@ -1371,6 +1373,12 @@ pub(crate) fn current_modifiers() -> Modifiers {
     }
 }
 
+#[inline]
+pub(crate) fn current_capslock() -> Capslock {
+    let on = unsafe { GetKeyState(VK_CAPITAL.0 as i32) & 1 } > 0;
+    Capslock { on: on }
+}
+
 fn get_client_area_insets(
     handle: HWND,
     is_maximized: bool,

crates/gpui/src/platform/windows/window.rs 🔗

@@ -559,6 +559,10 @@ impl PlatformWindow for WindowsWindow {
         current_modifiers()
     }
 
+    fn capslock(&self) -> Capslock {
+        current_capslock()
+    }
+
     fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
         self.0.state.borrow_mut().input_handler = Some(input_handler);
     }

crates/gpui/src/window.rs 🔗

@@ -2,15 +2,15 @@
 use crate::Inspector;
 use crate::{
     Action, AnyDrag, AnyElement, AnyImageCache, AnyTooltip, AnyView, App, AppContext, Arena, Asset,
-    AsyncWindowContext, AvailableSpace, Background, BorderStyle, Bounds, BoxShadow, Context,
-    Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener, DispatchNodeId,
-    DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, FontId,
-    Global, GlobalElementId, GlyphId, GpuSpecs, Hsla, InputHandler, IsZero, KeyBinding, KeyContext,
-    KeyDownEvent, KeyEvent, Keystroke, KeystrokeEvent, LayoutId, LineLayoutIndex, Modifiers,
-    ModifiersChangedEvent, MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent,
-    Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler,
-    PlatformWindow, Point, PolychromeSprite, PromptButton, PromptLevel, Quad, Render,
-    RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge,
+    AsyncWindowContext, AvailableSpace, Background, BorderStyle, Bounds, BoxShadow, Capslock,
+    Context, Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener,
+    DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter,
+    FileDropEvent, FontId, Global, GlobalElementId, GlyphId, GpuSpecs, Hsla, InputHandler, IsZero,
+    KeyBinding, KeyContext, KeyDownEvent, KeyEvent, Keystroke, KeystrokeEvent, LayoutId,
+    LineLayoutIndex, Modifiers, ModifiersChangedEvent, MonochromeSprite, MouseButton, MouseEvent,
+    MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
+    PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptButton, PromptLevel, Quad,
+    Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge,
     SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS, ScaledPixels, Scene, Shadow, SharedString, Size,
     StrikethroughStyle, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle,
     TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, WindowAppearance,
@@ -796,6 +796,7 @@ pub struct Window {
     mouse_position: Point<Pixels>,
     mouse_hit_test: HitTest,
     modifiers: Modifiers,
+    capslock: Capslock,
     scale_factor: f32,
     pub(crate) bounds_observers: SubscriberSet<(), AnyObserver>,
     appearance: WindowAppearance,
@@ -907,6 +908,7 @@ impl Window {
         let sprite_atlas = platform_window.sprite_atlas();
         let mouse_position = platform_window.mouse_position();
         let modifiers = platform_window.modifiers();
+        let capslock = platform_window.capslock();
         let content_size = platform_window.content_size();
         let scale_factor = platform_window.scale_factor();
         let appearance = platform_window.appearance();
@@ -1015,6 +1017,7 @@ impl Window {
                     .update(&mut cx, |_, window, cx| {
                         window.active.set(active);
                         window.modifiers = window.platform_window.modifiers();
+                        window.capslock = window.platform_window.capslock();
                         window
                             .activation_observers
                             .clone()
@@ -1100,6 +1103,7 @@ impl Window {
             mouse_position,
             mouse_hit_test: HitTest::default(),
             modifiers,
+            capslock,
             scale_factor,
             bounds_observers: SubscriberSet::new(),
             appearance,
@@ -1728,6 +1732,11 @@ impl Window {
         self.modifiers
     }
 
+    /// The current state of the keyboard's capslock
+    pub fn capslock(&self) -> Capslock {
+        self.capslock
+    }
+
     fn complete_frame(&self) {
         self.platform_window.completed_frame();
     }
@@ -3352,6 +3361,7 @@ impl Window {
             }
             PlatformInput::ModifiersChanged(modifiers_changed) => {
                 self.modifiers = modifiers_changed.modifiers;
+                self.capslock = modifiers_changed.capslock;
                 PlatformInput::ModifiersChanged(modifiers_changed)
             }
             PlatformInput::ScrollWheel(scroll_wheel) => {

crates/recent_projects/src/ssh_connections.rs 🔗

@@ -289,6 +289,9 @@ impl Render for SshPrompt {
                         .child(MarkdownElement::new(prompt.0.clone(), markdown_style))
                         .child(self.editor.clone()),
                 )
+                .when(window.capslock().on, |el| {
+                    el.child(Label::new("⚠️ ⇪ is on"))
+                })
             })
     }
 }