mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Controllers: Replay Studio's recordings and buttons take the focus
Its rows and buttons only painted, so a pad reached them through the crosshair alone. The D-pad now walks the recordings to the buttons, the right stick scrolls them, and Done declares its button. A notch is now one whole row, since nothing clips a row that's partly scrolled off, and the scroll limit counts the rows that fit, so the last recording can be reached with the mouse too. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
13f70ad482
commit
f8a7a59085
4 files changed
+98
-40
No files matched your search
@@ -4459,6 +4459,10 @@ impl OptionsUi {
|
||||
self.graphics.scroll(s, size, amount);
|
||||
return;
|
||||
}
|
||||
if self.page() == Page::Studio {
|
||||
self.studio.wheel(amount);
|
||||
return;
|
||||
}
|
||||
if Self::controller_page(self.page()).is_some() {
|
||||
self.controller.scroll(s, size, amount);
|
||||
return;
|
||||
|
||||
@@ -103,6 +103,12 @@ pub struct StudioUi {
|
||||
pub record_dir: PathBuf,
|
||||
}
|
||||
|
||||
/// A recording's row and the gap under it, in GUI pixels.
|
||||
const ROW_STEP: f32 = 40.0;
|
||||
const ROW_GAP: f32 = 4.0;
|
||||
/// The panel's margins above and below its rows.
|
||||
const LIST_MARGINS: f32 = 10.0;
|
||||
|
||||
impl Default for StudioUi {
|
||||
fn default() -> Self {
|
||||
let dir = PathBuf::from("recordings");
|
||||
@@ -270,18 +276,19 @@ impl StudioUi {
|
||||
);
|
||||
} else {
|
||||
// Render scrollable cards
|
||||
let card_h = 36.0;
|
||||
let card_gap = 4.0;
|
||||
let row_step = card_h + card_gap;
|
||||
let visible_cards = (panel_h / row_step).floor() as usize;
|
||||
let card_h = ROW_STEP - ROW_GAP;
|
||||
let row_step = ROW_STEP;
|
||||
// Rows that fit whole between the panel's margins, as the loop below draws them.
|
||||
let visible_cards = ((panel_h - LIST_MARGINS + ROW_GAP) / row_step).floor() as usize;
|
||||
let max_scroll =
|
||||
(self.recordings.len().saturating_sub(visible_cards) as f32 * row_step).max(0.0);
|
||||
self.scroll = self.scroll.clamp(0.0, max_scroll);
|
||||
|
||||
let card_w = panel_w - 12.0;
|
||||
let card_x = panel_x + 6.0;
|
||||
let mut y = panel_y + 6.0 - (self.scroll % row_step);
|
||||
let start_idx = (self.scroll / row_step) as usize;
|
||||
// Whole rows: nothing here clips one that's partly scrolled off.
|
||||
let mut y = panel_y + 6.0;
|
||||
let start_idx = (self.scroll / row_step).round() as usize;
|
||||
|
||||
for (idx, item) in self.recordings.iter().enumerate().skip(start_idx) {
|
||||
if y + card_h > panel_y + panel_h - 4.0 {
|
||||
@@ -289,6 +296,8 @@ impl StudioUi {
|
||||
}
|
||||
|
||||
let rect = [card_x, y, card_w, card_h];
|
||||
crate::widgets::record_as(&item.title, rect, true, crate::widgets::Kind::Row);
|
||||
crate::widgets::target_as(rect, crate::widgets::Kind::Row);
|
||||
let is_hovered = ui::hovered(cursor, card_x, y, card_w, card_h);
|
||||
let is_selected = self.selected == Some(idx);
|
||||
|
||||
@@ -334,6 +343,12 @@ impl StudioUi {
|
||||
y += row_step;
|
||||
}
|
||||
|
||||
crate::widgets::scroll_area(
|
||||
[panel_x, panel_y, panel_w, panel_h],
|
||||
self.scroll,
|
||||
max_scroll,
|
||||
);
|
||||
|
||||
// Scrollbar track & thumb if scrollable
|
||||
if max_scroll > 0.0 {
|
||||
let track_x = panel_x + panel_w - 4.0;
|
||||
@@ -357,35 +372,24 @@ impl StudioUi {
|
||||
// Button 1: Open Folder (Left)
|
||||
let f_btn = [panel_x, btn_y, 130.0, btn_h];
|
||||
let f_hover = ui::hovered(cursor, f_btn[0], f_btn[1], f_btn[2], f_btn[3]);
|
||||
style::button(
|
||||
batch,
|
||||
atlas,
|
||||
f_btn,
|
||||
&Text::tr("horizon.studio.open_folder", &[]),
|
||||
(f_hover, true, false),
|
||||
);
|
||||
let label = Text::tr("horizon.studio.open_folder", &[]);
|
||||
crate::widgets::record(&label.plain(), f_btn, true);
|
||||
style::button(batch, atlas, f_btn, &label, (f_hover, true, false));
|
||||
|
||||
// Button 2: Refresh (Center-Left)
|
||||
let r_btn = [panel_x + 136.0, btn_y, 65.0, btn_h];
|
||||
let r_hover = ui::hovered(cursor, r_btn[0], r_btn[1], r_btn[2], r_btn[3]);
|
||||
style::button(
|
||||
batch,
|
||||
atlas,
|
||||
r_btn,
|
||||
&Text::tr("horizon.studio.refresh", &[]),
|
||||
(r_hover, true, false),
|
||||
);
|
||||
let label = Text::tr("horizon.studio.refresh", &[]);
|
||||
crate::widgets::record(&label.plain(), r_btn, true);
|
||||
style::button(batch, atlas, r_btn, &label, (r_hover, true, false));
|
||||
|
||||
// Button 3: Done / Back (Right)
|
||||
let d_btn = [panel_x + panel_w - 64.0, btn_y, 64.0, btn_h];
|
||||
let d_hover = ui::hovered(cursor, d_btn[0], d_btn[1], d_btn[2], d_btn[3]);
|
||||
style::button(
|
||||
batch,
|
||||
atlas,
|
||||
d_btn,
|
||||
&Text::tr("gui.done", &[]),
|
||||
(d_hover, true, false),
|
||||
);
|
||||
let label = Text::tr("gui.done", &[]);
|
||||
crate::widgets::record(&label.plain(), d_btn, true);
|
||||
style::button(batch, atlas, d_btn, &label, (d_hover, true, false));
|
||||
crate::widgets::declare_action(crate::widgets::Action::Done, d_btn, true);
|
||||
}
|
||||
|
||||
/// Process mouse click on the Studio Browser screen.
|
||||
@@ -418,13 +422,12 @@ impl StudioUi {
|
||||
}
|
||||
|
||||
// Card clicks
|
||||
let card_h = 36.0;
|
||||
let card_gap = 4.0;
|
||||
let row_step = card_h + card_gap;
|
||||
let card_h = ROW_STEP - ROW_GAP;
|
||||
let row_step = ROW_STEP;
|
||||
let card_w = panel_w - 12.0;
|
||||
let card_x = panel_x + 6.0;
|
||||
let start_idx = (self.scroll / row_step) as usize;
|
||||
let mut y = panel_y + 6.0 - (self.scroll % row_step);
|
||||
let start_idx = (self.scroll / row_step).round() as usize;
|
||||
let mut y = panel_y + 6.0;
|
||||
|
||||
for (idx, _) in self.recordings.iter().enumerate().skip(start_idx) {
|
||||
if y + card_h > panel_y + panel_h - 4.0 {
|
||||
@@ -444,7 +447,8 @@ impl StudioUi {
|
||||
|
||||
/// Scroll list by mouse wheel delta.
|
||||
pub fn wheel(&mut self, amount: f32) {
|
||||
self.scroll = (self.scroll - amount * 24.0).max(0.0);
|
||||
// A notch is a row, as a focus step needs (`gamepad::list_step`).
|
||||
self.scroll = (self.scroll - amount * ROW_STEP).max(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,6 +531,6 @@ mod tests {
|
||||
assert_eq!(ui.scroll, 0.0); // clamped at 0
|
||||
ui.scroll = 100.0;
|
||||
ui.wheel(1.0);
|
||||
assert_eq!(ui.scroll, 76.0);
|
||||
assert_eq!(ui.scroll, 100.0 - ROW_STEP);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -369,7 +369,7 @@ Steam + X opens it at any time. "On-Screen Keyboard: Auto / Horizon / Steam"
|
||||
Both modes send the same pointer events, but neither reaches everything. Focus mode
|
||||
registers targets on: the title menus (main, world list and its search, create
|
||||
and edit world, delete, multiplayer list and its edit screens), every options
|
||||
page (vanilla's and Horizon Settings with the pages it opens except Replay Studio, key and
|
||||
page (vanilla's and Horizon Settings with the pages it opens, key and
|
||||
controller binds, language list), pause and Play with Friends, death, chat, signs and books (their buttons), the
|
||||
survival and creative inventories, every container and station (chests,
|
||||
furnaces, brewing, crafter, crafting table with its recipe book, anvil,
|
||||
@@ -377,7 +377,7 @@ grindstone, smithing, stonecutter, loom, cartography, enchanting, beacon,
|
||||
villager and wandering-trader trades, mount inventories), and the LAN /
|
||||
world options. Where a screen draws something clickable no target names
|
||||
(the advancements map, the statistics lists, the pack screen's rows, the
|
||||
book's page text), the pad drives the pointer instead, as it does on Replay Studio, which registers nothing yet.
|
||||
book's page text), the pad drives the pointer instead.
|
||||
|
||||
## Glyphs
|
||||
|
||||
|
||||
+54
-4
@@ -32,6 +32,7 @@ Cases:
|
||||
- long_label: a label too wide for its button slides, beside its glyph.
|
||||
- every_screen_has_targets: the title's submenus and every options page give the focus something to land on.
|
||||
- pause_icon: the pause menu's icons are targets, and A presses one.
|
||||
- studio_rows: the D-pad walks Replay Studio's recordings to its buttons, and B leaves.
|
||||
- erase_checkbox: the D-pad reaches Optimize World's Erase Cached Data box, and A ticks it.
|
||||
Needs built fast binaries (`cargo build --profile fast -p horizon-client -p horizon-client-lib --bins`)
|
||||
and prepared resources in .local/horizon-client/26.3. Exit status 0 when every case passes.
|
||||
@@ -45,6 +46,7 @@ import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import zipfile
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
@@ -1486,14 +1488,12 @@ OPTIONS_PAGES = ('main', 'video', 'sound', 'controls', 'keybinds', 'controller',
|
||||
'accessibility', 'language', 'font', 'packs', 'mods', 'shader-packs', 'graphics', 'graphics-advanced',
|
||||
'graphics-hdr', 'extras', 'pvp', 'credits', 'horizon', 'agent')
|
||||
# Screens allowed to draw no target, and why. The pad falls back to the crosshair on these.
|
||||
NO_TARGETS = {'studio': 'its buttons and rows only paint, nothing registers them yet'}
|
||||
TARGETS_WAIT = 5
|
||||
STUDIO_RECORDINGS, STUDIO_BUTTONS = 12, 3
|
||||
|
||||
|
||||
def check_has_targets(game, check, screen):
|
||||
"""A focus-mode screen draws a target soon after it opens; pointer screens (packs) use the crosshair by design."""
|
||||
if screen in NO_TARGETS:
|
||||
return
|
||||
end = time.time() + TARGETS_WAIT
|
||||
while True:
|
||||
menu = game.menu()
|
||||
@@ -1532,6 +1532,56 @@ def every_screen_has_targets(game, check, single, options, quit_game):
|
||||
check_has_targets(game, check, 'studio')
|
||||
|
||||
|
||||
def seed_recordings(game_home):
|
||||
"""More saved recordings than the Studio page shows at once."""
|
||||
folder = game_home / 'recordings'
|
||||
folder.mkdir()
|
||||
for i in range(STUDIO_RECORDINGS):
|
||||
with zipfile.ZipFile(folder / f'run{i:02}.mcpr', 'w') as archive:
|
||||
archive.writestr('metaData.json', json.dumps({'serverName': f'Server{i:02}', 'date': i, 'duration': 1000}))
|
||||
|
||||
|
||||
def studio_rows(args, name):
|
||||
"""The D-pad walks Replay Studio's recordings, scrolling the hidden ones, to its buttons."""
|
||||
game = Game(args, name, seed=seed_recordings)
|
||||
check = Check()
|
||||
try:
|
||||
game.wait_targets()
|
||||
game.ctl('options', 'horizon')
|
||||
time.sleep(SETTLE)
|
||||
click_widget(game, widget(game, 'studio'))
|
||||
time.sleep(SETTLE)
|
||||
game.wait_targets()
|
||||
first = game.rows()[0]['label']
|
||||
for _ in range(4):
|
||||
game.stick()
|
||||
check(game.rows()[0]['label'] != first, f'the right stick did not scroll the list: {first} still on top')
|
||||
for _ in range(8):
|
||||
game.stick(1)
|
||||
check(game.rows()[0]['label'] == first, f'the right stick did not scroll back: {game.rows()[0]["label"]} on top')
|
||||
kinds = [t['kind'] for t in game.menu()['targets']]
|
||||
check(kinds.count('row') > 0 and kinds.count('row') < STUDIO_RECORDINGS, f'recordings in view: {kinds}')
|
||||
check(kinds.count('button') == STUDIO_BUTTONS, f'its buttons: {kinds}')
|
||||
game.pad('down')
|
||||
walked = set()
|
||||
for _ in range(STUDIO_RECORDINGS + STUDIO_BUTTONS):
|
||||
walked.add(game.focused_label())
|
||||
before = game.focus()
|
||||
kind = next((t['kind'] for t in game.menu()['targets'] if tuple(t['rect']) == before), None)
|
||||
if kind == 'button':
|
||||
break
|
||||
game.pad('down')
|
||||
check(kind == 'button', f'the D-pad never left the list for the buttons: on {kind}')
|
||||
check(len(walked) >= STUDIO_RECORDINGS, f'the D-pad skipped hidden recordings: {sorted(map(str, walked))}')
|
||||
check(game.menu()['actions'], 'Done declares no button')
|
||||
game.pad('east')
|
||||
time.sleep(SETTLE)
|
||||
check(not any(w['id'] == 'open-folder' for w in game.widgets()), 'B stayed on the Studio page')
|
||||
finally:
|
||||
game.quit()
|
||||
return check.failures
|
||||
|
||||
|
||||
def pause_icon(args, name):
|
||||
game = Game(args, name, world=True)
|
||||
check = Check()
|
||||
@@ -1984,7 +2034,7 @@ CASES = {'idle_a': idle_a, 'idle_b': idle_b, 'idle_x': idle_x, 'release': releas
|
||||
'controller_settings_keys': controller_settings_keys,
|
||||
'controller_notices': controller_notices,
|
||||
'every_screen_has_targets': every_screen_has_targets,
|
||||
'pause_icon': pause_icon,
|
||||
'studio_rows': studio_rows, 'pause_icon': pause_icon,
|
||||
'erase_checkbox': erase_checkbox,
|
||||
'glyph_art': glyph_art,
|
||||
'glyph_art_world': glyph_art_world,
|
||||
|
||||
Reference in new issue
Block a user