Files
ElyOffice/crates/ely-web-content/src/input.rs
T
ZacharyZhang-NY b7fca0146d Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git),
ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
2026-10-08 21:20:08 -04:00

345 lines
12 KiB
Rust

//! GPUI's mouse, keys, focus and IME, as CEF events.
use std::ops::Range;
use cef::*;
use gpui::{
App, Bounds, Context, Div, Entity, EntityInputHandler, InteractiveElement as _, KeyDownEvent,
KeyUpEvent, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
Point, ScrollDelta, ScrollWheelEvent, UTF16Selection, Window, point, px, size,
};
use crate::{keys, view::WebView};
gpui::actions!(web, [PageTab, PageBackTab]);
/// The page's key context: Tab stays in the page.
const CONTEXT: &str = "WebView";
/// Tab and Shift-Tab go to the page, not to focus.
pub(crate) fn bind(cx: &mut App) {
cx.bind_keys([
gpui::KeyBinding::new("tab", PageTab, Some(CONTEXT)),
gpui::KeyBinding::new("shift-tab", PageBackTab, Some(CONTEXT)),
]);
}
/// CEF's `cef_event_flags_t` for these modifiers.
pub(crate) fn flags(modifiers: &Modifiers) -> u32 {
let mut flags = 0;
for (on, bit) in [
(modifiers.shift, 1 << 1),
(modifiers.control, 1 << 2),
(modifiers.alt, 1 << 3),
(modifiers.platform, 1 << 7),
] {
if on {
flags |= bit;
}
}
flags
}
fn button(button: MouseButton) -> Option<(MouseButtonType, u32)> {
match button {
MouseButton::Left => Some((MouseButtonType::LEFT, 1 << 4)),
MouseButton::Middle => Some((MouseButtonType::MIDDLE, 1 << 5)),
MouseButton::Right => Some((MouseButtonType::RIGHT, 1 << 6)),
_ => None,
}
}
impl WebView {
/// A window point as a page point.
fn at(&self, position: Point<Pixels>, modifiers: u32) -> MouseEvent {
let local = position - self.bounds.origin;
MouseEvent {
x: f32::from(local.x).round() as i32,
y: f32::from(local.y).round() as i32,
modifiers,
}
}
fn mouse_down(&mut self, event: &MouseDownEvent, window: &mut Window, cx: &mut Context<Self>) {
window.focus(&self.focus, cx);
let Some((kind, held)) = button(event.button) else {
return;
};
let at = self.at(event.position, flags(&event.modifiers) | held);
let count = event.click_count as i32;
self.on_host(move |host| host.send_mouse_click_event(Some(&at), kind, false as _, count));
}
fn mouse_up(&mut self, event: &MouseUpEvent, _: &mut Window, _: &mut Context<Self>) {
let Some((kind, _)) = button(event.button) else {
return;
};
let at = self.at(event.position, flags(&event.modifiers));
let count = event.click_count as i32;
self.on_host(move |host| host.send_mouse_click_event(Some(&at), kind, true as _, count));
}
fn mouse_move(&mut self, event: &MouseMoveEvent, _: &mut Window, _: &mut Context<Self>) {
let held = event
.pressed_button
.and_then(button)
.map_or(0, |(_, bit)| bit);
let at = self.at(event.position, flags(&event.modifiers) | held);
self.on_host(move |host| host.send_mouse_move_event(Some(&at), false as _));
}
fn scroll(&mut self, event: &ScrollWheelEvent, _: &mut Window, _: &mut Context<Self>) {
// Lines as Chromium counts them: forty points.
let delta = match event.delta {
ScrollDelta::Pixels(delta) => (f32::from(delta.x), f32::from(delta.y)),
ScrollDelta::Lines(delta) => (delta.x * 40.0, delta.y * 40.0),
};
let at = self.at(event.position, flags(&event.modifiers));
let (x, y) = (delta.0.round() as i32, delta.1.round() as i32);
self.on_host(move |host| host.send_mouse_wheel_event(Some(&at), x, y));
}
fn key_down(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
let stroke = &event.keystroke;
// Edit commands Chromium only takes from its own menu.
let edit = stroke.key.clone();
if keys::edit_chord(&stroke.modifiers) && matches!(edit.as_str(), "c" | "x" | "v" | "a") {
// The owner may take a paste, such as an image.
if edit == "v" && self.paste.clone().is_some_and(|take| take(window, cx)) {
return cx.stop_propagation();
}
self.on_browser(move |browser| {
let Some(frame) = browser.main_frame() else {
return;
};
match edit.as_str() {
"c" => frame.copy(),
"x" => frame.cut(),
"v" => frame.paste(),
_ => frame.select_all(),
}
});
return cx.stop_propagation();
}
let Some(code) = keys::code(&stroke.key) else {
return log::debug!("web: no key code for {}", stroke.key);
};
let event = KeyEvent {
modifiers: flags(&stroke.modifiers),
windows_key_code: code.windows,
native_key_code: code.native,
..Default::default()
};
let chord = stroke.modifiers.control || stroke.modifiers.platform;
match (keys::character(&stroke.key), &stroke.key_char) {
// Typed text arrives by the input handler, which sends it.
(None, Some(_)) if !chord => self.typing = Some(event),
(character, typed) => {
let character = character
.or_else(|| typed.as_deref().and_then(|t| t.encode_utf16().next()))
.unwrap_or(0);
self.press(event, character);
}
}
}
/// Tab, with Shift when `back`, down and up.
fn tab(&mut self, back: bool) {
let code = keys::code("tab").expect("tab has a key code");
let event = KeyEvent {
modifiers: if back { 1 << 1 } else { 0 },
windows_key_code: code.windows,
native_key_code: code.native,
..Default::default()
};
self.press(event.clone(), 0x09);
let up = KeyEvent {
type_: KeyEventType::KEYUP,
character: 0x09,
unmodified_character: 0x09,
..event
};
self.on_host(move |host| host.send_key_event(Some(&up)));
}
/// A key down and its character, as cefclient sends them.
fn press(&self, event: KeyEvent, character: u16) {
let mut event = KeyEvent {
type_: KeyEventType::KEYDOWN,
character,
unmodified_character: character,
..event
};
self.on_host(move |host| {
host.send_key_event(Some(&event));
event.type_ = KeyEventType::CHAR;
host.send_key_event(Some(&event));
});
}
fn key_up(&mut self, event: &KeyUpEvent, _: &mut Window, _: &mut Context<Self>) {
let stroke = &event.keystroke;
let Some(code) = keys::code(&stroke.key) else {
return;
};
// An up without its character reads as another down.
let character = keys::character(&stroke.key)
.or_else(|| {
stroke
.key_char
.as_deref()
.and_then(|t| t.encode_utf16().next())
})
.or_else(|| stroke.key.encode_utf16().next())
.unwrap_or(0);
let up = KeyEvent {
type_: KeyEventType::KEYUP,
modifiers: flags(&stroke.modifiers),
windows_key_code: code.windows,
native_key_code: code.native,
character,
unmodified_character: character,
..Default::default()
};
self.on_host(move |host| host.send_key_event(Some(&up)));
}
}
/// The view's listeners on its outer element.
pub(crate) fn listen(element: Div, cx: &mut Context<WebView>) -> Div {
element
.key_context(CONTEXT)
.on_action(cx.listener(|view, _: &PageTab, _, _| view.tab(false)))
.on_action(cx.listener(|view, _: &PageBackTab, _, _| view.tab(true)))
.on_mouse_down(MouseButton::Left, cx.listener(WebView::mouse_down))
.on_mouse_down(MouseButton::Right, cx.listener(WebView::mouse_down))
.on_mouse_down(MouseButton::Middle, cx.listener(WebView::mouse_down))
.on_mouse_up(MouseButton::Left, cx.listener(WebView::mouse_up))
.on_mouse_up(MouseButton::Right, cx.listener(WebView::mouse_up))
.on_mouse_up(MouseButton::Middle, cx.listener(WebView::mouse_up))
.on_mouse_move(cx.listener(WebView::mouse_move))
.on_scroll_wheel(cx.listener(WebView::scroll))
.on_key_down(cx.listener(WebView::key_down))
.on_key_up(cx.listener(WebView::key_up))
}
/// CEF's page focus follows the view's.
pub(crate) fn follow_focus(view: &Entity<WebView>, window: &mut Window, cx: &mut App) {
view.update(cx, |this, cx| {
let focus = this.focus.clone();
cx.on_focus(&focus, window, |view, _, _| {
view.on_host(|host| host.set_focus(true as _));
})
.detach();
cx.on_blur(&focus, window, |view, _, _| {
view.on_host(|host| host.set_focus(false as _));
})
.detach();
});
}
/// CEF's "no range": both ends at the maximum.
const NO_RANGE: cef::Range = cef::Range {
from: u32::MAX,
to: u32::MAX,
};
fn utf16(text: &str) -> Vec<u16> {
text.encode_utf16().collect()
}
impl EntityInputHandler for WebView {
/// The IME sees none of the page's text.
fn text_for_range(
&mut self,
_: Range<usize>,
_: &mut Option<Range<usize>>,
_: &mut Window,
_: &mut Context<Self>,
) -> Option<String> {
None
}
fn selected_text_range(
&mut self,
_: bool,
_: &mut Window,
_: &mut Context<Self>,
) -> Option<UTF16Selection> {
None
}
fn marked_text_range(&self, _: &mut Window, _: &mut Context<Self>) -> Option<Range<usize>> {
self.composing.clone()
}
fn unmark_text(&mut self, _: &mut Window, _: &mut Context<Self>) {
if self.composing.take().is_some() {
self.on_host(|host| host.ime_finish_composing_text(false as _));
}
}
fn replace_text_in_range(
&mut self,
_: Option<Range<usize>>,
text: &str,
_: &mut Window,
_: &mut Context<Self>,
) {
let typed = self.typing.take();
let was = self.composing.take();
let units = utf16(text);
// One character, no composition: a key press, as cefclient.
if let (Some(event), None, [character]) = (typed, was, units.as_slice()) {
return self.press(event, *character);
}
let text = CefString::from(text);
self.on_host(move |host| host.ime_commit_text(Some(&text), Some(&NO_RANGE), 0));
}
fn replace_and_mark_text_in_range(
&mut self,
_: Option<Range<usize>>,
new_text: &str,
new_selected_range: Option<Range<usize>>,
_: &mut Window,
_: &mut Context<Self>,
) {
let length = utf16(new_text).len();
self.composing = (length > 0).then_some(0..length);
let selection = new_selected_range.unwrap_or(length..length);
let range = cef::Range {
from: selection.start as _,
to: selection.end as _,
};
let text = CefString::from(new_text);
self.on_host(move |host| {
host.ime_set_composition(Some(&text), None, Some(&NO_RANGE), Some(&range))
});
}
fn bounds_for_range(
&mut self,
range: Range<usize>,
element: Bounds<Pixels>,
_: &mut Window,
_: &mut Context<Self>,
) -> Option<Bounds<Pixels>> {
let surface = self.surface.lock().expect("the surface lock");
let (x, y, w, h) = *surface.ime.get(range.start).or(surface.ime.last())?;
Some(Bounds::new(
element.origin + point(px(x), px(y)),
size(px(w), px(h)),
))
}
fn character_index_for_point(
&mut self,
_: Point<Pixels>,
_: &mut Window,
_: &mut Context<Self>,
) -> Option<usize> {
None
}
}