mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 17:21:49 -10:00
Navigate menus with Tab, the arrows and Enter as vanilla does
Screen.keyPressed's focus navigation on the targets the controller's focus mode already reaches: Tab and Shift+Tab in tab order (reading order), arrows with the ported nextFocusPathInDirection step, Enter, Space and keypad Enter press the focused widget. Sliders follow canChangeValue, text fields keep their caret keys, a list is one Tab stop and the world list selects its focused row. The focused widget is the narration's focused one, the keyboard's own pointer events don't reach the narrator, and the real mouse takes the pointer back where it left it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
bc791418ca
commit
27f9488135
6 files changed
+970
-65
No files matched your search
@@ -130,6 +130,7 @@ mod item_particles;
|
||||
mod jigsaw_blocks;
|
||||
mod jigsaw_edit;
|
||||
mod jukebox;
|
||||
mod key_focus;
|
||||
mod lan;
|
||||
mod lan_actions;
|
||||
mod lan_attributes;
|
||||
@@ -7192,7 +7193,9 @@ impl Game<'_> {
|
||||
fn apply_cursor_icon(&mut self) {
|
||||
use horizon_client::widgets::Cursor;
|
||||
let wanted = horizon_client::widgets::take_cursor();
|
||||
let effective = if self.settings.allow_cursor_changes {
|
||||
// The widgets under the keyboard's hidden pointer aren't under the
|
||||
// mouse (`handleCursor` asks only for a hovered widget).
|
||||
let effective = if self.settings.allow_cursor_changes && !self.key_focus_driving() {
|
||||
wanted
|
||||
} else {
|
||||
Cursor::Arrow
|
||||
@@ -14919,8 +14922,13 @@ impl ApplicationHandler for Game<'_> {
|
||||
) {
|
||||
self.last_input = Instant::now();
|
||||
}
|
||||
// The screen's narration hears of the input before the screen takes it.
|
||||
narrator::narrate_input(&mut self.narration, &event, self.cursor);
|
||||
// The real mouse takes the pointer back from the keyboard's focus.
|
||||
self.key_focus_mouse(event_loop, &event);
|
||||
// The screen's narration hears of the input before the screen takes
|
||||
// it; the pointer events the keyboard's focus sends aren't the mouse.
|
||||
if !self.key_focus_quiet() {
|
||||
narrator::narrate_input(&mut self.narration, &event, self.cursor);
|
||||
}
|
||||
// FileFixerProgressScreen: Cancel is the only way out (`shouldCloseOnEsc`
|
||||
// is false), so it takes the pointer and Escape does nothing.
|
||||
if let Some(loader) = &self.world_loader
|
||||
@@ -15027,6 +15035,17 @@ impl ApplicationHandler for Game<'_> {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Screen.keyPressed: Tab, the arrows and Enter move and press the
|
||||
// menu's focus, unless the focused widget or the screen keeps them.
|
||||
if let WindowEvent::KeyboardInput {
|
||||
event: key_event, ..
|
||||
} = &event
|
||||
&& key_event.state == ElementState::Pressed
|
||||
&& let PhysicalKey::Code(key) = key_event.physical_key
|
||||
&& self.key_focus_key(event_loop, key)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let result: Result<()> = (|| {
|
||||
match event {
|
||||
WindowEvent::CloseRequested => {
|
||||
|
||||
@@ -0,0 +1,473 @@
|
||||
//! Vanilla's keyboard focus navigation in Horizon's menus
|
||||
//! (`Screen.keyPressed`, `ContainerEventHandler.nextFocusPath`, 26.3): Tab
|
||||
//! and Shift+Tab step the focus in tab order, the arrows move it the way they
|
||||
//! point, Enter, Space and keypad Enter press the focused widget
|
||||
//! (`AbstractButton.keyPressed`), and a focused slider's left and right move
|
||||
//! its value once Enter (or Tab) let them (`AbstractSliderButton.keyPressed`).
|
||||
//!
|
||||
//! The focus lands on what the controller's focus mode reaches: the targets
|
||||
//! each screen registers (`widgets::target_as`, docs/controllers.md). This
|
||||
//! module is the pure part: which key does what, the tab order, the slider
|
||||
//! rules. `pad_menu.rs` applies it in the game (it shares the controller's
|
||||
//! focus), `startup_menu.rs` on the title shown while resources load.
|
||||
use horizon_client::{
|
||||
gamepad::{self, Button, Rect},
|
||||
hud::{HudAtlas, HudBatch, rgba},
|
||||
widgets::{self, Kind, Target},
|
||||
};
|
||||
use winit::keyboard::KeyCode;
|
||||
|
||||
/// A navigation key, as `Screen.keyPressed` and the widgets read it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(super) enum Nav {
|
||||
/// Tab (forward) or Shift+Tab (`FocusNavigationEvent.TabNavigation`).
|
||||
Tab { forward: bool },
|
||||
/// An arrow (`ArrowNavigation`), as the D-pad direction it steps.
|
||||
Arrow(Button),
|
||||
/// Enter, Space or keypad Enter (`InputWithModifiers.isSelection`).
|
||||
Select,
|
||||
}
|
||||
|
||||
/// The navigation a key press is, if any.
|
||||
pub(super) fn nav_of(key: KeyCode, shift: bool) -> Option<Nav> {
|
||||
Some(match key {
|
||||
KeyCode::Tab => Nav::Tab { forward: !shift },
|
||||
KeyCode::ArrowUp => Nav::Arrow(Button::DPadUp),
|
||||
KeyCode::ArrowDown => Nav::Arrow(Button::DPadDown),
|
||||
KeyCode::ArrowLeft => Nav::Arrow(Button::DPadLeft),
|
||||
KeyCode::ArrowRight => Nav::Arrow(Button::DPadRight),
|
||||
KeyCode::Enter | KeyCode::NumpadEnter | KeyCode::Space => Nav::Select,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// What a navigation key does on the screen as it is.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(super) enum Act {
|
||||
/// The screen takes the key itself: a text field's caret, Enter on a
|
||||
/// list whose screen joins its selected row, nothing focused.
|
||||
Pass,
|
||||
/// Focus `targets[i]` (Tab, or an arrow from no focus).
|
||||
Focus(usize),
|
||||
/// Move the focus that way (the controller's step, which scrolls lists).
|
||||
Step(Button),
|
||||
/// Press the focused widget (a click on it).
|
||||
Press,
|
||||
/// Enter on a focused slider: whether the arrows change its value flips.
|
||||
ToggleSlider,
|
||||
/// Left (-1) or right (1) on a slider that can change: one step.
|
||||
Nudge(f32),
|
||||
}
|
||||
|
||||
/// Where the screen is: what `act` reads besides the focus.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub(super) struct Context {
|
||||
/// The focused slider's arrows change its value (`canChangeValue`).
|
||||
pub slider_edit: bool,
|
||||
/// A text field has the keyboard (a click focused it): with nothing
|
||||
/// focused, its caret takes left and right.
|
||||
pub typing: bool,
|
||||
/// The screen joins its list's selected row on Enter itself
|
||||
/// (`WorldListEntry.keyPressed`): a focused row passes Enter on.
|
||||
pub rows_take_enter: bool,
|
||||
/// The GUI size: an arrow from no focus starts at the screen's edge.
|
||||
pub gui: (f32, f32),
|
||||
}
|
||||
|
||||
fn horizontal(button: Button) -> bool {
|
||||
matches!(button, Button::DPadLeft | Button::DPadRight)
|
||||
}
|
||||
|
||||
/// What `nav` does, `focus` being the focused target (the one a click
|
||||
/// focused counts) among `targets`.
|
||||
pub(super) fn act(nav: Nav, focus: Option<Target>, targets: &[Target], ctx: Context) -> Act {
|
||||
match (nav, focus) {
|
||||
(Nav::Tab { forward }, _) => {
|
||||
tab_step(targets, focus.map(|t| t.rect), forward).map_or(Act::Pass, Act::Focus)
|
||||
}
|
||||
// EditBox.keyPressed takes left and right for its caret.
|
||||
(
|
||||
Nav::Arrow(b),
|
||||
Some(Target {
|
||||
kind: Kind::Field, ..
|
||||
}),
|
||||
) if horizontal(b) => Act::Pass,
|
||||
(Nav::Arrow(b), None) if horizontal(b) && ctx.typing => Act::Pass,
|
||||
(
|
||||
Nav::Arrow(b),
|
||||
Some(Target {
|
||||
kind: Kind::Slider { .. },
|
||||
..
|
||||
}),
|
||||
) if horizontal(b) && ctx.slider_edit => {
|
||||
Act::Nudge(if b == Button::DPadLeft { -1.0 } else { 1.0 })
|
||||
}
|
||||
(Nav::Arrow(b), Some(_)) => Act::Step(b),
|
||||
// No focus: from the screen's edge behind the arrow
|
||||
// (`getBorderForArrowNavigation(direction.getOpposite())`).
|
||||
(Nav::Arrow(b), None) => {
|
||||
let rects: Vec<Rect> = targets.iter().map(|t| t.rect).collect();
|
||||
gamepad::focus_step_rect(edge(ctx.gui, b), b, &rects).map_or(Act::Pass, Act::Focus)
|
||||
}
|
||||
(Nav::Select, None) => Act::Pass,
|
||||
(Nav::Select, Some(t)) => match t.kind {
|
||||
Kind::Field => Act::Pass,
|
||||
Kind::Row if ctx.rows_take_enter => Act::Pass,
|
||||
Kind::Slider { .. } => Act::ToggleSlider,
|
||||
_ => Act::Press,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// The strip just outside the screen on the side an arrow comes from.
|
||||
fn edge((w, h): (f32, f32), button: Button) -> Rect {
|
||||
match button {
|
||||
Button::DPadUp => [0.0, h, w, 1.0],
|
||||
Button::DPadLeft => [w, 0.0, 1.0, h],
|
||||
Button::DPadRight => [-1.0, 0.0, 1.0, h],
|
||||
_ => [0.0, -1.0, w, 1.0],
|
||||
}
|
||||
}
|
||||
|
||||
/// `AbstractSliderButton.setFocused`: the arrows change a slider's value
|
||||
/// when the focus came from the mouse or Tab, not from an arrow.
|
||||
pub(super) fn slider_edit_on_focus(nav: Nav) -> bool {
|
||||
matches!(nav, Nav::Tab { .. })
|
||||
}
|
||||
|
||||
fn same(a: Rect, b: Rect) -> bool {
|
||||
a.iter().zip(b).all(|(p, q)| (p - q).abs() < 0.5)
|
||||
}
|
||||
|
||||
/// The tab order (`children()` sorted by `getTabOrderGroup`): the order
|
||||
/// vanilla added the widgets in, which its layouts do row by row. Horizon
|
||||
/// has no widget tree (a screen registers buttons and other targets
|
||||
/// separately), so this is reading order: top to bottom, then left to right,
|
||||
/// ties in the order they were drawn.
|
||||
pub(super) fn tab_order(targets: &[Target]) -> Vec<usize> {
|
||||
let mut order: Vec<usize> = (0..targets.len()).collect();
|
||||
let key = |t: &Target| (t.rect[1].round() as i32, t.rect[0].round() as i32);
|
||||
order.sort_by_key(|&i| key(&targets[i]));
|
||||
order
|
||||
}
|
||||
|
||||
/// Tab or Shift+Tab from `from` (none focused: from the start or the end),
|
||||
/// wrapping as `Screen.keyPressed` does (no next one: `clearFocus`, then
|
||||
/// again). A list's rows are one stop, as vanilla's list is one widget: Tab
|
||||
/// enters it on its first row (Shift+Tab on its last) and leaves it at once.
|
||||
pub(super) fn tab_step(targets: &[Target], from: Option<Rect>, forward: bool) -> Option<usize> {
|
||||
// The stops in tab order: a run of rows is one.
|
||||
let mut stops: Vec<Vec<usize>> = Vec::new();
|
||||
for i in tab_order(targets) {
|
||||
let row = targets[i].kind == Kind::Row;
|
||||
match stops.last_mut() {
|
||||
Some(stop) if row && targets[stop[0]].kind == Kind::Row => stop.push(i),
|
||||
_ => stops.push(vec![i]),
|
||||
}
|
||||
}
|
||||
if stops.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let at = from.and_then(|r| {
|
||||
stops
|
||||
.iter()
|
||||
.position(|s| s.iter().any(|&i| same(targets[i].rect, r)))
|
||||
});
|
||||
let n = stops.len();
|
||||
let next = match at {
|
||||
Some(k) if forward => (k + 1) % n,
|
||||
Some(k) => (k + n - 1) % n,
|
||||
None if forward => 0,
|
||||
None => n - 1,
|
||||
};
|
||||
let stop = &stops[next];
|
||||
Some(if forward {
|
||||
stop[0]
|
||||
} else {
|
||||
stop[stop.len() - 1]
|
||||
})
|
||||
}
|
||||
|
||||
/// What the last drawn frame's screen can focus: its enabled buttons and
|
||||
/// every target it registered (sliders, fields, rows, slots...).
|
||||
pub(super) fn drawn_targets() -> Vec<Target> {
|
||||
let mut targets: Vec<Target> = widgets::last()
|
||||
.iter()
|
||||
.filter(|w| w.enabled)
|
||||
.map(|w| Target {
|
||||
rect: [w.x, w.y, w.w, w.h],
|
||||
kind: Kind::Button,
|
||||
})
|
||||
.collect();
|
||||
for t in widgets::last_targets() {
|
||||
// A button can be both a labelled button and a target.
|
||||
if let Some(same) = targets.iter_mut().find(|o| o.rect == t.rect) {
|
||||
same.kind = t.kind;
|
||||
} else {
|
||||
targets.push(t);
|
||||
}
|
||||
}
|
||||
targets
|
||||
}
|
||||
|
||||
/// The targets inside the GUI (`gui`: its size in GUI pixels).
|
||||
pub(super) fn on_screen(mut targets: Vec<Target>, (w, h): (f32, f32)) -> Vec<Target> {
|
||||
let (w, h) = (w + 0.5, h + 0.5);
|
||||
targets.retain(|t| {
|
||||
let [x, y, tw, th] = t.rect;
|
||||
x >= -0.5 && y >= -0.5 && x + tw <= w && y + th <= h
|
||||
});
|
||||
targets
|
||||
}
|
||||
|
||||
/// Which targets the keyboard reaches: vanilla focuses widgets only, never
|
||||
/// an inventory's slots, and the creative and recipe-book tabs aren't the
|
||||
/// screen's widgets either (`inventory`).
|
||||
pub(super) fn keyboard_targets(targets: Vec<Target>, inventory: bool) -> Vec<Target> {
|
||||
targets
|
||||
.into_iter()
|
||||
.filter(|t| match t.kind {
|
||||
Kind::Slot => false,
|
||||
Kind::Tab(_) => !inventory,
|
||||
_ => true,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Where a click focuses a text field with its caret at the end of the
|
||||
/// text, as Tab leaves it (`EditBox.setFocused` keeps the cursor, which
|
||||
/// `setValue` put at the end): the box's right end.
|
||||
pub(super) fn field_click(rect: Rect) -> (f32, f32) {
|
||||
let [x, y, w, h] = rect;
|
||||
(x + w - 2.0, y + h / 2.0)
|
||||
}
|
||||
|
||||
/// Vanilla's focus look for what has no highlighted sprite: the white
|
||||
/// one-pixel outline of a focused list's selection
|
||||
/// (`AbstractSelectionList`) and of a focused `EditBox`. Buttons, sliders
|
||||
/// and tabs show their highlighted sprite, drawn under the hidden pointer.
|
||||
pub(super) fn draw_outline(atlas: &HudAtlas, batch: &mut HudBatch, focus: Target) {
|
||||
let [x, y, w, h] = focus.rect;
|
||||
if matches!(focus.kind, Kind::Row | Kind::Field) {
|
||||
let white = rgba(255, 255, 255, 255);
|
||||
batch.fill(atlas, x, y, w, 1.0, white);
|
||||
batch.fill(atlas, x, y + h - 1.0, w, 1.0, white);
|
||||
batch.fill(atlas, x, y, 1.0, h, white);
|
||||
batch.fill(atlas, x + w - 1.0, y, 1.0, h, white);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn button(x: f32, y: f32) -> Target {
|
||||
Target {
|
||||
rect: [x, y, 98.0, 20.0],
|
||||
kind: Kind::Button,
|
||||
}
|
||||
}
|
||||
fn row(y: f32) -> Target {
|
||||
Target {
|
||||
rect: [100.0, y, 220.0, 36.0],
|
||||
kind: Kind::Row,
|
||||
}
|
||||
}
|
||||
fn slider() -> Target {
|
||||
Target {
|
||||
rect: [0.0, 0.0, 150.0, 20.0],
|
||||
kind: Kind::Slider {
|
||||
value: 0.5,
|
||||
track: [4.0, 142.0],
|
||||
},
|
||||
}
|
||||
}
|
||||
fn field() -> Target {
|
||||
Target {
|
||||
rect: [0.0, 0.0, 200.0, 20.0],
|
||||
kind: Kind::Field,
|
||||
}
|
||||
}
|
||||
const CTX: Context = Context {
|
||||
slider_edit: false,
|
||||
typing: false,
|
||||
rows_take_enter: false,
|
||||
gui: (427.0, 240.0),
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn keys_map_to_vanillas_navigation() {
|
||||
assert_eq!(
|
||||
nav_of(KeyCode::Tab, false),
|
||||
Some(Nav::Tab { forward: true })
|
||||
);
|
||||
assert_eq!(
|
||||
nav_of(KeyCode::Tab, true),
|
||||
Some(Nav::Tab { forward: false })
|
||||
);
|
||||
assert_eq!(
|
||||
nav_of(KeyCode::ArrowUp, false),
|
||||
Some(Nav::Arrow(Button::DPadUp))
|
||||
);
|
||||
assert_eq!(
|
||||
nav_of(KeyCode::ArrowRight, true),
|
||||
Some(Nav::Arrow(Button::DPadRight))
|
||||
);
|
||||
// isSelection: Enter (40), Space (44), keypad Enter (88).
|
||||
for key in [KeyCode::Enter, KeyCode::Space, KeyCode::NumpadEnter] {
|
||||
assert_eq!(nav_of(key, false), Some(Nav::Select));
|
||||
}
|
||||
assert_eq!(nav_of(KeyCode::KeyA, false), None);
|
||||
assert_eq!(nav_of(KeyCode::Escape, false), None);
|
||||
}
|
||||
|
||||
/// The title screen's rows (TitleScreen.init's add order).
|
||||
#[test]
|
||||
fn tab_order_reads_rows_top_to_bottom_then_left_to_right() {
|
||||
let targets = [
|
||||
button(164.0, 108.0), // Options
|
||||
button(266.0, 108.0), // Quit
|
||||
button(164.0, 60.0), // Singleplayer
|
||||
Target {
|
||||
rect: [140.0, 84.0, 20.0, 20.0],
|
||||
kind: Kind::Button,
|
||||
}, // Friends
|
||||
button(164.0, 84.0), // Realms
|
||||
];
|
||||
assert_eq!(tab_order(&targets), vec![2, 3, 4, 0, 1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_wraps_both_ways_and_starts_at_either_end() {
|
||||
let targets = [button(0.0, 0.0), button(0.0, 30.0), button(0.0, 60.0)];
|
||||
assert_eq!(tab_step(&targets, None, true), Some(0));
|
||||
assert_eq!(tab_step(&targets, None, false), Some(2));
|
||||
assert_eq!(tab_step(&targets, Some(targets[0].rect), true), Some(1));
|
||||
assert_eq!(tab_step(&targets, Some(targets[2].rect), true), Some(0));
|
||||
assert_eq!(tab_step(&targets, Some(targets[0].rect), false), Some(2));
|
||||
// A rect no longer there: from the start.
|
||||
assert_eq!(tab_step(&targets, Some([9.0; 4]), true), Some(0));
|
||||
assert_eq!(tab_step(&[], None, true), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_lists_rows_are_one_tab_stop() {
|
||||
let search = field();
|
||||
let targets = [search, row(40.0), row(76.0), row(112.0), button(0.0, 200.0)];
|
||||
// Into the list on its first row, then out of it.
|
||||
assert_eq!(tab_step(&targets, Some(search.rect), true), Some(1));
|
||||
assert_eq!(tab_step(&targets, Some(targets[2].rect), true), Some(4));
|
||||
// Backwards: in on the last row, out to the search box.
|
||||
assert_eq!(tab_step(&targets, Some(targets[4].rect), false), Some(3));
|
||||
assert_eq!(tab_step(&targets, Some(targets[1].rect), false), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_presses_buttons_and_toggles_sliders() {
|
||||
let b = button(0.0, 0.0);
|
||||
assert_eq!(act(Nav::Select, Some(b), &[b], CTX), Act::Press);
|
||||
let s = slider();
|
||||
assert_eq!(act(Nav::Select, Some(s), &[s], CTX), Act::ToggleSlider);
|
||||
// Nothing focused: the screen's own Enter (a world list joins).
|
||||
assert_eq!(act(Nav::Select, None, &[b], CTX), Act::Pass);
|
||||
let f = field();
|
||||
assert_eq!(act(Nav::Select, Some(f), &[f], CTX), Act::Pass);
|
||||
let r = row(40.0);
|
||||
assert_eq!(act(Nav::Select, Some(r), &[r], CTX), Act::Press);
|
||||
let joins = Context {
|
||||
rows_take_enter: true,
|
||||
..CTX
|
||||
};
|
||||
assert_eq!(act(Nav::Select, Some(r), &[r], joins), Act::Pass);
|
||||
}
|
||||
|
||||
/// AbstractSliderButton.keyPressed and setFocused.
|
||||
#[test]
|
||||
fn slider_arrows_change_the_value_only_once_allowed() {
|
||||
let s = slider();
|
||||
let left = Nav::Arrow(Button::DPadLeft);
|
||||
let right = Nav::Arrow(Button::DPadRight);
|
||||
assert_eq!(
|
||||
act(left, Some(s), &[s], CTX),
|
||||
Act::Step(Button::DPadLeft),
|
||||
"focused by an arrow: left moves the focus"
|
||||
);
|
||||
let edit = Context {
|
||||
slider_edit: true,
|
||||
..CTX
|
||||
};
|
||||
assert_eq!(act(left, Some(s), &[s], edit), Act::Nudge(-1.0));
|
||||
assert_eq!(act(right, Some(s), &[s], edit), Act::Nudge(1.0));
|
||||
// Up and down always move the focus.
|
||||
assert_eq!(
|
||||
act(Nav::Arrow(Button::DPadDown), Some(s), &[s], edit),
|
||||
Act::Step(Button::DPadDown)
|
||||
);
|
||||
assert!(slider_edit_on_focus(Nav::Tab { forward: true }));
|
||||
assert!(slider_edit_on_focus(Nav::Tab { forward: false }));
|
||||
assert!(!slider_edit_on_focus(Nav::Arrow(Button::DPadDown)));
|
||||
}
|
||||
|
||||
/// EditBox.keyPressed consumes left and right, not Tab, up or down.
|
||||
#[test]
|
||||
fn a_text_field_keeps_left_and_right() {
|
||||
let f = field();
|
||||
let b = button(0.0, 40.0);
|
||||
let targets = [f, b];
|
||||
for b in [Button::DPadLeft, Button::DPadRight] {
|
||||
assert_eq!(act(Nav::Arrow(b), Some(f), &targets, CTX), Act::Pass);
|
||||
}
|
||||
assert_eq!(
|
||||
act(Nav::Arrow(Button::DPadDown), Some(f), &targets, CTX),
|
||||
Act::Step(Button::DPadDown)
|
||||
);
|
||||
assert_eq!(
|
||||
act(Nav::Tab { forward: true }, Some(f), &targets, CTX),
|
||||
Act::Focus(1)
|
||||
);
|
||||
// A field a click focused (no keyboard focus) keeps them too.
|
||||
let typing = Context {
|
||||
typing: true,
|
||||
..CTX
|
||||
};
|
||||
assert_eq!(
|
||||
act(Nav::Arrow(Button::DPadLeft), None, &targets, typing),
|
||||
Act::Pass
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_arrow_from_no_focus_starts_at_the_screens_edge() {
|
||||
let targets = [
|
||||
button(164.0, 60.0),
|
||||
button(164.0, 84.0),
|
||||
button(10.0, 200.0),
|
||||
];
|
||||
let down = act(Nav::Arrow(Button::DPadDown), None, &targets, CTX);
|
||||
assert_eq!(down, Act::Focus(0));
|
||||
let up = act(Nav::Arrow(Button::DPadUp), None, &targets, CTX);
|
||||
assert_eq!(up, Act::Focus(2));
|
||||
let right = act(Nav::Arrow(Button::DPadRight), None, &targets, CTX);
|
||||
assert_eq!(right, Act::Focus(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_keyboard_skips_slots_and_inventory_tabs() {
|
||||
let slot = Target {
|
||||
rect: [0.0, 0.0, 16.0, 16.0],
|
||||
kind: Kind::Slot,
|
||||
};
|
||||
let tab = Target {
|
||||
rect: [0.0, 0.0, 26.0, 32.0],
|
||||
kind: Kind::Tab(false),
|
||||
};
|
||||
let b = button(0.0, 0.0);
|
||||
assert_eq!(keyboard_targets(vec![slot, tab, b], true), vec![b]);
|
||||
assert_eq!(keyboard_targets(vec![slot, tab, b], false), vec![tab, b]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_field_is_clicked_at_its_end() {
|
||||
assert_eq!(field_click([10.0, 20.0, 200.0, 20.0]), (208.0, 30.0));
|
||||
}
|
||||
}
|
||||
@@ -252,7 +252,7 @@ impl Game<'_> {
|
||||
/// book (no `EditBox`), the recipe book (its component has the focus) and
|
||||
/// the creative search (focused as a widget, not as the screen's focus)
|
||||
/// do not.
|
||||
fn edit_box_focused(&mut self) -> bool {
|
||||
pub(super) fn edit_box_focused(&mut self) -> bool {
|
||||
if self.dialog_open() {
|
||||
return self.dialog_typing();
|
||||
}
|
||||
|
||||
@@ -101,6 +101,8 @@ impl Game<'_> {
|
||||
self.pad_edge(event_loop, button, down);
|
||||
}
|
||||
}
|
||||
// The keyboard's menu focus follows its widget all the same.
|
||||
self.pad_menu_frame(event_loop);
|
||||
return;
|
||||
}
|
||||
let hardware = self.pad.read();
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
//! behaviour (drags, sliders, scrollbars, hover highlights) with no pad code
|
||||
//! of its own. A screen with nothing registered falls back to the pointer,
|
||||
//! and a right stick click swaps modes for the open screen.
|
||||
use super::{Game, Screen};
|
||||
use super::{
|
||||
Game, Screen,
|
||||
key_focus::{self, Act},
|
||||
};
|
||||
use horizon_client::{
|
||||
Result,
|
||||
creative::Creative,
|
||||
@@ -33,6 +36,7 @@ use winit::{
|
||||
event::{DeviceId, ElementState, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent},
|
||||
event_loop::ActiveEventLoop,
|
||||
keyboard::KeyCode,
|
||||
window::WindowId,
|
||||
};
|
||||
|
||||
/// D-pad auto-repeat: the first step at once, the next after a pause.
|
||||
@@ -90,6 +94,23 @@ pub(super) struct MenuCursor {
|
||||
pub(super) osk: Option<Keyboard>,
|
||||
/// The keyboard was closed on this screen (it doesn't open by itself again).
|
||||
osk_dismissed: bool,
|
||||
/// The keyboard drives the focus (`key_focus`: Tab, the arrows, Enter):
|
||||
/// focus mode whatever the controller options say, the OS pointer shown.
|
||||
keyboard: bool,
|
||||
/// The focused slider's arrows change its value (`canChangeValue`).
|
||||
slider_edit: bool,
|
||||
/// What the last click landed on: vanilla focuses it, so the next Tab
|
||||
/// or arrow goes on from there (`ContainerEventHandler.mouseClicked`).
|
||||
origin: Option<Rect>,
|
||||
/// A pointer event the pad or the keyboard sends is being handled.
|
||||
synthetic: bool,
|
||||
/// Where the real mouse last moved the pointer (window pixels).
|
||||
real_pointer: Option<PhysicalPosition<f64>>,
|
||||
/// The screen the keyboard's focus is on (`Game::key_screen`), and the
|
||||
/// frame drawn when another one opened: the focus starts again on the
|
||||
/// new screen's first drawn frame.
|
||||
key_screen: u64,
|
||||
key_restart: Option<u64>,
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
@@ -108,43 +129,41 @@ impl Game<'_> {
|
||||
.map(|r| HudBatch::new([r.config.width, r.config.height]))
|
||||
}
|
||||
|
||||
/// Focus mode is on for the open screen.
|
||||
/// Focus mode is on for the open screen (the keyboard always focuses).
|
||||
fn pad_focus_mode(&self) -> bool {
|
||||
(self.settings.controller.menu_nav == MenuNav::Focus) != self.pad.menu.swapped
|
||||
self.pad.menu.keyboard
|
||||
|| (self.settings.controller.menu_nav == MenuNav::Focus) != self.pad.menu.swapped
|
||||
}
|
||||
|
||||
/// Where focus can land: buttons drawn last frame, every registered
|
||||
/// target (sliders, fields, rows, keyboard keys), and the slots
|
||||
/// and tabs of inventories (GUI pixels), on screen.
|
||||
/// and tabs of inventories (GUI pixels), on screen. The keyboard's
|
||||
/// focus skips what vanilla's keyboard can't reach (`key_targets`).
|
||||
fn pad_targets(&self) -> Vec<Target> {
|
||||
let mut targets = self.pad_all_targets();
|
||||
if let Some(batch) = self.gui_scale() {
|
||||
let (w, h) = (batch.width + 0.5, batch.height + 0.5);
|
||||
targets.retain(|t| {
|
||||
let [x, y, tw, th] = t.rect;
|
||||
x >= -0.5 && y >= -0.5 && x + tw <= w && y + th <= h
|
||||
});
|
||||
if self.pad.menu.keyboard {
|
||||
self.key_targets()
|
||||
} else {
|
||||
self.pad_visible_targets()
|
||||
}
|
||||
}
|
||||
|
||||
/// The targets the keyboard reaches: no slots, no inventory tabs.
|
||||
fn key_targets(&self) -> Vec<Target> {
|
||||
let inventory =
|
||||
self.title.is_none() && matches!(self.screen, Screen::Inventory | Screen::Creative);
|
||||
key_focus::keyboard_targets(self.pad_visible_targets(), inventory)
|
||||
}
|
||||
|
||||
fn pad_visible_targets(&self) -> Vec<Target> {
|
||||
let targets = self.pad_all_targets();
|
||||
match self.gui_scale() {
|
||||
Some(batch) => key_focus::on_screen(targets, (batch.width, batch.height)),
|
||||
None => targets,
|
||||
}
|
||||
targets
|
||||
}
|
||||
|
||||
fn pad_all_targets(&self) -> Vec<Target> {
|
||||
let mut targets: Vec<Target> = widgets::last()
|
||||
.iter()
|
||||
.filter(|w| w.enabled)
|
||||
.map(|w| Target {
|
||||
rect: [w.x, w.y, w.w, w.h],
|
||||
kind: Kind::Button,
|
||||
})
|
||||
.collect();
|
||||
for t in widgets::last_targets() {
|
||||
// A button can be both a labelled button and a target.
|
||||
if let Some(same) = targets.iter_mut().find(|o| o.rect == t.rect) {
|
||||
same.kind = t.kind;
|
||||
} else {
|
||||
targets.push(t);
|
||||
}
|
||||
}
|
||||
let mut targets = key_focus::drawn_targets();
|
||||
if self.title.is_some() || self.sign_edit.is_some() || self.book.is_some() {
|
||||
return targets;
|
||||
}
|
||||
@@ -209,6 +228,10 @@ impl Game<'_> {
|
||||
/// inventory, else the first button, else the first target that isn't
|
||||
/// a tab.
|
||||
fn pad_first_target(&self, targets: &[Target]) -> Option<Target> {
|
||||
if self.pad.menu.keyboard {
|
||||
// Screen.setInitialFocus: a forward Tab from nothing.
|
||||
return key_focus::tab_step(targets, None, true).map(|i| targets[i]);
|
||||
}
|
||||
let batch = self.gui_scale()?;
|
||||
let at = |x: f32, y: f32| {
|
||||
targets
|
||||
@@ -254,7 +277,7 @@ impl Game<'_> {
|
||||
);
|
||||
let position =
|
||||
PhysicalPosition::new(f64::from(at.0 * batch.scale), f64::from(at.1 * batch.scale));
|
||||
self.window_event(
|
||||
self.pad_synthetic_event(
|
||||
event_loop,
|
||||
id,
|
||||
WindowEvent::CursorMoved {
|
||||
@@ -281,7 +304,7 @@ impl Game<'_> {
|
||||
} else {
|
||||
ElementState::Released
|
||||
};
|
||||
self.window_event(
|
||||
self.pad_synthetic_event(
|
||||
event_loop,
|
||||
id,
|
||||
WindowEvent::MouseInput {
|
||||
@@ -293,6 +316,19 @@ impl Game<'_> {
|
||||
}
|
||||
|
||||
/// A click (press and release) at a point, the pointer staying there.
|
||||
/// A pointer event of the pad's or the keyboard's, as the window would
|
||||
/// send it (marked, so it isn't taken for the real mouse).
|
||||
fn pad_synthetic_event(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
id: WindowId,
|
||||
event: WindowEvent,
|
||||
) {
|
||||
let was = std::mem::replace(&mut self.pad.menu.synthetic, true);
|
||||
self.window_event(event_loop, id, event);
|
||||
self.pad.menu.synthetic = was;
|
||||
}
|
||||
|
||||
fn pad_click_at(&mut self, event_loop: &ActiveEventLoop, at: (f32, f32), right: bool) {
|
||||
self.pad_pointer_to(event_loop, at);
|
||||
self.pad_pointer_button(event_loop, right, true);
|
||||
@@ -303,7 +339,7 @@ impl Game<'_> {
|
||||
let Some(id) = self.window.as_ref().map(|w| w.id()) else {
|
||||
return;
|
||||
};
|
||||
self.window_event(
|
||||
self.pad_synthetic_event(
|
||||
event_loop,
|
||||
id,
|
||||
WindowEvent::MouseWheel {
|
||||
@@ -354,6 +390,8 @@ impl Game<'_> {
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
// The pad takes the focus back from the keyboard.
|
||||
self.pad.menu.keyboard = false;
|
||||
// Controller Binds waiting for a button: this one (Start cancels).
|
||||
if self.screen == Screen::Options
|
||||
&& self.ui.options.page() == Page::PadBinds
|
||||
@@ -505,25 +543,46 @@ impl Game<'_> {
|
||||
self.pad_take_pointer(event_loop);
|
||||
return;
|
||||
};
|
||||
if let Kind::Slider { value, track } = focus.kind
|
||||
if matches!(focus.kind, Kind::Slider { .. })
|
||||
&& matches!(button, Button::DPadLeft | Button::DPadRight)
|
||||
{
|
||||
let from = match self.pad.menu.nudge {
|
||||
Some((rect, x)) if rect == focus.rect => x,
|
||||
_ => gamepad::slider_x(track, value),
|
||||
};
|
||||
let step = gamepad::slider_step(self.pad.menu.repeats);
|
||||
let x = (from
|
||||
+ if button == Button::DPadLeft {
|
||||
-step
|
||||
} else {
|
||||
step
|
||||
})
|
||||
.clamp(track[0], track[0] + track[1]);
|
||||
self.pad.menu.nudge = Some((focus.rect, x));
|
||||
self.pad_click_at(event_loop, (x, focus.center().1), false);
|
||||
let sign = if button == Button::DPadLeft {
|
||||
-1.0
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
self.pad_nudge(event_loop, focus, sign * step);
|
||||
return;
|
||||
}
|
||||
self.pad_focus_move(event_loop, button, focus, targets);
|
||||
}
|
||||
|
||||
/// Move a focused slider's handle by `by` GUI pixels (one pixel is
|
||||
/// vanilla's arrow-key step, `value + 1 / (width - 8)`), as a click there.
|
||||
fn pad_nudge(&mut self, event_loop: &ActiveEventLoop, focus: Target, by: f32) {
|
||||
let Kind::Slider { value, track } = focus.kind else {
|
||||
return;
|
||||
};
|
||||
let from = match self.pad.menu.nudge {
|
||||
Some((rect, x)) if rect == focus.rect => x,
|
||||
_ => gamepad::slider_x(track, value),
|
||||
};
|
||||
let x = (from + by).clamp(track[0], track[0] + track[1]);
|
||||
self.pad.menu.nudge = Some((focus.rect, x));
|
||||
self.pad_click_at(event_loop, (x, focus.center().1), false);
|
||||
}
|
||||
|
||||
/// Step the focus that way: vanilla's arrow-key navigation; past a
|
||||
/// list's last visible row, the list scrolls and the step goes on once
|
||||
/// the next frame is drawn.
|
||||
fn pad_focus_move(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
button: Button,
|
||||
focus: Target,
|
||||
targets: &[Target],
|
||||
) {
|
||||
let rects: Vec<Rect> = targets.iter().map(|t| t.rect).collect();
|
||||
let vertical = matches!(button, Button::DPadUp | Button::DPadDown);
|
||||
let step = if vertical {
|
||||
@@ -551,12 +610,17 @@ impl Game<'_> {
|
||||
}
|
||||
|
||||
fn pad_focus_on(&mut self, event_loop: &ActiveEventLoop, target: Target) {
|
||||
self.pad.menu.focus = Some(target);
|
||||
let left = self.pad.menu.focus.replace(target);
|
||||
self.pad.menu.pending = None;
|
||||
self.pad.menu.nudge = None;
|
||||
if !self.pad.menu.held[0] {
|
||||
self.pad_pointer_to(event_loop, target.center());
|
||||
}
|
||||
if self.pad.menu.keyboard {
|
||||
// The key itself was narrated (`narrate_input`).
|
||||
self.key_focus_landed(event_loop, left, target);
|
||||
return;
|
||||
}
|
||||
// The screen's narration hears it as an arrow key moving the focus.
|
||||
self.narration.set_input(InputType::KeyboardArrow);
|
||||
self.narration.keyboard_action();
|
||||
@@ -645,9 +709,13 @@ impl Game<'_> {
|
||||
.last_poll
|
||||
.map_or(0.0, |t| now.duration_since(t).as_secs_f32().min(0.05));
|
||||
self.pad.menu.last_poll = Some(now);
|
||||
let in_menu = self.pad_in_menu() && self.settings.controller.enabled;
|
||||
self.key_focus_frame();
|
||||
let in_menu =
|
||||
self.pad_in_menu() && (self.settings.controller.enabled || self.pad.menu.keyboard);
|
||||
if !in_menu {
|
||||
let menu = &mut self.pad.menu;
|
||||
menu.keyboard = false;
|
||||
menu.slider_edit = false;
|
||||
menu.open_polls = 0;
|
||||
menu.pos = None;
|
||||
menu.repeat = None;
|
||||
@@ -662,12 +730,16 @@ impl Game<'_> {
|
||||
return;
|
||||
}
|
||||
self.pad.menu.open_polls = self.pad.menu.open_polls.saturating_add(1);
|
||||
// A real mouse move hands the pointer back.
|
||||
// A real mouse move hands the pointer back. The keyboard's focus
|
||||
// stays where the next Tab or arrow goes on from.
|
||||
if let Some(at) = self.pad.menu.pos
|
||||
&& ((at.0 - self.cursor.0).abs() > 0.5 || (at.1 - self.cursor.1).abs() > 0.5)
|
||||
{
|
||||
self.pad.menu.pos = None;
|
||||
self.pad.menu.focus = None;
|
||||
let focus = self.pad.menu.focus.take();
|
||||
if std::mem::take(&mut self.pad.menu.keyboard) {
|
||||
self.pad.menu.origin = focus.map(|t| t.rect);
|
||||
}
|
||||
self.pad.in_use = false;
|
||||
}
|
||||
// Focus lands on the menu as it opens when the pad was in use.
|
||||
@@ -681,7 +753,7 @@ impl Game<'_> {
|
||||
Vec::new()
|
||||
};
|
||||
let focusing = focus_mode && !targets.is_empty();
|
||||
if focusing {
|
||||
if focusing && !self.key_focus_restart(event_loop, &targets) {
|
||||
self.pad_follow_focus(event_loop, &targets);
|
||||
}
|
||||
let (move_deadzone, look_deadzone) = (
|
||||
@@ -755,7 +827,8 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
self.pad_auto_keyboard();
|
||||
self.pad_show_os_pointer(self.pad.menu.pos.is_none());
|
||||
// Vanilla's keyboard focus leaves the mouse pointer alone.
|
||||
self.pad_show_os_pointer(self.pad.menu.pos.is_none() || self.pad.menu.keyboard);
|
||||
}
|
||||
|
||||
/// Focus mode, each poll: find the focused widget in the new frame (a
|
||||
@@ -808,7 +881,15 @@ impl Game<'_> {
|
||||
self.pad.menu.focus = gamepad::refind(focus.rect, &rects, 12.0).map(|i| targets[i]);
|
||||
if self.pad.menu.focus.is_none() && self.pad.menu.pos.is_some() {
|
||||
// The widget went away (a page changed): start again.
|
||||
self.pad.menu.focus = self.pad_first_target(targets);
|
||||
let first = self.pad_first_target(targets);
|
||||
if self.pad.menu.keyboard
|
||||
&& let Some(first) = first
|
||||
{
|
||||
// Screen.setInitialFocus after a keyboard press.
|
||||
self.pad_focus_on(event_loop, first);
|
||||
} else {
|
||||
self.pad.menu.focus = first;
|
||||
}
|
||||
}
|
||||
} else if self.pad.menu.pos.is_some() {
|
||||
// The pad drives but nothing is focused (switched from the pointer).
|
||||
@@ -827,6 +908,13 @@ impl Game<'_> {
|
||||
Some((rect, x)) if rect == focus.rect => (x, focus.center().1),
|
||||
_ => focus.center(),
|
||||
};
|
||||
// A text field the keyboard focused keeps the pointer where its
|
||||
// click put the caret.
|
||||
let [x, y, w, h] = focus.rect;
|
||||
let inside = (x..x + w).contains(&self.cursor.0) && (y..y + h).contains(&self.cursor.1);
|
||||
if self.pad.menu.keyboard && focus.kind == Kind::Field && inside {
|
||||
return;
|
||||
}
|
||||
if (at.0 - self.cursor.0).abs() > 0.5 || (at.1 - self.cursor.1).abs() > 0.5 {
|
||||
self.pad_pointer_to(event_loop, at);
|
||||
}
|
||||
@@ -866,6 +954,292 @@ impl Game<'_> {
|
||||
})
|
||||
}
|
||||
|
||||
// ---- the keyboard: vanilla's focus navigation (`key_focus`) ----
|
||||
|
||||
/// The keyboard drives the open menu's focus.
|
||||
pub(super) fn key_focus_driving(&self) -> bool {
|
||||
self.pad.menu.keyboard && self.pad_in_menu()
|
||||
}
|
||||
|
||||
/// A pointer event the keyboard sends (a press, the hidden pointer on
|
||||
/// the focused widget) is being handled: the screen's narration doesn't
|
||||
/// take it for the mouse (`InputType` stays the keyboard's).
|
||||
pub(super) fn key_focus_quiet(&self) -> bool {
|
||||
self.pad.menu.synthetic && self.pad.menu.keyboard
|
||||
}
|
||||
|
||||
/// The screen keeps the navigation keys for itself: typing in chat (Tab
|
||||
/// completes a command), a sign, a book, the creative, recipe-book and
|
||||
/// mods searches, the anvil's name, a command block, a dialog, a key or
|
||||
/// button being bound, the credits, the on-screen keyboard.
|
||||
fn key_focus_blocked(&self) -> bool {
|
||||
let playing_screen = |screen| self.title.is_none() && self.screen == screen;
|
||||
self.pad.menu.osk.is_some()
|
||||
|| playing_screen(Screen::Chat)
|
||||
|| self.sign_edit.is_some()
|
||||
|| matches!(self.book, Some(super::books::BookScreen::Edit { .. }))
|
||||
|| (playing_screen(Screen::Creative) && self.creative_screen.tab().search)
|
||||
|| self.recipe_search_focused()
|
||||
|| self.mod_browser_typing()
|
||||
|| self.station_typing()
|
||||
|| self.screen == Screen::CommandBlock
|
||||
|| self.screen == Screen::Win
|
||||
|| self.dialog_open()
|
||||
|| (self.screen == Screen::Options
|
||||
&& (self.options_waiting_for_key()
|
||||
|| self.ui.options.selected_pad_action.is_some()))
|
||||
}
|
||||
|
||||
/// A key pressed in a menu, before the screen takes it
|
||||
/// (`Screen.keyPressed`): Tab, Shift+Tab and the arrows move the focus,
|
||||
/// Enter, Space and keypad Enter press the focused widget; the focused
|
||||
/// widget's own keys come first (a text field's caret, a slider's
|
||||
/// value). Whether the key was used; when not, the screen has it.
|
||||
pub(super) fn key_focus_key(&mut self, event_loop: &ActiveEventLoop, key: KeyCode) -> bool {
|
||||
let Some(nav) = key_focus::nav_of(key, self.has_shift_down()) else {
|
||||
return false;
|
||||
};
|
||||
if !self.pad_in_menu() || self.key_focus_blocked() {
|
||||
return false;
|
||||
}
|
||||
let targets = self.key_targets();
|
||||
if targets.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let find = |rect: Rect| {
|
||||
targets
|
||||
.iter()
|
||||
.find(|t| t.rect.iter().zip(rect).all(|(a, b)| (a - b).abs() < 0.5))
|
||||
.copied()
|
||||
};
|
||||
// The focus: the keyboard's or the controller's, else what a click
|
||||
// focused (a slider a click focused changes with the arrows).
|
||||
let mut slider_edit = self.pad.menu.slider_edit;
|
||||
let mut focus = self
|
||||
.pad
|
||||
.menu
|
||||
.focus
|
||||
.filter(|_| self.pad_focus_mode())
|
||||
.and_then(|f| find(f.rect));
|
||||
if focus.is_none()
|
||||
&& let Some(clicked) = self.pad.menu.origin.and_then(find)
|
||||
{
|
||||
focus = Some(clicked);
|
||||
slider_edit = true;
|
||||
}
|
||||
let ctx = key_focus::Context {
|
||||
slider_edit,
|
||||
typing: self.edit_box_focused(),
|
||||
rows_take_enter: self.title.as_ref().is_some_and(|t| t.list_focused()),
|
||||
gui: self.gui_size(),
|
||||
};
|
||||
let act = key_focus::act(nav, focus, &targets, ctx);
|
||||
if act == Act::Pass {
|
||||
return false;
|
||||
}
|
||||
if !self.pad.menu.keyboard {
|
||||
self.pad.menu.key_screen = self.key_screen();
|
||||
self.pad.menu.key_restart = None;
|
||||
}
|
||||
let menu = &mut self.pad.menu;
|
||||
menu.keyboard = true;
|
||||
menu.origin = None;
|
||||
menu.focus = focus;
|
||||
menu.slider_edit = slider_edit;
|
||||
if menu.pos.is_none() {
|
||||
menu.pos = Some(self.cursor);
|
||||
}
|
||||
match (act, focus) {
|
||||
(Act::Focus(i), _) => {
|
||||
self.pad_focus_on(event_loop, targets[i]);
|
||||
self.pad.menu.slider_edit = key_focus::slider_edit_on_focus(nav);
|
||||
}
|
||||
(Act::Step(button), Some(focus)) => {
|
||||
self.pad_focus_move(event_loop, button, focus, &targets);
|
||||
}
|
||||
(Act::Press, Some(focus)) => {
|
||||
// AbstractButton.keyPressed: onPress, with its click sound.
|
||||
self.pad_click_at(event_loop, focus.center(), false);
|
||||
}
|
||||
(Act::ToggleSlider, _) => self.pad.menu.slider_edit = !slider_edit,
|
||||
(Act::Nudge(sign), Some(focus)) => self.pad_nudge(event_loop, focus, sign),
|
||||
_ => {}
|
||||
}
|
||||
// The narration reads a focused slider's `canChangeValue` from the
|
||||
// input type (Tab: it can, an arrow: it can't); keep them in step
|
||||
// after Enter flipped it or a click let it.
|
||||
if let Some(Target {
|
||||
kind: Kind::Slider { .. },
|
||||
..
|
||||
}) = self.pad.menu.focus
|
||||
{
|
||||
self.narration.set_input(if self.pad.menu.slider_edit {
|
||||
InputType::KeyboardTab
|
||||
} else {
|
||||
InputType::KeyboardArrow
|
||||
});
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Which screen is open, as far as the keyboard's focus cares.
|
||||
fn key_screen(&self) -> u64 {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
std::mem::discriminant(&self.screen).hash(&mut hasher);
|
||||
if self.screen == Screen::Options {
|
||||
std::mem::discriminant(&self.ui.options.page()).hash(&mut hasher);
|
||||
}
|
||||
self.title
|
||||
.as_ref()
|
||||
.map(|t| t.screen_key())
|
||||
.hash(&mut hasher);
|
||||
(self.sign_edit.is_some(), self.book.is_some()).hash(&mut hasher);
|
||||
(
|
||||
self.link_prompt.is_some(),
|
||||
self.server_packs.prompt.is_some(),
|
||||
)
|
||||
.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
/// Each poll while the keyboard has the focus: another screen opened
|
||||
/// (a pressed button opened it), so once it is drawn the focus starts on
|
||||
/// its first widget (`Screen.setInitialFocus` after a key). Whether the
|
||||
/// focus is waiting for that or just started again.
|
||||
fn key_focus_restart(&mut self, event_loop: &ActiveEventLoop, targets: &[Target]) -> bool {
|
||||
if !self.pad.menu.keyboard {
|
||||
return false;
|
||||
}
|
||||
let screen = self.key_screen();
|
||||
if screen != self.pad.menu.key_screen {
|
||||
self.pad.menu.key_screen = screen;
|
||||
self.pad.menu.key_restart = Some(widgets::generation());
|
||||
self.pad.menu.pending = None;
|
||||
}
|
||||
let Some(changed_at) = self.pad.menu.key_restart else {
|
||||
return false;
|
||||
};
|
||||
// Two frames on: the first may have been drawn before the click.
|
||||
if widgets::generation() > changed_at + 1 {
|
||||
self.pad.menu.key_restart = None;
|
||||
self.pad.menu.focus = None;
|
||||
if let Some(first) = self.pad_first_target(targets) {
|
||||
self.pad_focus_on(event_loop, first);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// The keyboard's focus moved from `left` to `target`
|
||||
/// (`ComponentPath.applyFocus`): a slider's arrows don't change it yet,
|
||||
/// a text field takes the typing (a click at its end, where Tab leaves
|
||||
/// the caret) and the one left behind lets go of it, and a world row is
|
||||
/// selected (`AbstractSelectionList.setFocused`).
|
||||
fn key_focus_landed(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
left: Option<Target>,
|
||||
target: Target,
|
||||
) {
|
||||
self.pad.menu.slider_edit = false;
|
||||
if left.is_some_and(|l| l.rect == target.rect) {
|
||||
return;
|
||||
}
|
||||
if left.is_some_and(|l| l.kind == Kind::Field)
|
||||
&& target.kind != Kind::Field
|
||||
&& let Some(title) = self.title.as_mut()
|
||||
{
|
||||
title.blur_field();
|
||||
}
|
||||
match target.kind {
|
||||
Kind::Field => {
|
||||
self.pad_click_at(event_loop, key_focus::field_click(target.rect), false);
|
||||
}
|
||||
Kind::Row => {
|
||||
let size = self.gui_size();
|
||||
if let Some(title) = self.title.as_mut() {
|
||||
title.focus_world_row(target.center(), size);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// The real mouse, before the screen takes it: where it is, and a press
|
||||
/// or a scroll hands it the pointer back from the keyboard, at the
|
||||
/// place the mouse left it (`setLastInputType(MOUSE)`). A left press
|
||||
/// focuses what it lands on, for the next Tab or arrow.
|
||||
pub(super) fn key_focus_mouse(&mut self, event_loop: &ActiveEventLoop, event: &WindowEvent) {
|
||||
if self.pad.menu.synthetic {
|
||||
return;
|
||||
}
|
||||
match event {
|
||||
WindowEvent::CursorMoved { position, .. } => {
|
||||
self.pad.menu.real_pointer = Some(*position);
|
||||
}
|
||||
WindowEvent::MouseInput {
|
||||
state: ElementState::Pressed,
|
||||
button,
|
||||
..
|
||||
} => {
|
||||
self.key_focus_to_mouse(event_loop);
|
||||
if *button == MouseButton::Left && self.pad_in_menu() {
|
||||
let (cx, cy) = self.cursor;
|
||||
self.pad.menu.origin = self
|
||||
.key_targets()
|
||||
.into_iter()
|
||||
.find(|t| {
|
||||
let [x, y, w, h] = t.rect;
|
||||
cx >= x && cy >= y && cx < x + w && cy < y + h
|
||||
})
|
||||
.map(|t| t.rect);
|
||||
}
|
||||
}
|
||||
WindowEvent::MouseWheel { .. } => self.key_focus_to_mouse(event_loop),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// The keyboard lets go of the pointer: it goes back to the real mouse.
|
||||
fn key_focus_to_mouse(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if !std::mem::take(&mut self.pad.menu.keyboard) {
|
||||
return;
|
||||
}
|
||||
let menu = &mut self.pad.menu;
|
||||
menu.origin = menu.focus.take().map(|t| t.rect);
|
||||
menu.pos = None;
|
||||
menu.pending = None;
|
||||
menu.nudge = None;
|
||||
let real = menu.real_pointer;
|
||||
if let (Some(position), Some(id)) = (real, self.window.as_ref().map(|w| w.id())) {
|
||||
self.pad_synthetic_event(
|
||||
event_loop,
|
||||
id,
|
||||
WindowEvent::CursorMoved {
|
||||
device_id: DeviceId::dummy(),
|
||||
position,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Each poll: what a click focused is forgotten once it is gone (the
|
||||
/// screen changed), and out of menus.
|
||||
fn key_focus_frame(&mut self) {
|
||||
let Some(origin) = self.pad.menu.origin else {
|
||||
return;
|
||||
};
|
||||
let there = self.pad_in_menu()
|
||||
&& self
|
||||
.key_targets()
|
||||
.iter()
|
||||
.any(|t| t.rect.iter().zip(origin).all(|(a, b)| (a - b).abs() < 0.5));
|
||||
if !there {
|
||||
self.pad.menu.origin = None;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- text fields ----
|
||||
|
||||
/// A text field has keyboard focus: chat, a sign, a book being written,
|
||||
@@ -1053,14 +1427,6 @@ impl Game<'_> {
|
||||
/// one-pixel outline (`AbstractSelectionList`'s focused selection,
|
||||
/// `EditBox`'s highlighted border).
|
||||
fn draw_pad_focus(&self, batch: &mut HudBatch, focus: Target) {
|
||||
let [x, y, w, h] = focus.rect;
|
||||
if matches!(focus.kind, Kind::Row | Kind::Field) {
|
||||
let atlas = &self.gui.atlas;
|
||||
let white = rgba(255, 255, 255, 255);
|
||||
batch.fill(atlas, x, y, w, 1.0, white);
|
||||
batch.fill(atlas, x, y + h - 1.0, w, 1.0, white);
|
||||
batch.fill(atlas, x, y, 1.0, h, white);
|
||||
batch.fill(atlas, x + w - 1.0, y, 1.0, h, white);
|
||||
}
|
||||
key_focus::draw_outline(&self.gui.atlas, batch, focus);
|
||||
}
|
||||
}
|
||||
@@ -3544,6 +3544,51 @@ Would you like to enable the Narrator or visit the Accessibility Settings?",
|
||||
};
|
||||
}
|
||||
}
|
||||
/// The keyboard's focus landed on the world row at `point`
|
||||
/// (`AbstractSelectionList.setFocused`): it is selected, never joined,
|
||||
/// and the search box no longer has the keys. Whether a row was there.
|
||||
pub fn focus_world_row(&mut self, point: (f32, f32), size: (f32, f32)) -> bool {
|
||||
if self.rules.get_mut().is_some() {
|
||||
return false;
|
||||
}
|
||||
let Mode::Select { worlds, .. } = &self.mode else {
|
||||
return false;
|
||||
};
|
||||
let size = (size.0.ceil(), size.1.ceil());
|
||||
let Some(index) = self
|
||||
.world_row_at(worlds, point, size)
|
||||
.and_then(|(row, _)| self.visible_worlds(worlds).nth(row).map(|(i, _)| i))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
self.search_focused = false;
|
||||
if let Mode::Select { selected, .. } = &mut self.mode {
|
||||
*selected = Some(index);
|
||||
}
|
||||
true
|
||||
}
|
||||
/// Which screen the title shows (a new one starts the keyboard's focus
|
||||
/// again, `Screen.setInitialFocus`).
|
||||
pub fn screen_key(&self) -> u64 {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
std::mem::discriminant(&self.mode).hash(&mut hasher);
|
||||
if let Mode::Multiplayer(screen) = &self.mode {
|
||||
std::mem::discriminant(screen).hash(&mut hasher);
|
||||
}
|
||||
self.rules.borrow().is_some().hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
/// The keyboard's focus left a text field for another widget: the field
|
||||
/// loses the keys (`EditBox.setFocused(false)`).
|
||||
pub fn blur_field(&mut self) {
|
||||
match &mut self.mode {
|
||||
Mode::Select { .. } => self.search_focused = false,
|
||||
Mode::Edit { focused, .. } => *focused = false,
|
||||
Mode::Create { focus, .. } => *focus = 2,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
/// The server auto-reconnect retries now, once its countdown is over.
|
||||
pub fn due_retry(&mut self) -> Option<crate::reconnect::Retry> {
|
||||
match &mut self.mode {
|
||||
|
||||
Reference in new issue
Block a user