Files
wordcraft/crates/ui-egui/src/app/input.rs
T

162 lines
8.9 KiB
Rust

#[cfg(not(target_arch = "wasm32"))]
use super::i18n;
use super::{WordApp, json};
impl WordApp {
/// Read a Control-click as the right-click on macOS, then offer at most one synthetic gesture
/// step to the host. eframe carries an unconsumed RawInput into the next pass and into hidden
/// logic ticks, so offering it again would duplicate input. The step stays queued until a
/// final (not discarded) UI pass acknowledges it, and a press/release pair stays intact.
pub fn raw_input_hook(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) {
// Capture the surface from the raw input itself. `InputState::viewport_rect` and the
// last painted screen rect are stale in a hidden `run_logic` tick; RawInput is the host's
// current answer, including minimized/occluded and a Wayland `inner_rect: None`.
if raw.viewport_id == egui::ViewportId::ROOT
&& let Some(geometry) = crate::control::SurfaceGeometry::from_raw(raw, ctx)
{
// A hidden resize and change back still invalidate an older capture, even if
// neither intermediate geometry reaches a drawing or logic pass.
if self.surface_geometry != Some(geometry) {
self.capture_epoch = self.capture_epoch.wrapping_add(1);
}
self.surface_geometry = Some(geometry);
}
if cfg!(target_os = "macos") {
control_click_as_secondary(&mut raw.events, &mut self.control_click);
}
if self.synthetic_pending || self.synthetic.is_empty() || self.fonts_frames < 2 || !self.control_input_is_roots(ctx) {
return;
}
let Some((events, n)) = self.peek_synthetic() else { return };
if events.iter().any(|e| matches!(e, egui::Event::Key { .. } | egui::Event::Text(_))) {
// Control input belongs to this window's remembered widget even while another app
// is active. Give only this input frame egui focus, without requesting OS activation.
raw.focused = true;
if let (Some(ctx), Some((id, _))) = (&self.ctx, self.canvas.widget_id) {
// Canvas keys run before painting updates last frame's native focus flag.
self.canvas.focused = ctx.memory(|m| m.has_focus(id));
}
}
raw.events.extend(events);
self.synthetic_pending = true;
self.synthetic_injected = n;
}
/// The control channel's next input frame, taken off the queue.
pub(crate) fn next_synthetic(&mut self) -> Option<Vec<egui::Event>> {
let (events, n) = self.peek_synthetic()?;
self.synthetic.drain(..n);
Some(events)
}
/// The control channel's next input frame: one pointer event (after a move to it), or keys
/// and text up to the next key release; with how many queued events it takes.
fn peek_synthetic(&self) -> Option<(Vec<egui::Event>, usize)> {
if self.synthetic.is_empty() {
return None;
}
let n = match self.synthetic.first() {
Some(egui::Event::PointerMoved(_) | egui::Event::PointerButton { .. }) => 1,
_ => self.synthetic.iter().position(|e| matches!(e, egui::Event::Key { pressed: false, .. })).map_or(self.synthetic.len(), |i| i + 1),
};
let mut events = Vec::new();
if let Some(egui::Event::PointerMoved(p) | egui::Event::PointerButton { pos: p, .. }) = self.synthetic.first() {
events.push(egui::Event::PointerMoved(*p));
}
let n = n.min(self.synthetic.len());
events.extend(self.synthetic.iter().take(n).cloned());
Some((events, n))
}
/// Raw input read in the known UI phase, first pass only: dropped files and the SmartArt Text
/// Pane's Tab/Shift+Tab, before the font warm-up return. `logic` also runs hidden, where its
/// input events are from the previous shown frame.
pub(super) fn handle_ui_input_events(&mut self, ctx: &egui::Context) {
// eframe does not preserve dropped files for a later frame, so a drop in the first two
// passes would otherwise disappear.
#[cfg(not(target_arch = "wasm32"))]
if ctx.current_pass_index() == 0 {
let dropped = ctx.input_mut(|i| std::mem::take(&mut i.raw.dropped_files));
if !dropped.is_empty() {
self.open_dropped_files(dropped);
let lang = i18n::Lang::from_pref(&self.ui.language);
self.install_ui_fonts(ctx, lang);
}
}
// SmartArt Text Pane (#2701): Tab demotes and Shift+Tab promote the focused item, as
// Word's pane does. This is an input action, so it runs in the first UI pass only; hidden
// `run_logic` ticks still see the previous shown frame's keys.
if ctx.current_pass_index() == 0 && self.session.view.smart_art_pane && self.smart_art_row_focused && self.smart_art_item.is_some() {
let shift_tab = ctx.input_mut(|i| i.consume_key(egui::Modifiers::SHIFT, egui::Key::Tab));
let tab = ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Tab));
if shift_tab || tab {
ctx.memory_mut(|m| m.move_focus(egui::FocusDirection::None));
let _ = self.run(if shift_tab { "smartArt.promote" } else { "smartArt.demote" }, json!({"index": self.smart_art_item.unwrap_or(0)}));
}
}
}
/// A click on the window's chrome (ribbon tabs and empty ribbon space, galleries, check
/// boxes, the status bar, panes) hands keyboard focus back to the document, as in Word, so the
/// caret keeps blinking and typing continues where it was (#1093). egui drops the page's focus
/// on any click elsewhere; only a click that left nothing focused counts, so a text field
/// (font size, zoom, command search) keeps the focus it took, and clicks on a dialog, menu or
/// popup, or while one is open, leave focus alone. Clicks on the page are the canvas's own.
pub(super) fn refocus_after_chrome_click(&mut self, ctx: &egui::Context) {
if self.ui.backstage || self.dialog.is_some() || self.colors.is_some() {
return;
}
#[cfg(not(target_arch = "wasm32"))]
if self.file_browser.open.is_some() {
return;
}
let Some(pos) = ctx.input(|i| if i.pointer.any_click() { i.pointer.interact_pos() } else { None }) else { return };
// The ribbon dropped over the page in Focus (#1678) is chrome, though it lies over the page.
let on_focus_ribbon = self.focus_ribbon && ctx.layer_id_at(pos).is_some_and(|l| l.id == egui::Id::unique("focus_ribbon"));
// The page handles its own clicks (comment balloons there take the keyboard themselves).
if !on_focus_ribbon && (self.canvas.balloon.is_some() || self.canvas.viewport.is_some_and(|v| v.contains(pos))) {
return;
}
let on_chrome = on_focus_ribbon || ctx.layer_id_at(pos).is_none_or(|l| l.order == egui::Order::Background);
if on_chrome && ctx.memory(|m| m.focused().is_none()) && !egui::Popup::is_any_open(ctx) {
self.canvas.want_focus = true;
// The page has already been drawn, so it only sees `want_focus` next frame, and its own
// `focused` flag (which gates the keys) isn't recomputed until then either. Claim both
// now, or the click swallows the first keystroke.
self.canvas.focused = true;
// Only a page this pass drew. The File page's back arrow closes the File page in the
// pass that drew it instead of the page, and focus on a widget missing from the pass
// names a node AccessKit doesn't have: with accessibility on, AccessKit ends the program
// over that. `want_focus` hands the keyboard over next frame instead.
if let Some((id, pass)) = self.canvas.widget_id
&& pass == ctx.cumulative_pass_nr()
{
ctx.memory_mut(|m| m.request_focus(id));
}
// The canvas reads keys before it takes focus back (#1107), so it counts as focused
// now: a keystroke in the very next frame reaches the page instead of being dropped.
self.canvas.focused = true;
ctx.request_repaint();
}
}
}
/// macOS reads a Control-click as the right-click (winit reports the primary button with Control
/// held), so menus open on it as on a two-finger click. The release follows its press even if
/// Control was let go in between.
pub(crate) fn control_click_as_secondary(events: &mut [egui::Event], held: &mut bool) {
for e in events {
if let egui::Event::PointerButton { button: button @ egui::PointerButton::Primary, pressed, modifiers, .. } = e {
if *pressed {
*held = modifiers.ctrl && !modifiers.mac_cmd;
}
if *held {
*button = egui::PointerButton::Secondary;
}
if !*pressed {
*held = false;
}
}
}
}