mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #11534 from martipops/settings/shell-api
🤖 Agent: Extras moves onto the new settings_shell (one row per setting, search, description strip), same items and saves; design-system.md now lists Extras with Graphics/Controller as Blocks-frame pages; size (+1748) accepted because ~860 lines are the shell the next pages reuse (#11487 plan). (union of 74 heads on main: fmt, clippy -D warnings, tests green).
This commit is contained in:
10 files changed
+1753
-212
No files matched your search
@@ -604,6 +604,7 @@ impl ControllerUi {
|
||||
hover,
|
||||
focused,
|
||||
held: self.dragging == Some(item),
|
||||
key: None,
|
||||
};
|
||||
shell::row(batch, atlas, &cols, y, &view);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
//! The Horizon Extras page, on the settings shell (`settings_shell`): which rows it has and what
|
||||
//! pressing them does. Each one's value and what it does stay in `extras::Item`.
|
||||
use crate::{
|
||||
extras::{self, Item},
|
||||
lang,
|
||||
options_ui::{Event, Page},
|
||||
settings::Settings,
|
||||
settings_shell::{Group, Output, Row, Screen, Sheet, Value},
|
||||
};
|
||||
|
||||
const TITLE: &str = "horizon.pages.extras";
|
||||
const ABOUT: &str = "horizon.pages.extras.tooltip";
|
||||
/// The group of the mod that replaced an extra.
|
||||
const MODS: &str = "horizon.pages.mods";
|
||||
const ZOOM: &str = "horizon.extras.zoom";
|
||||
const ZOOM_INSTALL: &str = "horizon.extras.zoom_mod_install";
|
||||
const MOD_SETTINGS: &str = "horizon.mods.settings";
|
||||
/// Its descriptions run long, as tooltips they were: most need more than two lines.
|
||||
const NOTE_LINES: usize = 5;
|
||||
/// The official native mod that took over from the Zoom extra (#8713).
|
||||
const ZOOM_ID: &str = "zoom";
|
||||
|
||||
/// A row of the page.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum ExtraRow {
|
||||
Item(Item),
|
||||
/// The Zoom mod, at this place among the installed ones, on or off.
|
||||
Zoom(usize),
|
||||
/// Its own settings page.
|
||||
ZoomSettings(usize),
|
||||
/// It isn't installed: where to get it. A note, not a button: Options > Mods already has the
|
||||
/// one way into the Mods page (`every_setting_and_page_is_reached_from_one_place`).
|
||||
ZoomInstall,
|
||||
}
|
||||
|
||||
fn item_row(item: Item, s: &Settings) -> Row<ExtraRow> {
|
||||
let e = &s.extras;
|
||||
let value = match (item.key_name(), item.switch(e)) {
|
||||
(Some(key), _) => Value::Key(key, item.value(e)),
|
||||
(None, Some(on)) => Value::Switch(on),
|
||||
(None, None) => Value::Choice(item.value(e)),
|
||||
};
|
||||
// A setting of a switch that is off does nothing, so it is greyed out.
|
||||
let live = item
|
||||
.parent()
|
||||
.is_none_or(|parent| parent.switch(e) != Some(false));
|
||||
Row::new(ExtraRow::Item(item), item.caption_text(), value)
|
||||
.about(item.description_text())
|
||||
.live(live)
|
||||
}
|
||||
|
||||
/// Zoom, the FPS counter, Keystrokes and the Minimap are official mods now; Extras keeps a way to
|
||||
/// the one players look for here.
|
||||
fn zoom_rows(s: &Settings) -> Vec<Row<ExtraRow>> {
|
||||
let installed = &s.mods.installed;
|
||||
let found = installed
|
||||
.iter()
|
||||
.position(|m| m.id == ZOOM_ID && m.native.is_some());
|
||||
let Some(i) = found else {
|
||||
let note = Row::new(ExtraRow::ZoomInstall, lang::get(ZOOM_INSTALL), Value::Note);
|
||||
return vec![note.live(false)];
|
||||
};
|
||||
let about = installed[i].problem.as_ref();
|
||||
let about = about.unwrap_or(&installed[i].description).clone();
|
||||
let on = Value::Switch(s.mods.is_on(i));
|
||||
let mut rows = vec![Row::new(ExtraRow::Zoom(i), lang::get(ZOOM), on).about(about)];
|
||||
let page = installed[i].native.as_ref();
|
||||
if page.is_some_and(|page| !page.keys.is_empty() || !page.buttons.is_empty()) {
|
||||
let name = lang::get(MOD_SETTINGS);
|
||||
rows.push(Row::new(ExtraRow::ZoomSettings(i), name, Value::Press));
|
||||
}
|
||||
rows
|
||||
}
|
||||
|
||||
/// The page itself holds nothing: everything it shows is in the settings.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct ExtrasScreen;
|
||||
|
||||
impl Screen for ExtrasScreen {
|
||||
type Page = ();
|
||||
type Row = ExtraRow;
|
||||
fn title(&self) -> &'static str {
|
||||
TITLE
|
||||
}
|
||||
fn note_lines(&self) -> usize {
|
||||
NOTE_LINES
|
||||
}
|
||||
fn sheets(&self, s: &Settings) -> Vec<Sheet<(), ExtraRow>> {
|
||||
let mut groups = vec![Group {
|
||||
name: MODS,
|
||||
rows: zoom_rows(s),
|
||||
}];
|
||||
for row in extras::rows() {
|
||||
match row {
|
||||
Err(name) => groups.push(Group {
|
||||
name,
|
||||
rows: Vec::new(),
|
||||
}),
|
||||
Ok(item) => {
|
||||
if let Some(group) = groups.last_mut() {
|
||||
group.rows.push(item_row(item, s));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
vec![Sheet {
|
||||
id: (),
|
||||
name: lang::get(TITLE),
|
||||
about: lang::get(ABOUT),
|
||||
groups,
|
||||
}]
|
||||
}
|
||||
fn press(&mut self, s: &mut Settings, row: ExtraRow, back: bool) -> Vec<Output> {
|
||||
match row {
|
||||
ExtraRow::Item(item) => match item.key_name() {
|
||||
Some(key) => vec![Output::BindKey(key)],
|
||||
None => {
|
||||
item.cycle(&mut s.extras, back);
|
||||
vec![Output::Event(Event::Changed)]
|
||||
}
|
||||
},
|
||||
ExtraRow::Zoom(i) => {
|
||||
s.mods.toggle(i);
|
||||
vec![Output::Event(Event::Changed)]
|
||||
}
|
||||
ExtraRow::ZoomSettings(i) => vec![
|
||||
Output::Event(Event::ButtonClick),
|
||||
Output::Push(Page::NativeMod(i)),
|
||||
Output::Event(Event::NativeSettings(i)),
|
||||
],
|
||||
ExtraRow::ZoomInstall => Vec::new(),
|
||||
}
|
||||
}
|
||||
/// With the wheel extra on, the wheel over an item changes it, without a click; the mod's rows
|
||||
/// let the list scroll.
|
||||
fn wheel_step(&mut self, s: &mut Settings, row: ExtraRow, back: bool) -> Option<Vec<Output>> {
|
||||
let ExtraRow::Item(item) = row else {
|
||||
return None;
|
||||
};
|
||||
item.cycle(&mut s.extras, back);
|
||||
Some(vec![Output::Event(Event::Changed)])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::settings_shell::{Hosted, Shell};
|
||||
|
||||
const SIZE: (f32, f32) = (854.0, 480.0);
|
||||
|
||||
fn rows_of(s: &Settings) -> Vec<Row<ExtraRow>> {
|
||||
let sheets = ExtrasScreen.sheets(s);
|
||||
let groups = sheets.into_iter().flat_map(|sheet| sheet.groups);
|
||||
groups.flat_map(|group| group.rows).collect()
|
||||
}
|
||||
|
||||
/// Greyed exactly while its own switch is off, so it takes no press then.
|
||||
#[test]
|
||||
fn a_setting_of_a_switch_that_is_off_is_greyed_and_takes_no_press() {
|
||||
let mut s = Settings::default();
|
||||
let live = |s: &Settings| {
|
||||
let wheel = ExtraRow::Item(Item::MouseTweaksWheel);
|
||||
rows_of(s).iter().find(|r| r.id == wheel).unwrap().live
|
||||
};
|
||||
s.extras.mouse_tweaks = false;
|
||||
assert!(!live(&s));
|
||||
let mut shell = Shell::<ExtrasScreen>::default();
|
||||
let row = ExtraRow::Item(Item::MouseTweaksWheel);
|
||||
let at = shell.point_at(&s, SIZE, row);
|
||||
let before = s.extras.mouse_tweaks_wheel;
|
||||
assert!(
|
||||
shell
|
||||
.click(&mut s, at, SIZE, false, false, false)
|
||||
.is_empty()
|
||||
);
|
||||
assert_eq!(s.extras.mouse_tweaks_wheel, before);
|
||||
s.extras.mouse_tweaks = true;
|
||||
assert!(live(&s));
|
||||
assert!(
|
||||
!shell
|
||||
.click(&mut s, at, SIZE, false, false, false)
|
||||
.is_empty()
|
||||
);
|
||||
assert_ne!(s.extras.mouse_tweaks_wheel, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_three_way_cosmetics_row_turns_both_ways() {
|
||||
use horizon_cosmetics::prefs::Others;
|
||||
let mut s = Settings::default();
|
||||
let mut screen = ExtrasScreen;
|
||||
let row = ExtraRow::Item(Item::CosmeticsOthers);
|
||||
// Not chosen, nothing owned: it shows Hidden, and forward wraps to Shown.
|
||||
assert_eq!(s.extras.effective_others(), Others::Hidden);
|
||||
screen.press(&mut s, row, false);
|
||||
assert_eq!(s.extras.cosmetics_others, Some(Others::Shown));
|
||||
screen.press(&mut s, row, true);
|
||||
assert_eq!(s.extras.cosmetics_others, Some(Others::Hidden));
|
||||
screen.press(&mut s, row, true);
|
||||
assert_eq!(s.extras.cosmetics_others, Some(Others::NameTags));
|
||||
}
|
||||
|
||||
/// The sort key binds instead of cycling; the wheel leaves it be.
|
||||
#[test]
|
||||
fn the_key_row_waits_for_a_key() {
|
||||
let mut s = Settings::default();
|
||||
let mut screen = ExtrasScreen;
|
||||
let row = ExtraRow::Item(Item::SortKey);
|
||||
let key = s.extras.sort_key.clone();
|
||||
assert_eq!(
|
||||
screen.press(&mut s, row, false),
|
||||
vec![Output::BindKey(extras::SORT_KEY)]
|
||||
);
|
||||
screen.wheel_step(&mut s, row, true);
|
||||
assert_eq!(s.extras.sort_key, key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_wheel_flips_a_switch_and_leaves_the_mod_to_scroll() {
|
||||
let mut s = Settings::default();
|
||||
let mut screen = ExtrasScreen;
|
||||
let row = ExtraRow::Item(Item::Sort);
|
||||
assert_eq!(
|
||||
screen.wheel_step(&mut s, row, true),
|
||||
Some(vec![Output::Event(Event::Changed)])
|
||||
);
|
||||
assert!(s.extras.sort);
|
||||
assert_eq!(screen.wheel_step(&mut s, ExtraRow::Zoom(0), true), None);
|
||||
}
|
||||
}
|
||||
@@ -1663,6 +1663,7 @@ impl GraphicsUi {
|
||||
hover,
|
||||
focused,
|
||||
held: self.dragging == Some(item),
|
||||
key: None,
|
||||
};
|
||||
shell::row(batch, atlas, cols, y, &view);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ pub mod dynamic_light;
|
||||
pub mod effects;
|
||||
pub mod environment;
|
||||
pub mod extras;
|
||||
pub(crate) mod extras_ui;
|
||||
pub mod fast_hash;
|
||||
pub mod fluid_render;
|
||||
pub mod fog;
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::{
|
||||
keybinds::{self, CATEGORIES, KeyBinds, all_mappings},
|
||||
lang::{self, LanguageInfo},
|
||||
settings::{SOUND_SOURCES, Settings},
|
||||
settings_shell as shell,
|
||||
text::{self, Style, Text},
|
||||
ui,
|
||||
};
|
||||
@@ -1619,6 +1620,8 @@ pub struct OptionsUi {
|
||||
center_language: bool,
|
||||
pub graphics: crate::graphics_ui::GraphicsUi,
|
||||
pub controller: crate::controller_ui::ControllerUi,
|
||||
/// Horizon Extras, on the settings shell.
|
||||
extras: shell::Shell<crate::extras_ui::ExtrasScreen>,
|
||||
pub shader: crate::shader_ui::ShaderUi,
|
||||
pub split_profiles: crate::split_profiles::SplitProfiles,
|
||||
/// HDR calibration while its page is open.
|
||||
@@ -1651,7 +1654,7 @@ pub struct OptionsUi {
|
||||
/// The list's rows as last laid out: each row's top and bottom (GUI
|
||||
/// pixels), for the narration's row under the mouse.
|
||||
row_bands: Vec<[f32; 2]>,
|
||||
/// The Extras or PvP & HUD items that take a whole row (`wide_extras`), measured
|
||||
/// The PvP & HUD items that take a whole row (`wide_extras`), measured
|
||||
/// when the page is drawn.
|
||||
wide_extras: Vec<crate::extras::Item>,
|
||||
}
|
||||
@@ -1697,6 +1700,7 @@ impl Default for OptionsUi {
|
||||
center_language: false,
|
||||
graphics: Default::default(),
|
||||
controller: Default::default(),
|
||||
extras: Default::default(),
|
||||
shader: Default::default(),
|
||||
split_profiles: Default::default(),
|
||||
calibration: None,
|
||||
@@ -1765,24 +1769,23 @@ fn option_widget(s: &Settings, opt: Opt, x: f32, y: f32, w: f32) -> Widget {
|
||||
Kind::Cycle => button(x, y, w, opt.label(s), Act::Cycle(opt)),
|
||||
}
|
||||
}
|
||||
/// The Extras or PvP & HUD page's rows: its items under their headers.
|
||||
fn extras_rows(page: Page) -> Vec<Result<crate::extras::Item, &'static str>> {
|
||||
match page {
|
||||
Page::Pvp => crate::pvp::rows()
|
||||
.into_iter()
|
||||
.map(|r| r.map(crate::extras::Item::Pvp))
|
||||
.collect(),
|
||||
_ => crate::extras::rows(),
|
||||
}
|
||||
/// The least GUI size a shell screen lays its keys out for, before it has been drawn at its own.
|
||||
const MIN_KEY_GUI: (f32, f32) = (480.0, 240.0);
|
||||
/// The PvP & HUD page's rows: its items under their headers.
|
||||
fn pvp_rows() -> Vec<Result<crate::extras::Item, &'static str>> {
|
||||
crate::pvp::rows()
|
||||
.into_iter()
|
||||
.map(|r| r.map(crate::extras::Item::Pvp))
|
||||
.collect()
|
||||
}
|
||||
/// The items of `page`'s rows whose label, with any of their first values, is wider than a
|
||||
/// The items of PvP & HUD's rows whose label, with any of their first values, is wider than a
|
||||
/// half-width button holds (`width` measures text with the screen's font): those take the
|
||||
/// whole row, so no label slides past its edges, in any language.
|
||||
fn wide_extras(s: &Settings, page: Page, width: impl Fn(&str) -> f32) -> Vec<crate::extras::Item> {
|
||||
fn wide_extras(s: &Settings, width: impl Fn(&str) -> f32) -> Vec<crate::extras::Item> {
|
||||
// A button's label keeps 2 px off each edge (`AbstractButton.renderString`'s margin),
|
||||
// past which it slides back and forth.
|
||||
let room = 150.0 - 4.0;
|
||||
extras_rows(page)
|
||||
pvp_rows()
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|&item| {
|
||||
@@ -2006,6 +2009,9 @@ impl OptionsUi {
|
||||
if !self.focus_from_keyboard {
|
||||
self.focus = None;
|
||||
}
|
||||
if let Some(shell) = self.shell_mut() {
|
||||
shell.blur_search();
|
||||
}
|
||||
}
|
||||
/// A switch of `horizon-extras.txt` as the Extras page draws it: "Caption: On".
|
||||
fn extra_button(&self, s: &Settings, item: crate::extras::Item) -> Widget {
|
||||
@@ -2058,7 +2064,10 @@ impl OptionsUi {
|
||||
}
|
||||
Page::Controller | Page::PadBinds => self.controller.typing(),
|
||||
_ if Self::shader_page(self.page()).is_some() => self.shader.typing(),
|
||||
_ => self.settings_search_active(),
|
||||
_ => match self.shell() {
|
||||
Some(shell) => shell.typing(),
|
||||
None => self.settings_search_active(),
|
||||
},
|
||||
}
|
||||
}
|
||||
/// Jump straight to a page (control socket / tests).
|
||||
@@ -2174,6 +2183,9 @@ impl OptionsUi {
|
||||
if let Some(at) = Self::shader_page(page) {
|
||||
self.shader.open(at);
|
||||
}
|
||||
if let Some(shell) = self.shell_mut() {
|
||||
shell.open();
|
||||
}
|
||||
if page == Page::SplitSetup {
|
||||
self.split_setup.reset();
|
||||
}
|
||||
@@ -2184,6 +2196,34 @@ impl OptionsUi {
|
||||
self.studio.refresh();
|
||||
}
|
||||
}
|
||||
/// The open page's screen on the settings shell. This, with `shell_mut`, is all a screen
|
||||
/// needs here: drawing, clicks, the wheel, a stick's scrolling, keys and typing all go
|
||||
/// through them, so no input path can miss one.
|
||||
fn shell(&self) -> Option<&dyn shell::Hosted> {
|
||||
match self.page() {
|
||||
Page::Extras => Some(&self.extras),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn shell_mut(&mut self) -> Option<&mut dyn shell::Hosted> {
|
||||
match self.page() {
|
||||
Page::Extras => Some(&mut self.extras),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// What a shell screen's outputs do to these options.
|
||||
fn shell_events(&mut self, s: &mut Settings, outputs: Vec<shell::Output>) -> Vec<Event> {
|
||||
let mut events = Vec::new();
|
||||
for output in outputs {
|
||||
match output {
|
||||
shell::Output::Event(event) => events.push(event),
|
||||
shell::Output::BindKey(name) => self.selected_key = Some(name),
|
||||
shell::Output::Push(page) => self.push(page),
|
||||
shell::Output::Done => events.extend(self.back(s)),
|
||||
}
|
||||
}
|
||||
events
|
||||
}
|
||||
/// What the graphics screen's actions do to these options.
|
||||
fn graphics_events(
|
||||
&mut self,
|
||||
@@ -2534,41 +2574,6 @@ impl OptionsUi {
|
||||
}
|
||||
rows
|
||||
}
|
||||
/// The row that reaches the Zoom mod that replaced the extra: its switch and settings page
|
||||
/// when it is built in, else a note saying where it is installed (Options > Mods). Zoom
|
||||
/// became an official native mod, so Extras has no setting of its own for it (#8713).
|
||||
fn zoom_mod_row(s: &Settings) -> Row {
|
||||
let Some(i) = s
|
||||
.mods
|
||||
.installed
|
||||
.iter()
|
||||
.position(|m| m.id == "zoom" && m.native.is_some())
|
||||
else {
|
||||
// A note, not a button: Options > Mods already has the one way into the Mods page
|
||||
// (`every_setting_and_page_is_reached_from_one_place`).
|
||||
return Row::Note(lang::get("horizon.extras.zoom_mod_install"));
|
||||
};
|
||||
let label = || Text::option_name_value(tr("horizon.extras.zoom"), on_off(s.mods.is_on(i)));
|
||||
let toggle = |width| button(0.0, 0.0, width, label(), Act::ToggleMod(i));
|
||||
let settings = s.mods.installed[i]
|
||||
.native
|
||||
.as_ref()
|
||||
.is_some_and(|page| !page.keys.is_empty() || !page.buttons.is_empty());
|
||||
if settings {
|
||||
Row::Buttons(
|
||||
toggle(150.0),
|
||||
Some(button(
|
||||
0.0,
|
||||
0.0,
|
||||
150.0,
|
||||
tr("horizon.mods.settings"),
|
||||
Act::ModSettings(i),
|
||||
)),
|
||||
)
|
||||
} else {
|
||||
Row::Buttons(toggle(310.0), None)
|
||||
}
|
||||
}
|
||||
/// A native mod's page: its name, its keys as Key Binds rows (the
|
||||
/// player's choice, else the default), then its buttons.
|
||||
fn native_rows(&self, s: &Settings, i: usize) -> Vec<Row> {
|
||||
@@ -2609,7 +2614,7 @@ impl OptionsUi {
|
||||
fn list_rows(&self, s: &Settings) -> Vec<Row> {
|
||||
match self.page() {
|
||||
Page::SplitSetup => Vec::new(),
|
||||
Page::Extras | Page::Pvp => {
|
||||
Page::Pvp => {
|
||||
// Each section's switches two to a row, as vanilla's
|
||||
// OptionsList.addSmall lays out an options screen; one whose label would run
|
||||
// past half a row in this language (`wide_extras`) gets the whole row.
|
||||
@@ -2637,21 +2642,12 @@ impl OptionsUi {
|
||||
widget
|
||||
}
|
||||
};
|
||||
// PvP & HUD opens with its preset; Extras doesn't repeat it, nor the way there
|
||||
// (Horizon Settings has it).
|
||||
// PvP & HUD opens with its preset (Horizon Settings has the way there).
|
||||
let preset = button(0.0, 0.0, 150.0, tr("horizon.pvp.preset"), Act::PvpPreset);
|
||||
let mut rows = Vec::new();
|
||||
if self.page() == Page::Extras {
|
||||
// Zoom, the FPS counter, Keystrokes and the Minimap are official mods now;
|
||||
// Extras keeps a way to the one players look for here (#8713).
|
||||
rows.push(Self::zoom_mod_row(s));
|
||||
}
|
||||
if self.page() == Page::Pvp {
|
||||
// The editor moves what the game shows: only from a world.
|
||||
let edit = button(0.0, 0.0, 150.0, tr("horizon.pvp.editor"), Act::HudEditor);
|
||||
let edit = if self.in_world { edit } else { inactive(edit) };
|
||||
rows.push(Row::Buttons(preset, Some(edit)));
|
||||
}
|
||||
// The editor moves what the game shows: only from a world.
|
||||
let edit = button(0.0, 0.0, 150.0, tr("horizon.pvp.editor"), Act::HudEditor);
|
||||
let edit = if self.in_world { edit } else { inactive(edit) };
|
||||
let mut rows = vec![Row::Buttons(preset, Some(edit))];
|
||||
let mut section = Vec::new();
|
||||
let flush = |rows: &mut Vec<Row>, section: &mut Vec<crate::extras::Item>| {
|
||||
let mut half = None;
|
||||
@@ -2667,7 +2663,7 @@ impl OptionsUi {
|
||||
}
|
||||
rows.extend(half.map(|h| Row::Buttons(extra(h, 150.0), None)));
|
||||
};
|
||||
for row in extras_rows(self.page()) {
|
||||
for row in pvp_rows() {
|
||||
match row {
|
||||
Err(header) => {
|
||||
flush(&mut rows, &mut section);
|
||||
@@ -3649,6 +3645,12 @@ impl OptionsUi {
|
||||
self.shader.draw(s, batch, atlas, cursor, self.in_world);
|
||||
return;
|
||||
}
|
||||
let waiting = self.selected_key;
|
||||
let in_world = self.in_world;
|
||||
if let Some(shell) = self.shell_mut() {
|
||||
shell.draw(s, batch, atlas, cursor, in_world, waiting);
|
||||
return;
|
||||
}
|
||||
if self.page() == Page::SplitSetup {
|
||||
self.split_setup.on = s.extras.splitscreen;
|
||||
// The controller picks a tile up between presses: the screen's size from the draw.
|
||||
@@ -3677,8 +3679,8 @@ impl OptionsUi {
|
||||
crate::screen_draw::paint(batch, atlas, &draws);
|
||||
return;
|
||||
}
|
||||
if matches!(self.page(), Page::Extras | Page::Pvp) {
|
||||
self.wide_extras = wide_extras(s, self.page(), |t| atlas.font.width(t));
|
||||
if self.page() == Page::Pvp {
|
||||
self.wide_extras = wide_extras(s, |t| atlas.font.width(t));
|
||||
}
|
||||
let (widgets, top, bottom, content) = self.layout(s, w, h);
|
||||
self.size = (w, h);
|
||||
@@ -4015,6 +4017,22 @@ impl OptionsUi {
|
||||
.click(s, (x, y), size, framebuffer, drag, right, self.shift);
|
||||
return self.graphics_events(s, actions);
|
||||
}
|
||||
if self.shell().is_some() {
|
||||
// Waiting on a key: a mouse button binds, as on Key Binds.
|
||||
if self.selected_key.is_some() && !drag {
|
||||
let button = if right {
|
||||
winit::event::MouseButton::Right
|
||||
} else {
|
||||
winit::event::MouseButton::Left
|
||||
};
|
||||
return self.bind_mouse(s, button);
|
||||
}
|
||||
let shift = self.shift;
|
||||
let outputs = self.shell_mut().map_or_else(Vec::new, |shell| {
|
||||
shell.click(s, (x, y), size, drag, right, shift)
|
||||
});
|
||||
return self.shell_events(s, outputs);
|
||||
}
|
||||
if Self::controller_page(self.page()).is_some() {
|
||||
// Waiting on a button: a click cancels the wait.
|
||||
if !drag && self.selected_pad_action.take().is_some() {
|
||||
@@ -4291,7 +4309,9 @@ impl OptionsUi {
|
||||
// release, even if the pointer has left the widget or its value stayed.
|
||||
let slider = self.dragging.take().is_some();
|
||||
let (shader_slider, shader_pack) = self.shader.release();
|
||||
let graphics_slider = self.graphics.release() | self.controller.release() | shader_slider;
|
||||
let shell_slider = self.shell_mut().is_some_and(|shell| shell.release());
|
||||
let graphics_slider =
|
||||
self.graphics.release() | self.controller.release() | shader_slider | shell_slider;
|
||||
let calibration_slider = self.calibration.as_mut().is_some_and(|c| c.release());
|
||||
let split_drop = self.split_setup.release();
|
||||
if slider || graphics_slider || calibration_slider {
|
||||
@@ -4369,6 +4389,13 @@ impl OptionsUi {
|
||||
) -> Vec<Event> {
|
||||
self.size = size;
|
||||
self.framebuffer = framebuffer;
|
||||
if self.resource_error.is_some() {
|
||||
return Vec::new();
|
||||
}
|
||||
if let Some(shell) = self.shell_mut() {
|
||||
let outputs = shell.wheel(s, size, cursor, amount);
|
||||
return self.shell_events(s, outputs);
|
||||
}
|
||||
if Self::graphics_page(self.page()).is_some() {
|
||||
let actions = self.graphics.wheel(s, size, cursor, framebuffer, amount);
|
||||
return self.graphics_events(s, actions);
|
||||
@@ -4460,6 +4487,10 @@ impl OptionsUi {
|
||||
if self.resource_error.is_some() {
|
||||
return;
|
||||
}
|
||||
if let Some(shell) = self.shell_mut() {
|
||||
shell.scroll(s, size, amount);
|
||||
return;
|
||||
}
|
||||
if Self::graphics_page(self.page()).is_some() {
|
||||
self.graphics.scroll(s, size, amount);
|
||||
return;
|
||||
@@ -4656,6 +4687,10 @@ impl OptionsUi {
|
||||
self.scroll = 0.0;
|
||||
} else if self.page() == Page::SplitProfiles {
|
||||
self.split_profiles.type_text(text);
|
||||
} else if let Some(shell) = self.shell_mut() {
|
||||
if shell.typing() {
|
||||
shell.type_text(text);
|
||||
}
|
||||
} else if Self::graphics_page(self.page()).is_some() && self.graphics.typing() {
|
||||
self.graphics.type_text(text);
|
||||
} else if Self::controller_page(self.page()).is_some() && self.controller.typing() {
|
||||
@@ -4693,6 +4728,9 @@ impl OptionsUi {
|
||||
if self.page() == Page::SplitProfiles {
|
||||
return self.split_profiles.shortcut(shortcut, clipboard);
|
||||
}
|
||||
if let Some(shell) = self.shell_mut() {
|
||||
return shell.shortcut(shortcut, clipboard);
|
||||
}
|
||||
if Self::graphics_page(self.page()).is_some() {
|
||||
return self.graphics.shortcut(shortcut, clipboard);
|
||||
}
|
||||
@@ -4826,6 +4864,14 @@ impl OptionsUi {
|
||||
}
|
||||
return events;
|
||||
}
|
||||
let gui = (
|
||||
self.size.0.max(MIN_KEY_GUI.0),
|
||||
self.size.1.max(MIN_KEY_GUI.1),
|
||||
);
|
||||
if let Some(shell) = self.shell_mut() {
|
||||
let outputs = shell.key(s, key, text, shift, gui);
|
||||
return self.shell_events(s, outputs);
|
||||
}
|
||||
if Self::controller_page(self.page()).is_some() {
|
||||
let size = (self.size.0.max(480.0), self.size.1.max(240.0));
|
||||
let fb = [self.framebuffer[0].max(480), self.framebuffer[1].max(240)];
|
||||
@@ -5184,6 +5230,7 @@ impl OptionsUi {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::extras_ui::ExtraRow;
|
||||
|
||||
/// The window allows the input method only while `typing` says a search
|
||||
/// box has the focus (`Game::text_field_focused`), so a page without one
|
||||
@@ -5629,6 +5676,9 @@ mod tests {
|
||||
if ui.controller_open() {
|
||||
texts.extend(ui.controller.row_names(&s));
|
||||
}
|
||||
if ui.page() == Page::Extras {
|
||||
texts.extend(ui.extras.row_labels(&s));
|
||||
}
|
||||
}
|
||||
texts
|
||||
})
|
||||
@@ -5997,7 +6047,7 @@ mod tests {
|
||||
let atlas = atlas();
|
||||
let mut s = Settings::default();
|
||||
let mut ui = OptionsUi::default();
|
||||
ui.show(Page::Extras);
|
||||
ui.show(Page::Pvp);
|
||||
let (widgets, _, _, _) = ui.layout(&s, 480.0, 480.0);
|
||||
let extra = widgets
|
||||
.iter()
|
||||
@@ -6007,7 +6057,7 @@ mod tests {
|
||||
WKind::Button {
|
||||
act: Act::Extra(item),
|
||||
..
|
||||
} if !matches!(item, crate::extras::Item::SortKey)
|
||||
} if item.key_name().is_none()
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
@@ -6053,19 +6103,17 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn extras_reaches_the_zoom_mod_that_replaced_the_extra() {
|
||||
let atlas = atlas();
|
||||
let mut s = Settings::default();
|
||||
let mut ui = OptionsUi::default();
|
||||
ui.show(Page::Extras);
|
||||
let english = || lang::Language::load(&[], "en_us");
|
||||
// Without the mod, Extras says where it is installed (a note, not a second way into
|
||||
// Options > Mods).
|
||||
let (widgets, _, _, _) = lang::scoped(english(), || ui.layout(&s, 480.0, 4000.0)).0;
|
||||
let labels = lang::scoped(english(), || ui.extras.row_labels(&s)).0;
|
||||
assert!(
|
||||
widgets.iter().any(|w| matches!(
|
||||
&w.kind,
|
||||
WKind::Text { text, .. } if text.plain() == "Zoom: install from Mods"
|
||||
)),
|
||||
"Extras has no hint for the Zoom mod"
|
||||
labels.iter().any(|l| l == "Zoom: install from Mods"),
|
||||
"Extras has no hint for the Zoom mod: {labels:?}"
|
||||
);
|
||||
// Built in, Zoom's settings live on its page: Extras opens that page.
|
||||
s.mods.installed.push(crate::mod_list::Installed {
|
||||
@@ -6079,30 +6127,28 @@ mod tests {
|
||||
buttons: Vec::new(),
|
||||
}),
|
||||
});
|
||||
let (widgets, _, _, _) = lang::scoped(english(), || ui.layout(&s, 480.0, 4000.0)).0;
|
||||
assert!(
|
||||
widgets.iter().any(|w| matches!(
|
||||
&w.kind,
|
||||
WKind::Button {
|
||||
act: Act::ModSettings(0),
|
||||
label,
|
||||
..
|
||||
} if label.plain() == "Settings"
|
||||
)),
|
||||
"Extras doesn't open the Zoom mod's settings"
|
||||
let labels = lang::scoped(english(), || ui.extras.row_labels(&s)).0;
|
||||
assert!(labels.iter().any(|l| l == "Settings"), "{labels:?}");
|
||||
// The mod's switch is beside it, so Extras keeps the Zoom setting it had.
|
||||
assert!(labels.iter().any(|l| l.starts_with("Zoom: ")), "{labels:?}");
|
||||
let size = (480.0, 480.0);
|
||||
let on = s.mods.is_on(0);
|
||||
let at = ui.extras.point_at(&s, size, ExtraRow::Zoom(0));
|
||||
assert_eq!(
|
||||
ui.click(&mut s, &atlas, at.0, at.1, size, [960, 960], false, false),
|
||||
vec![Event::ButtonClick, Event::Changed]
|
||||
);
|
||||
// The mod's switch is on the same row, so Extras keeps the Zoom setting it had.
|
||||
assert!(
|
||||
widgets.iter().any(|w| matches!(
|
||||
&w.kind,
|
||||
WKind::Button {
|
||||
act: Act::ToggleMod(0),
|
||||
label,
|
||||
..
|
||||
} if label.plain().starts_with("Zoom")
|
||||
)),
|
||||
"Extras doesn't switch the Zoom mod"
|
||||
assert_ne!(s.mods.is_on(0), on);
|
||||
let at = ui.extras.point_at(&s, size, ExtraRow::ZoomSettings(0));
|
||||
assert_eq!(
|
||||
ui.click(&mut s, &atlas, at.0, at.1, size, [960, 960], false, false),
|
||||
vec![
|
||||
Event::ButtonClick,
|
||||
Event::ButtonClick,
|
||||
Event::NativeSettings(0)
|
||||
]
|
||||
);
|
||||
assert_eq!(ui.page(), Page::NativeMod(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -6446,32 +6492,27 @@ mod tests {
|
||||
);
|
||||
assert_eq!(ui.page(), page);
|
||||
}
|
||||
let (widgets, _, _, _) = ui.layout(&s, size.0, size.1);
|
||||
assert_eq!(
|
||||
widgets
|
||||
.iter()
|
||||
.filter(|w| matches!(
|
||||
w.kind,
|
||||
WKind::Button {
|
||||
act: Act::Extra(_),
|
||||
..
|
||||
}
|
||||
))
|
||||
.count(),
|
||||
crate::extras::rows().iter().filter(|r| r.is_ok()).count()
|
||||
ui.extras.row_labels(&s).len(),
|
||||
// And the note about the Zoom mod over them.
|
||||
1 + crate::extras::rows().iter().filter(|r| r.is_ok()).count()
|
||||
);
|
||||
let press = |ui: &mut OptionsUi, s: &mut Settings, item: crate::extras::Item| {
|
||||
let at = ui.extras.point_at(s, size, ExtraRow::Item(item));
|
||||
ui.click(s, &atlas, at.0, at.1, size, [960, 960], false, false)
|
||||
};
|
||||
assert_eq!(
|
||||
click(&mut ui, &mut s, Act::Extra(crate::extras::Item::Sort)),
|
||||
press(&mut ui, &mut s, crate::extras::Item::Sort),
|
||||
vec![Event::ButtonClick, Event::Changed]
|
||||
);
|
||||
assert!(s.extras.sort);
|
||||
assert_eq!(
|
||||
click(&mut ui, &mut s, Act::Extra(crate::extras::Item::Pregen)),
|
||||
press(&mut ui, &mut s, crate::extras::Item::Pregen),
|
||||
vec![Event::ButtonClick, Event::Changed]
|
||||
);
|
||||
assert!(s.extras.pregen);
|
||||
assert_eq!(
|
||||
click(&mut ui, &mut s, Act::Extra(crate::extras::Item::SortKey)),
|
||||
press(&mut ui, &mut s, crate::extras::Item::SortKey),
|
||||
vec![Event::ButtonClick]
|
||||
);
|
||||
assert_eq!(ui.selected_key, Some(crate::extras::SORT_KEY));
|
||||
@@ -6484,12 +6525,6 @@ mod tests {
|
||||
crate::extras::Extras::parse(&s.extras.serialize()),
|
||||
s.extras
|
||||
);
|
||||
// In a window too short for the page, the wheel scrolls to its end.
|
||||
let short = (480.0, 180.0);
|
||||
ui.scroll(&s, short, -1000.0);
|
||||
let (_, top, bottom, content) = ui.layout(&s, short.0, short.1);
|
||||
assert!(content > bottom - top);
|
||||
assert_eq!(ui.scroll, content - (bottom - top));
|
||||
assert_eq!(
|
||||
ui.key(&mut s, &atlas, winit::keyboard::KeyCode::Escape, None),
|
||||
vec![Event::Changed]
|
||||
@@ -6497,6 +6532,24 @@ mod tests {
|
||||
assert_eq!(ui.page(), Page::Horizon);
|
||||
}
|
||||
|
||||
/// Every way a pad or a wheel scrolls a page on the shell moves the same list: Replay Studio's
|
||||
/// took the wheel and missed `scroll_at`, so a stick couldn't scroll it.
|
||||
#[test]
|
||||
fn the_wheel_and_the_stick_scroll_a_page_on_the_shell_alike() {
|
||||
let mut s = Settings::default();
|
||||
let mut ui = OptionsUi::default();
|
||||
ui.show(Page::Extras);
|
||||
// Too short for the page.
|
||||
let short = (480.0, 180.0);
|
||||
ui.wheel(&mut s, short, (0.0, 0.0), [960, 360], -1.0);
|
||||
let one = ui.extras.scroll_amount();
|
||||
assert!(one > 0.0);
|
||||
ui.scroll_at(&s, short, (0.0, 0.0), -1.0);
|
||||
assert_eq!(ui.extras.scroll_amount(), 2.0 * one);
|
||||
ui.scroll_at(&s, short, (0.0, 0.0), 2.0);
|
||||
assert_eq!(ui.extras.scroll_amount(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classic_graphics_presets_set_the_distances() {
|
||||
let atlas = atlas();
|
||||
@@ -7327,7 +7380,7 @@ mod tests {
|
||||
/// An Extras or PvP & HUD label wider than half a row takes the whole row, so every label
|
||||
/// fits its button: "Keep channels for UI sounds: Off" did not.
|
||||
#[test]
|
||||
fn extras_labels_fit_their_buttons() {
|
||||
fn pvp_labels_fit_their_buttons() {
|
||||
// Vanilla's font: 6 px a glyph, a few narrower ones.
|
||||
let width = |t: &str| {
|
||||
t.chars()
|
||||
@@ -7342,9 +7395,9 @@ mod tests {
|
||||
};
|
||||
let s = Settings::default();
|
||||
let mut ui = OptionsUi::default();
|
||||
for page in [Page::Extras, Page::Pvp] {
|
||||
ui.stack = vec![Page::Main, page];
|
||||
ui.wide_extras = wide_extras(&s, page, width);
|
||||
{
|
||||
ui.stack = vec![Page::Main, Page::Pvp];
|
||||
ui.wide_extras = wide_extras(&s, width);
|
||||
let rows = ui.list_rows(&s);
|
||||
let buttons: Vec<&Widget> = rows
|
||||
.iter()
|
||||
@@ -7358,20 +7411,11 @@ mod tests {
|
||||
continue;
|
||||
};
|
||||
let label = label.plain();
|
||||
assert!(width(&label) <= b.w - 4.0, "{page:?}: {label} on {}", b.w);
|
||||
assert!(width(&label) <= b.w - 4.0, "{label} on {}", b.w);
|
||||
}
|
||||
// Short ones keep vanilla's two to a row.
|
||||
assert!(buttons.iter().any(|b| b.w == 150.0), "{page:?}");
|
||||
assert!(buttons.iter().any(|b| b.w == 150.0));
|
||||
}
|
||||
ui.stack = vec![Page::Main, Page::Extras];
|
||||
ui.wide_extras = wide_extras(&s, Page::Extras, width);
|
||||
for item in [
|
||||
crate::extras::Item::UiSoundReserve,
|
||||
crate::extras::Item::WheelCyclesSettings,
|
||||
] {
|
||||
assert!(ui.wide_extras.contains(&item), "{item:?}");
|
||||
}
|
||||
assert!(!ui.wide_extras.contains(&crate::extras::Item::AutoReconnect));
|
||||
}
|
||||
|
||||
/// Every setting a player reaches, from the Options screen through every button that opens
|
||||
@@ -7787,20 +7831,9 @@ mod tests {
|
||||
s.extras.wheel_cycles_settings = true;
|
||||
ui.show(Page::Extras);
|
||||
let size = (480.0, 600.0);
|
||||
let (widgets, _, _, _) = ui.layout(&s, size.0, size.1);
|
||||
let sort_wd = widgets
|
||||
.iter()
|
||||
.find(|w| {
|
||||
matches!(
|
||||
w.kind,
|
||||
WKind::Button {
|
||||
act: Act::Extra(crate::extras::Item::Sort),
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
let cursor = (sort_wd.x + 5.0, sort_wd.y + 5.0);
|
||||
let cursor = ui
|
||||
.extras
|
||||
.point_at(&s, size, ExtraRow::Item(crate::extras::Item::Sort));
|
||||
assert!(!s.extras.sort);
|
||||
ui.wheel(&mut s, size, cursor, [854, 480], 1.0);
|
||||
assert!(s.extras.sort);
|
||||
@@ -8283,55 +8316,5 @@ mod tests {
|
||||
ui.click(&mut s, &atlas, px, py, size, [854, 480], false, false);
|
||||
assert_eq!(s.particles, 0);
|
||||
ui.shift = false;
|
||||
|
||||
// Also verify Act::Extra on Page::Extras
|
||||
ui.show(Page::Extras);
|
||||
let (widgets, top, _, _) = ui.layout(&s, size.0, size.1);
|
||||
if let Some(extra_btn) = widgets.iter().find(|wd| {
|
||||
matches!(
|
||||
wd.kind,
|
||||
WKind::Button {
|
||||
active: true,
|
||||
act: Act::Extra(crate::extras::Item::CosmeticsOthers),
|
||||
..
|
||||
}
|
||||
)
|
||||
}) {
|
||||
// Extras has enough rows to scroll: click the cycle button inside
|
||||
// the list's viewport, as a player would after scrolling to it.
|
||||
ui.scroll = (extra_btn.y - top).max(0.0);
|
||||
let (widgets, _, _, _) = ui.layout(&s, size.0, size.1);
|
||||
let extra_btn = widgets
|
||||
.iter()
|
||||
.find(|wd| {
|
||||
matches!(
|
||||
wd.kind,
|
||||
WKind::Button {
|
||||
act: Act::Extra(crate::extras::Item::CosmeticsOthers),
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
let (ex, ey) = (extra_btn.x + 1.0, extra_btn.y + 1.0);
|
||||
use horizon_cosmetics::prefs::Others;
|
||||
// Not chosen, nothing owned: shows Hidden.
|
||||
assert_eq!(s.extras.cosmetics_others, None);
|
||||
assert_eq!(s.extras.effective_others(), Others::Hidden);
|
||||
|
||||
// Forward (wraps to Shown), now a choice
|
||||
ui.click(&mut s, &atlas, ex, ey, size, [854, 480], false, false);
|
||||
assert_eq!(s.extras.cosmetics_others, Some(Others::Shown));
|
||||
|
||||
// Backward with shift
|
||||
ui.shift = true;
|
||||
ui.click(&mut s, &atlas, ex, ey, size, [854, 480], false, false);
|
||||
assert_eq!(s.extras.cosmetics_others, Some(Others::Hidden));
|
||||
|
||||
// Backward again
|
||||
ui.click(&mut s, &atlas, ex, ey, size, [854, 480], false, false);
|
||||
assert_eq!(s.extras.cosmetics_others, Some(Others::NameTags));
|
||||
ui.shift = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -644,6 +644,7 @@ impl ShaderUi {
|
||||
hover,
|
||||
focused,
|
||||
held: self.dragging == Some(item),
|
||||
key: None,
|
||||
};
|
||||
shell::row(batch, atlas, &cols, y, &view);
|
||||
},
|
||||
|
||||
@@ -147,19 +147,20 @@ Geist (400, 500, 600) and Geist Mono (400), bundled under the SIL Open Font Lice
|
||||
sunset) behind the hero. Light and dark follow the system, with a toggle. A giant wordmark in
|
||||
pixels sits in the footer.
|
||||
- **Game**: vanilla's screens. The on-screen keyboard is a rounded plate of keys in the page
|
||||
colour, the focused key in the accent. Horizon's graphics settings screen takes the Blocks
|
||||
palette below in a lighter form (`graphics_style.rs`).
|
||||
colour, the focused key in the accent. Horizon's graphics, controller and Extras settings
|
||||
screens take the Blocks palette below in a lighter form (`graphics_style.rs`; Extras through
|
||||
the shared `settings_shell.rs`).
|
||||
|
||||
## Blocks
|
||||
|
||||
A blocky, Minecraft-like look in Horizon's purple, for the website, the redesigned launcher and
|
||||
the game's graphics settings screen:
|
||||
the game's graphics, controller and Extras settings screens:
|
||||
Minecraft's inventory rather than a document. Its colours are `horizon_theme::Blocks`
|
||||
(`BLOCKS_LIGHT`, `BLOCKS_DARK`), which `tokens.css` carries under the same names as before plus
|
||||
`--panel`, `--raised`, `--button`, `--well`, `--bevel`, `--lip`, `--outline` and the `--face`
|
||||
of the main action. The game's other menus keep `LIGHT` and `DARK`.
|
||||
|
||||
In the game the graphics settings screen draws the Blocks palette in GUI pixels, lighter than the
|
||||
In the game these settings screens draw the Blocks palette in GUI pixels, lighter than the
|
||||
website's blocks so the world stays in view: square panels of the page colour, see-through, with a
|
||||
1 px edge and no bevel or lip, flat controls, the violet face on Done, and vanilla's font rather
|
||||
than Pixelify. Hover and focus are vanilla's white edge.
|
||||
|
||||
+3
-3
@@ -20,10 +20,10 @@ again restores vanilla's behaviour completely.
|
||||
| Sort order and its clicks | `crates/horizon-client/src/inv_sort.rs` (pure, unit tested) |
|
||||
| The music page's rows, and the pools it filters | `options_ui.rs` (`Page::Music`), `crates/horizon-client/src/music.rs` (`track_groups`, `MusicManager::set_off`), `sound_events.rs` (`sound_except`) |
|
||||
| Cosmetics (show mine, show others'), off with their flag | `extras.rs`, `crates/horizon-client/src/game/cosmetics.rs` ([cosmetics.md](cosmetics.md)) |
|
||||
| The settings page | `options_ui.rs` (`Page::Extras`): **Options → Horizon Settings... → Extras...**; fullbright, dynamic lights and hiding block-break particles change the picture, so they are on the graphics screen's Quality page instead (`graphics_ui.rs`, **Options → Video Settings...**) |
|
||||
| The settings page | `extras_ui.rs` (`Page::Extras`, on `settings_shell.rs`): **Options → Horizon Settings... → Extras...**; fullbright, dynamic lights and hiding block-break particles change the picture, so they are on the graphics screen's Quality page instead (`graphics_ui.rs`, **Options → Video Settings...**) |
|
||||
|
||||
The settings are grouped by extra, under a header each, two buttons to a row as
|
||||
vanilla's options lists are, and each says what it does in its tooltip. An
|
||||
The settings are grouped by extra, under a header each, one to a row, and the strip
|
||||
under the list says what the row in focus or under the pointer does. An
|
||||
extra's own settings (its key, corner, size...) are greyed out while its switch
|
||||
is off (`Item::parent`).
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ Cases:
|
||||
- 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.
|
||||
- extras_shell: Horizon Extras on the settings shell: the D-pad walks its rows and scrolls through the hidden ones, past
|
||||
a greyed row, the right stick scrolls it, A changes and saves a toggle, the strip follows the row, B leaves.
|
||||
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.
|
||||
"""
|
||||
@@ -1492,6 +1494,102 @@ TARGETS_WAIT = 5
|
||||
STUDIO_RECORDINGS, STUDIO_BUTTONS = 12, 3
|
||||
|
||||
|
||||
EXTRAS_ABOUT = 'Optional additions, all off by default'
|
||||
AUTO_RECONNECT = 'Auto-reconnect'
|
||||
AUTO_RECONNECT_ABOUT = 'Rejoins a server after a lost connection or a restart'
|
||||
FIRST_EXTRA = 'Own name tag'
|
||||
LAST_MENU_EXTRA = 'Resource pack browser'
|
||||
GREYED_EXTRA = 'Sort key'
|
||||
ROW_WALK = 40
|
||||
|
||||
|
||||
def label_of(game, text):
|
||||
"""The widget whose label has this in it, or None while the list hides it."""
|
||||
return next((w for w in game.widgets() if text in w.get('label', '')), None)
|
||||
|
||||
|
||||
def ring_on(game, rect):
|
||||
"""Whether a white ring is drawn along the top of this rectangle, by the pixels: state can't show a stale one."""
|
||||
path = game.home / 'ring.png'
|
||||
game.ctl('screenshot', path, timeout=120)
|
||||
rows, width, channels = png_rows(path)
|
||||
scale = width / game.ctl('ui')['gui_size'][0]
|
||||
x, y, w, _ = rect
|
||||
pixel = rows[int((y + 0.5) * scale)][int((x + w / 2) * scale) * channels:][:3]
|
||||
return pixel == bytes((255, 255, 255))
|
||||
|
||||
|
||||
def extras_saved(game):
|
||||
path = game.game_home / 'horizon-extras.txt'
|
||||
return path.read_text() if path.exists() else ''
|
||||
|
||||
|
||||
def extras_shell(args, name):
|
||||
game = Game(args, name)
|
||||
check = Check()
|
||||
try:
|
||||
game.press_through('west')
|
||||
game.ctl('options', 'extras')
|
||||
time.sleep(SETTLE)
|
||||
game.wait_targets()
|
||||
check(declared(game).get('Done') is True, f'Extras declares {declared(game)}')
|
||||
check(game.menu()['undeclared'] == [], f"undeclared: {game.menu()['undeclared']}")
|
||||
check(has_label(game, EXTRAS_ABOUT), f'the strip does not say what the page holds: {labels(game)}')
|
||||
check(label_of(game, LAST_MENU_EXTRA) is None, 'the page fits: this case cannot see it scroll')
|
||||
shot(game, 'extras-open')
|
||||
|
||||
# From the search field across to the first row, then down: a greyed row is stepped over, hidden rows scroll in.
|
||||
game.pad('right')
|
||||
check(FIRST_EXTRA in (game.focused_label() or ''), f'the D-pad never reached {FIRST_EXTRA}: {game.focused_label()}')
|
||||
# A presses with a click: the row it pressed doesn't stay lit once the focus moves on.
|
||||
game.pad('south')
|
||||
pressed = game.focus()
|
||||
game.pad('down')
|
||||
check(game.focus() != pressed, 'the D-pad did not leave the row A pressed')
|
||||
check(not ring_on(game, pressed), f'the row A pressed keeps a ring at {pressed} with the focus gone')
|
||||
reached = {game.focused_label()}
|
||||
for _ in range(ROW_WALK):
|
||||
game.pad('down')
|
||||
reached.add(game.focused_label())
|
||||
if LAST_MENU_EXTRA in (game.focused_label() or ''):
|
||||
break
|
||||
check(any(LAST_MENU_EXTRA in (r or '') for r in reached), f'the D-pad never scrolled to the last rows: {reached}')
|
||||
check(not any(GREYED_EXTRA in (r or '') for r in reached), f'the focus landed on a greyed row: {reached}')
|
||||
shot(game, 'extras-scrolled')
|
||||
|
||||
# Back up the list to a row near its top; the right stick moves the list and the focus stays on it.
|
||||
for _ in range(ROW_WALK):
|
||||
if AUTO_RECONNECT in (game.focused_label() or ''):
|
||||
break
|
||||
game.pad('up')
|
||||
label = game.focused_label()
|
||||
check(AUTO_RECONNECT in (label or ''), f'the D-pad never came back to {AUTO_RECONNECT}: {label}')
|
||||
check(has_label(game, AUTO_RECONNECT_ABOUT), f'the strip does not follow the row: {labels(game)}')
|
||||
check(AUTO_RECONNECT in (label or '') and label.endswith('Off'), f'{AUTO_RECONNECT} reads {label}')
|
||||
shot(game, 'extras-described')
|
||||
y = game.focus()[1]
|
||||
game.stick()
|
||||
moved = game.focus()
|
||||
check(game.focused_label() == label, f'the stick moved the focus from {label} to {game.focused_label()}')
|
||||
check(moved and moved[1] < y, f'the stick did not scroll the list: {y} to {moved}')
|
||||
|
||||
# A changes the toggle: what the row reads, and the file it is saved in.
|
||||
check('autoReconnect:true' not in extras_saved(game), 'Auto-reconnect began on')
|
||||
game.pad('south')
|
||||
label = game.focused_label()
|
||||
check(label and label.endswith('On'), f'A left {AUTO_RECONNECT} as {label}')
|
||||
check('autoReconnect:true' in extras_saved(game), 'the toggle was not saved to horizon-extras.txt')
|
||||
shot(game, 'extras-toggled')
|
||||
|
||||
# B presses Done.
|
||||
game.pad('east')
|
||||
game.wait_targets()
|
||||
check(not has_label(game, AUTO_RECONNECT), f'B did not leave Extras: {labels(game)}')
|
||||
finally:
|
||||
game.quit()
|
||||
return check.failures
|
||||
|
||||
|
||||
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."""
|
||||
end = time.time() + TARGETS_WAIT
|
||||
@@ -2036,6 +2134,7 @@ CASES = {'idle_a': idle_a, 'idle_b': idle_b, 'idle_x': idle_x, 'release': releas
|
||||
'every_screen_has_targets': every_screen_has_targets,
|
||||
'studio_rows': studio_rows, 'pause_icon': pause_icon,
|
||||
'erase_checkbox': erase_checkbox,
|
||||
'extras_shell': extras_shell,
|
||||
'glyph_art': glyph_art,
|
||||
'glyph_art_world': glyph_art_world,
|
||||
'key_row_a_x': key_row_a_x,
|
||||
|
||||
Reference in new issue
Block a user