#[cfg(any(gpui_mobile, test))]
#[cfg_attr(not(gpui_mobile), allow(dead_code))]
pub(crate) mod mobile;
use gpui::{
Capslock, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection,
PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase, WindowAppearance, point, px,
};
use tauri_runtime_wry::tao::{
dpi::PhysicalPosition,
event::{
ElementState, KeyEvent, MouseButton as TaoMouseButton, MouseScrollDelta,
TouchPhase as TaoTouchPhase, WindowEvent,
},
keyboard::{Key, ModifiersState},
window::Theme,
};
use crate::platform::window::WindowInner;
use gpui::{DevicePixels, size};
pub(crate) fn dispatch(inner: &WindowInner, event: &WindowEvent<'_>) -> bool {
match event {
WindowEvent::Resized(physical) => {
inner.window_mode_changed();
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
if inner.wayland.is_some() {
return false;
}
let scale = inner.state.borrow().scale_factor;
inner.resized(physical_size(physical.width, physical.height), scale);
}
WindowEvent::ScaleFactorChanged {
scale_factor,
new_inner_size,
} => {
inner.window_mode_changed();
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
if let Some(content) = &inner.wayland {
content.rescale(inner, *scale_factor);
return false;
}
inner.resized(
physical_size(new_inner_size.width, new_inner_size.height),
*scale_factor as f32,
);
}
WindowEvent::Moved(position) => {
inner.window_moved();
{
let mut state = inner.state.borrow_mut();
let scale = state.scale_factor;
state.origin = point(px(position.x as f32 / scale), px(position.y as f32 / scale));
}
inner.call_unit(|c| &mut c.moved);
}
WindowEvent::Focused(focused) => {
inner.window_mode_changed();
inner.state.borrow_mut().active = *focused;
if !focused {
inner.state.borrow_mut().typed_text.reset();
}
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
if let Some(ime) = &inner.ime {
ime.set_active(*focused);
}
inner.call_bool(|c| &mut c.active_status_change, *focused);
#[cfg(not(gpui_mobile))]
inner.set_a11y_focused(*focused);
}
WindowEvent::CursorEntered { .. } => {
inner.state.borrow_mut().hovered = true;
inner.call_bool(|c| &mut c.hover_status_change, true);
}
WindowEvent::CursorLeft { .. } => {
let (position, pressed_button, modifiers) = {
let mut state = inner.state.borrow_mut();
state.hovered = false;
(state.mouse_position, state.pressed_button, state.modifiers)
};
inner.handle_input(PlatformInput::MouseExited(MouseExitEvent {
position,
pressed_button,
modifiers,
}));
inner.call_bool(|c| &mut c.hover_status_change, false);
}
WindowEvent::CursorMoved { position, .. } => {
let gdk = gdk_pointer_position(inner);
let (position, pressed_button, modifiers) = {
let mut state = inner.state.borrow_mut();
state.mouse_position = gdk.unwrap_or_else(|| {
logical_position(*position, state.scale_factor) - inner.content_origin()
});
(state.mouse_position, state.pressed_button, state.modifiers)
};
inner.handle_input(PlatformInput::MouseMove(MouseMoveEvent {
position,
pressed_button,
modifiers,
}));
#[cfg(not(gpui_mobile))]
inner.pointer_moved(pressed_button);
}
WindowEvent::MouseInput { state, button, .. } => {
let Some(button) = mouse_button(*button, Os::CURRENT) else {
return false;
};
let (position, modifiers) = {
let window_state = inner.state.borrow();
(window_state.mouse_position, window_state.modifiers)
};
match state {
ElementState::Pressed => {
inner.state.borrow_mut().pressed_button = Some(button);
let click_count = inner.register_click(button, position);
let handled = inner.handle_input(PlatformInput::MouseDown(MouseDownEvent {
button,
position,
modifiers,
click_count,
first_mouse: false,
}));
#[cfg(not(gpui_mobile))]
if button == MouseButton::Left && !handled {
inner.window_control_pressed(click_count);
}
#[cfg(gpui_mobile)]
let _ = handled;
}
ElementState::Released => {
let click_count = {
let mut window_state = inner.state.borrow_mut();
window_state.pressed_button = None;
window_state.click.count.max(1)
};
let handled = inner.handle_input(PlatformInput::MouseUp(MouseUpEvent {
button,
position,
modifiers,
click_count,
}));
#[cfg(not(gpui_mobile))]
if button == MouseButton::Left && !handled {
inner.window_control_released();
}
#[cfg(gpui_mobile)]
let _ = handled;
}
_ => {}
}
}
WindowEvent::MouseWheel { delta, phase, .. } => {
let (position, modifiers, scale) = {
let state = inner.state.borrow();
(state.mouse_position, state.modifiers, state.scale_factor)
};
let delta = match delta {
MouseScrollDelta::LineDelta(x, y) => ScrollDelta::Lines(point(*x, *y)),
MouseScrollDelta::PixelDelta(p) => {
ScrollDelta::Pixels(point(px(p.x as f32 / scale), px(p.y as f32 / scale)))
}
_ => return false,
};
inner.handle_input(PlatformInput::ScrollWheel(ScrollWheelEvent {
position,
delta,
modifiers,
touch_phase: touch_phase(*phase),
}));
}
WindowEvent::ModifiersChanged(state) => {
let modifiers = modifiers(*state);
let capslock = {
let mut window_state = inner.state.borrow_mut();
window_state.modifiers = modifiers;
window_state.capslock
};
inner.handle_input(PlatformInput::ModifiersChanged(ModifiersChangedEvent {
modifiers,
capslock,
}));
}
WindowEvent::KeyboardInput { event, .. } => {
let modifiers = inner.state.borrow().modifiers;
if matches!(event.logical_key, Key::CapsLock) && event.state == ElementState::Pressed {
let mut state = inner.state.borrow_mut();
state.capslock = Capslock {
on: !state.capslock.on,
};
}
let Some(keystroke) = keystroke(event, modifiers) else {
return false;
};
match event.state {
ElementState::Pressed => {
inner.handle_input(PlatformInput::KeyDown(KeyDownEvent {
keystroke,
is_held: event.repeat,
prefer_character_input: false,
}));
}
ElementState::Released => {
inner.handle_input(PlatformInput::KeyUp(KeyUpEvent { keystroke }));
}
_ => {}
}
}
WindowEvent::ReceivedImeText(text) => inner.handle_ime_commit(text),
#[cfg(not(gpui_mobile))]
WindowEvent::HoveredFile(path) => {
inner.file_drop.borrow_mut().hovered(path.clone());
inner.waker.wake();
}
#[cfg(not(gpui_mobile))]
WindowEvent::DroppedFile(path) => {
inner.file_drop.borrow_mut().dropped(path.clone());
inner.waker.wake();
}
#[cfg(not(gpui_mobile))]
WindowEvent::HoveredFileCancelled => {
inner.file_drop.borrow_mut().cancelled();
inner.waker.wake();
}
WindowEvent::ThemeChanged(theme) => {
inner.state.borrow_mut().appearance = appearance(*theme);
inner.call_unit(|c| &mut c.appearance_changed);
}
WindowEvent::Destroyed => return true,
_ => {}
}
false
}
pub(crate) fn physical_size(width: u32, height: u32) -> gpui::Size<DevicePixels> {
size(DevicePixels(width as i32), DevicePixels(height as i32))
}
pub(crate) fn appearance(theme: Theme) -> WindowAppearance {
match theme {
Theme::Dark => WindowAppearance::Dark,
_ => WindowAppearance::Light,
}
}
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
fn gdk_pointer_position(inner: &WindowInner) -> Option<gpui::Point<gpui::Pixels>> {
use gtk::prelude::*;
let window = inner.tauri_window.gtk_window().ok()?.window()?;
let pointer = window.display().default_seat()?.pointer()?;
let (_, x, y, _) = window.device_position_double(&pointer);
Some(point(px(x as f32), px(y as f32)) - inner.toplevel_content_origin())
}
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
fn gdk_pointer_position(_inner: &WindowInner) -> Option<gpui::Point<gpui::Pixels>> {
None
}
fn logical_position(position: PhysicalPosition<f64>, scale: f32) -> gpui::Point<gpui::Pixels> {
point(px(position.x as f32 / scale), px(position.y as f32 / scale))
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum Os {
Linux,
Windows,
Mac,
}
impl Os {
pub(crate) const CURRENT: Os = if cfg!(target_os = "windows") {
Os::Windows
} else if cfg!(target_os = "macos") {
Os::Mac
} else {
Os::Linux
};
}
fn mouse_button(button: TaoMouseButton, os: Os) -> Option<MouseButton> {
let (back, forward) = match os {
Os::Windows => (1, 2),
_ => (8, 9),
};
Some(match button {
TaoMouseButton::Left => MouseButton::Left,
TaoMouseButton::Right => MouseButton::Right,
TaoMouseButton::Middle => MouseButton::Middle,
TaoMouseButton::Other(n) if n == back => MouseButton::Navigate(NavigationDirection::Back),
TaoMouseButton::Other(n) if n == forward => {
MouseButton::Navigate(NavigationDirection::Forward)
}
_ => return None,
})
}
fn produces_text(modifiers: &Modifiers, os: Os) -> bool {
if modifiers.platform {
return false;
}
match os {
Os::Mac => !modifiers.control,
Os::Windows => modifiers.control == modifiers.alt,
Os::Linux => !modifiers.control && !modifiers.alt,
}
}
fn touch_phase(phase: TaoTouchPhase) -> TouchPhase {
match phase {
TaoTouchPhase::Started => TouchPhase::Started,
TaoTouchPhase::Moved => TouchPhase::Moved,
_ => TouchPhase::Ended,
}
}
fn modifiers(state: ModifiersState) -> Modifiers {
Modifiers {
control: state.control_key(),
alt: state.alt_key(),
shift: state.shift_key(),
platform: state.super_key(),
function: false,
}
}
fn typed_char(character: Option<&str>, key: &str, modifiers: &Modifiers, os: Os) -> Option<String> {
let character = character.filter(|_| produces_text(modifiers, os))?;
if os == Os::Windows && modifiers.control && character.to_lowercase() == key {
return None;
}
Some(character.to_owned())
}
fn keystroke(event: &KeyEvent, modifiers: Modifiers) -> Option<Keystroke> {
let key = match &event.logical_key {
Key::Character(_) | Key::Dead(_) | Key::Unidentified(_) => {
match event.key_without_modifiers() {
Key::Character(c) => c.to_lowercase(),
_ => return None,
}
}
named => named_key(named)?.to_string(),
};
let character = match &event.logical_key {
Key::Space => Some(" "),
Key::Character(c) => Some(&**c),
_ => None,
};
let key_char = typed_char(character, &key, &modifiers, Os::CURRENT);
Some(Keystroke {
modifiers,
key,
key_char,
})
}
fn named_key(key: &Key<'_>) -> Option<&'static str> {
Some(match key {
Key::Enter => "enter",
Key::Tab => "tab",
Key::Space => "space",
Key::Backspace => "backspace",
Key::Delete => "delete",
Key::Escape => "escape",
Key::Insert => "insert",
Key::Home => "home",
Key::End => "end",
Key::PageUp => "pageup",
Key::PageDown => "pagedown",
Key::ArrowUp => "up",
Key::ArrowDown => "down",
Key::ArrowLeft => "left",
Key::ArrowRight => "right",
Key::ContextMenu => "menu",
Key::F1 => "f1",
Key::F2 => "f2",
Key::F3 => "f3",
Key::F4 => "f4",
Key::F5 => "f5",
Key::F6 => "f6",
Key::F7 => "f7",
Key::F8 => "f8",
Key::F9 => "f9",
Key::F10 => "f10",
Key::F11 => "f11",
Key::F12 => "f12",
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn named_keys_use_gpui_names() {
assert_eq!(named_key(&Key::Enter), Some("enter"));
assert_eq!(named_key(&Key::ArrowLeft), Some("left"));
assert_eq!(named_key(&Key::PageDown), Some("pagedown"));
assert_eq!(named_key(&Key::F12), Some("f12"));
assert_eq!(named_key(&Key::Shift), None);
}
#[test]
fn super_maps_to_platform_modifier() {
let m = modifiers(ModifiersState::SUPER | ModifiersState::SHIFT);
assert!(m.platform && m.shift && !m.control && !m.alt);
}
#[test]
fn navigation_buttons() {
let back = Some(MouseButton::Navigate(NavigationDirection::Back));
let forward = Some(MouseButton::Navigate(NavigationDirection::Forward));
assert_eq!(mouse_button(TaoMouseButton::Other(8), Os::Linux), back);
assert_eq!(mouse_button(TaoMouseButton::Other(9), Os::Linux), forward);
assert_eq!(mouse_button(TaoMouseButton::Other(1), Os::Windows), back);
assert_eq!(mouse_button(TaoMouseButton::Other(2), Os::Windows), forward);
assert_eq!(
mouse_button(TaoMouseButton::Right, Os::Linux),
Some(MouseButton::Right)
);
}
#[test]
fn unknown_buttons_are_dropped_not_left_clicks() {
assert_eq!(mouse_button(TaoMouseButton::Other(1), Os::Linux), None);
assert_eq!(mouse_button(TaoMouseButton::Other(8), Os::Windows), None);
assert_eq!(mouse_button(TaoMouseButton::Other(12), Os::Mac), None);
}
fn mods(control: bool, alt: bool, shift: bool, platform: bool) -> Modifiers {
Modifiers {
control,
alt,
shift,
platform,
function: false,
}
}
#[test]
fn plain_and_shifted_keys_type_text_everywhere() {
for os in [Os::Linux, Os::Windows, Os::Mac] {
assert!(produces_text(&mods(false, false, false, false), os));
assert!(produces_text(&mods(false, false, true, false), os));
}
}
#[test]
fn option_types_text_on_macos() {
assert!(produces_text(&mods(false, true, false, false), Os::Mac));
assert!(produces_text(&mods(false, true, true, false), Os::Mac));
assert!(!produces_text(&mods(true, false, false, false), Os::Mac));
assert!(!produces_text(&mods(false, false, false, true), Os::Mac));
}
#[test]
fn altgr_types_text_on_windows() {
assert!(produces_text(&mods(true, true, false, false), Os::Windows));
assert!(!produces_text(
&mods(true, false, false, false),
Os::Windows
));
assert!(!produces_text(
&mods(false, true, false, false),
Os::Windows
));
assert!(!produces_text(&mods(true, true, false, true), Os::Windows));
}
#[test]
fn windows_ctrl_alt_types_only_altgr_characters() {
let ctrl_alt = mods(true, true, false, false);
assert_eq!(
typed_char(Some("@"), "q", &ctrl_alt, Os::Windows).as_deref(),
Some("@")
);
assert_eq!(typed_char(Some("a"), "a", &ctrl_alt, Os::Windows), None);
assert_eq!(typed_char(Some("A"), "a", &ctrl_alt, Os::Windows), None);
let plain = mods(false, false, false, false);
assert_eq!(
typed_char(Some("a"), "a", &plain, Os::Windows).as_deref(),
Some("a")
);
assert_eq!(typed_char(None, "enter", &plain, Os::Windows), None);
}
#[test]
fn linux_shortcuts_type_no_text() {
assert!(!produces_text(&mods(true, false, false, false), Os::Linux));
assert!(!produces_text(&mods(false, true, false, false), Os::Linux));
assert!(!produces_text(&mods(true, true, false, false), Os::Linux));
assert!(!produces_text(&mods(false, false, false, true), Os::Linux));
}
}