use crate::common::WebviewSource;
use crate::focus::FocusedWebview;
use bevy::input::keyboard::KeyboardInput;
use bevy::prelude::*;
#[cfg(not(target_os = "windows"))]
use bevy_cef_core::prelude::Browsers;
#[cfg(target_os = "windows")]
use bevy_cef_core::prelude::BrowsersProxy;
use bevy_cef_core::prelude::{EditCommand, create_cef_key_events, keyboard_modifiers};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default, Debug)]
pub struct ModifiersState {
pub alt: bool,
pub ctrl: bool,
pub shift: bool,
pub logo: bool,
}
#[derive(Resource, Default)]
pub struct CefKeyboardFilter {
suppressed: HashSet<(Entity, KeyCode, ModifiersState)>,
}
impl CefKeyboardFilter {
pub fn set(&mut self, entries: impl IntoIterator<Item = (Entity, KeyCode, ModifiersState)>) {
self.suppressed = entries.into_iter().collect();
}
pub fn contains(&self, webview: Entity, code: KeyCode, mods: ModifiersState) -> bool {
self.suppressed.contains(&(webview, code, mods))
}
}
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub struct KeyboardDeliverSet;
pub(super) struct KeyboardPlugin;
impl Plugin for KeyboardPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<IsImeCommiting>()
.init_resource::<IsImeComposing>()
.init_resource::<CefKeyboardFilter>();
#[cfg(not(target_os = "windows"))]
app.add_systems(
Update,
(
activate_ime,
ime_event.run_if(on_message::<Ime>),
send_key_event.run_if(on_message::<KeyboardInput>),
)
.chain()
.in_set(KeyboardDeliverSet),
);
#[cfg(target_os = "windows")]
app.add_systems(
Update,
(
activate_ime,
ime_event_win.run_if(on_message::<Ime>),
send_key_event_win.run_if(on_message::<KeyboardInput>),
)
.chain()
.in_set(KeyboardDeliverSet),
);
}
}
fn activate_ime(mut windows: Query<&mut Window>, mut state: Local<ImeActivationState>) {
match *state {
ImeActivationState::Pending => {
for mut window in windows.iter_mut() {
if window.ime_enabled {
window.ime_enabled = false;
*state = ImeActivationState::Toggled;
}
}
}
ImeActivationState::Toggled => {
for mut window in windows.iter_mut() {
if !window.ime_enabled {
window.ime_enabled = true;
*state = ImeActivationState::Done;
}
}
}
ImeActivationState::Done => {}
}
}
#[derive(Default)]
enum ImeActivationState {
#[default]
Pending,
Toggled,
Done,
}
#[derive(Resource, Default, Serialize, Deserialize, Reflect)]
#[reflect(Default, Serialize, Deserialize)]
struct IsImeCommiting(bool);
#[derive(Resource, Default, Serialize, Deserialize, Reflect)]
#[reflect(Default, Serialize, Deserialize)]
struct IsImeComposing(bool);
#[cfg(not(target_os = "windows"))]
#[allow(clippy::too_many_arguments)]
fn send_key_event(
mut er: MessageReader<KeyboardInput>,
mut is_ime_commiting: ResMut<IsImeCommiting>,
mut is_ime_composing: ResMut<IsImeComposing>,
input: Res<ButtonInput<KeyCode>>,
browsers: NonSend<Browsers>,
focused: Res<FocusedWebview>,
filter: Res<CefKeyboardFilter>,
webviews: Query<Entity, With<WebviewSource>>,
) {
let modifiers = keyboard_modifiers(&input);
let target = focused.0.filter(|e| webviews.get(*e).is_ok());
for event in er.read() {
if (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Backspace)
&& is_ime_commiting.0
{
is_ime_commiting.0 = false;
continue;
}
if event.key_code == KeyCode::Backspace && is_ime_composing.0 {
is_ime_composing.0 = false;
continue;
}
let Some(webview) = target else {
continue;
};
let ms = ModifiersState {
alt: input.pressed(KeyCode::AltLeft) || input.pressed(KeyCode::AltRight),
ctrl: input.pressed(KeyCode::ControlLeft) || input.pressed(KeyCode::ControlRight),
shift: input.pressed(KeyCode::ShiftLeft) || input.pressed(KeyCode::ShiftRight),
logo: input.pressed(KeyCode::SuperLeft) || input.pressed(KeyCode::SuperRight),
};
if filter.contains(webview, event.key_code, ms) {
continue;
}
for key_event in create_cef_key_events(modifiers, event) {
browsers.send_key(&webview, key_event);
}
#[cfg(target_os = "macos")]
if event.state == bevy::input::ButtonState::Pressed
&& !event.repeat
&& !is_ime_composing.0
&& let Some(cmd) = edit_command_for(event.key_code, ms)
{
browsers.exec_edit_command(&webview, cmd);
}
}
}
#[cfg(not(target_os = "windows"))]
fn ime_event(
mut er: MessageReader<Ime>,
mut is_ime_commiting: ResMut<IsImeCommiting>,
mut is_ime_composing: ResMut<IsImeComposing>,
browsers: NonSend<Browsers>,
focused: Res<FocusedWebview>,
webviews: Query<Entity, With<WebviewSource>>,
) {
let has_target = focused.0.filter(|e| webviews.get(*e).is_ok()).is_some();
if !has_target {
if is_ime_composing.0 {
browsers.ime_cancel_composition();
}
is_ime_composing.0 = false;
is_ime_commiting.0 = false;
}
for event in er.read() {
if !has_target {
continue;
}
match event {
Ime::Preedit { value, cursor, .. } => {
if value.is_empty() {
browsers.ime_cancel_composition();
} else {
browsers.set_ime_composition(value, cursor.map(|(_, e)| e as u32));
is_ime_composing.0 = true;
}
}
Ime::Commit { value, .. } => {
browsers.set_ime_commit_text(value);
is_ime_commiting.0 = true;
is_ime_composing.0 = false;
}
Ime::Disabled { .. } => {
browsers.ime_cancel_composition();
is_ime_composing.0 = false;
}
_ => {}
}
}
}
#[cfg(target_os = "windows")]
#[allow(clippy::too_many_arguments)]
fn send_key_event_win(
mut er: MessageReader<KeyboardInput>,
mut is_ime_commiting: ResMut<IsImeCommiting>,
mut is_ime_composing: ResMut<IsImeComposing>,
input: Res<ButtonInput<KeyCode>>,
proxy: Res<BrowsersProxy>,
focused: Res<FocusedWebview>,
filter: Res<CefKeyboardFilter>,
webviews: Query<Entity, With<WebviewSource>>,
) {
let modifiers = keyboard_modifiers(&input);
let target = focused.0.filter(|e| webviews.get(*e).is_ok());
for event in er.read() {
if (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Backspace)
&& is_ime_commiting.0
{
is_ime_commiting.0 = false;
continue;
}
if event.key_code == KeyCode::Backspace && is_ime_composing.0 {
is_ime_composing.0 = false;
continue;
}
let Some(webview) = target else {
continue;
};
let ms = ModifiersState {
alt: input.pressed(KeyCode::AltLeft) || input.pressed(KeyCode::AltRight),
ctrl: input.pressed(KeyCode::ControlLeft) || input.pressed(KeyCode::ControlRight),
shift: input.pressed(KeyCode::ShiftLeft) || input.pressed(KeyCode::ShiftRight),
logo: input.pressed(KeyCode::SuperLeft) || input.pressed(KeyCode::SuperRight),
};
if filter.contains(webview, event.key_code, ms) {
continue;
}
for key_event in create_cef_key_events(modifiers, event) {
proxy.send_key(&webview, key_event);
}
}
}
#[cfg(target_os = "windows")]
fn ime_event_win(
mut er: MessageReader<Ime>,
mut is_ime_commiting: ResMut<IsImeCommiting>,
mut is_ime_composing: ResMut<IsImeComposing>,
proxy: Res<BrowsersProxy>,
focused: Res<FocusedWebview>,
webviews: Query<Entity, With<WebviewSource>>,
) {
let has_target = focused.0.filter(|e| webviews.get(*e).is_ok()).is_some();
if !has_target {
if is_ime_composing.0 {
proxy.ime_cancel_composition();
}
is_ime_composing.0 = false;
is_ime_commiting.0 = false;
}
for event in er.read() {
if !has_target {
continue;
}
match event {
Ime::Preedit { value, cursor, .. } => {
if value.is_empty() {
proxy.ime_cancel_composition();
} else {
proxy.set_ime_composition(value, cursor.map(|(_, e)| e as u32));
is_ime_composing.0 = true;
}
}
Ime::Commit { value, .. } => {
proxy.set_ime_commit_text(value);
is_ime_commiting.0 = true;
is_ime_composing.0 = false;
}
Ime::Disabled { .. } => {
proxy.ime_cancel_composition();
is_ime_composing.0 = false;
}
_ => {}
}
}
}
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
fn edit_command_for(code: KeyCode, ms: ModifiersState) -> Option<EditCommand> {
if !ms.logo || ms.ctrl || ms.alt {
return None;
}
Some(match (code, ms.shift) {
(KeyCode::KeyC, false) => EditCommand::Copy,
(KeyCode::KeyX, false) => EditCommand::Cut,
(KeyCode::KeyV, false) => EditCommand::Paste,
(KeyCode::KeyA, false) => EditCommand::SelectAll,
(KeyCode::KeyZ, false) => EditCommand::Undo,
(KeyCode::KeyZ, true) => EditCommand::Redo,
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filter_contains_matches_only_exact_triple() {
let mut f = CefKeyboardFilter::default();
let e = Entity::from_raw_u32(7).unwrap();
let alt = ModifiersState {
alt: true,
..Default::default()
};
f.set([(e, KeyCode::KeyH, alt)]);
assert!(f.contains(e, KeyCode::KeyH, alt));
assert!(!f.contains(e, KeyCode::KeyH, ModifiersState::default()));
assert!(!f.contains(Entity::from_raw_u32(8).unwrap(), KeyCode::KeyH, alt));
}
fn cmd(shift: bool, ctrl: bool, alt: bool) -> ModifiersState {
ModifiersState {
logo: true,
shift,
ctrl,
alt,
}
}
#[test]
fn edit_command_for_maps_plain_cmd_shortcuts() {
let m = cmd(false, false, false);
assert_eq!(edit_command_for(KeyCode::KeyC, m), Some(EditCommand::Copy));
assert_eq!(edit_command_for(KeyCode::KeyX, m), Some(EditCommand::Cut));
assert_eq!(edit_command_for(KeyCode::KeyV, m), Some(EditCommand::Paste));
assert_eq!(
edit_command_for(KeyCode::KeyA, m),
Some(EditCommand::SelectAll)
);
assert_eq!(edit_command_for(KeyCode::KeyZ, m), Some(EditCommand::Undo));
}
#[test]
fn edit_command_for_shift_cmd_z_is_redo() {
assert_eq!(
edit_command_for(KeyCode::KeyZ, cmd(true, false, false)),
Some(EditCommand::Redo)
);
}
#[test]
fn edit_command_for_rejects_extra_modifiers_and_bare_keys() {
assert_eq!(
edit_command_for(KeyCode::KeyC, cmd(false, true, false)),
None
);
assert_eq!(
edit_command_for(KeyCode::KeyC, cmd(false, false, true)),
None
);
assert_eq!(
edit_command_for(KeyCode::KeyC, cmd(true, false, false)),
None
);
assert_eq!(
edit_command_for(KeyCode::KeyC, ModifiersState::default()),
None
);
assert_eq!(
edit_command_for(KeyCode::KeyB, cmd(false, false, false)),
None
);
}
}