Files
benilla-octo/crates/benilla/src/ui_hide.rs
T
SamandClaude Fable 5 1531628a28 copy and paste works everywhere, and the sun keeps its colour
- One text-input law for every field. Login, character create and the delete
  dialog each improvised their own tiny keyboard handling — no caret movement,
  no selection, no clipboard, and Ctrl/Cmd+V typed a literal "v" into the
  password box. All of them now run the same byte-verified edit-box law as
  chat: clipboard, select-all, word moves, shift-selection, the real 0.5 s
  caret blink. The password box masks what you copy off it, like the original.
- Copy/paste on Linux had two independent causes, one per direction: our window
  is Wayland but the clipboard backend spoke X11 only, and a copy relinquished
  the X11 selection the moment it returned (an X11 clipboard lives only as long
  as its owning connection). Both fixed — Wayland windows get a real Wayland
  clipboard, X11 keeps a held handle. AltGr is not Ctrl anymore either, so
  Polish and German layouts can type ą, ć and ź in chat instead of triggering
  select-all. Compile-checked on Linux; wants a real-session retest.
- The exterior sun keeps its colour. A per-channel clamp saturated the warm
  continental sun to pale yellow-white on every sunlit fence, post and doodad;
  the clamp now happens once, at the sum, so the fiery orange survives — the
  reported Silverpine fence and Blackrock steps match the reference's own
  arithmetic.
- A pool has a floor. Undercity's upper slime channels claimed the dry room a
  storey below them — 115 yards down still read as submerged. A pool now stops
  at its own room's floor; a sweep of every building in the shipped world found
  51 such stacked sites across 6 buildings, all closed.
- Ctrl/Cmd+Z hides the whole UI layer for clean screenshots, and instrumented
  debug runs open their window in the background instead of stealing focus.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 19:37:42 +07:00

228 lines
9.4 KiB
Rust

