diff --git a/Engine/README.md b/Engine/README.md index 7e54af9a..04b7eeaf 100644 --- a/Engine/README.md +++ b/Engine/README.md @@ -83,4 +83,6 @@ GitHub access to this repository and are retained for 30 days. Explorer rename, clipboard, drag and script-scope controls are described in [`docs/EXPLORER.md`](docs/EXPLORER.md). +3D camera controls and keyboard-focus troubleshooting are described in [`docs/VIEWPORT_NAVIGATION.md`](docs/VIEWPORT_NAVIGATION.md). + Gameplay authors should start with [`docs/GAMEPLAY_PROGRAMMING.md`](docs/GAMEPLAY_PROGRAMMING.md). diff --git a/Engine/apps/editor/src/main.rs b/Engine/apps/editor/src/main.rs index e1ed2888..f345877d 100644 --- a/Engine/apps/editor/src/main.rs +++ b/Engine/apps/editor/src/main.rs @@ -1,6 +1,7 @@ mod explorer_selection; mod play_session; mod ui_theme; +mod viewport_keyboard; use eframe::egui; use egui_tiles::{Behavior, TileId, Tree, UiResponse}; @@ -1646,8 +1647,7 @@ impl EditorViewer<'_> { if response.hovered() && ui.input(|input| input.pointer.any_pressed()) { response.request_focus(); } - let keyboard_active = - (response.has_focus() || response.hovered()) && !ui.ctx().egui_wants_keyboard_input(); + let keyboard_active = viewport_keyboard::active(response); let keys = if keyboard_active { ui.input(|input| { let mut keys = BTreeSet::new(); @@ -1712,11 +1712,7 @@ impl EditorViewer<'_> { self.request_preview = true; } } - // Hovering is sufficient to steer the scene. Requiring an earlier LMB - // click made RMB-look and WASD appear broken because the viewport never - // acquired keyboard focus. - let keyboard_active = - (response.has_focus() || hovered) && !ui.ctx().egui_wants_keyboard_input(); + let keyboard_active = viewport_keyboard::active(response); if keyboard_active { ui.input(|input| { if input.key_pressed(egui::Key::F) { diff --git a/Engine/apps/editor/src/viewport_keyboard.rs b/Engine/apps/editor/src/viewport_keyboard.rs new file mode 100644 index 00000000..50f948df --- /dev/null +++ b/Engine/apps/editor/src/viewport_keyboard.rs @@ -0,0 +1,79 @@ +use eframe::egui; + +pub fn active(response: &egui::Response) -> bool { + // egui wants keyboard input for any focused widget, including this viewport. + // Give the viewport its own keys while respecting focus in other widgets. + response.ctx.input(|input| input.focused) + && (response.has_focus() + || (response.hovered() && !response.ctx.egui_wants_keyboard_input())) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Clone, Copy)] + enum Focus { + None, + Viewport, + Text, + } + + fn keyboard_active(focus: Focus, hovered: bool, window_focused: bool) -> bool { + let ctx = egui::Context::default(); + let mut active = false; + let mut text = String::new(); + // Warm up hit testing before checking the hovered response. + for _ in 0..2 { + let mut output = ctx.run_ui( + egui::RawInput { + screen_rect: Some(egui::Rect::from_min_size( + egui::Pos2::ZERO, + egui::vec2(800.0, 600.0), + )), + focused: window_focused, + events: vec![egui::Event::PointerMoved(if hovered { + egui::pos2(50.0, 50.0) + } else { + egui::pos2(700.0, 500.0) + })], + ..Default::default() + }, + |ui| { + let (_, viewport) = ui.allocate_exact_size( + egui::vec2(200.0, 200.0), + egui::Sense::click_and_drag(), + ); + let field = ui.text_edit_singleline(&mut text); + match focus { + Focus::None => {} + Focus::Viewport => viewport.request_focus(), + Focus::Text => field.request_focus(), + } + active = super::active(&viewport); + }, + ); + output.textures_delta.clear(); + } + active + } + + #[test] + fn clicking_viewport_does_not_block_its_keyboard_controls() { + assert!(keyboard_active(Focus::Viewport, true, true)); + assert!(keyboard_active(Focus::Viewport, false, true)); + } + + #[test] + fn hovering_allows_navigation_without_a_prior_click() { + assert!(keyboard_active(Focus::None, true, true)); + assert!(!keyboard_active(Focus::None, false, true)); + } + + #[test] + fn typing_and_window_blur_do_not_drive_the_viewport() { + assert!(!keyboard_active(Focus::Text, true, true)); + assert!(!keyboard_active(Focus::Viewport, true, false)); + assert!(!keyboard_active(Focus::None, true, false)); + } +} diff --git a/Engine/docs/VIEWPORT_NAVIGATION.md b/Engine/docs/VIEWPORT_NAVIGATION.md new file mode 100644 index 00000000..1c0ff4e0 --- /dev/null +++ b/Engine/docs/VIEWPORT_NAVIGATION.md @@ -0,0 +1,30 @@ +# 3D editor navigation + +Open a project and stop Play before navigating the authoring camera. Open a missing +3D viewport through **View**. Hover the viewport to navigate without a prior click, +or click it to give it keyboard focus. A focused viewport keeps receiving navigation +keys when the pointer moves outside it. Text fields in other panels keep their own +keyboard input until you click the viewport. Navigation pauses when the editor +window loses focus. + +| Control | Action | +| --- | --- | +| W / S | Move forward / backward. | +| A / D | Move left / right. | +| Q / E | Pan left / right horizontally. | +| Middle, right or left drag away from a gizmo | Orbit. | +| Shift + middle drag | Pan. | +| Mouse wheel | Dolly. | +| F | Frame the selected object. | +| Home | Reset the editor camera. | + +Hold movement keys for continuous navigation; release them to stop. Only the +authoring camera moves. These controls do not move objects or the scene camera, +and keyboard navigation is unavailable in the 2D authoring viewport. During Play, +WASD is forwarded to gameplay scripts instead of navigating the authoring camera. + +If WASD or Q/E does not respond, stop Play, activate the editor window and click +inside the 3D viewport to leave any Inspector or Explorer rename field. Use +**Home** to reset the view or select an object in the hierarchy and press **F**. + +The corresponding website guide is **Editor overview > Viewport tools**.