//! **TOGGLEUI** — the hide-the-interface binding (`CTRL-Z`, and `Cmd-Z` as its Mac twin).
//!
//! The state ([`UiHidden`]) and the binding that flips it; the two things that *obey* it are the
//! quad pass's mesh rebuild (`ui_pass::rebuild_ui_mesh` draws nothing) and the UI's pointer feed
//! (`ui_script::input::feed_ui_input` stops hit-testing). Kept out of `ui_pass` deliberately: this
//! is a *game binding*, not part of the render substrate.
use bevy::input::keyboard::KeyboardInput;
use bevy::input::ButtonState;
use bevy::prelude::*;
use crate::char_select::ClientState;
use crate::ui_script::{UiInput, UiKeyboardCapture};
/// Is the player UI hidden right now? (`CTRL-Z`/`Cmd-Z` — [`toggle_ui_hidden`].)
///
/// Hidden means *"draw nothing"*, not *"stop producing"*: the widget arena keeps ticking and both
/// quad lanes keep filling, so open panels, tooltip state, cooldown sweeps and chat scrollback are
/// exactly where they were the moment the UI comes back — only the pass's mesh batches go away.
/// **Everything that lands in `ui_pass::UiQuads` goes dark together**: the FrameXML layer, the
/// minimap, the V-plates, chat bubbles and floating combat text — the world and nothing else,
/// which is the point of the binding. What stays up: the dev overlays (their own camera and their
/// own `Ctrl`+`Cmd` chords — an instrument, not the player's UI), the glue/loading screens (Bevy UI
/// nodes, not quads), and the cursor.
///
/// The UI also stops taking the **mouse** while hidden: an invisible action bar must not eat a
/// click or arm a tooltip. The keyboard feed stays live — a hidden UI is still the client you're
/// playing, and `ENTER`/`ESCAPE`/the bar keys keep working exactly as the reference's do.
#[derive(Resource, Default)]
pub(crate) struct UiHidden(pub bool);
/// The TOGGLEUI binding + its state.
pub(crate) struct UiHidePlugin;
impl Plugin for UiHidePlugin {
fn build(&self, app: &mut App) {
app.init_resource::<UiHidden>()
.add_systems(
Update,
// After `UiInput`, like every other gameplay key reader: an EditBox that consumed
// this frame's keys has already published its capture flag by then.
toggle_ui_hidden
.after(UiInput)
.run_if(in_state(ClientState::InWorld)),
)
.add_systems(OnExit(ClientState::InWorld), show_ui);
}
}
/// `CTRL-Z` / `Cmd-Z` — the reference's TOGGLEUI binding (`BINDING_NAME_TOGGLEUI` in
/// `GlobalStrings.lua`; the 1.12.1 install's own `bindings-cache.wtf` reads `bind CTRL-Z TOGGLEUI`
/// on all three accounts in it), plus the `Cmd` twin — the Mac keyboard the director plays the
/// reference on presses `Cmd` for that binding, and nothing in game binds the Cmd plane.
///
/// Exactly one of the two, and no `Alt`/`Shift`: the reference names a binding
/// `[ALT-][CTRL-][SHIFT-]<key>` and matches it by string equality, so `CTRL-ALT-Z` is a *different*
/// entry and a modified press must never fall through to another binding (decision 0585's
/// bare-binding rule — the discipline `vplates::toggle_vplates` spends `SHIFT` under), while
/// `Ctrl`+`Cmd` together is the dev-overlay plane. Inert while an EditBox holds the keyboard: a
/// `Z` in a chat line is text.
///
/// Driven off the key **message stream** rather than `ButtonInput`'s edges, because AppKit swallows
/// `keyUp` for a key released while `Cmd` is held: the release never reaches winit, `ButtonInput`
/// goes on believing `Z` is down, and the *second* `Cmd-Z` would never produce a `just_pressed`
/// edge — the UI would hide and refuse to come back. `repeat` presses are dropped so a held chord
/// doesn't strobe.
fn toggle_ui_hidden(
mut keyboard: MessageReader<KeyboardInput>,
keys: Res<ButtonInput<KeyCode>>,
ui_capture: Res<UiKeyboardCapture>,
mut hidden: ResMut<UiHidden>,
) {
let pressed = keyboard
.read()
.any(|ev| ev.key_code == KeyCode::KeyZ && ev.state == ButtonState::Pressed && !ev.repeat);
if !pressed || ui_capture.0 || !toggle_chord(&keys) {
return;
}
hidden.0 = !hidden.0;
info!(
"ui: {} (Ctrl/Cmd-Z)",
if hidden.0 { "HIDDEN" } else { "shown" }
);
}
/// The modifier half of the chord: exactly one of `Ctrl`/`Cmd` held, and neither `Alt` nor `Shift`.
/// See [`toggle_ui_hidden`] for why each arm is what it is.
fn toggle_chord(keys: &ButtonInput<KeyCode>) -> bool {
let ctrl = keys.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]);
let cmd = keys.any_pressed([KeyCode::SuperLeft, KeyCode::SuperRight]);
let blocked = keys.any_pressed([
KeyCode::AltLeft,
KeyCode::AltRight,
KeyCode::ShiftLeft,
KeyCode::ShiftRight,
]);
(ctrl ^ cmd) && !blocked
}
/// Leaving the world clears the hide — safety, not fidelity: the binding is `InWorld`-only, so a UI
/// left hidden at logout would come back invisible with no on-screen affordance to explain it.
fn show_ui(mut hidden: ResMut<UiHidden>) {
hidden.0 = false;
}
#[cfg(test)]
mod tests {
use super::*;
/// The plugin's own wiring, driven the way winit drives it: a real [`KeyboardInput`] message
/// plus the modifier mirror. Covers what the pure-chord tests can't — the message path (the
/// `Cmd`-swallowed-`keyUp` insurance), the repeat filter, and the typing gate.
fn app(held: &[KeyCode]) -> App {
let mut app = App::new();
app.add_plugins((
MinimalPlugins,
bevy::input::InputPlugin,
bevy::state::app::StatesPlugin,
))
.insert_state(ClientState::InWorld)
.init_resource::<UiKeyboardCapture>()
.add_plugins(UiHidePlugin);
for &k in held {
app.world_mut()
.resource_mut::<ButtonInput<KeyCode>>()
.press(k);
}
app
}
fn press_z(app: &mut App, repeat: bool) {
let window = Entity::PLACEHOLDER;
app.world_mut().write_message(KeyboardInput {
key_code: KeyCode::KeyZ,
logical_key: bevy::input::keyboard::Key::Character("z".into()),
state: ButtonState::Pressed,
text: Some("z".into()),
repeat,
window,
});
app.update();
}
fn hidden(app: &App) -> bool {
app.world().resource::<UiHidden>().0
}
/// The binding toggles both ways off the message stream — the second press is the one an
/// edge-driven read would lose on macOS (AppKit eats the `keyUp` under `Cmd`).
#[test]
fn cmd_z_hides_and_shows_again() {
let mut app = app(&[KeyCode::SuperLeft]);
press_z(&mut app, false);
assert!(hidden(&app), "Cmd-Z hides");
press_z(&mut app, false);
assert!(!hidden(&app), "Cmd-Z again shows");
}
/// A held chord auto-repeats; only the real press counts, or the UI would strobe.
#[test]
fn repeats_do_not_strobe() {
let mut app = app(&[KeyCode::ControlLeft]);
press_z(&mut app, false);
press_z(&mut app, true);
press_z(&mut app, true);
assert!(hidden(&app), "the repeats after the press changed nothing");
}
/// While an EditBox owns the keyboard the chord is inert — `Ctrl-Z` in a chat line is the
/// box's business, not a binding.
#[test]
fn inert_while_typing() {
let mut app = app(&[KeyCode::ControlLeft]);
app.world_mut().resource_mut::<UiKeyboardCapture>().0 = true;
press_z(&mut app, false);
assert!(!hidden(&app), "a captured keyboard eats the chord");
}
/// The bare-binding rule at the wiring level: an unmodified `Z` is the sheath toggle
/// (`player::control`), and it must not also hide the UI.
#[test]
fn bare_z_is_not_the_binding() {
let mut app = app(&[]);
press_z(&mut app, false);
assert!(!hidden(&app), "bare Z belongs to the sheath toggle");
}
fn keys(held: &[KeyCode]) -> ButtonInput<KeyCode> {
let mut k = ButtonInput::default();
for &c in held {
k.press(c);
}
k
}
/// The chord accepts either command modifier alone — `CTRL-Z` is the reference's own entry,
/// `Cmd-Z` the Mac twin — and both sides of each family.
#[test]
fn either_command_modifier_alone_fires() {
for held in [
KeyCode::ControlLeft,
KeyCode::ControlRight,
KeyCode::SuperLeft,
KeyCode::SuperRight,
] {
assert!(toggle_chord(&keys(&[held])), "{held:?} alone fires");
}
}
/// Decision 0585's bare-binding rule, both directions: an unmodified `Z` is the *sheath*
/// binding and must not hide the UI, and a press carrying an extra modifier names a different
/// binding entry (`CTRL-ALT-Z`, `CTRL-SHIFT-Z`) that this one must not answer for. `Ctrl`+`Cmd`
/// is the dev-overlay plane and is likewise not ours.
#[test]
fn bare_and_over_modified_presses_are_not_the_binding() {
assert!(!toggle_chord(&keys(&[])), "bare Z is the sheath binding");
for held in [
vec![KeyCode::ControlLeft, KeyCode::AltLeft],
vec![KeyCode::ControlLeft, KeyCode::ShiftLeft],
vec![KeyCode::SuperLeft, KeyCode::AltRight],
vec![KeyCode::SuperLeft, KeyCode::ShiftRight],
vec![KeyCode::ControlLeft, KeyCode::SuperLeft],
] {
assert!(!toggle_chord(&keys(&held)), "{held:?} is another binding");
}
}
}